Parcourir la source

Merge branch 'master' of http://39.97.59.228:8013/hrx/mky-vent-base

lxh il y a 4 semaines
Parent
commit
36f6ee0d99
37 fichiers modifiés avec 5429 ajouts et 1138 suppressions
  1. 1 0
      src/assets/images/ventAI/chatModalBlue/2-10.svg
  2. BIN
      src/assets/images/ventAI/chatModalBlue/2-7.png
  3. 1 0
      src/assets/images/ventAI/chatModalBlue/2-7.svg
  4. 1 0
      src/assets/images/ventAI/chatModalBlue/2-9.svg
  5. 6 0
      src/assets/images/ventAI/chatModalBlue/4-7.svg
  6. 2 1
      src/design/themify/deepblue.less
  7. 4 0
      src/design/themify/default.less
  8. 12 1
      src/qiankun/state.ts
  9. 11 0
      src/router/routes/basic.ts
  10. 163 168
      src/views/vent/deviceManager/comment/DeviceModal.vue
  11. 156 156
      src/views/vent/deviceManager/comment/DeviceModalLeather.vue
  12. 0 119
      src/views/vent/deviceManager/comment/FanLineModal .vue
  13. 195 195
      src/views/vent/deviceManager/comment/pointTabel/PointTable.vue
  14. 145 0
      src/views/vent/deviceManager/comment/warningTabel/BaseModalCtrl.vue
  15. 76 0
      src/views/vent/deviceManager/comment/warningTabel/DeviceCtrlPointTable.vue
  16. 186 0
      src/views/vent/deviceManager/comment/warningTabel/ctrlTable.vue
  17. 26 0
      src/views/vent/deviceManager/comment/warningTabel/warning.api.ts
  18. 137 2
      src/views/vent/deviceManager/comment/warningTabel/warning.data.ts
  19. 260 193
      src/views/vent/monitorManager/comment/HandlerHistoryTable.vue
  20. 83 88
      src/views/vent/monitorManager/deviceMonitor/index.vue
  21. 5 3
      src/views/vent/monitorManager/gateMonitor/components/GateShrinkSVG.vue
  22. 5 3
      src/views/vent/monitorManager/gateMonitor/components/GateYeYaSVG.vue
  23. 535 0
      src/views/vent/monitorManager/gateMonitor/components/gateSanDaoShenSuoSVG.vue
  24. 671 0
      src/views/vent/monitorManager/gateMonitor/components/gateXrSVG.vue
  25. 5 1
      src/views/vent/monitorManager/gateMonitor/gate.data.ts
  26. 31 1
      src/views/vent/packageUpload/components/DeployHistory.vue
  27. 3 0
      src/views/ventAI/dataPicker/useDataPicker.ts
  28. 178 7
      src/views/ventAI/manageAssistent/api.ts
  29. 647 80
      src/views/ventAI/manageAssistent/components/AiAssistantModal.vue
  30. 857 20
      src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue
  31. 177 51
      src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue
  32. 96 8
      src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue
  33. 515 0
      src/views/ventAI/manageAssistent/components/chatModal/SkillModal.vue
  34. 53 39
      src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue
  35. 156 0
      src/views/ventAI/manageAssistent/components/chatModal/TodoListBar.vue
  36. 15 0
      src/views/ventAI/manageAssistent/components/chatModal/types.ts
  37. 15 2
      src/views/ventAI/manageAssistent/components/chatModal/utils.ts

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/2-10.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1786064150244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4992" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M923.008 736.853333a25.6 25.6 0 0 1-12.8 22.186667l-29.653333 17.194667-222.165334 128.725333-90.325333 52.352a25.6 25.6 0 0 1-38.442667-22.186667V463.786667a25.6 25.6 0 0 1 14.208-22.954667L799.146667 314.026667l86.869333-43.093334a25.6 25.6 0 0 1 36.992 22.912V736.853333zM209.194667 314.026667l255.317333 126.805333a25.6 25.6 0 0 1 14.208 22.912V927.786667a25.6 25.6 0 0 1-36.949333 22.912l-86.954667-43.221334-222.165333-110.293333-33.109334-16.469333A25.6 25.6 0 0 1 85.333333 757.76V293.802667a25.6 25.6 0 0 1 36.992-22.954667L209.194667 314.026667z m-38.229334 391.210666a25.6 25.6 0 0 0 14.208 22.912l170.922667 84.906667a25.6 25.6 0 0 0 36.992-22.912V516.266667a25.6 25.6 0 0 0-14.208-22.954667l-170.922667-84.906667a25.6 25.6 0 0 0-36.992 22.954667v273.834667z m458.496-211.84a25.6 25.6 0 0 0-14.208 22.954667v270.677333c0 19.712 21.333333 32 38.4 22.144l170.965334-99.072a25.6 25.6 0 0 0 12.757333-22.144v-256.512a25.6 25.6 0 0 0-36.992-22.954666l-170.922667 84.906666zM537.088 54.186667l222.165333 91.946666 120.618667 49.962667a25.6 25.6 0 0 1 1.621333 46.592l-116.906666 58.026667-222.122667 110.378666-26.88 13.354667a25.6 25.6 0 0 1-22.784 0l-26.922667-13.354667-222.165333-110.336-116.906667-58.069333a25.6 25.6 0 0 1 1.621334-46.592l120.661333-49.92 222.122667-91.989333 23.168-9.557334a25.6 25.6 0 0 1 19.584 0l23.125333 9.557334zM334.037333 203.093333a25.6 25.6 0 0 0-1.578666 46.634667l160.341333 79.616a25.6 25.6 0 0 0 22.741333 0l160.341334-79.616a25.6 25.6 0 0 0-1.578667-46.592l-160.341333-66.346667a25.6 25.6 0 0 0-19.584 0L334.037333 203.093333z" p-id="4993" fill="#ffffff"></path></svg>

BIN
src/assets/images/ventAI/chatModalBlue/2-7.png


+ 1 - 0
src/assets/images/ventAI/chatModalBlue/2-7.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1786585089912" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6775" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M63.65 512c-0.01 0 0 0 0 0 2.04-148.61 27.84-271.66 103.31-345.05C240.34 91.49 363.39 65.68 512 63.65c148.61 2.04 271.66 27.84 345.05 103.31 75.46 73.38 101.27 196.43 103.3 345.04-2.04 148.61-27.85 271.66-103.31 345.05-73.38 75.46-196.43 101.27-345.04 103.3-148.61-2.04-271.66-27.84-345.05-103.31C91.49 783.66 65.68 660.61 63.65 512z" p-id="6776" fill="#ffffff"></path><path d="M452.66 353.01v317.98c0 24.52-20.06 44.59-44.59 44.59-24.52 0-44.59-20.06-44.59-44.59V353.01c0-24.52 20.06-44.59 44.59-44.59 24.53 0 44.59 20.07 44.59 44.59zM660.51 353.01v317.98c0 24.52-20.06 44.59-44.59 44.59-24.52 0-44.59-20.06-44.59-44.59V353.01c0-24.52 20.06-44.59 44.59-44.59 24.52 0 44.59 20.07 44.59 44.59z" fill="#ffffff" p-id="6777"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/2-9.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785993167154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5233" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M109.714286 900.608h804.571428c25.234286 0 45.714286 18.358857 45.714286 41.106286 0 22.674286-20.48 41.179429-45.714286 41.179428h-804.571428c-25.234286 0-45.714286-18.432-45.714286-41.179428 0-22.674286 20.48-41.179429 45.714286-41.179429z m433.956571-645.632l-337.92 337.92v142.189714h142.189714l337.92-337.92-142.189714-142.189714z m58.148572-58.221714l142.262857 142.262857 58.148571-58.221714-142.189714-142.189715-58.221714 58.148572zM136.850286 545.426286L627.565714 54.564571a45.714286 45.714286 0 0 1 64.731429 0l193.901714 193.901715a45.714286 45.714286 0 0 1 0 64.658285l-490.788571 490.861715a45.714286 45.714286 0 0 1-32.402286 13.385143H169.106286a45.714286 45.714286 0 0 1-45.641143-45.714286V577.828571c0-12.141714 4.754286-23.771429 13.385143-32.329142z" fill="#ffffff" p-id="5234"></path></svg>

+ 6 - 0
src/assets/images/ventAI/chatModalBlue/4-7.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="31.185" height="34.438" viewBox="0 0 31.185 34.438">
+  <g id="待审批" transform="translate(-256.546 -576.183)">
+    <path id="路径_58195" data-name="路径 58195" d="M61.9,14.775a4.894,4.894,0,0,1,4.883,4.594l.008.3v6.291a1.334,1.334,0,0,1-2.655.178l-.013-.178V19.666a2.223,2.223,0,0,0-1.995-2.21l-.229-.013H44.242a2.223,2.223,0,0,0-2.21,2l-.013.229V44.322a2.222,2.222,0,0,0,2,2.21l.229.013h5.729a1.334,1.334,0,0,1,.183,2.655l-.183.013H44.242a4.892,4.892,0,0,1-4.881-4.594l-.01-.3V19.666a4.892,4.892,0,0,1,4.594-4.881l.3-.01H61.9Zm-9.373,13.5a1.334,1.334,0,0,1,.18,2.655l-.178.013H46.877a1.334,1.334,0,0,1-.183-2.655l.183-.013h5.648Zm6.194-5.559a1.334,1.334,0,0,1,.178,2.655l-.178.013H46.877a1.334,1.334,0,0,1-.183-2.655l.183-.013H58.722Z" transform="translate(217.195 561.408)" fill="#fff"/>
+    <path id="路径_58196" data-name="路径 58196" d="M143.718,138.188h-4.075l-.688-2.063a4.366,4.366,0,1,0-4.446,0l-.688,2.063h-4.075A1.626,1.626,0,0,0,128,139.643v4.366a1.626,1.626,0,0,0,1.746,1.455h13.972a1.626,1.626,0,0,0,1.746-1.455v-4.366A1.626,1.626,0,0,0,143.718,138.188Z" transform="translate(142.267 465.156)" fill="#fff"/>
+  </g>
+</svg>

+ 2 - 1
src/design/themify/deepblue.less

@@ -87,7 +87,6 @@ html[data-theme='deepblue'] {
   --img-chat-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/2-4.svg');
   --img-chat-copy-success-icon: url('/src/assets/images/ventAI/chatModalBlue/2-5.svg');
   --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.svg');
-  --img-chat-close-bg: url('/src/assets/images/ventAI/chatModalBlue/2-7.png');
   --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
   --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/2-13.png');
   --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
@@ -104,7 +103,9 @@ html[data-theme='deepblue'] {
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');
   --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-skill-item-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
   --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
   --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
   --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');

+ 4 - 0
src/design/themify/default.less

@@ -94,6 +94,7 @@ html {
   --img-chat-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/2-4.svg');
   --img-chat-copy-success-icon: url('/src/assets/images/ventAI/chatModalBlue/2-5.svg');
   --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.svg');
+  --img-chat-stop-icon: url('/src/assets/images/ventAI/chatModalBlue/2-7.svg');
   --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
   --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/1-7.svg');
   --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
@@ -107,10 +108,13 @@ html {
   --img-chat-close-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-4.svg');
   --img-chat-download-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-5.svg');
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
+  --img-chat-session-approve-icon: url('/src/assets/images/ventAI/chatModalBlue/4-7.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');
   --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-skill-item-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
   --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
   --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
   --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');

+ 12 - 1
src/qiankun/state.ts

@@ -28,7 +28,17 @@ export function getProps() {
  * @param state 主应用穿的公共数据
  */
 export function initGlState(
-  info: any = { token: '', userInfo: {}, isMounted: false, locationObj: null, locationId: '', pageObj: null, widthScale: 1, heightScale: 1 }
+  info: any = {
+    token: '',
+    userInfo: {},
+    isMounted: false,
+    locationObj: null,
+    locationId: '',
+    pageObj: null,
+    widthScale: 1,
+    heightScale: 1,
+    dataPickerHoverEnabled: true,
+  }
 ) {
   if (actions) return;
   // 初始化state
@@ -41,6 +51,7 @@ export function initGlState(
     widthScale: 1,
     heightScale: 1,
     url: {},
+    dataPickerHoverEnabled: localStorage.getItem('dataPickerHoverEnabled') !== 'false',
   });
   // 注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
   actions.onGlobalStateChange((newState, prev) => {

+ 11 - 0
src/router/routes/basic.ts

@@ -141,6 +141,17 @@ export const QIANKUN_ROUTE: AppRouteRecordRaw = {
       },
       ver: '1',
     },
+    {
+      path: '/micro-vent-2dModal/ltauto-view',
+      name: 'micro-vent-2dModal-ltauto-view',
+      component: () => import('/@/components/vent/micro/ventModal2D.vue'),
+      meta: {
+        title: '通风立体示意图',
+        hideBreadcrumb: true,
+        hideMenu: true,
+      },
+      ver: '1',
+    },
     {
       path: '/micro-need-air/:path(.*)*',
       name: 'micro-need-air',

+ 163 - 168
src/views/vent/deviceManager/comment/DeviceModal.vue

@@ -31,14 +31,8 @@
           @delete="deletePointById"
         />
       </a-tab-pane>
-      <a-tab-pane v-if="deviceType === 'gate'" key="15" tab="控制点位关联">
-        <PointTable
-          :columns="pointColumns"
-          :pointType="deviceData.strtype"
-          :deviceId="deviceData.id"
-          @save="savePointData"
-          @delete="deletePointById"
-        />
+      <a-tab-pane key="15" tab="控制点位关联">
+        <CtrlTable :deviceId="deviceData.id" :deviceType="deviceData.strtype" />
       </a-tab-pane>
       <a-tab-pane key="8" tab="虚拟点表配置">
         <PointTable
@@ -107,176 +101,177 @@
   </BasicModal>
 </template>
 <script lang="ts" setup>
-import { computed, unref, inject, reactive, ref, watch } from 'vue';
-import { BasicModal, useModalInner } from '/@/components/Modal';
-import EditRowTable from '../../comment/EditRowTable.vue';
-import PointTable from './pointTabel/PointTable.vue';
-import WarningTable from './warningTabel/index.vue';
-import ManagerWarningTable from './warningTabel/index1.vue';
-import ManagerWarningDeviceTable from './warningTabel/index2.vue';
-import BackWindDeviceTable from './warningTabel/index3.vue';
-import AccidentPlanTable from './warningTabel/index4.vue';
-import LEDPlaylistTable from './warningTabel/index5.vue';
-import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
-import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
-import DeviceReportInfo from './DeviceReportInfo.vue';
-import FaultConfigTable from './faultConfigTable/index.vue';
-// import editWarnTable from './editWarnTable/index.vue'
-import FanLineModal from './FanLineModal .vue';
-import FormModal from './FormModal.vue';
-import { cloneDeep } from 'lodash-es';
-import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
-import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
-import { columns as cameraColumns } from './cameraTabel/camera.data';
+  import { computed, unref, inject, reactive, ref, watch } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import EditRowTable from '../../comment/EditRowTable.vue';
+  import PointTable from './pointTabel/PointTable.vue';
+  import WarningTable from './warningTabel/index.vue';
+  import ManagerWarningTable from './warningTabel/index1.vue';
+  import ManagerWarningDeviceTable from './warningTabel/index2.vue';
+  import BackWindDeviceTable from './warningTabel/index3.vue';
+  import AccidentPlanTable from './warningTabel/index4.vue';
+  import LEDPlaylistTable from './warningTabel/index5.vue';
+  import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
+  import CtrlTable from './warningTabel/ctrlTable.vue';
+  import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
+  import DeviceReportInfo from './DeviceReportInfo.vue';
+  import FaultConfigTable from './faultConfigTable/index.vue';
+  // import editWarnTable from './editWarnTable/index.vue'
+  import FanLineModal from './FanLineModal.vue';
+  import FormModal from './FormModal.vue';
+  import { cloneDeep } from 'lodash-es';
+  import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
+  import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
+  import { columns as cameraColumns } from './cameraTabel/camera.data';
 
-import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
+  import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
 
-const props = defineProps({
-  showTab: { type: Boolean, required: true },
-  // deviceType: { type: String },
-});
-// 声明Emits
-const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
-const isUpdate = inject('isUpdate');
-const deviceData = inject('formData') as any;
-const deviceType = inject('deviceType') as any;
-const record = reactive({});
-const activeKey = ref('1');
-const linkColumns = ref<any[]>([]);
+  const props = defineProps({
+    showTab: { type: Boolean, required: true },
+    // deviceType: { type: String },
+  });
+  // 声明Emits
+  const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
+  const isUpdate = inject('isUpdate');
+  const deviceData = inject('formData') as any;
+  const deviceType = inject('deviceType') as any;
+  const record = reactive({});
+  const activeKey = ref('1');
+  const linkColumns = ref<any[]>([]);
 
-//表单赋值
-const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
-  //重置表单
-  setModalProps({ confirmLoading: false });
-  Object.assign(deviceData, data.record);
-  // 判断是否是关键阻力路线
-});
+  //表单赋值
+  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+    //重置表单
+    setModalProps({ confirmLoading: false });
+    Object.assign(deviceData, data.record);
+    // 判断是否是关键阻力路线
+  });
 
-//设置标题
-const title = computed(() => {
-  if (!unref(isUpdate)) {
-    if (deviceData.strname || deviceData.systemname) {
-      return `新增(${deviceData.strname || deviceData.systemname})`;
-    }
-    return `新增`;
-  } else {
-    if (deviceData['strtype'] == 'sys_majorpath') {
-      linkColumns.value = [
-        ...workFaceColumns,
-        ...[
-          {
-            title: '是否在关键通风路线上',
-            width: 100,
-            dataIndex: 'pathflag',
-            edit: true,
-            editComponent: 'Switch',
-            editValueMap: (value) => {
-              return value ? '是' : '否';
+  //设置标题
+  const title = computed(() => {
+    if (!unref(isUpdate)) {
+      if (deviceData.strname || deviceData.systemname) {
+        return `新增(${deviceData.strname || deviceData.systemname})`;
+      }
+      return `新增`;
+    } else {
+      if (deviceData['strtype'] == 'sys_majorpath') {
+        linkColumns.value = [
+          ...workFaceColumns,
+          ...[
+            {
+              title: '是否在关键通风路线上',
+              width: 100,
+              dataIndex: 'pathflag',
+              edit: true,
+              editComponent: 'Switch',
+              editValueMap: (value) => {
+                return value ? '是' : '否';
+              },
             },
-          },
-          {
-            title: '传感器类型',
-            width: 100,
-            dataIndex: 'sensorType',
-            edit: true,
-            editComponent: 'Select',
-            editComponentProps: {
-              options: [
-                {
-                  label: '多参',
-                  value: '1',
-                },
-                {
-                  label: '测风',
-                  value: '2',
-                },
-              ],
+            {
+              title: '传感器类型',
+              width: 100,
+              dataIndex: 'sensorType',
+              edit: true,
+              editComponent: 'Select',
+              editComponentProps: {
+                options: [
+                  {
+                    label: '多参',
+                    value: '1',
+                  },
+                  {
+                    label: '测风',
+                    value: '2',
+                  },
+                ],
+              },
             },
-          },
-          {
-            title: '风向',
-            width: 100,
-            dataIndex: 'winddir',
-            edit: true,
-            editComponent: 'Select',
-            editComponentProps: {
-              options: [
-                {
-                  label: '进风',
-                  value: '1',
-                },
-                {
-                  label: '用风',
-                  value: '2',
-                },
-                {
-                  label: '回风',
-                  value: '3',
-                },
-              ],
+            {
+              title: '风向',
+              width: 100,
+              dataIndex: 'winddir',
+              edit: true,
+              editComponent: 'Select',
+              editComponentProps: {
+                options: [
+                  {
+                    label: '进风',
+                    value: '1',
+                  },
+                  {
+                    label: '用风',
+                    value: '2',
+                  },
+                  {
+                    label: '回风',
+                    value: '3',
+                  },
+                ],
+              },
             },
-          },
-          {
-            title: '是否总风量',
-            width: 100,
-            dataIndex: 'windflag',
-            edit: true,
-            editComponent: 'Switch',
-            editValueMap: (value) => {
-              return value ? '是' : '否';
+            {
+              title: '是否总风量',
+              width: 100,
+              dataIndex: 'windflag',
+              edit: true,
+              editComponent: 'Switch',
+              editValueMap: (value) => {
+                return value ? '是' : '否';
+              },
             },
-          },
-          {
-            title: '路线名称',
-            width: 100,
-            dataIndex: 'des',
-            edit: true,
-            editComponent: 'Input',
-          },
-          {
-            title: ' 阻力值',
-            width: 100,
-            dataIndex: 'testDrag',
-            edit: true,
-            editComponent: 'InputNumber',
-          },
-        ],
-      ];
-    } else {
-      linkColumns.value = [...workFaceColumns];
-    }
-    if (deviceData.strname || deviceData.systemname) {
-      return `编辑(${deviceData.strname || deviceData.systemname})`;
+            {
+              title: '路线名称',
+              width: 100,
+              dataIndex: 'des',
+              edit: true,
+              editComponent: 'Input',
+            },
+            {
+              title: ' 阻力值',
+              width: 100,
+              dataIndex: 'testDrag',
+              edit: true,
+              editComponent: 'InputNumber',
+            },
+          ],
+        ];
+      } else {
+        linkColumns.value = [...workFaceColumns];
+      }
+      if (deviceData.strname || deviceData.systemname) {
+        return `编辑(${deviceData.strname || deviceData.systemname})`;
+      }
+      return `编辑`;
     }
-    return `编辑`;
-  }
-});
+  });
 
-const closeModalFn = () => {
-  activeKey.value = '1';
-  closeModal();
-  emit('closeModal');
-};
+  const closeModalFn = () => {
+    activeKey.value = '1';
+    closeModal();
+    emit('closeModal');
+  };
 
-const savePointData = (data) => {
-  const record = cloneDeep(data.editValueRefs);
-  pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
-};
-const saveCameraData = (data: any, reload: Function) => {
-  cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
-};
-const deletePointById = (id, reload) => {
-  pointDeleteById({ id: id }, reload);
-};
-const deleteCameraById = (id, reload) => {
-  cameraDeleteById({ id: id }, reload);
-};
+  const savePointData = (data) => {
+    const record = cloneDeep(data.editValueRefs);
+    pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
+  };
+  const saveCameraData = (data: any, reload: Function) => {
+    cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
+  };
+  const deletePointById = (id, reload) => {
+    pointDeleteById({ id: id }, reload);
+  };
+  const deleteCameraById = (id, reload) => {
+    cameraDeleteById({ id: id }, reload);
+  };
 </script>
 <style scoped lang="less">
-::v-deep .suffix {
-  height: 32px;
-  line-height: 32px;
-  margin-left: 5px;
-  color: #fff;
-}
-</style>
+  ::v-deep .suffix {
+    height: 32px;
+    line-height: 32px;
+    margin-left: 5px;
+    color: #fff;
+  }
+</style>

+ 156 - 156
src/views/vent/deviceManager/comment/DeviceModalLeather.vue

@@ -98,173 +98,173 @@
   </BasicModal>
 </template>
 <script lang="ts" setup>
-import { computed, unref, inject, reactive, ref, watch } from 'vue';
-import { BasicModal, useModalInner } from '/@/components/Modal';
-import EditRowTable from '../../comment/EditRowTable.vue';
-import PointTable from './pointTabel/PointTable.vue';
-import WarningTable from './warningTabel/index.vue';
-import ManagerWarningTable from './warningTabel/index1.vue';
-import ManagerWarningDeviceTable from './warningTabel/indexLeather.vue';
-import BackWindDeviceTable from './warningTabel/index3.vue';
-import AccidentPlanTable from './warningTabel/index4.vue';
-import LEDPlaylistTable from './warningTabel/index5.vue';
-import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
-import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
-import DeviceReportInfo from './DeviceReportInfo.vue';
-import FaultConfigTable from './faultConfigTable/index.vue';
-// import editWarnTable from './editWarnTable/index.vue'
-import FanLineModal from './FanLineModal .vue';
-import FormModal from './FormModal.vue';
-import { cloneDeep } from 'lodash-es';
-import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
-import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
-import { columns as cameraColumns } from './cameraTabel/camera.data';
+  import { computed, unref, inject, reactive, ref, watch } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import EditRowTable from '../../comment/EditRowTable.vue';
+  import PointTable from './pointTabel/PointTable.vue';
+  import WarningTable from './warningTabel/index.vue';
+  import ManagerWarningTable from './warningTabel/index1.vue';
+  import ManagerWarningDeviceTable from './warningTabel/indexLeather.vue';
+  import BackWindDeviceTable from './warningTabel/index3.vue';
+  import AccidentPlanTable from './warningTabel/index4.vue';
+  import LEDPlaylistTable from './warningTabel/index5.vue';
+  import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
+  import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
+  import DeviceReportInfo from './DeviceReportInfo.vue';
+  import FaultConfigTable from './faultConfigTable/index.vue';
+  // import editWarnTable from './editWarnTable/index.vue'
+  import FanLineModal from './FanLineModal.vue';
+  import FormModal from './FormModal.vue';
+  import { cloneDeep } from 'lodash-es';
+  import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
+  import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
+  import { columns as cameraColumns } from './cameraTabel/camera.data';
 
-import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
+  import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
 
-const props = defineProps({
-  showTab: { type: Boolean, required: true },
-  // deviceType: { type: String },
-});
-// 声明Emits
-const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
-const isUpdate = inject('isUpdate');
-const deviceData = inject('formData') as any;
-const deviceType = inject('deviceType') as any;
-const record = reactive({});
-const activeKey = ref('1');
-const linkColumns = ref<any[]>([]);
+  const props = defineProps({
+    showTab: { type: Boolean, required: true },
+    // deviceType: { type: String },
+  });
+  // 声明Emits
+  const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
+  const isUpdate = inject('isUpdate');
+  const deviceData = inject('formData') as any;
+  const deviceType = inject('deviceType') as any;
+  const record = reactive({});
+  const activeKey = ref('1');
+  const linkColumns = ref<any[]>([]);
 
-const [registerModalLeather, { setModalProps, closeModal: closeModalLeather }] = useModalInner(async (data) => {
-  setModalProps({ confirmLoading: false });
-  Object.assign(deviceData, data.record);
-});
+  const [registerModalLeather, { setModalProps, closeModal: closeModalLeather }] = useModalInner(async (data) => {
+    setModalProps({ confirmLoading: false });
+    Object.assign(deviceData, data.record);
+  });
 
-//设置标题
-const title = computed(() => {
-  if (!unref(isUpdate)) {
-    if (deviceData.strname || deviceData.systemname) {
-      return `新增(${deviceData.strname || deviceData.systemname})`;
-    }
-    return `新增`;
-  } else {
-    if (deviceData['strtype'] == 'sys_majorpath') {
-      linkColumns.value = [
-        ...workFaceColumns,
-        ...[
-          {
-            title: '是否在关键通风路线上',
-            width: 100,
-            dataIndex: 'pathflag',
-            edit: true,
-            editComponent: 'Switch',
-            editValueMap: (value) => {
-              return value ? '是' : '否';
+  //设置标题
+  const title = computed(() => {
+    if (!unref(isUpdate)) {
+      if (deviceData.strname || deviceData.systemname) {
+        return `新增(${deviceData.strname || deviceData.systemname})`;
+      }
+      return `新增`;
+    } else {
+      if (deviceData['strtype'] == 'sys_majorpath') {
+        linkColumns.value = [
+          ...workFaceColumns,
+          ...[
+            {
+              title: '是否在关键通风路线上',
+              width: 100,
+              dataIndex: 'pathflag',
+              edit: true,
+              editComponent: 'Switch',
+              editValueMap: (value) => {
+                return value ? '是' : '否';
+              },
             },
-          },
-          {
-            title: '传感器类型',
-            width: 100,
-            dataIndex: 'sensorType',
-            edit: true,
-            editComponent: 'Select',
-            editComponentProps: {
-              options: [
-                {
-                  label: '多参',
-                  value: '1',
-                },
-                {
-                  label: '测风',
-                  value: '2',
-                },
-              ],
+            {
+              title: '传感器类型',
+              width: 100,
+              dataIndex: 'sensorType',
+              edit: true,
+              editComponent: 'Select',
+              editComponentProps: {
+                options: [
+                  {
+                    label: '多参',
+                    value: '1',
+                  },
+                  {
+                    label: '测风',
+                    value: '2',
+                  },
+                ],
+              },
             },
-          },
-          {
-            title: '风向',
-            width: 100,
-            dataIndex: 'winddir',
-            edit: true,
-            editComponent: 'Select',
-            editComponentProps: {
-              options: [
-                {
-                  label: '进风',
-                  value: '1',
-                },
-                {
-                  label: '用风',
-                  value: '2',
-                },
-                {
-                  label: '回风',
-                  value: '3',
-                },
-              ],
+            {
+              title: '风向',
+              width: 100,
+              dataIndex: 'winddir',
+              edit: true,
+              editComponent: 'Select',
+              editComponentProps: {
+                options: [
+                  {
+                    label: '进风',
+                    value: '1',
+                  },
+                  {
+                    label: '用风',
+                    value: '2',
+                  },
+                  {
+                    label: '回风',
+                    value: '3',
+                  },
+                ],
+              },
             },
-          },
-          {
-            title: '是否总风量',
-            width: 100,
-            dataIndex: 'windflag',
-            edit: true,
-            editComponent: 'Switch',
-            editValueMap: (value) => {
-              return value ? '是' : '否';
+            {
+              title: '是否总风量',
+              width: 100,
+              dataIndex: 'windflag',
+              edit: true,
+              editComponent: 'Switch',
+              editValueMap: (value) => {
+                return value ? '是' : '否';
+              },
             },
-          },
-          {
-            title: '路线名称',
-            width: 100,
-            dataIndex: 'des',
-            edit: true,
-            editComponent: 'Input',
-          },
-          {
-            title: ' 阻力值',
-            width: 100,
-            dataIndex: 'testDrag',
-            edit: true,
-            editComponent: 'InputNumber',
-          },
-        ],
-      ];
-    } else {
-      linkColumns.value = [...workFaceColumns];
-    }
-    if (deviceData.strname || deviceData.systemname) {
-      return `编辑(${deviceData.strname || deviceData.systemname})`;
+            {
+              title: '路线名称',
+              width: 100,
+              dataIndex: 'des',
+              edit: true,
+              editComponent: 'Input',
+            },
+            {
+              title: ' 阻力值',
+              width: 100,
+              dataIndex: 'testDrag',
+              edit: true,
+              editComponent: 'InputNumber',
+            },
+          ],
+        ];
+      } else {
+        linkColumns.value = [...workFaceColumns];
+      }
+      if (deviceData.strname || deviceData.systemname) {
+        return `编辑(${deviceData.strname || deviceData.systemname})`;
+      }
+      return `编辑`;
     }
-    return `编辑`;
-  }
-});
+  });
 
-const closeModalFn = () => {
-  activeKey.value = '1';
-  closeModalLeather();
-  emit('closeModal');
-};
+  const closeModalFn = () => {
+    activeKey.value = '1';
+    closeModalLeather();
+    emit('closeModal');
+  };
 
-const savePointData = (data) => {
-  const record = cloneDeep(data.editValueRefs);
-  pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
-};
-const saveCameraData = (data: any, reload: Function) => {
-  cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
-};
-const deletePointById = (id, reload) => {
-  pointDeleteById({ id: id }, reload);
-};
-const deleteCameraById = (id, reload) => {
-  cameraDeleteById({ id: id }, reload);
-};
+  const savePointData = (data) => {
+    const record = cloneDeep(data.editValueRefs);
+    pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
+  };
+  const saveCameraData = (data: any, reload: Function) => {
+    cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
+  };
+  const deletePointById = (id, reload) => {
+    pointDeleteById({ id: id }, reload);
+  };
+  const deleteCameraById = (id, reload) => {
+    cameraDeleteById({ id: id }, reload);
+  };
 </script>
 <style scoped lang="less">
-::v-deep .suffix {
-  height: 32px;
-  line-height: 32px;
-  margin-left: 5px;
-  color: #fff;
-}
+  ::v-deep .suffix {
+    height: 32px;
+    line-height: 32px;
+    margin-left: 5px;
+    color: #fff;
+  }
 </style>

+ 0 - 119
src/views/vent/deviceManager/comment/FanLineModal .vue

@@ -1,119 +0,0 @@
-<template>
-  <div> </div>
-  <div class="vent-form">
-    <BasicForm @register="registerForm" />
-    <div class="j-box-bottom-button offset-20" style="margin-top: 30px">
-      <div class="j-box-bottom-button-float">
-        <a-button preIcon="ant-design:sync-outlined" @click="onReset">重置</a-button>
-        <a-button type="primary" preIcon="ant-design:save-filled" @click="handleSubmit">保存</a-button>
-      </div>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { inject, nextTick, watch, unref } from 'vue';
-  import { BasicForm, useForm } from '/@/components/Form/index';
-  // 声明Emits
-  const emit = defineEmits(['saveOrUpdate']);
-  const testData = inject('formData') as any;
-  //表单配置
-  const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
-    schemas: [
-      {
-        label: 'R',
-        field: 'fr',
-        component: 'Input',
-        colProps: { span: 6 },
-      },
-      {
-        label: '角度',
-        field: 'strangle',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '频率',
-        field: 'fhz',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '最大风量',
-        field: 'fqmax',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '最小风量',
-        field: 'fqmin',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '系数a',
-        field: 'ha',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '系数b',
-        field: 'hb',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-      {
-        label: '系数c',
-        field: 'hc',
-        component: 'InputNumber',
-        colProps: { span: 6 },
-      },
-    ],
-    showActionButtonGroup: false,
-    layout: 'horizontal',
-    labelWidth: 80,
-  });
-
-  watch(
-    testData,
-    (newV) => {
-      nextTick(() => {
-        setFieldsValue({ ...newV });
-      });
-    },
-    { immediate: true }
-  );
-
-  // 重置表单
-  async function onReset() {
-    await resetFields();
-    await setFieldsValue({ ...testData });
-  }
-  //表单提交事件
-  async function handleSubmit(v) {
-    try {
-      let values = await validate();
-      emit('saveOrUpdate', values);
-    } finally {
-      // setModalProps({ confirmLoading: false });
-    }
-  }
-</script>
-<style lang="less" scoped>
-  @ventSpace: zxm;
-  .j-box-bottom-button-float {
-    border: none !important;
-    padding-bottom: 30px;
-    left: 0px !important;
-    right: 0px !important;
-    bottom: 0px !important;
-  }
-  .vent-form {
-    // width: 100%;
-    max-height: 700px;
-    overflow-y: auto;
-
-    .@{ventSpace}-select-selection-item {
-      color: rgba(255, 255, 255, 1) !important;
-    }
-  }
-</style>

+ 195 - 195
src/views/vent/deviceManager/comment/pointTabel/PointTable.vue

@@ -43,219 +43,219 @@
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent, ref, nextTick, inject, onMounted } from 'vue';
-  import { BasicTable, useTable, TableAction, BasicColumn, ActionItem, EditRecordRow } from '/@/components/Table';
-  import { Select } from 'ant-design-vue';
-  import { ApiTreeSelect } from '/@/components/Form';
-  import { deviceId, getDeviceId, deviceList, list, pointEdit } from './point.api';
-  // import { nextTick } from 'process';
+import { defineComponent, ref, nextTick, inject, onMounted } from 'vue';
+import { BasicTable, useTable, TableAction, BasicColumn, ActionItem, EditRecordRow } from '/@/components/Table';
+import { Select } from 'ant-design-vue';
+import { ApiTreeSelect } from '/@/components/Form';
+import { deviceId, getDeviceId, deviceList, list, pointEdit } from './point.api';
+// import { nextTick } from 'process';
 
-  export default defineComponent({
-    components: { BasicTable, TableAction, Select, ApiTreeSelect },
-    props: {
-      columns: {
-        type: Array,
-        requried: true,
-      },
-      deviceId: {
-        type: String || Number,
-        requried: true,
-      },
-      valuetype: {
-        type: Number,
-        requried: true,
-      },
-      pointType: {
-        type: String,
-        requried: true,
-      },
-      list: {
-        type: Function,
-        requried: true,
-      },
-      isAdd: {
-        type: Boolean,
-      },
+export default defineComponent({
+  components: { BasicTable, TableAction, Select, ApiTreeSelect },
+  props: {
+    columns: {
+      type: Array,
+      requried: true,
     },
-    emits: ['save', 'delete'],
-    setup(props, { emit }) {
-      const deviceType = inject('deviceType');
-      const options = ref([]);
-      const monitorParamsOptions = ref([]);
-      const currentEditKeyRef = ref('');
-      const dataSource = ref<any>([]);
-      const [registerTable, { insertTableDataRecord, reload }] = useTable({
-        title: '',
-        columns: props.columns as BasicColumn[],
-        showIndexColumn: false,
-        showTableSetting: false,
-        tableSetting: { fullScreen: true },
-        actionColumn: {
-          width: 160,
-          title: '操作',
-          dataIndex: 'action',
-          slots: { customRender: 'action' },
-        },
+    deviceId: {
+      type: String || Number,
+      requried: true,
+    },
+    valuetype: {
+      type: Number,
+      requried: true,
+    },
+    pointType: {
+      type: String,
+      requried: true,
+    },
+    list: {
+      type: Function,
+      requried: true,
+    },
+    isAdd: {
+      type: Boolean,
+    },
+  },
+  emits: ['save', 'delete'],
+  setup(props, { emit }) {
+    const deviceType = inject('deviceType');
+    const options = ref([]);
+    const monitorParamsOptions = ref([]);
+    const currentEditKeyRef = ref('');
+    const dataSource = ref<any>([]);
+    const [registerTable, { insertTableDataRecord, reload }] = useTable({
+      title: '',
+      columns: props.columns as BasicColumn[],
+      showIndexColumn: false,
+      showTableSetting: false,
+      tableSetting: { fullScreen: true },
+      actionColumn: {
+        width: 160,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+      },
+    });
+    const filterOption = (input: string, option: any) => {
+      return option.deviceName.toLowerCase().indexOf(input.toLowerCase()) >= 0;
+    };
+    function addRow() {
+      const record = {} as EditRecordRow;
+      insertTableDataRecord(record);
+      nextTick(() => {
+        handleEdit(record);
       });
-      const filterOption = (input: string, option: any) => {
-        return option.deviceName.toLowerCase().indexOf(input.toLowerCase()) >= 0;
-      };
-      function addRow() {
-        const record = {} as EditRecordRow;
-        insertTableDataRecord(record);
-        nextTick(() => {
-          handleEdit(record);
-        });
-      }
-      function handleEdit(record: EditRecordRow) {
-        currentEditKeyRef.value = record.key;
-        record.onEdit?.(true);
-        handleChange(record['link_id']);
-        handleChangeDeviceType(record['link_devicetype']);
-      }
-
-      function handleCancel(record: EditRecordRow) {
-        currentEditKeyRef.value = '';
-        record.onEdit?.(false, false);
-      }
+    }
+    function handleEdit(record: EditRecordRow) {
+      currentEditKeyRef.value = record.key;
+      record.onEdit?.(true);
+      handleChange(record['link_id']);
+      handleChangeDeviceType(record['link_devicetype']);
+    }
 
-      function handleDelete(record: EditRecordRow) {
-        emit('delete', record.id, reload);
-      }
-      function getOptions(value) {
-        console.log(value);
-      }
+    function handleCancel(record: EditRecordRow) {
+      currentEditKeyRef.value = '';
+      record.onEdit?.(false, false);
+    }
 
-      function handleChange(id, index?) {
-        if (!id) return;
-        if (index !== undefined) dataSource['value'][index]['link_id'] = id;
+    function handleDelete(record: EditRecordRow) {
+      emit('delete', record.id, reload);
+    }
+    function getOptions(value) {
+      console.log(value);
+    }
 
-        deviceId({ id: id }).then((res) => {
-          monitorParamsOptions.value = res;
-          console.log(res);
-        });
-      }
-      function handleSearch(val: string) {
-        return options.value.map((item) => {
-          return (item['deviceName'] as string).includes(val);
-        });
-      }
+    function handleChange(id, index?) {
+      if (!id) return;
+      if (index !== undefined) dataSource['value'][index]['link_id'] = id;
 
-      function handleChangeDeviceType(e, index?) {
-        if (!e) return;
-        if (index !== undefined) dataSource['value'][index]['link_devicetype'] = e;
-        getDeviceId({ devicetype: e }).then((res) => {
-          options.value = res;
-        });
-      }
+      deviceId({ id: id }).then((res) => {
+        monitorParamsOptions.value = res;
+        console.log(res);
+      });
+    }
+    function handleSearch(val: string) {
+      return options.value.map((item) => {
+        return (item['deviceName'] as string).includes(val);
+      });
+    }
 
-      function handleChangeLinkCode(e, index?) {
-        if (!e) return;
-        if (index !== undefined) dataSource['value'][index]['link_code'] = e;
-      }
+    function handleChangeDeviceType(e, index?) {
+      if (!e) return;
+      if (index !== undefined) dataSource['value'][index]['link_devicetype'] = e;
+      getDeviceId({ devicetype: e }).then((res) => {
+        options.value = res;
+      });
+    }
 
-      async function handleSave(record: EditRecordRow) {
-        dataSource.value.filter((item) => {
-          if (record.id && record.id === item.id) {
-            console.log(111);
-            Object.assign(item, record.editValueRefs);
-            item['test'] = '1212';
-            console.log(222, item, record.editValueRefs);
-          }
-        });
-        // await edit(dataSource.value);
-        await pointEdit({ deviceid: props.deviceId, linklist: dataSource.value });
-        currentEditKeyRef.value = '';
-        record.onEdit?.(false, false);
-        await getDataSource(props.pointType);
-      }
+    function handleChangeLinkCode(e, index?) {
+      if (!e) return;
+      if (index !== undefined) dataSource['value'][index]['link_code'] = e;
+    }
 
-      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
-        if (!record.editable) {
-          if (props.isAdd) {
-            return [
-              {
-                label: '编辑',
-                disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
-                onClick: handleEdit.bind(null, record),
-              },
-              {
-                label: '删除',
-                disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
-                onClick: handleDelete.bind(null, record),
-              },
-            ];
-          } else {
-            return [
-              {
-                label: '编辑',
-                disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
-                onClick: handleEdit.bind(null, record),
-              },
-            ];
-          }
+    async function handleSave(record: EditRecordRow) {
+      dataSource.value.filter((item) => {
+        if (record.id && record.id === item.id) {
+          console.log(111);
+          Object.assign(item, record.editValueRefs);
+          item['test'] = '1212';
+          console.log(222, item, record.editValueRefs);
         }
-        return [
-          {
-            label: '保存',
-            onClick: handleSave.bind(null, record, column),
-          },
-          {
-            label: '取消',
-            popConfirm: {
-              title: '是否取消编辑',
-              confirm: handleCancel.bind(null, record, column),
+      });
+      // await edit(dataSource.value);
+      await pointEdit({ deviceid: props.deviceId, linklist: dataSource.value });
+      currentEditKeyRef.value = '';
+      record.onEdit?.(false, false);
+      await getDataSource(props.pointType);
+    }
+
+    function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
+      if (!record.editable) {
+        if (props.isAdd) {
+          return [
+            {
+              label: '编辑',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleEdit.bind(null, record),
             },
-          },
-        ];
+            {
+              label: '删除',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleDelete.bind(null, record),
+            },
+          ];
+        } else {
+          return [
+            {
+              label: '编辑',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleEdit.bind(null, record),
+            },
+          ];
+        }
       }
+      return [
+        {
+          label: '保存',
+          onClick: handleSave.bind(null, record, column),
+        },
+        {
+          label: '取消',
+          popConfirm: {
+            title: '是否取消编辑',
+            confirm: handleCancel.bind(null, record, column),
+          },
+        },
+      ];
+    }
 
-      function onEditChange({ column, value, record }) {
-        // 本例
-        if (column.dataIndex === 'devicetype') {
-          // record.editValueRefs.name4.value = `${value}`;
-        }
-        // console.log(column, value, record);
+    function onEditChange({ column, value, record }) {
+      // 本例
+      if (column.dataIndex === 'devicetype') {
+        // record.editValueRefs.name4.value = `${value}`;
       }
+      // console.log(column, value, record);
+    }
 
-      // watch(() => props.pointType, async(pointType) => {
-      //   await getDataSource(pointType)
-      // })
+    // watch(() => props.pointType, async(pointType) => {
+    //   await getDataSource(pointType)
+    // })
 
-      async function getDataSource(pointType) {
-        const result = await list({ devicetype: pointType, valuetype: props.valuetype, deviceid: props.deviceId });
-        dataSource.value = result.records;
-      }
-      onMounted(async () => {
-        await getDataSource(props.pointType);
-      });
+    async function getDataSource(pointType) {
+      const result = await list({ devicetype: pointType, valuetype: props.valuetype, deviceid: props.deviceId });
+      dataSource.value = result.records;
+    }
+    onMounted(async () => {
+      await getDataSource(props.pointType);
+    });
 
-      return {
-        registerTable,
-        handleEdit,
-        createActions,
-        onEditChange,
-        addRow,
-        handleChange,
-        handleSearch,
-        handleChangeDeviceType,
-        handleChangeLinkCode,
-        getOptions,
-        options,
-        monitorParamsOptions,
-        deviceList,
-        dataSource,
-        filterOption,
-      };
-    },
-  });
+    return {
+      registerTable,
+      handleEdit,
+      createActions,
+      onEditChange,
+      addRow,
+      handleChange,
+      handleSearch,
+      handleChangeDeviceType,
+      handleChangeLinkCode,
+      getOptions,
+      options,
+      monitorParamsOptions,
+      deviceList,
+      dataSource,
+      filterOption,
+    };
+  },
+});
 </script>
 <style scoped lang="less">
-  @ventSpace: zxm;
-  :deep(.@{ventSpace}-table-body) {
-    height: auto !important;
-  }
-  .@{ventSpace}-dropdown-menu-item {
-    color: black;
-  }
+@ventSpace: zxm;
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+.@{ventSpace}-dropdown-menu-item {
+  color: black;
+}
 </style>

+ 145 - 0
src/views/vent/deviceManager/comment/warningTabel/BaseModalCtrl.vue

@@ -0,0 +1,145 @@
+<template>
+  <BasicModal @register="register" :title="title" :width="800" :min-height="400" v-bind="$attrs" @ok="onSubmit">
+    <BasicForm @register="registerForm">
+      <template #monitor="{ model, field }">
+        <div class="vent-flex-row-between">
+          <Select ref="selectRef" disabled v-model:value="pointData" :options="option" style="width: calc(100% - 65px)" />
+          <a-button class="vent-margin-b-5" type="primary" @click="selectPoint" style="position: absolute; right: 0; top: 1px">选择</a-button>
+        </div>
+      </template>
+    </BasicForm>
+  </BasicModal>
+  <DevicePointTable @register="registerModal" :data-source="devicePointList" :selection-row-keys="pointData" @reload="setPoint" />
+</template>
+<script lang="ts" setup>
+import { onMounted, ref, unref, nextTick } from 'vue';
+import { BasicForm, useForm } from '/@/components/Form/index';
+import { BasicModal, useModalInner, useModal } from '/@/components/Modal';
+import type { FormSchema } from '/@/components/Form/src/types/form';
+import { Select, message } from 'ant-design-vue';
+import { workFacePointList } from './warning.api';
+import DevicePointTable from './DeviceCtrlPointTable.vue';
+
+const props = defineProps({
+  formSchemas: {
+    type: Array as PropType<FormSchema[]>,
+    default: () => [],
+  },
+  deviceId: { type: String },
+  monitorType: {
+    type: String,
+    default: '2',
+  },
+  deviceType: { type: String },
+});
+const emit = defineEmits(['add', 'update', 'register']);
+const option = ref<any[]>([]);
+const devicePointList = ref<any[]>([]);
+const pointData = ref<String[]>([]);
+const selectedPoint = ref<any>(null);
+const selectedDevice = ref<any>(null);
+const title = ref('');
+const isUpdate = ref(false);
+// 注册 form
+const [registerForm, { resetFields, setFieldsValue, validate, getFieldsValue }] = useForm({
+  schemas: props.formSchemas,
+  showActionButtonGroup: false,
+});
+
+// 注册 modal
+const [register, { setModalProps }] = useModalInner(async (data) => {
+  isUpdate.value = unref(data.isUpdate);
+  title.value = unref(data.title);
+  await resetFields();
+  pointData.value = [];
+  selectedPoint.value = null;
+  selectedDevice.value = null;
+  option.value = [];
+  await resetFields();
+  if (data.isUpdate) {
+    await setFieldsValue({ ...data.record });
+    pointData.value = [data.record['linkMonitorCode']];
+
+    await getDevicePointList(data.record['deviceId'] || data.record['linkDeviceId']);
+    const matchItem = devicePointList.value.find((item) => item['id'] == pointData.value[0] || item['valuecode'] == pointData.value[0]);
+    if (matchItem) {
+      setPoint([matchItem]);
+    }
+  } else if (data.record) {
+    await getDevicePointList(data.record['linkDeviceType']);
+    devicePointList.value.forEach((item) => {
+      if (item['id'] == data.record['linkMonitorCode'] || item['valuecode'] == data.record['linkMonitorCode']) {
+        setPoint([item]);
+      }
+    });
+    pointData.value = [data.record['linkMonitorCode']];
+    await setFieldsValue({ linkMonitorCode: '' });
+  }
+});
+
+const [registerModal, { openModal }] = useModal();
+
+async function getDevicePointList(strtype) {
+  try {
+    const result = await workFacePointList({ deviceType: strtype, valueTypes: '1' });
+    devicePointList.value = result;
+  } catch (error) {
+    devicePointList.value = [];
+  }
+}
+
+async function onSubmit() {
+  try {
+    const data = await getFieldsValue();
+    await setFieldsValue({ ...data, linkMonitorCode: pointData.value[0] });
+    const values = await validate();
+    values.deviceId = props.deviceId;
+    if (selectedPoint.value) {
+      values.linkMonitorCode = selectedPoint.value.valuecode;
+      values.linkMonitorId = String(selectedPoint.value.id);
+      values.linkMonitorName = selectedPoint.value.valuename;
+    }
+    console.log(values, 'values');
+    setModalProps({ confirmLoading: true });
+    if (!isUpdate.value) {
+      emit('add', 'add', values);
+    } else {
+      emit('update', 'update', values);
+    }
+  } finally {
+    setModalProps({ confirmLoading: false });
+  }
+}
+
+async function selectPoint() {
+  const values = await getFieldsValue();
+  const strtype = values.linkDeviceType || props.deviceType;
+  if (!strtype) {
+    message.info('请先选择设备类型!');
+    return;
+  }
+  if (!values.linkDeviceId) {
+    message.info('请先选择关联设备!');
+    return;
+  }
+  await getDevicePointList(strtype);
+  openModal();
+}
+
+function setPoint(value) {
+  const data = value[0];
+  selectedPoint.value = data;
+  option.value = [
+    {
+      value: data.id,
+      label: data.valuename,
+    },
+  ];
+  nextTick(() => {
+    pointData.value = [data.id];
+  });
+}
+
+onMounted(async () => {});
+</script>
+<style scoped lang="less"></style>

+ 76 - 0
src/views/vent/deviceManager/comment/warningTabel/DeviceCtrlPointTable.vue

@@ -0,0 +1,76 @@
+<template>
+  <BasicModal
+    ref="DeviceModalTable"
+    v-bind="$attrs"
+    @register="registerModal"
+    title="选择控制点位"
+    width="1000px"
+    :showCancelBtn="false"
+    :showOkBtn="false"
+    :footer="null"
+    :destroy-on-close="true"
+  >
+    <div>
+      <a-button type="primary" @click="saveData"> 确定 </a-button>
+    </div>
+    <a-table
+      v-if="refresh"
+      :row-selection="{ type: 'radio', selectedRowKeys: selectedRowKeys, onSelect: onSelectChange }"
+      :columns="columns"
+      :dataSource="dataSource"
+      :row-key="(record) => record.id"
+      size="small"
+    />
+  </BasicModal>
+</template>
+<script lang="ts" setup>
+import { onMounted, ref, defineEmits, watch, nextTick } from 'vue';
+import { columns } from '../../pointTabel/point.data';
+import { BasicModal, useModalInner } from '/@/components/Modal';
+
+const DeviceModalTable = ref();
+const props = defineProps({
+  dataSource: { type: Array, default: () => [] },
+  selectionRowKeys: { type: Array, default: () => [] },
+});
+const emit = defineEmits(['reload', 'register']);
+
+const refresh = ref(true);
+const selectedRowKeys = ref<any[]>([]);
+const selectionRows = ref<any[]>([]);
+
+const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+  //重置表单
+  setModalProps({ confirmLoading: false });
+});
+
+/**
+ * 复选框选中事件
+ * @param rowKeys
+ * @param rows
+ */
+function onSelectChange(record) {
+  selectedRowKeys.value = [record['id']];
+  selectionRows.value = [record];
+}
+
+const saveData = async () => {
+  emit('reload', selectionRows.value);
+
+  closeModal();
+};
+
+watch(
+  () => props.selectionRowKeys,
+  (newVal: any[]) => {
+    selectedRowKeys.value = newVal;
+    refresh.value = false;
+    nextTick(() => {
+      refresh.value = true;
+    });
+  }
+);
+
+onMounted(async () => {});
+</script>
+<style scoped lang="less"></style>

+ 186 - 0
src/views/vent/deviceManager/comment/warningTabel/ctrlTable.vue

@@ -0,0 +1,186 @@
+<template>
+  <!-- 配置预警设备 -->
+  <div class="warning-device-box">
+    <div class="warning-box">
+      <div v-for="item in ctrlList" class="link-item" :class="{ 'active-device-title': item.id == activeID }" :key="item.id">
+        <span class="" @click="selectWarning(item.id)">{{ item.valuename }}</span>
+      </div>
+    </div>
+    <div class="device-box">
+      <a-button class="vent-margin-b-5" type="primary" @click="handleOpen(true, {})"> 新增 </a-button>
+      <a-table :columns="ctrlColumns" :data-source="dataSource" bordered :scroll="{ y: 500 }" :pagination="false">
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.dataIndex === 'operation'">
+            <a class="action-link" @click="handleOpen('update', record)">编辑</a>
+            <a-popconfirm title="确定删除?" @confirm="handleDelete(record)">
+              <a class="table-action-link">删除</a>
+            </a-popconfirm>
+          </template>
+        </template>
+      </a-table>
+    </div>
+  </div>
+  <BaseModal
+    @register="register"
+    @add="onSubmit"
+    @update="onSubmit"
+    :device-type="props.deviceType"
+    :device-id="props.deviceId"
+    :form-schemas="formSchemas"
+    :monitor-type="''"
+  />
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue';
+import { ctrlColumns, ctrlFormSchemas } from './warning.data';
+import { getCtrlDevicePointList, addCtrlDevicePoint, editCtrlDevicePoint, getCtrlDevicePointPage, deleteDevicePonit } from './warning.api';
+import BaseModal from './BaseModalCtrl.vue';
+import { useModal } from '/@/components/Modal';
+
+const emit = defineEmits(['register']);
+
+const props = defineProps({
+  deviceId: { type: String },
+  deviceType: { type: String },
+});
+
+const formSchemas = ctrlFormSchemas;
+const activeID = ref('');
+const dataSource = ref<any[]>([]);
+const ctrlList = ref<any[]>([]);
+const deviceType = ref('');
+const editRecord = ref<any>(null);
+function selectWarning(id) {
+  activeID.value = id;
+  getTableData(id);
+}
+async function getControlDevicePointList() {
+  const param = {
+    pageNo: 1,
+    pageSize: 1000,
+    devicetype: props.deviceType,
+    valuetype: 10,
+  };
+  const res = await getCtrlDevicePointList(param);
+  ctrlList.value = res.records || [];
+}
+const [register, { openModal, closeModal }] = useModal();
+
+function handleOpen(flag?, record?) {
+  console.log(record, 'record');
+  deviceType.value = props.deviceType;
+  editRecord.value = flag == 'update' ? record : null;
+  if (record) {
+    if (flag == 'update') {
+      openModal(true, {
+        isUpdate: true,
+        record,
+      });
+    } else {
+      openModal(true, {
+        isUpdate: false,
+        record,
+      });
+    }
+  } else {
+    openModal(true, {
+      isUpdate: false,
+      record,
+    });
+  }
+}
+
+async function onSubmit(flag, values) {
+  const currentItem = ctrlList.value.find((item) => item.id == activeID.value);
+  if (currentItem) {
+    values.monitorCode = currentItem.valuecode;
+    values.monitorId = currentItem.id;
+    values.monitorName = currentItem.valuename;
+  }
+  if (flag == 'update') {
+    await editCtrlDevicePoint({ id: editRecord.value?.id, ...editRecord.value, ...values });
+  } else {
+    await addCtrlDevicePoint({ ...values });
+  }
+  editRecord.value = null;
+  closeModal();
+  await getTableData(activeID.value);
+}
+
+async function handleDelete(record) {
+  await deleteDevicePonit({ id: record.id });
+  getTableData(activeID.value);
+}
+
+async function getTableData(id?: string) {
+  const param = {
+    pageNo: 1,
+    pageSize: 1000,
+    monitorId: id,
+    deviceId: props.deviceId,
+  };
+  const res = await getCtrlDevicePointPage(param);
+  dataSource.value = res.records || [];
+}
+
+onMounted(async () => {
+  await getControlDevicePointList();
+  if (ctrlList.value.length > 0) {
+    activeID.value = ctrlList.value[0].id;
+  }
+  await getTableData(activeID.value);
+});
+</script>
+
+<style lang="less" scoped>
+@ventSpace: zxm;
+
+.warning-device-box {
+  width: 100%;
+  height: 600px;
+  overflow-y: auto;
+  display: flex;
+  .warning-box {
+    width: 200px;
+    height: 100%;
+    overflow-y: auto;
+    background: #ffffff11;
+    padding: 5px;
+    border-radius: 5px;
+
+    .link-item {
+      position: relative;
+      cursor: pointer;
+      line-height: 30px;
+      padding-left: 30px;
+      color: #fff;
+      span:hover {
+        color: #89ffff;
+      }
+      &::after {
+        content: '';
+        position: absolute;
+        display: block;
+        width: 8px;
+        height: 8px;
+        top: 12px;
+        left: 10px;
+        transform: rotateZ(45deg) skew(10deg, 10deg);
+        background: #45d3fd;
+      }
+    }
+    .active-device-title {
+      color: aqua;
+    }
+  }
+  .device-box {
+    flex: 1;
+    margin-left: 10px;
+  }
+
+  .action-link {
+    color: #00e7ff;
+  }
+}
+</style>

+ 26 - 0
src/views/vent/deviceManager/comment/warningTabel/warning.api.ts

@@ -1,3 +1,4 @@
+import { getAllPostList } from './../../../../system/user/user.api';
 import { defHttp } from '/@/utils/http/axios';
 import { Modal } from 'ant-design-vue';
 
@@ -50,6 +51,19 @@ enum Api {
   addProgram = '/monitor/voice-alarm/addProgram',
   updateProgram = '/monitor/voice-alarm/updateProgram',
   deleteProgram = '/monitor/voice-alarm/deleteProgram',
+
+  // 获取控制点位列表
+  getCtrlDevicePointList = '/safety/ventanalyMonitorParams/list',
+  // 新增控制点位关联
+  addCtrlDevicePoint = '/ventanaly-device/safety/deviceLink/add',
+  // 编辑控制点位关联
+  editCtrlDevicePoint = '/ventanaly-device/safety/deviceLink/edit',
+  // 设备联动控制管理-分页查询
+  ctrlDevicePointPage = '/ventanaly-device/safety/deviceLink/list',
+  // 设备联动控制管理-删除
+  deleteDevicePonit = '/ventanaly-device/safety/deviceLink/delete',
+  // 获取设备下的设备数据
+  getDeviceList = '/safety/ventanalyDeviceInfo/list',
 }
 /**
  * 导出api
@@ -157,3 +171,15 @@ export const modelAlarmAutoEdit = (params) => defHttp.post({ url: Api.modelAlarm
 export const addProgram = (params) => defHttp.post({ url: Api.addProgram, params });
 export const updateProgram = (params) => defHttp.post({ url: Api.updateProgram, params });
 export const deleteProgram = (params) => defHttp.post({ url: Api.deleteProgram, params });
+// 控制点位关联
+export const getCtrlDevicePointList = (params) => defHttp.get({ url: Api.getCtrlDevicePointList, params });
+// 新增控制点位关联
+export const addCtrlDevicePoint = (params) => defHttp.post({ url: Api.addCtrlDevicePoint, params });
+// 编辑控制点位关联
+export const editCtrlDevicePoint = (params) => defHttp.post({ url: Api.editCtrlDevicePoint, params });
+// 设备联动控制管理-分页查询
+export const getCtrlDevicePointPage = (params) => defHttp.get({ url: Api.ctrlDevicePointPage, params });
+// 设备联动控制管理-删除
+export const deleteDevicePonit = (params) => defHttp.delete({ url: Api.deleteDevicePonit, params }, { joinParamsToUrl: true });
+// 获取设备下的设备数据
+export const getDeviceList = (params) => defHttp.get({ url: Api.getDeviceList, params });

+ 137 - 2
src/views/vent/deviceManager/comment/warningTabel/warning.data.ts

@@ -1,7 +1,7 @@
 import { BasicColumn, FormSchema } from '/@/components/Table';
 import { initDictOptions } from '/@/utils/dict';
 import type { TableColumnType } from 'ant-design-vue';
-import { workFaceDeviceList } from './warning.api';
+import { workFaceDeviceList, getDeviceList } from './warning.api';
 import { render } from '/@/utils/common/renderUtils';
 
 export const levelColumns: BasicColumn[] = [
@@ -909,7 +909,6 @@ export const controlColumns: TableColumnType[] = [
     align: 'center',
   },
 ];
-
 export const testData = [
   {
     key: '1',
@@ -1236,3 +1235,139 @@ export const ledPlaylistFormSchemas = () =>
       },
     },
   ];
+
+// 控制点位关联
+export const ctrlColumns: TableColumnType[] = [
+  {
+    title: '点位名称',
+    dataIndex: 'monitorName',
+    align: 'center',
+  },
+  {
+    title: '关联设备名称',
+    dataIndex: 'linkDeviceName',
+    align: 'center',
+  },
+  {
+    title: '关联设备安装位置',
+    dataIndex: 'linkDevicePos',
+    align: 'center',
+  },
+  {
+    title: '设备值',
+    dataIndex: 'value',
+    align: 'center',
+  },
+  {
+    title: '备注',
+    dataIndex: 'remark',
+    align: 'center',
+  },
+  {
+    title: '创建人',
+    dataIndex: 'createBy',
+    align: 'center',
+  },
+  {
+    title: '创建日期',
+    dataIndex: 'createTime',
+    align: 'center',
+  },
+  {
+    title: '执行顺序',
+    dataIndex: 'orderNum',
+    align: 'center',
+  },
+  {
+    title: '操作',
+    colSpan: 2,
+    dataIndex: 'operation',
+    width: 120,
+    align: 'center',
+  },
+];
+// 新增/编辑 表单部分
+export const ctrlFormSchemas: FormSchema[] = [
+  {
+    label: '关联设备类型',
+    field: 'linkDeviceType',
+    component: 'MTreeSelect',
+    componentProps: ({ formModel }) => ({
+      isSearch: false,
+      onChange: () => {
+        formModel['linkDeviceId'] = undefined;
+        formModel['linkDevicePos'] = undefined;
+        formModel['linkDeviceName'] = undefined;
+        formModel['linkDeviceKind'] = undefined;
+      },
+    }),
+    required: true,
+  },
+  {
+    label: '关联设备',
+    field: 'linkDeviceId',
+    component: 'ApiSelect',
+    componentProps: ({ formModel }) => ({
+      api: (params) =>
+        getDeviceList({
+          pageNo: 1,
+          pageSize: 1000,
+          strtype: formModel.linkDeviceType,
+          ...params,
+        }),
+      resultField: 'records',
+      labelField: 'strname',
+      valueField: 'id',
+      onChange: (e, option) => {
+        if (option) {
+          formModel['linkDeviceType'] = option.strtype;
+          formModel['linkDevicePos'] = option.strinstallpos;
+          formModel['linkDeviceName'] = option.strname;
+          formModel['linkDeviceKind'] = option.devicekind;
+        }
+        formModel['linkMonitorCode'] = undefined;
+      },
+    }),
+    required: true,
+  },
+  {
+    label: '关联点位code',
+    field: 'linkMonitorCode',
+    component: 'Select',
+    slot: 'monitor',
+    required: true,
+  },
+  {
+    label: '执行顺序',
+    field: 'orderNum',
+    component: 'InputNumber',
+  },
+  {
+    label: '设置值',
+    field: 'value',
+    component: 'InputNumber',
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'Input',
+  },
+  {
+    label: '',
+    field: 'linkDevicePos',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '',
+    field: 'linkDeviceName',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '',
+    field: 'linkDeviceKind',
+    component: 'Input',
+    show: false,
+  },
+];

+ 260 - 193
src/views/vent/monitorManager/comment/HandlerHistoryTable.vue

@@ -1,218 +1,285 @@
 <template>
   <div class="handler-history-table">
-    <BasicTable ref="handlerHistory" @register="registerTable" />
+    <BasicTable ref="handlerHistory" @register="registerTable">
+      <template #expandedRowRender="{ record }">
+        <a-table
+          size="small"
+          :columns="innerColumns"
+          :data-source="innerDataMap[record.groupcode] || []"
+          :pagination="false"
+          :loading="innerLoadingMap[record.groupcode]"
+          row-key="id"
+        >
+          <template #bodyCell="{ column }"> </template>
+        </a-table>
+      </template>
+    </BasicTable>
   </div>
 </template>
 
 <script lang="ts" name="system-user" setup>
-  //ts语法
-  import { watch, ref } from 'vue';
-  import { BasicTable } from '/@/components/Table';
-  import { useListPage } from '/@/hooks/system/useListPage';
-  import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
-  import { defHttp } from '/@/utils/http/axios';
-  import dayjs from 'dayjs';
-  import { getAutoScrollContainer } from '/@/utils/common/compUtils';
+//ts语法
+import { watch, ref, reactive } from 'vue';
+import { BasicTable } from '/@/components/Table';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { defHttp } from '/@/utils/http/axios';
+import dayjs from 'dayjs';
+import { getAutoScrollContainer } from '/@/utils/common/compUtils';
 
-  const list = (params) => defHttp.get({ url: '/safety/ventanalyDevicesetLog/list', params });
+const list = (params) => defHttp.get({ url: '/safety/ventanalyDevicesetLog/list', params });
+const getDevicesetLog = (params) => defHttp.post({ url: '/ventanaly-device/safety/ventanalyDevicesetLog/queryByGroupCode', params });
+const props = defineProps({
+  columnsType: {
+    type: String,
+    required: true,
+  },
+  deviceType: {
+    type: String,
+    required: true,
+  },
+  deviceListApi: {
+    type: Function,
+    required: false,
+  },
+  designScope: {
+    type: String,
+  },
+  sysId: {
+    type: String,
+  },
+  deviceId: {
+    type: String,
+    default: '',
+  },
+  scroll: {
+    type: Object,
+    default: { y: 0 },
+  },
+});
 
-  const props = defineProps({
-    columnsType: {
-      type: String,
-      required: true,
-    },
-    deviceType: {
-      type: String,
-      required: true,
-    },
-    deviceListApi: {
-      type: Function,
-      required: false,
-    },
-    designScope: {
-      type: String,
-    },
-    sysId: {
-      type: String,
-    },
-    deviceId: {
-      type: String,
-      default: '',
-    },
-    scroll: {
-      type: Object,
-      default: { y: 0 },
-    },
-  });
+const handlerHistory = ref();
+const columns = ref([]);
+const tableScroll = props.scroll.y ? ref({ y: props.scroll.y - 100 }) : ref({});
+const innerColumns = ref([
+  {
+    title: '设备名称',
+    dataIndex: 'devicename',
+    key: 'devicename',
+  },
+  {
+    title: '用户',
+    dataIndex: 'realname',
+    key: 'realname',
+  },
+  {
+    title: '操作记录',
+    dataIndex: 'strremark',
+    key: 'strremark',
+  },
 
-  const handlerHistory = ref();
-  const columns = ref([]);
-  const tableScroll = props.scroll.y ? ref({ y: props.scroll.y - 100 }) : ref({});
+  {
+    title: 'IP',
+    dataIndex: 'ip',
+    key: 'ip',
+  },
+  {
+    title: '操作时间',
+    dataIndex: 'createTime',
+    key: 'createTime',
+  },
+]);
+// 展开行相关
+const expandedRowKeys = ref<string[]>([]);
+const innerDataMap = reactive<Record<string, any[]>>({});
+const innerLoadingMap = reactive<Record<string, boolean>>({});
 
-  watch(
-    () => {
-      return props.columnsType;
-    },
-    (newVal) => {
-      const column = getTableHeaderColumns(newVal);
-      if (column && column.length < 1) {
-        const arr = newVal.split('_');
-        const columnKey = arr.reduce((prev, cur, index) => {
-          if (index !== arr.length - 2) {
-            return prev + '_' + cur;
-          } else {
-            return prev;
-          }
-        });
-        columns.value = getTableHeaderColumns(arr[0] + '_history');
-      } else {
-        columns.value = column;
-      }
-      if (handlerHistory.value) reload();
-    },
-    {
-      immediate: true,
+// 展开/收起事件
+const onExpand = async (expanded: boolean, record: Recordable) => {
+  if (!expanded) return;
+  const key = record.groupcode;
+  if (innerDataMap[key]) return;
+  innerLoadingMap[key] = true;
+  try {
+    const res = await getDevicesetLog({ groupCode: key });
+    innerDataMap[key] = res.records || res || [];
+  } catch (e) {
+    console.error('获取子数据失败:', e);
+    innerDataMap[key] = [];
+  } finally {
+    innerLoadingMap[key] = false;
+  }
+};
+watch(
+  () => {
+    return props.columnsType;
+  },
+  (newVal) => {
+    const column = getTableHeaderColumns(newVal);
+    if (column && column.length < 1) {
+      const arr = newVal.split('_');
+      const columnKey = arr.reduce((prev, cur, index) => {
+        if (index !== arr.length - 2) {
+          return prev + '_' + cur;
+        } else {
+          return prev;
+        }
+      });
+      columns.value = getTableHeaderColumns(arr[0] + '_history');
+    } else {
+      columns.value = column;
     }
-  );
+    if (handlerHistory.value) reload();
+  },
+  {
+    immediate: true,
+  }
+);
 
-  watch(
-    () => props.scroll.y,
-    (newVal) => {
-      if (newVal) {
-        tableScroll.value = { y: newVal - 100 };
-      } else {
-        tableScroll.value = {};
-      }
+watch(
+  () => props.scroll.y,
+  (newVal) => {
+    if (newVal) {
+      tableScroll.value = { y: newVal - 100 };
+    } else {
+      tableScroll.value = {};
     }
-  );
-
-  // 列表页面公共参数、方法
-  const { tableContext } = useListPage({
-    tableProps: {
-      api: list,
-      columns: columns,
-      canResize: true,
-      showTableSetting: false,
-      showActionColumn: false,
-      showIndexColumn: true,
-      bordered: false,
-      size: 'small',
-      scroll: tableScroll,
-      formConfig: {
-        labelAlign: 'left',
-        showAdvancedButton: false,
-        baseColProps: {
-          // offset: 0.5,
-          xs: 24,
-          sm: 24,
-          md: 24,
-          lg: 9,
-          xl: 7,
-          xxl: 4,
+  }
+);
+const onExpandedRowsChange = (keys: string[]) => {
+  expandedRowKeys.value = keys;
+};
+// 列表页面公共参数、方法
+const { tableContext } = useListPage({
+  tableProps: {
+    api: list,
+    columns: columns,
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    showIndexColumn: true,
+    bordered: false,
+    size: 'small',
+    scroll: tableScroll,
+    formConfig: {
+      labelAlign: 'left',
+      showAdvancedButton: false,
+      baseColProps: {
+        // offset: 0.5,
+        xs: 24,
+        sm: 24,
+        md: 24,
+        lg: 9,
+        xl: 7,
+        xxl: 4,
+      },
+      schemas: [
+        {
+          field: 'createTime_begin',
+          label: '开始时间',
+          component: 'DatePicker',
+          defaultValue: dayjs().add(-30, 'day').format('YYYY-MM-DD HH:mm:ss'),
+          required: true,
+          componentProps: {
+            showTime: true,
+            valueFormat: 'YYYY-MM-DD HH:mm:ss',
+            getPopupContainer: getAutoScrollContainer,
+          },
+          colProps: {
+            span: 4,
+          },
         },
-        schemas: [
-          {
-            field: 'createTime_begin',
-            label: '开始时间',
-            component: 'DatePicker',
-            defaultValue: dayjs().add(-30, 'day').format('YYYY-MM-DD HH:mm:ss'),
-            required: true,
-            componentProps: {
-              showTime: true,
-              valueFormat: 'YYYY-MM-DD HH:mm:ss',
-              getPopupContainer: getAutoScrollContainer,
-            },
-            colProps: {
-              span: 4,
-            },
+        {
+          field: 'createTime_end',
+          label: '结束时间',
+          component: 'DatePicker',
+          defaultValue: dayjs(),
+          required: true,
+          componentProps: {
+            showTime: true,
+            valueFormat: 'YYYY-MM-DD HH:mm:ss',
+            getPopupContainer: getAutoScrollContainer,
           },
-          {
-            field: 'createTime_end',
-            label: '结束时间',
-            component: 'DatePicker',
-            defaultValue: dayjs(),
-            required: true,
-            componentProps: {
-              showTime: true,
-              valueFormat: 'YYYY-MM-DD HH:mm:ss',
-              getPopupContainer: getAutoScrollContainer,
-            },
-            colProps: {
-              span: 4,
-            },
+          colProps: {
+            span: 4,
           },
-          // {
-          //   label: '查询设备',
-          //   field: 'gdeviceid',
-          //   component: 'ApiSelect',
-          //   componentProps: {
-          //     api: props.deviceListApi,
-          //     resultField: 'records',
-          //     labelField: 'strname',
-          //     valueField: 'id',
-          //   },
-          // },
-        ],
-      },
-      fetchSetting: {
-        listField: 'records',
-      },
-      pagination: {
-        current: 1,
-        pageSize: 10,
-        pageSizeOptions: ['10', '30', '50', '100'],
-      },
-      beforeFetch(params) {
-        params.devicetype = props.deviceType + '*';
-        if (props.sysId) {
-          params.sysId = props.sysId;
-        }
-        if (props.deviceId) {
-          params.deviceid = props.deviceId;
-        }
-      },
+        },
+        // {
+        //   label: '查询设备',
+        //   field: 'gdeviceid',
+        //   component: 'ApiSelect',
+        //   componentProps: {
+        //     api: props.deviceListApi,
+        //     resultField: 'records',
+        //     labelField: 'strname',
+        //     valueField: 'id',
+        //   },
+        // },
+      ],
+    },
+    fetchSetting: {
+      listField: 'records',
     },
-  });
-  //注册table数据
-  const [registerTable, { reload, setLoading }] = tableContext;
-  defineExpose({ setLoading });
+    pagination: {
+      current: 1,
+      pageSize: 10,
+      pageSizeOptions: ['10', '30', '50', '100'],
+    },
+    expandedRowKeys: expandedRowKeys,
+    onExpand: onExpand,
+    onExpandedRowsChange: onExpandedRowsChange,
+    beforeFetch(params) {
+      params.devicetype = props.deviceType + '*';
+      if (props.sysId) {
+        params.sysId = props.sysId;
+      }
+      if (props.deviceId) {
+        params.deviceid = props.deviceId;
+      }
+    },
+  },
+});
+
+//注册table数据
+const [registerTable, { reload, setLoading }] = tableContext;
+defineExpose({ setLoading });
 </script>
 
 <style scoped lang="less">
-  @ventSpace: zxm;
+@ventSpace: zxm;
 
-  // :deep(.@{ventSpace}-table-body) {
-  //   height: auto !important;
-  // }
-  :deep(.zxm-picker) {
-    height: 30px !important;
-  }
-  .handler-history-table {
-    width: 100%;
-    :deep(.jeecg-basic-table-form-container) {
-      .@{ventSpace}-form {
-        padding: 0 !important;
-        border: none !important;
-        margin-bottom: 0 !important;
-        .@{ventSpace}-picker,
-        .@{ventSpace}-select-selector {
-          width: 100% !important;
-          height: 100%;
-          background: #00000017;
-          border: 1px solid #b7b7b7;
-          input,
-          .@{ventSpace}-select-selection-item,
-          .@{ventSpace}-picker-suffix {
-            color: #fff;
-          }
-          .@{ventSpace}-select-selection-placeholder {
-            color: #ffffffaa;
-          }
+// :deep(.@{ventSpace}-table-body) {
+//   height: auto !important;
+// }
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+.handler-history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        height: 100%;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
         }
       }
-      .@{ventSpace}-table-title {
-        min-height: 0 !important;
-      }
+    }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
     }
   }
-</style>
+}
+</style>

+ 83 - 88
src/views/vent/monitorManager/deviceMonitor/index.vue

@@ -7,7 +7,7 @@
     </template>
     <Network ref="NetworkRef" v-if="routerParam === 'timesolution'" :pageResult="pageResult" @change-page-type="changePageType" />
     <!-- <VentModal style="width: 100%; height: 100%; position: absolute" /> -->
-    <SubApp style="width: 100%; height: 100%; position: absolute" />
+    <SubApp style="position: absolute; width: 100%; height: 100%" />
   </div>
 </template>
 
@@ -101,7 +101,7 @@
 </script>
 
 <style lang="less" scoped>
-  @import '/@/design/vent/modal.less';
+  @import url('/@/design/vent/modal.less');
   @ventSpace: zxm;
 
   .scene-box {
@@ -121,24 +121,24 @@
 
   .device-header {
     position: fixed;
+    z-index: -1;
     width: 100%;
     height: 56px;
     background: var(--image-modal-top);
+    color: #fffd;
     text-align: center;
     line-height: 56px;
     font-size: 28px;
-    color: #ffffffdd;
     font-weight: 600;
-    z-index: -1;
   }
 
   .select-node {
     position: fixed;
     top: 60px;
     left: 10px;
-    color: #fff;
     display: flex;
     justify-content: center;
+    color: #fff;
     font-size: 22px;
 
     .title {
@@ -147,11 +147,11 @@
   }
 
   .expansion-icon {
-    background: var(--image-tree-icon-bg) no-repeat;
-    background-size: contain;
     position: absolute;
-    left: 190px;
     top: 25px;
+    left: 190px;
+    background: var(--image-tree-icon-bg) no-repeat;
+    background-size: contain;
 
     &:hover {
       background: var(--image-tree-icon-hover-bg) no-repeat;
@@ -160,21 +160,21 @@
   }
 
   .device-select {
-    width: 250px;
-    height: 500px;
-    background: var(--image-tree-bg) no-repeat;
     position: fixed;
     top: 60px;
     left: 10px;
+    width: 250px;
+    height: 500px;
+    padding: 20px 10px 30px;
+    background: var(--image-tree-bg) no-repeat;
     background-size: contain;
     pointer-events: auto;
-    padding: 20px 10px 30px 10px;
   }
 
   .is-expansion-icon {
+    z-index: 999;
     padding: 5px;
     pointer-events: auto;
-    z-index: 999;
   }
 
   .device-select-show {
@@ -194,18 +194,18 @@
   }
 
   .node-select-show {
+    left: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
     width: 276px;
     height: 44px;
     background: var(--image-tree-expansion-bg) no-repeat;
-    left: 10px;
+    transition: all 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.5s;
     animation-name: treeShow;
     /* 持续时间 */
     animation-duration: 1s;
-    transition: all 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.5s;
-    display: flex;
-    align-items: center;
     margin-left: 0;
-    justify-content: flex-start;
     pointer-events: auto;
 
     &:hover {
@@ -214,8 +214,8 @@
 
     .put-away-icon {
       position: relative;
-      display: inline-block;
       left: 4px;
+      display: inline-block;
     }
   }
 
@@ -250,36 +250,36 @@
       }
 
       input {
-        height: 0px !important;
+        height: 0 !important;
       }
     }
 
     &::-webkit-scrollbar-track {
-      -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
-      border-radius: 10px;
-      background: #ededed22;
       height: 100px;
+      background: #ededed22;
+      box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
+      border-radius: 10px;
     }
 
     &::-webkit-scrollbar-thumb {
-      -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
+      box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
       background: #4288a444;
     }
   }
 
   .location-icon {
-    width: 46px;
-    height: 178px;
     position: absolute;
     top: 60px;
+    width: 46px;
+    height: 178px;
     background: var(--image-location-bg) no-repeat;
+    color: #fff;
+    cursor: pointer;
     background-size: contain;
     writing-mode: vertical-lr;
     line-height: 46px;
-    color: #fff;
     padding-top: 10px;
     pointer-events: auto;
-    cursor: pointer;
 
     &:hover {
       background: var(--image-location-hover-bg) no-repeat;
@@ -298,36 +298,36 @@
     pointer-events: auto;
 
     .location-select-box {
+      position: relative;
       width: 100%;
       height: 100%;
-      position: relative;
 
       &::before {
-        content: '';
         position: absolute;
-        width: 230px;
-        height: 500px;
         top: 0;
         left: 0;
+        z-index: -1;
+        width: 230px;
+        height: 500px;
         background: var(--image-tree-bg) no-repeat;
-        background-size: contain;
         transform: rotateY(180deg);
-        z-index: -1;
+        content: '';
+        background-size: contain;
       }
 
       .location-top-title {
-        color: #fff;
         position: absolute;
-        width: 225px;
-        height: 68px;
-        background: var(--image-turn-location-top-bg) no-repeat;
-        background-size: contain;
         top: 5px;
         left: 5px;
         display: flex;
         flex-direction: row;
-        justify-content: space-between;
         align-items: flex-end;
+        justify-content: space-between;
+        width: 225px;
+        height: 68px;
+        background: var(--image-turn-location-top-bg) no-repeat;
+        color: #fff;
+        background-size: contain;
 
         .title {
           font-size: 18px;
@@ -338,12 +338,12 @@
       }
 
       .location-expansion-icon {
-        background: var(--image-tree-icon-cover-bg) no-repeat;
-        background-size: contain;
         position: relative;
-        left: 10px;
         top: -15px;
+        left: 10px;
         padding: 5px;
+        background: var(--image-tree-icon-cover-bg) no-repeat;
+        background-size: contain;
 
         &:hover {
           background: var(--image-tree-icon-cover-hover-bg) no-repeat;
@@ -353,46 +353,46 @@
     }
 
     .location-container {
-      width: 200px;
-      height: 390px;
       position: absolute;
-      display: flex;
-      flex-direction: column;
       top: 80px;
       left: 18px;
+      display: flex;
+      flex-direction: column;
+      width: 200px;
+      height: 390px;
       overflow-y: auto;
 
       .location-item {
-        color: #fff;
-        line-height: 30px;
         display: flex;
         justify-content: space-between;
-        background-image: linear-gradient(to left, #39f5ff05, #39f5ff10);
         margin: 3px 0;
+        color: #fff;
+        line-height: 30px;
+        background-image: linear-gradient(to left, #39f5ff05, #39f5ff10);
 
         .item-title {
           width: 80px;
-          text-align: right;
           color: #87f1ff;
+          text-align: right;
         }
       }
 
       .location-bottom-btn {
-        width: 100%;
-        color: #fff;
         display: flex;
         justify-content: flex-end;
+        width: 100%;
+        color: #fff;
         margin-top: 20px;
 
         span {
           display: inline-block;
           width: 100%;
+          padding: 2px 0;
+          border: 1px solid rgb(174 243 255 / 30%);
           background: #00709955;
-          border-radius: 3px;
-          border: 1px solid rgba(174, 243, 255, 0.3);
           text-align: center;
-          padding: 2px 0;
           cursor: pointer;
+          border-radius: 3px;
 
           &:hover {
             background: #00557422;
@@ -419,7 +419,7 @@
   }
 
   .location-btn-show {
-    right: -0px;
+    right: -0;
     animation-name: locationBtnShow;
     /* 持续时间 */
     animation-duration: 1s;
@@ -438,20 +438,20 @@
     position: relative;
 
     .to-small {
+      position: absolute;
+      top: -65px;
+      right: 36px;
       width: 60px;
       height: 60px;
+      padding: 8px;
       background: var(--image-tosmall) no-repeat center;
       background-size: auto;
-      position: absolute;
-      top: -65px;
-      right: 36px;
       border-radius: 10px;
-      padding: 8px;
       backdrop-filter: blur(10px);
-      background-color: rgba(45, 86, 137, 0.418);
+      background-color: rgb(45 86 137 / 41.8%);
 
       &:hover {
-        background-color: rgba(79, 104, 134, 0.418);
+        background-color: rgb(79 104 134 / 41.8%);
       }
     }
 
@@ -462,16 +462,16 @@
       width: 100%;
 
       .device-button {
+        position: relative;
+        display: flex;
+        align-items: center;
+        justify-content: center;
         height: 26px;
         padding: 0 20px;
         background: linear-gradient(45deg, #04e6fb55, #0c5cab55);
-        clip-path: polygon(10px 0, 0 50%, 10px 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
-        display: flex;
-        justify-content: center;
-        align-items: center;
         color: #fff;
-        position: relative;
         cursor: pointer;
+        clip-path: polygon(10px 0, 0 50%, 10px 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
 
         &:nth-child(1) {
           left: calc(-6px * 1);
@@ -549,41 +549,36 @@
     }
 
     .enter-detail {
-      color: #fff;
-      cursor: pointer;
       position: absolute;
-      right: 120px;
       top: -6px;
-      padding: 5px;
-      border-radius: 5px;
-      margin-left: 8px;
-      margin-right: 8px;
-      width: auto;
-      height: 33px !important;
+      right: 120px;
       display: flex;
       align-items: center;
       justify-content: center;
+      width: auto;
+      height: 33px !important;
+      padding: 5px;
+      padding: 5px 15px;
       color: #fff;
-      padding: 5px 15px 5px 15px;
       cursor: pointer;
+      border-radius: 5px;
+      margin-left: 8px;
+      margin-right: 8px;
 
       &:hover {
         background: linear-gradient(#2cd1ff55, #1eb0ff55);
       }
 
       &::before {
+        position: absolute;
+        inset: 3px 0 0 3px;
+        z-index: -1;
         width: calc(100% - 6px);
         height: 27px;
+        /* important */
+        background: linear-gradient(#1fa6cb, #127cb5);
         content: '';
-        position: absolute;
-        top: 3px;
-        right: 0;
-        left: 3px;
-        bottom: 0;
-        z-index: -1;
         border-radius: inherit;
-        /*important*/
-        background: linear-gradient(#1fa6cb, #127cb5);
       }
     }
   }
@@ -614,7 +609,7 @@
 
   @keyframes locationShow {
     0% {
-      right: 0px;
+      right: 0;
       opacity: 0;
     }
 
@@ -666,6 +661,6 @@
 
   :deep(.zxm-select-dropdown) {
     left: 0 !important;
-    color: #000000 !important;
+    color: #000 !important;
   }
 </style>

+ 5 - 3
src/views/vent/monitorManager/gateMonitor/components/GateShrinkSVG.vue

@@ -9,7 +9,7 @@
     y="0px"
     width="1920px"
     height="1080px"
-    viewBox="0 0 1920 1080"
+    viewBox="0 230 1920 1080"
   >
     <defs>
       <linearGradient id="Gradient_1" gradientUnits="userSpaceOnUse" x1="188.175" y1="25.7" x2="189.825" y2="25.7" spreadMethod="reflect">
@@ -857,9 +857,11 @@ onMounted(() => {
 });
 
 /** 根据SVG的使用场景播放动画 */
-function animate() {
+function animate(frontDoorIsOpen: boolean, middleDoorIsOpen: boolean, rearDoorIsOpen: boolean) {
   // 在SVG图片中,找到需要动起来的元素(类似<use xlink:href="#RE_L_0_Layer0_0_FILL"></use>),并填入id即可控制该元素的动画(如有)
-  // triggerAnimation(["___2_0_Layer0_0_FILL","___3_0_Layer0_0_FILL"], false);
+  // triggerAnimation(["___4_0_Layer0_0_FILL","___3_0_Layer0_0_FILL"], false);
+  triggerAnimation([`anim____2_0_Layer0_0_FILL_0${props.identify}`, `anim____2_0_Layer0_0_FILL_1${props.identify}`], !rearDoorIsOpen);
+  triggerAnimation([`anim____3_0_Layer0_0_FILL_0${props.identify}`, `anim____3_0_Layer0_0_FILL_1${props.identify}`], !frontDoorIsOpen);
 }
 
 // 导出方法以便外部调用

+ 5 - 3
src/views/vent/monitorManager/gateMonitor/components/GateYeYaSVG.vue

@@ -9,7 +9,7 @@
     y="0px"
     width="1920px"
     height="1080px"
-    viewBox="0 0 1920 1080"
+    viewBox="0 230 1920 1080"
   >
     <defs>
       <linearGradient id="Gradient_1" gradientUnits="userSpaceOnUse" x1="188.175" y1="25.7" x2="189.825" y2="25.7" spreadMethod="reflect">
@@ -1200,9 +1200,11 @@ onMounted(() => {
 });
 
 /** 根据SVG的使用场景播放动画 */
-function animate() {
+function animate(frontDoorIsOpen: boolean, middleDoorIsOpen: boolean, rearDoorIsOpen: boolean) {
   // 在SVG图片中,找到需要动起来的元素(类似<use xlink:href="#RE_L_0_Layer0_0_FILL"></use>),并填入id即可控制该元素的动画(如有)
-  // triggerAnimation(["___11_0_Layer0_0_FILL","___12_0_Layer0_0_FILL","___7_0_Layer0_0_FILL","___8_0_Layer0_0_FILL"], false);
+  // triggerAnimation(["___4_0_Layer0_0_FILL","___3_0_Layer0_0_FILL"], false);
+  triggerAnimation([`anim____11_0_Layer0_0_FILL_0${props.identify}`, `anim____12_0_Layer0_0_FILL_0${props.identify}`], !rearDoorIsOpen);
+  triggerAnimation([`anim____7_0_Layer0_0_FILL_1${props.identify}`, `anim____8_0_Layer0_0_FILL_1${props.identify}`], !frontDoorIsOpen);
 }
 
 // 导出方法以便外部调用

Fichier diff supprimé car celui-ci est trop grand
+ 535 - 0
src/views/vent/monitorManager/gateMonitor/components/gateSanDaoShenSuoSVG.vue


Fichier diff supprimé car celui-ci est trop grand
+ 671 - 0
src/views/vent/monitorManager/gateMonitor/components/gateXrSVG.vue


+ 5 - 1
src/views/vent/monitorManager/gateMonitor/gate.data.ts

@@ -432,7 +432,11 @@ export function getModelComponent(is2DModel: boolean = false, type?: string) {
         return getComponent('gateDualSVG');
       default:
         // return getComponent('gateTripleSVG');
-        return getComponent('gateTripleSVG');
+        // return getComponent('gateTripleSVG');
+        // return getComponent('gateSanDaoShenSuoSVG'); //三道伸缩风门
+        // return getComponent('gateXrSVG'); //行人风门
+        // return getComponent('GateYeYaSVG'); //液压风门
+        return getComponent('GateShrinkSVG'); //收缩风门
     }
   });
 }

+ 31 - 1
src/views/vent/packageUpload/components/DeployHistory.vue

@@ -1,6 +1,21 @@
 <template>
   <div class="deploy-history">
     <div class="search-bar">
+      <div class="search-item">
+        <span class="search-label">操作人:</span>
+        <a-input v-model:value="searchForm.createBy" placeholder="请输入操作人" allow-clear style="width: 150px" @keydown.enter="handleSearch" />
+      </div>
+      <div class="search-item">
+        <span class="search-label">部署包名称:</span>
+        <a-input v-model:value="searchForm.packName" placeholder="请输入部署包名称" allow-clear style="width: 200px" @keydown.enter="handleSearch" />
+      </div>
+      <div class="search-item">
+        <span class="search-label">部署类型:</span>
+        <a-select v-model:value="searchForm.packType" placeholder="请选择部署类型" allow-clear style="width: 150px">
+          <a-select-option value="jar">后端包</a-select-option>
+          <a-select-option value="page">前端包</a-select-option>
+        </a-select>
+      </div>
       <div class="search-item">
         <span class="search-label">时间范围:</span>
         <a-range-picker
@@ -35,6 +50,9 @@
   import { deployLogListApi } from '../package.api';
 
   const searchForm = reactive({
+    createBy: '',
+    packName: '',
+    packType: undefined as string | undefined,
     timeRange: null as any,
   });
 
@@ -89,6 +107,15 @@
     bordered: true,
     showIndexColumn: true,
     beforeFetch: (params) => {
+      if (searchForm.createBy) {
+        params.createBy = searchForm.createBy;
+      }
+      if (searchForm.packName) {
+        params.packName = searchForm.packName;
+      }
+      if (searchForm.packType) {
+        params.packType = searchForm.packType;
+      }
       if (searchForm.timeRange && searchForm.timeRange.length === 2) {
         params.startTime = dayjs(searchForm.timeRange[0]).format('YYYY-MM-DD HH:mm:ss');
         params.endTime = dayjs(searchForm.timeRange[1]).format('YYYY-MM-DD HH:mm:ss');
@@ -104,6 +131,9 @@
   }
 
   function handleReset() {
+    searchForm.createBy = '';
+    searchForm.packName = '';
+    searchForm.packType = undefined;
     searchForm.timeRange = null;
     reload();
   }
@@ -118,7 +148,7 @@
       border-radius: 4px;
       display: flex;
       align-items: center;
-      gap: 12px;
+      gap: 32px;
 
       .search-item {
         display: flex;

+ 3 - 0
src/views/ventAI/dataPicker/useDataPicker.ts

@@ -4,9 +4,11 @@ import { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
 import type { TodoItem, ToolCallRecord, SSERawEvent } from './types';
 import DataPickerModal from './components/DataPickerModal.vue';
 import aiIcon from '@/assets/images/ventAI/dataPicker/AI-icon.svg';
+import { getActions } from '/@/qiankun/state';
 
 // ─── 全局开关:控制悬停模式浮动按钮是否启用 ─────────────────────────────────
 const STORAGE_KEY = 'dataPickerHoverEnabled';
+const actions = getActions();
 
 export const dataPickerHoverEnabled = ref(localStorage.getItem(STORAGE_KEY) !== 'false');
 
@@ -31,6 +33,7 @@ export function setDataPickerHoverEnabled(val: boolean) {
       inst.attachMove();
     });
   }
+  actions.setGlobalState({ dataPickerHoverEnabled: val });
 }
 
 // ─── 按钮 DOM 构建(悬停模式专用)──────────────────────────────────────────

+ 178 - 7
src/views/ventAI/manageAssistent/api.ts

@@ -6,7 +6,10 @@ enum Api {
   getHistoryList = '/ventAI/api/sessions',
   getDetail = '/ventAI/api/chat/history/',
   deleteSession = '/ventAI/api/sessions/',
-  reviewPdf = '/ventAI/api/vent/review/pdf',
+  chatResume = '/ventAI/api/chat/resume',
+  getModelMsg = '/ventAI/api/model',
+  switchThinking = '/ventAI/api/model/thinking',
+  getSkills = '/ventAI/api/skills',
 }
 
 /**
@@ -26,6 +29,76 @@ export const getDetail = (session_id: string) => defHttp.get({ url: Api.getDetai
  */
 export const deleteSession = (session_id: string) => defHttp.delete({ url: Api.deleteSession + session_id }, { isTransformResponse: false });
 
+/**
+ * 置顶会话
+ * @param session_id - 会话ID
+ */
+export const pinSession = (session_id: string) => defHttp.put({ url: `/ventAI/api/sessions/${session_id}/pin` }, { isTransformResponse: false });
+
+/**
+ * 编辑会话名称
+ * @param session_id - 会话ID
+ * @param title - 新标题
+ */
+export const editSessionTitle = (session_id: string, title: string) =>
+  defHttp.put({ url: `/ventAI/api/sessions/${session_id}/title`, data: { title } }, { isTransformResponse: false });
+
+/**
+ * 获取对话权限模式
+ * @param session_id - 会话ID
+ */
+export const getSessionMode = (session_id: string) => defHttp.get({ url: `/ventAI/api/sessions/${session_id}/mode` }, { isTransformResponse: false });
+
+/**
+ * 设置对话权限模式
+ * @param session_id - 会话ID
+ */
+export const putSessionMode = (session_id: string, mode: string) =>
+  defHttp.put({ url: `/ventAI/api/sessions/${session_id}/mode`, data: { mode } }, { isTransformResponse: false });
+/**
+ * 上下文用量查询
+ * @param session_id - 会话ID
+ */
+export const getContext = (session_id: string) => defHttp.get({ url: `/ventAI/api/sessions/${session_id}/context` }, { isTransformResponse: false });
+
+/**
+ * 查询模型信息
+ */
+export const getModelMsg = () => defHttp.get({ url: Api.getModelMsg }, { isTransformResponse: false });
+
+/**
+ * 切换模型思考深度
+ * @param level - 思考级别 off/high/highest
+ */
+export const switchThinking = (level: string) => defHttp.post({ url: Api.switchThinking, data: { level } }, { isTransformResponse: false });
+
+/**
+ * 查询模型技能
+ */
+export const getSkills = () => defHttp.get({ url: Api.getSkills }, { isTransformResponse: false });
+
+/**
+ * 查询模型技能详情
+ */
+export const getSkillDetails = (name: string) => defHttp.get({ url: Api.getSkills + '/' + name }, { isTransformResponse: false });
+
+/**
+ * 启用/禁用模型技能
+ */
+export const toggleSkill = (name: string) => defHttp.post({ url: Api.getSkills + '/' + name + '/toggle' }, { isTransformResponse: false });
+
+/**
+ * 上传新技能
+ * @param file - zip附件
+ */
+export const uploadSkill = (file: File) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  return defHttp.post(
+    { url: Api.getSkills + '/upload', data: formData, headers: { 'Content-Type': 'multipart/form-data' } },
+    { isTransformResponse: false }
+  );
+};
 /**
  * 统一对话接口(SSE流式响应)
  * @param params.message - 用户输入的文本,必填
@@ -35,7 +108,7 @@ export const deleteSession = (session_id: string) => defHttp.delete({ url: Api.d
  * @returns Promise<{ session_id: string }>
  */
 export const unifiedStream = async (
-  params: { message: string; session_id?: string; file?: File },
+  params: { message: string; session_id?: string; file?: File; mode?: string; signal?: AbortSignal },
   onChunk: (chunk: string) => void
 ): Promise<{ session_id: string }> => {
   try {
@@ -47,11 +120,15 @@ export const unifiedStream = async (
     if (params.file) {
       formData.append('file', params.file);
     }
+    if (params.mode) {
+      formData.append('mode', params.mode);
+    }
 
     const response = await fetch(Api.unified, {
       method: 'POST',
       body: formData,
       headers: { 'X-Access-Token': getToken() },
+      signal: params.signal,
     });
 
     if (!response.ok) {
@@ -84,7 +161,7 @@ export const unifiedStream = async (
 
             onChunk(line);
 
-            if (data.type === 'done' || data.type === 'error') {
+            if (data.type === 'done' || data.type === 'error' || data.type === 'interrupt') {
               if (data.session_id) {
                 sessionId = data.session_id;
               }
@@ -102,7 +179,9 @@ export const unifiedStream = async (
 
     return { session_id: sessionId };
   } catch (error) {
-    console.error('统一对话流式请求失败:', error);
+    if ((error as Error)?.name !== 'AbortError') {
+      console.error('统一对话流式请求失败:', error);
+    }
     throw error;
   }
 };
@@ -116,7 +195,7 @@ export const unifiedStream = async (
  * @returns Promise<{ session_id: string }>
  */
 export const reviewPdfStream = async (
-  params: { file: File; session_id?: string; message?: string },
+  params: { file: File; session_id?: string; message?: string; mode?: string; signal?: AbortSignal },
   onChunk: (chunk: string) => void
 ): Promise<{ session_id: string }> => {
   try {
@@ -128,11 +207,101 @@ export const reviewPdfStream = async (
     if (params.message) {
       formData.append('message', params.message);
     }
+    if (params.mode) {
+      formData.append('mode', params.mode);
+    }
 
-    const response = await fetch(Api.reviewPdf, {
+    const response = await fetch(Api.unified, {
       method: 'POST',
       body: formData,
       headers: { 'X-Access-Token': getToken() },
+      signal: params.signal,
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP错误: ${response.status}`);
+    }
+
+    const reader = response.body!.getReader();
+    const decoder = new TextDecoder();
+    let buffer = '';
+    let sessionId = '';
+    let isDone = false;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done || isDone) break;
+
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split('\n');
+      buffer = lines.pop() || '';
+
+      for (const line of lines) {
+        if (line.trim() === '') continue;
+
+        if (line.startsWith('data: ')) {
+          const dataStr = line.replace(/^data: /, '');
+
+          try {
+            const data = JSON.parse(dataStr);
+
+            onChunk(line);
+
+            if (data.type === 'done' || data.type === 'error' || data.type === 'interrupt') {
+              if (data.session_id) {
+                sessionId = data.session_id;
+              }
+              isDone = true;
+              break;
+            }
+          } catch (e) {
+            console.warn('SSE数据解析失败:', dataStr);
+          }
+        }
+      }
+
+      if (isDone) break;
+    }
+
+    return { session_id: sessionId };
+  } catch (error) {
+    if ((error as Error)?.name !== 'AbortError') {
+      console.error('PDF审查流式请求失败:', error);
+    }
+    throw error;
+  }
+};
+
+/**
+ * 恢复会话接口(SSE流式响应)
+ * @param params.session_id - 会话ID,必填
+ * @param params.thread_id - LangGraph线程ID,可选
+ * @param params.action - "approve"(批准)或 "reject"(拒绝),必填
+ * @param onChunk - 流式数据回调函数
+ * @returns Promise<{ session_id: string }>
+ */
+export const chatResumeStream = async (
+  params: { session_id: string; thread_id?: string; action: string; signal?: AbortSignal },
+  onChunk: (chunk: string) => void
+): Promise<{ session_id: string }> => {
+  try {
+    const body: Record<string, string> = {
+      session_id: params.session_id,
+      action: params.action,
+    };
+    if (params.thread_id) {
+      body.thread_id = params.thread_id;
+    }
+
+    const response = await fetch(Api.chatResume, {
+      method: 'POST',
+      body: JSON.stringify(body),
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Access-Token': getToken(),
+      },
+      signal: params.signal,
     });
 
     if (!response.ok) {
@@ -183,7 +352,9 @@ export const reviewPdfStream = async (
 
     return { session_id: sessionId };
   } catch (error) {
-    console.error('PDF审查流式请求失败:', error);
+    if ((error as Error)?.name !== 'AbortError') {
+      console.error('恢复会话流式请求失败:', error);
+    }
     throw error;
   }
 };

Fichier diff supprimé car celui-ci est trop grand
+ 647 - 80
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue


+ 857 - 20
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -13,15 +13,37 @@
       </div>
     </div>
 
+    <!-- 已选会话引用标签 -->
+    <div v-if="selectedSessions.length > 0" class="session-chips">
+      <div v-for="session in selectedSessions" :key="session.id" class="session-chip">
+        <div class="session-chip-icon"></div>
+        <span class="session-chip-name">{{ session.name }}</span>
+        <CloseOutlined class="session-chip-remove" @click.stop="removeSession(session.id)" />
+      </div>
+    </div>
+
+    <!-- 已选技能引用标签 -->
+    <div v-if="selectedSkill" class="skill-chips">
+      <div class="skill-chip">
+        <div class="skill-chip-icon"></div>
+        <span class="skill-chip-name">{{ selectedSkill.display_name }}</span>
+        <CloseOutlined class="skill-chip-remove" @click.stop="removeSkill()" />
+      </div>
+    </div>
+
     <a-textarea
+      ref="textareaRef"
       :value="modelValue"
-      @update:value="emit('update:modelValue', $event)"
+      @update:value="handleTextareaInput"
       :rows="3"
       class="chat-input"
       placeholder="请输入你的问题"
       :auto-size="{ minRows: 3, maxRows: 6 }"
+      @keydown="handleSessionKeydown"
       @keydown.enter.prevent="handleEnterKey"
+      @keydown.backspace="handleBackspace"
     />
+
     <div class="input-actions">
       <div class="action-left">
         <div class="btn-upload-wrapper">
@@ -35,18 +57,22 @@
               <div class="popup-item-icon attach-icon"></div>
               <span class="popup-item-text">添加附件</span>
             </div>
-            <div class="popup-item" @click="handleInsertMention">
+            <!-- <div class="popup-item" @click="handleInsertMention">
               <div class="popup-item-icon mention-icon"></div>
               <span class="popup-item-text">插入 @ 提及</span>
-            </div>
+            </div> -->
             <div class="popup-item" @click="handleInsertSession">
               <div class="popup-item-icon session-icon"></div>
               <span class="popup-item-text">插入 # 会话</span>
             </div>
-            <div class="popup-item" @click="handleInsertCommand">
+            <div class="popup-item" @click="handleInsertSkill">
+              <div class="popup-item-icon skill-icon"></div>
+              <span class="popup-item-text">插入 $ 技能</span>
+            </div>
+            <!-- <div class="popup-item" @click="handleInsertCommand">
               <div class="popup-item-icon command-icon"></div>
               <span class="popup-item-text">插入 / 命令</span>
-            </div>
+            </div> -->
           </div>
         </div>
         <div class="action-divider"></div>
@@ -75,11 +101,11 @@
             </div>
           </div>
         </div>
-        <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" accept=".pdf" />
+        <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" />
       </div>
       <div class="action-right">
-        <div class="btn-context-wrapper" ref="contextPopupRef">
-          <div class="btn-context" @click="toggleContextPopup" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
+        <div class="btn-context-wrapper" ref="contextPopupRef" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
+          <div class="btn-context" @click="toggleContextPopup">
             <svg class="context-ring" viewBox="0 0 32 32">
               <circle class="context-ring-bg" cx="16" cy="16" r="13" />
               <circle
@@ -105,9 +131,31 @@
             <div class="context-popup-bar-track">
               <div class="context-popup-bar-fill" :style="{ width: contextPercent + '%', background: contextColor }"></div>
             </div>
+            <div class="context-breakdown">
+              <div class="breakdown-item">
+                <span class="breakdown-label">历史消息</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.messages) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">MCP工具</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.mcp) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">技能描述</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.skills) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">系统提示词</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.system_prompt) }}</span>
+              </div>
+              <div class="breakdown-item">
+                <span class="breakdown-label">其他</span>
+                <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.other) }}</span>
+              </div>
+            </div>
           </div>
         </div>
-        <div class="action-divider"></div>
+        <!-- <div class="action-divider"></div>
         <div class="btn-model-wrapper" ref="modelPopupRef">
           <a-tooltip title="选择模型">
             <div class="btn-model" @click="toggleModelPopup">
@@ -130,7 +178,7 @@
               <div class="model-item-check" :class="{ visible: selectedModel.key === item.key }"></div>
             </div>
           </div>
-        </div>
+        </div> -->
         <div class="action-divider"></div>
         <div class="btn-think-wrapper" ref="thinkPopupRef">
           <a-tooltip title="思考级别">
@@ -157,8 +205,68 @@
           </div>
         </div>
         <div class="action-divider"></div>
-        <div class="btn-send" :class="{ loading, disabled: !modelValue.trim() }" @click="handleSend">
-          <div class="send-icon"></div>
+        <div class="btn-send" :class="{ disabled: !streaming && (!modelValue.trim() || loading) }" @click="handleSendClick">
+          <div class="send-icon" :class="{ 'stop-icon': streaming }"></div>
+        </div>
+      </div>
+      <!-- 会话列表弹框 -->
+      <div v-if="showSessionPopup" ref="sessionPopupRef" class="session-popup" @click.stop @pointerdown.stop>
+        <div class="session-popup-header">
+          <span>选择会话</span>
+          <span class="session-count">{{ selectedSessions.length }}/5</span>
+        </div>
+        <div class="session-list">
+          <div
+            v-for="(task, index) in filteredSessionList"
+            :key="task.id"
+            class="session-item"
+            :class="{
+              active: index === activeSessionIndex,
+              selected: isSessionSelected(task.id),
+              disabled: (selectedSessions.length >= 5 && !isSessionSelected(task.id)) || task.id === props.currentTaskId,
+            }"
+            @click="handleSelectSession(task)"
+          >
+            <div class="session-checkbox" :class="{ checked: isSessionSelected(task.id) }">
+              <span v-if="isSessionSelected(task.id)" class="check-mark">✓</span>
+            </div>
+            <div class="session-item-icon"></div>
+            <span class="session-item-name">{{ task.name }}</span>
+          </div>
+          <div v-if="filteredSessionList.length === 0" class="session-empty">暂无匹配会话</div>
+        </div>
+        <div class="session-popup-footer">
+          <span v-if="selectedSessions.length >= 5" class="max-hint">最多选择5个会话</span>
+          <span v-else>点击选择会话(最多5个)</span>
+        </div>
+      </div>
+      <!-- 技能列表弹框 -->
+      <div v-if="showSkillPopup" ref="skillPopupRef" class="skill-popup" @click.stop @pointerdown.stop>
+        <div class="skill-popup-header">
+          <span>选择技能</span>
+          <span class="skill-count">0/1</span>
+        </div>
+        <div class="skill-list">
+          <div
+            v-for="(skill, index) in filteredSkillList"
+            :key="skill.name"
+            class="skill-item"
+            :class="{
+              active: index === activeSkillIndex,
+              selected: isSkillSelected(skill.name),
+            }"
+            @click="handleSelectSkill(skill)"
+          >
+            <div class="skill-item-checkbox" :class="{ checked: isSkillSelected(skill.name) }">
+              <span v-if="isSkillSelected(skill.name)" class="check-mark">✓</span>
+            </div>
+            <div class="skill-item-icon"></div>
+            <span class="skill-item-name">{{ skill.display_name }}</span>
+          </div>
+          <div v-if="filteredSkillList.length === 0" class="skill-empty">暂无匹配技能</div>
+        </div>
+        <div class="skill-popup-footer">
+          <span>点击选择技能(仅可选1个)</span>
         </div>
       </div>
     </div>
@@ -166,34 +274,53 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, computed } from 'vue';
+  import { ref, computed, watch, nextTick, onMounted } from 'vue';
   import { onClickOutside } from '@vueuse/core';
   import { CloseOutlined } from '@ant-design/icons-vue';
   import { formatFileSize } from './utils';
-  import type { AttachedFile } from './types';
+  import { getSkills } from '../../api';
+  import type { AttachedFile, Task } from './types';
 
   const props = withDefaults(
     defineProps<{
       modelValue: string;
       pendingFile: AttachedFile | null;
       loading: boolean;
+      streaming?: boolean;
       contextUsed?: number;
       contextMax?: number;
+      contextPercent?: number;
+      contextBreakdown?: { messages: number; mcp: number; skills: number; system_prompt: number; other: number };
+      initialThinkLevel?: string;
+      initialEditMode?: string;
+      taskList?: Task[];
+      currentTaskId?: string;
     }>(),
     {
       contextUsed: 0,
-      contextMax: 200000,
+      contextMax: 1000000,
+      contextPercent: 0,
+      streaming: false,
+      contextBreakdown: () => ({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 }),
+      initialThinkLevel: 'off',
+      initialEditMode: 'full',
+      taskList: () => [],
+      currentTaskId: '',
     }
   );
 
   const emit = defineEmits<{
     (e: 'update:modelValue', value: string): void;
     (e: 'send'): void;
+    (e: 'stop'): void;
     (e: 'file-upload'): void;
     (e: 'file-change', event: Event): void;
     (e: 'remove-pending-file'): void;
     (e: 'model-change', modelKey: string): void;
     (e: 'think-level-change', levelKey: string): void;
+    (e: 'edit-mode-change', modeKey: string): void;
+    (e: 'update:selectedSessions', sessions: Array<{ id: string; name: string }>): void;
+    (e: 'update:selectedSkill', skill: { name: string; display_name: string } | null): void;
   }>();
 
   const handleSelectModel = (item: ModelOption) => {
@@ -211,8 +338,7 @@
   const showContextPopup = ref(false);
 
   const contextPercent = computed(() => {
-    if (!props.contextMax) return 0;
-    return Math.min(100, Math.round((props.contextUsed / props.contextMax) * 100));
+    return props.contextPercent;
   });
 
   const contextCircumference = computed(() => 2 * Math.PI * 13);
@@ -234,6 +360,11 @@
     return props.contextMax.toLocaleString();
   });
 
+  const formatTokens = (tokens: number) => {
+    if (tokens >= 10000) return (tokens / 10000).toFixed(1) + '万';
+    return tokens.toLocaleString();
+  };
+
   const toggleContextPopup = () => {
     showContextPopup.value = !showContextPopup.value;
   };
@@ -289,6 +420,15 @@
 
   const selectedThinkLevel = ref<ThinkLevelOption>(thinkLevelOptions[0]);
 
+  watch(
+    () => props.initialThinkLevel,
+    (level) => {
+      const found = thinkLevelOptions.find((opt) => opt.key === level);
+      if (found) selectedThinkLevel.value = found;
+    },
+    { immediate: true }
+  );
+
   const toggleThinkPopup = () => {
     showThinkPopup.value = !showThinkPopup.value;
   };
@@ -313,14 +453,23 @@
   }
 
   const editModeOptions: EditModeOption[] = [
-    { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
-    { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
+    // { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
+    // { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
     { key: 'plan', label: '计划模式', desc: '编辑前先出计划' },
     { key: 'full', label: '完全访问', desc: '减少确认次数' },
   ];
 
   const selectedEditMode = ref<EditModeOption>(editModeOptions[1]);
 
+  watch(
+    () => props.initialEditMode,
+    (mode) => {
+      const found = editModeOptions.find((opt) => opt.key === mode);
+      if (found) selectedEditMode.value = found;
+    },
+    { immediate: true }
+  );
+
   onClickOutside(editModePopupRef, () => {
     showEditModePopup.value = false;
   });
@@ -332,6 +481,7 @@
   const handleSelectEditMode = (item: EditModeOption) => {
     selectedEditMode.value = item;
     showEditModePopup.value = false;
+    emit('edit-mode-change', item.key);
   };
 
   onClickOutside(popupRef, () => {
@@ -351,16 +501,311 @@
     showPopup.value = false;
   };
 
+  // 会话列表弹框
+  const textareaRef = ref<InstanceType<any>>();
+  const sessionPopupRef = ref<HTMLElement>();
+  const showSessionPopup = ref(false);
+  const activeSessionIndex = ref(0);
+  const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
+  const sessionPopupManual = ref(false);
+  let isRemovingTag = false;
+  let sessionJustSelected = false;
+
+  // 技能列表弹框
+  interface SkillItem {
+    name: string;
+    display_name: string;
+    path: string;
+    description: string;
+    enabled: boolean;
+    agents: string[];
+    has_skill_md: boolean;
+  }
+
+  const skillPopupRef = ref<HTMLElement>();
+  const showSkillPopup = ref(false);
+  const activeSkillIndex = ref(0);
+  const selectedSkill = ref<{ name: string; display_name: string } | null>(null);
+  const skillPopupManual = ref(false);
+  const skillList = ref<SkillItem[]>([]);
+  let skillJustSelected = false;
+
+  const fetchSkillList = async () => {
+    try {
+      const res = await getSkills();
+      skillList.value = res?.skills || [];
+    } catch {
+      skillList.value = [];
+    }
+  };
+
+  onMounted(() => {
+    fetchSkillList();
+  });
+
+  const isSkillSelected = (name: string) => {
+    return selectedSkill.value?.name === name;
+  };
+
+  const isSessionSelected = (sessionId: string) => {
+    return selectedSessions.value.some((s) => s.id === sessionId);
+  };
+
+  const sessionSearchText = computed(() => {
+    const val = props.modelValue || '';
+    const hashIndex = val.lastIndexOf('#');
+    if (hashIndex === -1) return '';
+    const afterHash = val.substring(hashIndex + 1);
+    if (afterHash.includes(' ')) return '';
+    return afterHash.toLowerCase();
+  });
+
+  const filteredSessionList = computed(() => {
+    const list = props.taskList.filter((t) => t.sessionId);
+    if (!sessionSearchText.value) return list;
+    return list.filter((t) => t.name.toLowerCase().includes(sessionSearchText.value));
+  });
+
+  watch(filteredSessionList, () => {
+    activeSessionIndex.value = 0;
+  });
+
+  watch(showSessionPopup, (val) => {
+    if (val) activeSessionIndex.value = 0;
+  });
+
+  const skillSearchText = computed(() => {
+    const val = props.modelValue || '';
+    const dollarIndex = val.lastIndexOf('$');
+    if (dollarIndex === -1) return '';
+    const afterDollar = val.substring(dollarIndex + 1);
+    if (afterDollar.includes(' ')) return '';
+    return afterDollar.toLowerCase();
+  });
+
+  const filteredSkillList = computed(() => {
+    if (!skillSearchText.value) return skillList.value;
+    return skillList.value.filter(
+      (s) => s.display_name.toLowerCase().includes(skillSearchText.value) || s.name.toLowerCase().includes(skillSearchText.value)
+    );
+  });
+
+  watch(filteredSkillList, () => {
+    activeSkillIndex.value = 0;
+  });
+
+  watch(showSkillPopup, (val) => {
+    if (val) activeSkillIndex.value = 0;
+  });
+
+  const handleSessionKeydown = (e: KeyboardEvent) => {
+    if (showSkillPopup.value && filteredSkillList.value.length > 0) {
+      if (e.key === 'ArrowDown') {
+        e.preventDefault();
+        activeSkillIndex.value = (activeSkillIndex.value + 1) % filteredSkillList.value.length;
+      } else if (e.key === 'ArrowUp') {
+        e.preventDefault();
+        activeSkillIndex.value = (activeSkillIndex.value - 1 + filteredSkillList.value.length) % filteredSkillList.value.length;
+      } else if (e.key === 'Enter') {
+        e.preventDefault();
+        const skill = filteredSkillList.value[activeSkillIndex.value];
+        if (skill) {
+          skillJustSelected = true;
+          handleSelectSkill(skill);
+        }
+      }
+      return;
+    }
+    if (showSessionPopup.value && filteredSessionList.value.length > 0) {
+      if (e.key === 'ArrowDown') {
+        e.preventDefault();
+        activeSessionIndex.value = (activeSessionIndex.value + 1) % filteredSessionList.value.length;
+      } else if (e.key === 'ArrowUp') {
+        e.preventDefault();
+        activeSessionIndex.value = (activeSessionIndex.value - 1 + filteredSessionList.value.length) % filteredSessionList.value.length;
+      } else if (e.key === 'Enter') {
+        e.preventDefault();
+        const task = filteredSessionList.value[activeSessionIndex.value];
+        if (task) {
+          sessionJustSelected = true;
+          handleSelectSession(task);
+        }
+      }
+    }
+  };
+
   const handleInsertSession = () => {
     showPopup.value = false;
+    sessionPopupManual.value = true;
+    showSessionPopup.value = true;
+    const cur = props.modelValue || '';
+    const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
+    emit('update:modelValue', `${cur}${needsSpace}#`);
+    nextTick(() => {
+      const el = textareaRef.value?.$el?.querySelector('textarea') || textareaRef.value?.$el;
+      if (el) el.focus();
+    });
+  };
+
+  const handleSelectSession = (task: Task) => {
+    if (task.id === props.currentTaskId) return;
+    const idx = selectedSessions.value.findIndex((s) => s.id === task.id);
+    if (idx >= 0) {
+      selectedSessions.value.splice(idx, 1);
+    } else {
+      if (selectedSessions.value.length >= 5) return;
+      selectedSessions.value.push({ id: task.id, name: task.name });
+    }
+    removeSearchTag();
+    emit('update:selectedSessions', [...selectedSessions.value]);
+  };
+
+  const handleInsertSkill = () => {
+    showPopup.value = false;
+    showSessionPopup.value = false;
+    skillPopupManual.value = true;
+    showSkillPopup.value = true;
+    const cur = props.modelValue || '';
+    const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
+    emit('update:modelValue', `${cur}${needsSpace}$`);
+    nextTick(() => {
+      const el = textareaRef.value?.$el?.querySelector('textarea') || textareaRef.value?.$el;
+      if (el) el.focus();
+    });
+  };
+
+  const handleSelectSkill = (skill: SkillItem) => {
+    selectedSkill.value = { name: skill.name, display_name: skill.display_name };
+    removeSkillSearchTag();
+    showSkillPopup.value = false;
+    skillPopupManual.value = false;
+    emit('update:selectedSkill', { name: skill.name, display_name: skill.display_name });
   };
 
+  const removeSkillSearchTag = () => {
+    isRemovingTag = true;
+    const val = props.modelValue || '';
+    const dollarIndex = val.lastIndexOf('$');
+    if (dollarIndex === -1) {
+      isRemovingTag = false;
+      return;
+    }
+    emit('update:modelValue', val.substring(0, dollarIndex).trimEnd());
+    nextTick(() => {
+      isRemovingTag = false;
+    });
+  };
+
+  const removeSkill = () => {
+    selectedSkill.value = null;
+    emit('update:selectedSkill', null);
+  };
+
+  const removeSearchTag = () => {
+    isRemovingTag = true;
+    const val = props.modelValue || '';
+    const hashIndex = val.lastIndexOf('#');
+    if (hashIndex === -1) {
+      isRemovingTag = false;
+      return;
+    }
+    emit('update:modelValue', val.substring(0, hashIndex).trimEnd());
+    nextTick(() => {
+      isRemovingTag = false;
+    });
+  };
+
+  watch(
+    () => props.modelValue,
+    (newVal, oldVal) => {
+      if (isRemovingTag) return;
+      if (oldVal && !newVal) {
+        selectedSessions.value = [];
+        selectedSkill.value = null;
+      }
+    }
+  );
+
+  const removeSession = (sessionId: string) => {
+    selectedSessions.value = selectedSessions.value.filter((s) => s.id !== sessionId);
+    emit('update:selectedSessions', [...selectedSessions.value]);
+  };
+
+  const handleTextareaInput = (val: string) => {
+    emit('update:modelValue', val);
+
+    // 技能弹框 $ 检测
+    if (!skillPopupManual.value) {
+      const dollarIndex = val.lastIndexOf('$');
+      if (dollarIndex === -1) {
+        showSkillPopup.value = false;
+      } else {
+        const afterDollar = val.substring(dollarIndex + 1);
+        if (afterDollar.includes(' ')) {
+          showSkillPopup.value = false;
+        } else {
+          showSkillPopup.value = true;
+          showSessionPopup.value = false;
+        }
+      }
+    }
+
+    // 会话弹框 # 检测
+    if (!sessionPopupManual.value) {
+      const hashIndex = val.lastIndexOf('#');
+      if (hashIndex === -1) {
+        showSessionPopup.value = false;
+      } else {
+        const afterHash = val.substring(hashIndex + 1);
+        if (afterHash.includes(' ')) {
+          showSessionPopup.value = false;
+        } else {
+          showSessionPopup.value = true;
+          showSkillPopup.value = false;
+        }
+      }
+    }
+  };
+
+  const handleBackspace = (e: KeyboardEvent) => {
+    const val = props.modelValue || '';
+    if (val) return;
+    if (selectedSkill.value) {
+      e.preventDefault();
+      selectedSkill.value = null;
+      emit('update:selectedSkill', null);
+    } else if (selectedSessions.value.length > 0) {
+      e.preventDefault();
+      selectedSessions.value.pop();
+      emit('update:selectedSessions', [...selectedSessions.value]);
+    }
+  };
+
+  onClickOutside(sessionPopupRef, () => {
+    showSessionPopup.value = false;
+    sessionPopupManual.value = false;
+  });
+
+  onClickOutside(skillPopupRef, () => {
+    showSkillPopup.value = false;
+    skillPopupManual.value = false;
+  });
+
   const handleInsertCommand = () => {
     showPopup.value = false;
   };
 
   const handleEnterKey = (e: KeyboardEvent) => {
-    if (!e.shiftKey && props.modelValue.trim()) {
+    if (showSessionPopup.value || showSkillPopup.value) return;
+    if (sessionJustSelected) {
+      sessionJustSelected = false;
+      return;
+    }
+    if (skillJustSelected) {
+      skillJustSelected = false;
+      return;
+    }
+    if (!e.shiftKey && props.modelValue.trim() && !props.loading) {
       emit('send');
     }
   };
@@ -371,11 +816,20 @@
     }
   };
 
+  const handleSendClick = () => {
+    if (props.streaming) {
+      emit('stop');
+      return;
+    }
+    handleSend();
+  };
+
   defineExpose({ fileInputRef });
 </script>
 
 <style scoped lang="less">
   .chat-input-area {
+    position: relative;
     padding: 5px 10px;
     flex-shrink: 0;
     background-color: #002f4a;
@@ -582,6 +1036,10 @@
               background-image: var(--img-chat-popup-session-icon);
             }
 
+            .skill-icon {
+              background-image: var(--img-chat-options-skill-icon);
+            }
+
             .command-icon {
               background-image: var(--img-chat-popup-command-icon);
             }
@@ -746,6 +1204,15 @@
           border-radius: 8px;
           min-width: 200px;
           padding: 14px 16px;
+
+          &::before {
+            content: '';
+            position: absolute;
+            bottom: -12px;
+            left: 0;
+            right: 0;
+            height: 12px;
+          }
           z-index: 1000;
           box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
 
@@ -796,6 +1263,29 @@
               transition: width 0.3s ease;
             }
           }
+
+          .context-breakdown {
+            margin-top: 10px;
+            padding-top: 10px;
+            border-top: 1px solid rgba(255, 255, 255, 0.08);
+
+            .breakdown-item {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              padding: 3px 0;
+
+              .breakdown-label {
+                font-size: 12px;
+                color: #8b949e;
+              }
+
+              .breakdown-value {
+                font-size: 12px;
+                color: #e0e6ed;
+              }
+            }
+          }
         }
         .btn-model-wrapper {
           position: relative;
@@ -996,9 +1486,356 @@
             background-image: var(--img-chat-send-icon);
             background-repeat: no-repeat;
             background-size: 100% 100%;
+
+            &.stop-icon {
+              background-image: var(--img-chat-stop-icon);
+            }
           }
         }
       }
     }
+
+    .session-chips {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+      margin-bottom: 5px;
+
+      .session-chip {
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+        padding: 3px 6px 3px 4px;
+        background: rgba(10, 132, 255, 0.15);
+        border: 1px solid rgba(10, 132, 255, 0.3);
+        border-radius: 4px;
+        font-size: 12px;
+        color: #e0e6ed;
+        cursor: default;
+
+        .session-chip-icon {
+          width: 16px;
+          height: 16px;
+          background-image: var(--img-chat-popup-session-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+
+        .session-chip-name {
+          max-width: 150px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .session-chip-remove {
+          font-size: 10px;
+          color: #8b949e;
+          cursor: pointer;
+          padding: 0 2px;
+          transition: color 0.2s;
+
+          &:hover {
+            color: #ff4d4f;
+          }
+        }
+      }
+    }
+
+    .skill-chips {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+      margin-bottom: 5px;
+
+      .skill-chip {
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+        padding: 3px 6px 3px 4px;
+        background: rgba(10, 132, 255, 0.15);
+        border: 1px solid rgba(10, 132, 255, 0.3);
+        border-radius: 4px;
+        font-size: 12px;
+        color: #e0e6ed;
+        cursor: default;
+
+        .skill-chip-icon {
+          width: 16px;
+          height: 16px;
+          background-image: var(--img-chat-options-skill-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+
+        .skill-chip-name {
+          max-width: 150px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .skill-chip-remove {
+          font-size: 10px;
+          color: #8b949e;
+          cursor: pointer;
+          padding: 0 2px;
+          transition: color 0.2s;
+
+          &:hover {
+            color: #ff4d4f;
+          }
+        }
+      }
+    }
+
+    .session-popup {
+      position: absolute;
+      bottom: 100%;
+      left: 0;
+      right: 0;
+      margin-bottom: 8px;
+      background: #0a2a3f;
+      border: 1px solid #1a4a6f;
+      border-radius: 8px;
+      max-height: 300px;
+      display: flex;
+      flex-direction: column;
+      z-index: 1000;
+      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+      .session-popup-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 10px 14px 8px;
+        font-size: 13px;
+        color: #e0e6ed;
+        border-bottom: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+
+        .session-count {
+          font-size: 12px;
+          color: #3a8fd4;
+          font-weight: 500;
+        }
+      }
+
+      .session-list {
+        flex: 1;
+        overflow-y: auto;
+        padding: 6px;
+
+        .session-item {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          padding: 8px 10px;
+          cursor: pointer;
+          border-radius: 6px;
+          transition: background 0.2s;
+
+          &:hover {
+            background: rgba(10, 132, 255, 0.15);
+          }
+
+          &.active {
+            background: rgba(10, 132, 255, 0.2);
+          }
+
+          &.selected {
+            background: rgba(10, 132, 255, 0.12);
+          }
+
+          &.disabled {
+            opacity: 0.4;
+            cursor: not-allowed;
+
+            &:hover {
+              background: transparent;
+            }
+          }
+
+          .session-checkbox {
+            width: 16px;
+            height: 16px;
+            border: 1.5px solid rgba(255, 255, 255, 0.25);
+            border-radius: 3px;
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.2s;
+
+            &.checked {
+              background: #3a8fd4;
+              border-color: #3a8fd4;
+            }
+
+            .check-mark {
+              font-size: 11px;
+              color: #fff;
+              line-height: 1;
+            }
+          }
+
+          .session-item-icon {
+            width: 18px;
+            height: 18px;
+            background-image: var(--img-chat-popup-session-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            flex-shrink: 0;
+          }
+
+          .session-item-name {
+            font-size: 13px;
+            color: #e0e6ed;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+
+        .session-empty {
+          text-align: center;
+          color: #8b949e;
+          font-size: 13px;
+          padding: 20px 0;
+        }
+      }
+
+      .session-popup-footer {
+        padding: 8px 14px;
+        font-size: 12px;
+        color: #8b949e;
+        text-align: center;
+        border-top: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+
+        .max-hint {
+          color: #faad14;
+        }
+      }
+    }
+
+    .skill-popup {
+      position: absolute;
+      bottom: 100%;
+      left: 0;
+      right: 0;
+      margin-bottom: 8px;
+      background: #0a2a3f;
+      border: 1px solid #1a4a6f;
+      border-radius: 8px;
+      max-height: 300px;
+      display: flex;
+      flex-direction: column;
+      z-index: 1000;
+      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+      .skill-popup-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 10px 14px 8px;
+        font-size: 13px;
+        color: #e0e6ed;
+        border-bottom: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+
+        .skill-count {
+          font-size: 12px;
+          color: #3a8fd4;
+          font-weight: 500;
+        }
+      }
+
+      .skill-list {
+        flex: 1;
+        overflow-y: auto;
+        padding: 6px;
+
+        .skill-item {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          padding: 8px 10px;
+          cursor: pointer;
+          border-radius: 6px;
+          transition: background 0.2s;
+
+          &:hover {
+            background: rgba(10, 132, 255, 0.15);
+          }
+
+          &.active {
+            background: rgba(10, 132, 255, 0.2);
+          }
+
+          &.selected {
+            background: rgba(10, 132, 255, 0.12);
+          }
+
+          .skill-item-checkbox {
+            width: 16px;
+            height: 16px;
+            border: 1.5px solid rgba(255, 255, 255, 0.25);
+            border-radius: 3px;
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.2s;
+
+            &.checked {
+              background: #3a8fd4;
+              border-color: #3a8fd4;
+            }
+
+            .check-mark {
+              font-size: 11px;
+              color: #fff;
+              line-height: 1;
+            }
+          }
+
+          .skill-item-icon {
+            width: 18px;
+            height: 18px;
+            background-image: var(--img-chat-options-skill-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            flex-shrink: 0;
+          }
+
+          .skill-item-name {
+            font-size: 13px;
+            color: #e0e6ed;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+
+        .skill-empty {
+          text-align: center;
+          color: #8b949e;
+          font-size: 13px;
+          padding: 20px 0;
+        }
+      }
+
+      .skill-popup-footer {
+        padding: 8px 14px;
+        font-size: 12px;
+        color: #8b949e;
+        text-align: center;
+        border-top: 1px solid rgba(63, 80, 106, 0.3);
+        flex-shrink: 0;
+      }
+    }
   }
 </style>

+ 177 - 51
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -24,6 +24,20 @@
                     <span class="thinking-title">思考过程 持续了几秒</span>
                   </div>
                 </div>
+
+                <!-- thinking_token 思考内容 -->
+                <div v-if="message.thinkingContent" class="thinking-content-card">
+                  <div class="thinking-content-header" @click="toggleThinkingContent(index)">
+                    <span class="thinking-content-title">模型思考</span>
+                    <div :class="['thinking-arrow', { expanded: !collapsedThinkingContent[index] }]">
+                      <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
+                    </div>
+                  </div>
+                  <div v-show="!collapsedThinkingContent[index]" class="thinking-content-body">
+                    {{ message.thinkingContent }}
+                  </div>
+                </div>
+
                 <div class="thinking-body">
                   <div
                     v-for="(group, groupIdx) in groupStepsByAgent(message.thinkingSteps)"
@@ -66,19 +80,6 @@
                             <span v-if="step.tool" class="step-tool-name">{{ step.tool }}</span>
                           </div>
                         </div>
-                        <!-- updated_todo_list -->
-                        <div v-else-if="step.type === 'updated_todo_list'" class="step-item step-todo">
-                          <div class="step-content">
-                            <div class="todo-list">
-                              <div v-for="(todo, todoIdx) in step.todos" :key="todoIdx" class="todo-item">
-                                <span :class="['todo-status', todo.status]">
-                                  {{ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '●' : '○' }}
-                                </span>
-                                <span class="todo-text">{{ todo.content }}</span>
-                              </div>
-                            </div>
-                          </div>
-                        </div>
                         <!-- tool_result -->
                         <div v-else-if="step.type === 'tool_result'" class="step-item step-tool-result">
                           <div class="step-content">
@@ -109,6 +110,19 @@
               <span class="dot"></span>
               <span class="dot"></span>
             </div>
+
+            <!-- 计划审批 UI -->
+            <div v-if="message.isPendingApproval" class="approval-card">
+              <div class="approval-icon"></div>
+              <div class="approval-content">
+                <div class="approval-title">执行计划待审批</div>
+                <div class="approval-desc">{{ message.pendingApprovalData?.message }}</div>
+                <div class="approval-actions">
+                  <button class="approval-btn approve-btn" @click="emit('approve')">同意</button>
+                  <button class="approval-btn reject-btn" @click="emit('reject')">拒绝</button>
+                </div>
+              </div>
+            </div>
           </div>
           <!-- AI消息操作栏 -->
           <div class="message-actions">
@@ -193,16 +207,24 @@
   let timerInterval: ReturnType<typeof setInterval> | null = null;
 
   const getDurationText = (message: Message): string => {
-    // 优先使用后端返回的 duration_ms
+    // 优先使用后端返回的 duration_ms(已包含 baseDurationMs 累加)
     if (message.durationMs) {
       const totalSec = Math.round(message.durationMs / 1000);
       const min = Math.floor(totalSec / 60);
       const sec = totalSec % 60;
       return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
     }
-    // 前端实时计时
+    // 审批等待中 - 暂停显示中断前的累计时长
+    if (message.isPendingApproval && message.pendingApprovalData?.elapsedMs) {
+      const totalSec = Math.round(message.pendingApprovalData.elapsedMs / 1000);
+      const min = Math.floor(totalSec / 60);
+      const sec = totalSec % 60;
+      return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
+    }
+    // 前端实时计时(含中断前累计时长)
     if (message.generateStartTime && message.isLoading) {
-      const elapsed = Math.floor((nowTs.value - message.generateStartTime) / 1000);
+      const baseMs = message.baseDurationMs || 0;
+      const elapsed = Math.floor((nowTs.value - message.generateStartTime + baseMs) / 1000);
       const min = Math.floor(elapsed / 60);
       const sec = elapsed % 60;
       return `已工作 ${min}分${sec.toString().padStart(2, '0')}秒`;
@@ -217,10 +239,13 @@
   const emit = defineEmits<{
     (e: 'file-preview', file: AttachedFile): void;
     (e: 'download-word', url: string): void;
+    (e: 'approve'): void;
+    (e: 'reject'): void;
   }>();
 
   const messagesRef = ref<HTMLElement>();
   const collapsedThinking = reactive<Record<number, boolean>>({});
+  const collapsedThinkingContent = reactive<Record<number, boolean>>({});
   const expandedAgents = reactive<Record<string, boolean>>({});
   const isAtBottom = ref(true);
   const copiedMessageIndex = ref(-1);
@@ -342,6 +367,10 @@
     collapsedThinking[index] = !collapsedThinking[index];
   };
 
+  const toggleThinkingContent = (index: number) => {
+    collapsedThinkingContent[index] = !collapsedThinkingContent[index];
+  };
+
   const toggleAgent = (key: string) => {
     expandedAgents[key] = !expandedAgents[key];
   };
@@ -450,7 +479,6 @@
           display: flex;
           margin-bottom: 8px;
           opacity: 0;
-
         }
 
         .message-body {
@@ -530,6 +558,54 @@
               }
             }
 
+            .thinking-content-card {
+              margin: 4px 14px 8px;
+              border: 1px solid rgba(63, 80, 106, 0.25);
+              border-radius: 6px;
+              overflow: hidden;
+
+              .thinking-content-header {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                padding: 8px 12px;
+                cursor: pointer;
+                user-select: none;
+                transition: background 0.2s;
+
+                &:hover {
+                  background: rgba(10, 132, 255, 0.08);
+                }
+
+                .thinking-content-title {
+                  font-size: 12px;
+                  font-weight: 500;
+                  color: #8b949e;
+                }
+
+                .thinking-arrow {
+                  display: flex;
+                  align-items: center;
+                  transition: transform 0.2s;
+
+                  &.expanded {
+                    transform: rotate(180deg);
+                  }
+                }
+              }
+
+              .thinking-content-body {
+                padding: 8px 12px;
+                max-height: 200px;
+                overflow-y: auto;
+                font-size: 13px;
+                line-height: 1.6;
+                color: #8b949e;
+                white-space: pre-wrap;
+                word-break: break-word;
+              }
+            }
+
             .thinking-body {
               padding: 8px 14px;
 
@@ -653,40 +729,6 @@
                     }
                   }
                 }
-
-                /* updated_todo_list */
-                .step-todo {
-                  .todo-list {
-                    .todo-item {
-                      display: flex;
-                      align-items: flex-start;
-                      gap: 6px;
-                      padding: 2px 0;
-
-                      .todo-status {
-                        flex-shrink: 0;
-                        width: 16px;
-                        text-align: center;
-                        font-size: 12px;
-
-                        &.completed {
-                          color: #3fb950;
-                        }
-                        &.in_progress {
-                          color: #0a84ff;
-                        }
-                        &.pending {
-                          color: #8b949e;
-                        }
-                      }
-
-                      .todo-text {
-                        font-size: 13px;
-                        color: #b0b8c4;
-                      }
-                    }
-                  }
-                }
               }
             }
           }
@@ -699,6 +741,12 @@
             word-break: break-word;
             overflow-wrap: break-word;
 
+            :deep(.session-ref-icon) {
+              width: 14px;
+              height: 14px;
+              vertical-align: -2px;
+            }
+
             :deep(h1),
             :deep(h2),
             :deep(h3),
@@ -834,6 +882,78 @@
             opacity: 1;
           }
         }
+
+        .approval-card {
+          display: flex;
+          align-items: flex-start;
+          gap: 12px;
+          padding: 14px 16px;
+          margin-top: 12px;
+          background: #002f4a;
+          border: 1px solid #005482;
+          border-radius: 8px;
+
+          .approval-icon {
+            width: 36px;
+            height: 36px;
+            background-image: var(--img-chat-session-approve-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            flex-shrink: 0;
+          }
+
+          .approval-content {
+            flex: 1;
+
+            .approval-title {
+              font-size: 14px;
+              font-weight: 600;
+              color: #e0e6ed;
+              margin-bottom: 6px;
+            }
+
+            .approval-desc {
+              font-size: 13px;
+              color: #8b949e;
+              line-height: 1.5;
+              margin-bottom: 12px;
+            }
+
+            .approval-actions {
+              display: flex;
+              gap: 10px;
+
+              .approval-btn {
+                padding: 6px 20px;
+                border-radius: 6px;
+                font-size: 13px;
+                font-weight: 500;
+                cursor: pointer;
+                border: none;
+                transition: all 0.2s;
+
+                &.approve-btn {
+                  background: #0a84ff;
+                  color: #fff;
+
+                  &:hover {
+                    background: #3a9bfd;
+                  }
+                }
+
+                &.reject-btn {
+                  background: rgba(255, 255, 255, 0.08);
+                  color: #e0e6ed;
+                  border: 1px solid rgba(63, 80, 106, 0.5);
+
+                  &:hover {
+                    background: rgba(255, 255, 255, 0.15);
+                  }
+                }
+              }
+            }
+          }
+        }
       }
 
       .user-message {
@@ -864,6 +984,12 @@
             word-break: break-word;
             overflow-wrap: break-word;
 
+            :deep(.session-ref-icon) {
+              width: 14px;
+              height: 14px;
+              vertical-align: -2px;
+            }
+
             :deep(h1, h2, h3, h4, h5, h6) {
               color: #fff;
             }

+ 96 - 8
src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue

@@ -12,17 +12,40 @@
       </div>
       <div v-else-if="isPdfFile(file.name)" class="pdf-preview">
         <iframe v-if="file.preview" :src="file.preview" frameborder="0"></iframe>
-        <div v-else class="pdf-no-preview">无法加载PDF预览</div>
+        <div v-else class="no-preview-text">无法加载PDF预览</div>
       </div>
-      <div v-else class="text-preview">
+      <div v-else-if="isVideoFile(file.name)" class="video-preview">
+        <video v-if="file.preview" controls>
+          <source :src="file.preview" />
+          您的浏览器不支持视频预览
+        </video>
+        <div v-else class="no-preview-text">无法加载视频预览</div>
+      </div>
+      <div v-else-if="isAudioFile(file.name)" class="audio-preview">
+        <div class="audio-info">
+          <div class="audio-icon"></div>
+          <span class="audio-name">{{ file.name }}</span>
+        </div>
+        <audio v-if="file.preview" controls>
+          <source :src="file.preview" />
+          您的浏览器不支持音频预览
+        </audio>
+        <div v-else class="no-preview-text">无法加载音频预览</div>
+      </div>
+      <div v-else-if="isTextFile(file.name)" class="text-preview">
         <pre>{{ file.content || '文件内容加载中...' }}</pre>
       </div>
+      <div v-else class="no-preview">
+        <div class="no-preview-icon"></div>
+        <div class="no-preview-text">不支持预览此文件格式</div>
+        <div class="no-preview-hint">{{ getFileExtension(file.name) }} 格式文件暂不支持在线预览</div>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { isImageFile, isPdfFile } from './utils';
+  import { isImageFile, isPdfFile, isVideoFile, isAudioFile, isTextFile, getFileExtension } from './utils';
   import type { AttachedFile } from './types';
 
   defineProps<{
@@ -117,14 +140,51 @@
         height: 100%;
         border: none;
       }
+    }
+
+    .video-preview {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      video {
+        width: 100%;
+        max-height: 100%;
+        border-radius: 4px;
+      }
+    }
+
+    .audio-preview {
+      width: 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 16px;
+      padding: 20px 0;
 
-      .pdf-no-preview {
+      .audio-info {
         display: flex;
         align-items: center;
-        justify-content: center;
-        height: 100%;
-        color: #8b949e;
-        font-size: 14px;
+        gap: 10px;
+
+        .audio-icon {
+          width: 40px;
+          height: 40px;
+          background-image: var(--img-chat-attach-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+        }
+
+        .audio-name {
+          font-size: 14px;
+          color: #e0e6ed;
+        }
+      }
+
+      audio {
+        width: 100%;
       }
     }
 
@@ -137,5 +197,33 @@
         color: #e0e6ed;
       }
     }
+
+    .no-preview {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      height: 100%;
+      gap: 12px;
+
+      .no-preview-icon {
+        width: 64px;
+        height: 64px;
+        background-image: var(--img-chat-attach-icon);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        opacity: 0.4;
+      }
+    }
+
+    .no-preview-text {
+      color: #8b949e;
+      font-size: 14px;
+    }
+
+    .no-preview-hint {
+      color: #6e7681;
+      font-size: 12px;
+    }
   }
 </style>

+ 515 - 0
src/views/ventAI/manageAssistent/components/chatModal/SkillModal.vue

@@ -0,0 +1,515 @@
+<template>
+  <a-modal :visible="visible" title="模型技能" :width="1000" :footer="null" destroyOnClose @cancel="emit('close')" class="skill-modal">
+    <div class="skill-filter-bar">
+      <a-input v-model:value="searchKeyword" placeholder="搜索技能..." allow-clear size="small" class="skill-search-input" />
+      <a-select v-model:value="filterStatus" size="small" class="skill-filter-select">
+        <a-select-option value="all">全部</a-select-option>
+        <a-select-option value="enabled">已启用</a-select-option>
+        <a-select-option value="disabled">未启用</a-select-option>
+      </a-select>
+    </div>
+    <div class="skill-summary">
+      <span class="skill-summary-label">技能</span>
+      <span class="skill-summary-count">{{ filteredSkills.length }}项</span>
+    </div>
+    <div class="skill-list" v-if="!loading">
+      <div v-for="skill in filteredSkills" :key="skill.name" class="skill-item" @click="handleSkillClick(skill)">
+        <div class="skill-header">
+          <div class="skill-name-wrapper">
+            <div class="skill-item-icon"></div>
+            <span class="skill-name">{{ skill.display_name }}</span>
+          </div>
+          <a-tag :color="skill.enabled ? 'green' : 'default'" class="skill-toggle-tag" @click.stop="handleToggleSkill(skill)">
+            {{ skill.enabled ? '已启用' : '未启用' }}
+          </a-tag>
+        </div>
+        <div class="skill-desc">{{ skill.description }}</div>
+        <div v-if="skill.agents && skill.agents.length > 0" class="skill-agents">
+          <div class="agents-label">
+            <div class="agents-label-icon"></div>
+            <span>关联智能体</span>
+          </div>
+          <div class="agents-tags">
+            <span v-for="agent in skill.agents" :key="agent" class="agent-tag">{{ agent }}</span>
+          </div>
+        </div>
+        <div v-if="skillDetailMap[skill.name]" class="skill-detail">
+          <div class="skill-detail-content" v-html="renderMarkdown(skillDetailMap[skill.name])"></div>
+        </div>
+        <div v-else-if="detailLoadingMap[skill.name]" class="skill-detail-loading">
+          <a-spin size="small" />
+        </div>
+      </div>
+      <a-empty v-if="filteredSkills.length === 0" description="暂无技能" />
+    </div>
+    <div class="skill-loading" v-else>
+      <a-spin />
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch } from 'vue';
+  import { getSkills, toggleSkill, getSkillDetails } from '../../api';
+  import { renderMarkdown } from './utils';
+  import { message } from 'ant-design-vue';
+
+  interface Skill {
+    name: string;
+    display_name: string;
+    path: string;
+    description: string;
+    enabled: boolean;
+    agents: string[];
+    has_skill_md: boolean;
+  }
+
+  const props = defineProps<{ visible: boolean }>();
+  const emit = defineEmits<{ (e: 'close'): void }>();
+
+  const skills = ref<Skill[]>([]);
+  const loading = ref(false);
+  const skillDetailMap = reactive<Record<string, string>>({});
+  const detailLoadingMap = reactive<Record<string, boolean>>({});
+  const searchKeyword = ref('');
+  const filterStatus = ref<'all' | 'enabled' | 'disabled'>('all');
+
+  const filteredSkills = computed(() => {
+    return skills.value.filter((skill) => {
+      const keyword = searchKeyword.value.trim();
+      const matchKeyword = !keyword || skill.display_name.includes(keyword) || skill.name.includes(keyword);
+      const matchStatus = filterStatus.value === 'all' || (filterStatus.value === 'enabled' ? skill.enabled : !skill.enabled);
+      return matchKeyword && matchStatus;
+    });
+  });
+
+  const fetchSkills = async () => {
+    loading.value = true;
+    try {
+      const res = await getSkills();
+      skills.value = res?.skills || [];
+    } catch {
+      skills.value = [];
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const handleToggleSkill = async (skill: Skill) => {
+    try {
+      await toggleSkill(skill.name);
+      skill.enabled = !skill.enabled;
+      message.success(`${skill.display_name} 已${skill.enabled ? '启用' : '禁用'}`);
+    } catch {
+      message.error('操作失败,请重试');
+    }
+  };
+
+  const handleSkillClick = async (skill: Skill) => {
+    return;
+    if (skillDetailMap[skill.name]) {
+      delete skillDetailMap[skill.name];
+      return;
+    }
+    detailLoadingMap[skill.name] = true;
+    try {
+      const res = await getSkillDetails(skill.name);
+      skillDetailMap[skill.name] = res?.content || '';
+    } catch {
+      message.error('获取技能详情失败');
+    } finally {
+      detailLoadingMap[skill.name] = false;
+    }
+  };
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) fetchSkills();
+    }
+  );
+</script>
+
+<style scoped lang="less">
+  .skill-filter-bar {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 16px;
+    padding: 0 2px;
+
+    .skill-search-input {
+      flex: 1;
+      height: 36px;
+      background: rgba(10, 132, 255, 0.08);
+      border: 1px solid rgba(63, 80, 106, 0.4);
+      border-radius: 6px;
+      color: #e0e6ed;
+      transition: all 0.3s;
+
+      &:hover,
+      &:focus {
+        border-color: rgba(10, 132, 255, 0.5);
+        background: rgba(10, 132, 255, 0.12);
+      }
+
+      :deep(.zxm-input) {
+        background: transparent;
+        color: #e0e6ed;
+        font-size: 13px;
+      }
+    }
+
+    .skill-filter-select {
+      width: 130px;
+      height: 36px;
+
+      :deep(.zxm-select-selector) {
+        background: rgba(10, 132, 255, 0.08) !important;
+        border: 1px solid rgba(63, 80, 106, 0.4) !important;
+        border-radius: 6px !important;
+        color: #e0e6ed;
+        height: 36px;
+      }
+      :deep(.zxm-select-selection-item) {
+        line-height: 34px !important;
+      }
+    }
+  }
+
+  .skill-summary {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 0 2px;
+    margin-bottom: 12px;
+    font-size: 13px;
+
+    .skill-summary-label {
+      color: #e8edf3;
+      font-weight: 500;
+    }
+
+    .skill-summary-count {
+      color: #6a7a8a;
+    }
+  }
+
+  .skill-list {
+    max-height: 420px;
+    overflow-y: auto;
+    padding: 0 2px;
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background: transparent;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: rgba(63, 80, 106, 0.4);
+      border-radius: 3px;
+
+      &:hover {
+        background: rgba(63, 80, 106, 0.6);
+      }
+    }
+  }
+
+  .skill-loading {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 80px 0;
+  }
+
+  .skill-item {
+    padding: 16px 18px;
+    margin-bottom: 8px;
+    border-radius: 8px;
+    border: 1px solid rgba(63, 80, 106, 0.15);
+    background: rgba(10, 132, 255, 0.03);
+    cursor: pointer;
+    transition: all 0.25s ease;
+
+    &:hover {
+      background: rgba(10, 132, 255, 0.08);
+      border-color: rgba(10, 132, 255, 0.25);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .skill-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+
+  .skill-name-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+
+  .skill-item-icon {
+    width: 24px;
+    height: 24px;
+    flex-shrink: 0;
+    background-image: var(--img-chat-skill-item-icon);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: center;
+    filter: drop-shadow(0 0 4px rgba(10, 132, 255, 0.3));
+  }
+
+  .skill-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: #e8edf3;
+    letter-spacing: 0.3px;
+  }
+
+  .skill-toggle-tag {
+    cursor: pointer;
+    user-select: none;
+    padding: 2px 12px;
+    border-radius: 12px;
+    font-size: 12px;
+    transition: all 0.2s;
+    letter-spacing: 0.5px;
+
+    &:hover {
+      opacity: 0.85;
+      transform: scale(1.02);
+    }
+  }
+
+  .skill-desc {
+    font-size: 13px;
+    color: #8b949e;
+    line-height: 1.7;
+    margin-bottom: 10px;
+    padding-left: 34px;
+  }
+
+  .skill-label {
+    color: #5a6a7a;
+    margin-right: 4px;
+    font-size: 12px;
+  }
+
+  .skill-agents {
+    margin-top: 10px;
+    margin-left: 34px;
+    padding: 10px 14px;
+    background: rgba(10, 132, 255, 0.04);
+    border: 1px solid rgba(63, 80, 106, 0.18);
+    border-radius: 6px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .agents-label {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 12px;
+      color: #6a7a8a;
+      white-space: nowrap;
+      flex-shrink: 0;
+
+      .agents-label-icon {
+        width: 14px;
+        height: 14px;
+        background-image: var(--img-chat-agent-icon);
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        background-position: center;
+        opacity: 0.6;
+      }
+    }
+
+    .agents-tags {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: 6px;
+
+      .agent-tag {
+        display: inline-flex;
+        align-items: center;
+        padding: 3px 10px;
+        font-size: 12px;
+        color: #5cb8ff;
+        background: rgba(10, 132, 255, 0.1);
+        border: 1px solid rgba(10, 132, 255, 0.2);
+        border-radius: 4px;
+        letter-spacing: 0.3px;
+        transition: all 0.2s;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.18);
+          border-color: rgba(10, 132, 255, 0.35);
+        }
+      }
+    }
+  }
+
+  .skill-detail {
+    margin-top: 14px;
+    margin-left: 34px;
+    padding: 14px 16px;
+    background: rgba(10, 132, 255, 0.05);
+    border: 1px solid rgba(10, 132, 255, 0.15);
+    border-radius: 6px;
+    position: relative;
+    overflow: hidden;
+
+    .skill-detail-content {
+      font-size: 13px;
+      color: #c9d1d9;
+      line-height: 1.8;
+      word-break: break-word;
+
+      :deep(h1),
+      :deep(h2),
+      :deep(h3),
+      :deep(h4) {
+        color: #e8edf3;
+        margin: 12px 0 8px;
+        font-weight: 600;
+      }
+      :deep(h1) {
+        font-size: 18px;
+      }
+      :deep(h2) {
+        font-size: 16px;
+      }
+      :deep(h3) {
+        font-size: 14px;
+      }
+
+      :deep(p) {
+        margin: 6px 0;
+      }
+
+      :deep(ul),
+      :deep(ol) {
+        padding-left: 20px;
+        margin: 6px 0;
+      }
+
+      :deep(code) {
+        background: rgba(10, 132, 255, 0.12);
+        padding: 1px 5px;
+        border-radius: 3px;
+        font-size: 12px;
+        color: #79c0ff;
+      }
+
+      :deep(pre) {
+        background: rgba(0, 0, 0, 0.25);
+        padding: 12px;
+        border-radius: 6px;
+        overflow-x: auto;
+        margin: 8px 0;
+      }
+
+      :deep(pre code) {
+        background: transparent;
+        padding: 0;
+        color: #c9d1d9;
+      }
+
+      :deep(blockquote) {
+        border-left: 3px solid rgba(10, 132, 255, 0.4);
+        padding-left: 12px;
+        margin: 8px 0;
+        color: #8b949e;
+      }
+
+      :deep(table) {
+        border-collapse: collapse;
+        margin: 8px 0;
+        width: 100%;
+      }
+
+      :deep(th),
+      :deep(td) {
+        border: 1px solid rgba(63, 80, 106, 0.4);
+        padding: 6px 10px;
+        text-align: left;
+      }
+
+      :deep(th) {
+        background: rgba(10, 132, 255, 0.1);
+        font-weight: 600;
+      }
+
+      :deep(strong) {
+        font-weight: 600;
+        color: #e8edf3;
+      }
+
+      :deep(a) {
+        color: #58a6ff;
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  .skill-detail-loading {
+    margin-top: 14px;
+    margin-left: 34px;
+    display: flex;
+    justify-content: center;
+    padding: 16px 0;
+  }
+</style>
+
+<style lang="less">
+  .skill-modal {
+    .zxm-modal-content {
+      background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
+      border: 1px solid rgba(63, 80, 106, 0.4);
+      border-radius: 12px;
+      box-shadow:
+        0 8px 32px rgba(0, 0, 0, 0.4),
+        0 0 0 1px rgba(10, 132, 255, 0.1);
+    }
+
+    .zxm-modal-header {
+      background: transparent;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+      padding: 18px 24px;
+      border-radius: 10px 10px 0 0;
+    }
+
+    .zxm-modal-title {
+      color: #e8edf3;
+      font-size: 18px;
+      font-weight: 600;
+      letter-spacing: 1px;
+    }
+
+    .zxm-modal-body {
+      padding: 20px 24px 24px;
+    }
+
+    .zxm-modal-close {
+      .zxm-modal-close-x {
+        color: #8b949e;
+      }
+    }
+  }
+</style>

+ 53 - 39
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -2,13 +2,24 @@
   <div class="task-list-panel">
     <!-- 操作区域(顶部) -->
     <div class="options-area">
-      <div v-for="item in optionItems" :key="item.type" :class="['options-area-item', `options-${item.type}`]" @click="handleOptionClick(item.type)">
+      <div
+        v-for="item in optionItems"
+        :key="item.type"
+        v-show="item.type !== 'search'"
+        :class="['options-area-item', `options-${item.type}`]"
+        @click="handleOptionClick(item.type)"
+      >
         <div class="options-img">
           <div class="options-icon"></div>
         </div>
         <a-button size="small" type="primary">{{ item.label }}</a-button>
       </div>
     </div>
+
+    <!-- 搜索模式 -->
+    <div class="search-header">
+      <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
+    </div>
     <!-- 置顶任务区域 -->
     <div v-if="pinnedTaskList.length > 0" class="pinned-task-list">
       <div
@@ -17,18 +28,15 @@
         :class="['task-item', { active: currentTaskId === task.id }]"
         @click="emit('switch-task', task.id)"
       >
-        <div class="task-pin-icon pinned" @click.stop="togglePin(task.id)" title="取消置顶"></div>
+        <div class="task-pin-icon pinned" @click.stop="togglePin(task)" title="取消置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
-          <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
+          <span v-if="task.isStreaming" class="task-loading"></span>
+          <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
           <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
     </div>
-    <!-- 搜索模式 -->
-    <div v-if="activeOption === 'search'" class="search-header">
-      <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
-    </div>
 
     <div class="task-list">
       <div
@@ -37,22 +45,26 @@
         :class="['task-item', { active: currentTaskId === task.id }]"
         @click="emit('switch-task', task.id)"
       >
-        <div class="task-pin-icon" @click.stop="togglePin(task.id)" title="置顶"></div>
+        <div class="task-pin-icon" @click.stop="togglePin(task)" title="置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
-          <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
+          <span v-if="task.isStreaming" class="task-loading"></span>
+          <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
           <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
-      <div v-if="activeOption === 'search' && searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
+      <div v-if="searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
       <div v-else-if="filteredTaskList.length === 0" class="empty-hint"> 还没有任务 </div>
     </div>
+
+    <SkillModal :visible="skillVisible" @close="skillVisible = false" />
   </div>
 </template>
 
 <script setup lang="ts">
   import { ref, computed } from 'vue';
   import type { Task } from './types';
+  import SkillModal from './SkillModal.vue';
 
   const props = defineProps<{
     taskList: Task[];
@@ -63,49 +75,35 @@
     (e: 'switch-task', taskId: string): void;
     (e: 'create-task'): void;
     (e: 'delete-task', task: Task, index: number): void;
+    (e: 'pin-task', task: Task): void;
   }>();
 
   type OptionType = 'add' | 'search' | 'skill';
 
   const activeOption = ref<OptionType>('add');
   const searchKeyword = ref('');
-  const PINNED_TASK_KEY = 'chat_pinned_tasks';
-  const pinnedTaskIds = ref<string[]>(loadPinnedTasks());
-
-  function loadPinnedTasks(): string[] {
-    try {
-      const cached = localStorage.getItem(PINNED_TASK_KEY);
-      return cached ? JSON.parse(cached) : [];
-    } catch {
-      return [];
-    }
-  }
-
-  function savePinnedTasks(ids: string[]) {
-    localStorage.setItem(PINNED_TASK_KEY, JSON.stringify(ids));
-  }
+  const skillVisible = ref(false);
 
   const pinnedTaskList = computed(() => {
-    return props.taskList.filter((task) => pinnedTaskIds.value.includes(task.id));
+    let list = props.taskList.filter((task) => task.sortOrder === 1);
+    if (searchKeyword.value.trim()) {
+      const keyword = searchKeyword.value.trim().toLowerCase();
+      list = list.filter((task) => task.name.toLowerCase().includes(keyword));
+    }
+    return list;
   });
 
   const filteredTaskList = computed(() => {
-    let list = props.taskList.filter((task) => !pinnedTaskIds.value.includes(task.id));
-    if (activeOption.value === 'search' && searchKeyword.value.trim()) {
+    let list = props.taskList.filter((task) => task.sortOrder !== 1);
+    if (searchKeyword.value.trim()) {
       const keyword = searchKeyword.value.trim().toLowerCase();
       list = list.filter((task) => task.name.toLowerCase().includes(keyword));
     }
     return list;
   });
 
-  const togglePin = (taskId: string) => {
-    const index = pinnedTaskIds.value.indexOf(taskId);
-    if (index >= 0) {
-      pinnedTaskIds.value.splice(index, 1);
-    } else {
-      pinnedTaskIds.value.push(taskId);
-    }
-    savePinnedTasks(pinnedTaskIds.value);
+  const togglePin = (task: Task) => {
+    emit('pin-task', task);
   };
 
   const optionItems = [
@@ -114,7 +112,6 @@
     { type: 'skill' as OptionType, label: '技能' },
   ];
 
-
   const formatRelativeTime = (dateStr?: string) => {
     if (!dateStr) return '刚刚';
     const now = Date.now();
@@ -130,7 +127,7 @@
     if (type === 'add') {
       emit('create-task');
     } else if (type === 'skill') {
-      // TODO: 技能点击事件
+      skillVisible.value = true;
     } else {
       activeOption.value = type;
       searchKeyword.value = '';
@@ -263,6 +260,16 @@
           white-space: nowrap;
         }
 
+        .task-loading {
+          width: 14px;
+          height: 14px;
+          flex-shrink: 0;
+          border: 2px solid rgba(10, 132, 255, 0.2);
+          border-top-color: rgba(10, 132, 255, 0.8);
+          border-radius: 50%;
+          animation: task-spin 0.8s linear infinite;
+        }
+
         .task-delete {
           flex-shrink: 0;
           width: 28px;
@@ -280,7 +287,8 @@
           }
         }
 
-        &:hover .task-time {
+        &:hover .task-time,
+        &:hover .task-loading {
           display: none;
         }
 
@@ -297,6 +305,12 @@
       }
     }
 
+    @keyframes task-spin {
+      to {
+        transform: rotate(360deg);
+      }
+    }
+
     .options-area {
       display: flex;
       flex-direction: column;

+ 156 - 0
src/views/ventAI/manageAssistent/components/chatModal/TodoListBar.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="todo-list-bar">
+    <div class="todo-list-bar-header" @click="expanded = !expanded">
+      <div class="todo-list-bar-header-left">
+        <span class="todo-list-bar-title">任务进度</span>
+        <span class="todo-list-bar-summary">{{ completedCount }}/{{ todos.length }}</span>
+      </div>
+      <div :class="['todo-list-bar-arrow', { expanded }]">
+        <img :src="arrowIcon" width="12" height="12" />
+      </div>
+    </div>
+    <div v-show="expanded" class="todo-list-bar-body">
+      <div v-for="(todo, idx) in todos" :key="idx" :class="['todo-bar-item', todo.status]">
+        <span :class="['todo-bar-dot', todo.status]"></span>
+        <span :class="['todo-bar-text', todo.status]">{{ todo.content }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue';
+
+  const props = defineProps<{
+    todos: Array<{ content: string; status: string }>;
+  }>();
+
+  const expanded = ref(true);
+
+  const completedCount = computed(() => props.todos.filter((t) => t.status === 'completed').length);
+
+  const getCssUrl = (varName: string) => {
+    const val = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
+    return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
+  };
+
+  const arrowIcon = ref('');
+  onMounted(() => {
+    arrowIcon.value = getCssUrl('--img-chat-arrow-icon');
+  });
+</script>
+
+<style scoped lang="less">
+  .todo-list-bar {
+    display: flex;
+    flex-direction: column;
+    margin: 0 16px;
+    background: #0a2a3f;
+    border: 1px solid #1a4a6f;
+    border-radius: 8px;
+    flex-shrink: 0;
+    overflow: hidden;
+
+    .todo-list-bar-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 10px 16px;
+      cursor: pointer;
+      user-select: none;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(10, 132, 255, 0.08);
+      }
+    }
+
+    .todo-list-bar-header-left {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .todo-list-bar-title {
+      font-size: 13px;
+      font-weight: 500;
+      color: #e0e6ed;
+    }
+
+    .todo-list-bar-summary {
+      font-size: 12px;
+      color: #8b949e;
+      background: rgba(10, 132, 255, 0.12);
+      padding: 1px 8px;
+      border-radius: 10px;
+    }
+
+    .todo-list-bar-arrow {
+      display: flex;
+      align-items: center;
+      transition: transform 0.2s;
+
+      &.expanded {
+        transform: rotate(180deg);
+      }
+    }
+
+    .todo-list-bar-body {
+      padding: 0 16px 10px;
+      display: flex;
+      flex-direction: column;
+      gap: 2px;
+    }
+
+    .todo-bar-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 6px 10px;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover {
+        background: rgba(10, 132, 255, 0.06);
+      }
+
+      &.completed .todo-bar-text {
+        color: #5a6a7e;
+        text-decoration: line-through;
+      }
+    }
+
+    .todo-bar-dot {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      flex-shrink: 0;
+
+      &.completed {
+        background: #3fb950;
+      }
+      &.in_progress {
+        background: #0a84ff;
+        box-shadow: 0 0 6px rgba(10, 132, 255, 0.5);
+      }
+      &.pending {
+        background: #484f5a;
+      }
+    }
+
+    .todo-bar-text {
+      font-size: 13px;
+      line-height: 1.4;
+
+      &.completed {
+        color: #5a6a7e;
+      }
+      &.in_progress {
+        color: #e0e6ed;
+      }
+      &.pending {
+        color: #8b949e;
+      }
+    }
+  }
+</style>

+ 15 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -30,19 +30,32 @@ export interface ThinkingStep {
   timestamp: number;
 }
 
+export interface PendingApprovalData {
+  message: string;
+  sessionId: string;
+  threadId?: string;
+  interrupts?: any[];
+  elapsedMs?: number;
+  prevThinkingContent?: string;
+}
+
 export interface Message {
   type: 'user' | 'ai';
   content: string;
+  thinkingContent?: string;
   time: string;
   createdAt?: string;
   isTerminal?: boolean;
   isLoading?: boolean;
+  isPendingApproval?: boolean;
+  pendingApprovalData?: PendingApprovalData;
   attachedFile?: AttachedFile;
   agentAnswers?: AgentAnswer[];
   thinkingSteps?: ThinkingStep[];
   sessionId?: string;
   wordDownloadUrl?: string;
   durationMs?: number;
+  baseDurationMs?: number;
   generateStartTime?: number;
 }
 
@@ -54,6 +67,8 @@ export interface Task {
   attachedFiles: AttachedFile[];
   wordUrl?: string;
   updatedAt?: string;
+  sortOrder?: number;
+  isStreaming?: boolean;
 }
 
 export type SectionType = 'select' | 'params' | 'result';

+ 15 - 2
src/views/ventAI/manageAssistent/components/chatModal/utils.ts

@@ -107,7 +107,20 @@ export const formatFileSize = (bytes: number): string => {
   return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
 };
 
-export const isImageFile = (filename: string): boolean =>
-  /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(filename);
+export const isImageFile = (filename: string): boolean => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif)$/i.test(filename);
 
 export const isPdfFile = (filename: string): boolean => /\.pdf$/i.test(filename);
+
+export const isVideoFile = (filename: string): boolean => /\.(mp4|webm|ogg|mov|avi|mkv|flv|wmv|m4v)$/i.test(filename);
+
+export const isAudioFile = (filename: string): boolean => /\.(mp3|wav|ogg|aac|flac|wma|m4a|opus)$/i.test(filename);
+
+export const isTextFile = (filename: string): boolean =>
+  /\.(txt|md|json|csv|xml|yaml|yml|log|ini|toml|cfg|conf|env|gitignore|dockerignore|editorconfig|prettierrc|eslintrc|babelrc|properties|sh|bat|ps1|cmd|bash|zsh|fish|sql|graphql|proto|js|ts|jsx|tsx|vue|css|scss|sass|less|html|htm|svelte|py|java|kt|kts|c|cpp|h|hpp|cc|cxx|cs|go|rs|rb|php|swift|m|mm|r|R|lua|pl|pm|hs|ex|exs|erl|clj|scala|dart|zig|nim|v|sol|tf|hcl|gradle|cmake|makefile|mk)$/i.test(
+    filename
+  );
+
+export const getFileExtension = (filename: string): string => {
+  const match = filename.match(/\.([^.]+)$/);
+  return match ? `.${match[1]}`.toUpperCase() : '';
+};

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff