Prechádzať zdrojové kódy

feat(frontend): 增加设备配置排序功能

- 新增排序模式,用户可拖拽行进行排序
- 添加保存排序和取消排序功能
- 实现重命名配置和重新初始化配置功能- 优化用户界面,增加排序提示和禁用某些按钮
- 新增后端接口 updateDeviceConfigSort 用于保存排序
panqiuyao 3 mesiacov pred
rodič
commit
2008fc707a

+ 2 - 1
frontend/src/utils/ipc.ts

@@ -32,7 +32,8 @@ const icpList = {
         updateLeftRightConfig:'controller.setting.updateLeftRightConfig',
         getSysConfig: 'controller.setting.getSysConfig',
         updateSysConfigs: 'controller.setting.updateSysConfigs',
-        updateTabName: 'controller.setting.updateTabName'
+        updateTabName: 'controller.setting.updateTabName',
+        updateDeviceConfigSort: 'controller.setting.updateDeviceConfigSort'
     },
     takePhoto:{
         getPhotoRecords: 'controller.takephoto.getPhotoRecords',

+ 330 - 10
frontend/src/views/Setting/components/action_config.vue

@@ -1,6 +1,6 @@
 <template>
 
-  <el-tabs v-model="topsTab" type="card" class="top_tabs">
+  <el-tabs v-model="topsTab" type="card" class="top_tabs" :disabled="isSortMode">
     <el-tab-pane label="执行左脚程序" name="left">
     </el-tab-pane>
     <el-tab-pane label="执行右脚程序" name="right"></el-tab-pane>
@@ -11,19 +11,47 @@
            :class="item.id === activeTab.id ? 'active' : ''"
            v-for="item in tabs" :key="item.id"
            @click="toggleTab(item)" v-log="{ describe: { action: '点击切换动作Tab', tabName: item.mode_name, tabId: item.id } }"
+           :style="{ cursor: isSortMode ? 'not-allowed' : 'pointer', opacity: isSortMode ? 0.5 : 1 }"
       >{{item.mode_name}}</div>
   </div>
   <div class="form-table">
+    <div v-if="isSortMode" class="sort-tip">
+      <el-icon><Warning /></el-icon>
+      <span>排序模式:请拖拽行进行排序,完成后点击"保存排序"</span>
+    </div>
     <div class="btnBox">
-      <div class="primary-btn" @click="addRow" v-log="{ describe: { action: '点击新增一行' } }">新增一行</div>
-      <div class="primary-btn" @click="resetConfig" v-log="{ describe: { action: '点击重新初始化', tab: topsTab } }">重新初始化</div>
-      <div class="primary-btn" @click="reName" v-log="{ describe: { action: '点击重命名配置' } }">重命名配置</div>
-      <el-radio-group style="margin-left: 10px" v-model="selectID" @click="changeSelectId(activeTab.id)" v-log="{ describe: { action: '点击切换执行配置', tabId: activeTab.id } }">
+      <div class="primary-btn" @click="addRow" v-log="{ describe: { action: '点击新增一行' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">新增一行</div>
+      <div class="primary-btn" @click="resetConfig" v-log="{ describe: { action: '点击重新初始化', tab: topsTab } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">重新初始化</div>
+      <div class="primary-btn" @click="reName" v-log="{ describe: { action: '点击重命名配置' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">重命名配置</div>
+      <div class="primary-btn" @click="toggleSortMode" v-log="{ describe: { action: isSortMode ? '点击保存排序' : '点击排序' } }">
+        {{ isSortMode ? '保存排序' : '排序' }}
+      </div>
+      <div v-if="isSortMode" class="normal-btn" @click="cancelSort" v-log="{ describe: { action: '点击取消排序' } }">
+        取消排序
+      </div>
+      <el-radio-group style="margin-left: 10px" v-model="selectID" @click="changeSelectId(activeTab.id)" v-log="{ describe: { action: '点击切换执行配置', tabId: activeTab.id } }" :disabled="isSortMode">
         <el-radio :label="activeTab.id">切换成执行配置</el-radio>
       </el-radio-group>
     </div>
-    <el-table max-height="700" :data="tableData" style="width: 100%" border>
-<!--      <el-table-column prop="id" label="id" />-->
+    <el-table
+      max-height="700"
+      :data="tableData"
+      style="width: 100%"
+      border
+      row-key="id"
+      :row-class-name="getRowClassName"
+      ref="tableRef"
+    >
+      <el-table-column prop="sort" label="排序" width="80" v-if="isSortMode">
+        <template #default="scope">
+          <div class="sort-content">
+            <span class="sort-number">{{ scope.row.sort }}</span>
+            <div class="sort-handle">
+              <el-icon><Rank /></el-icon>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column prop="action_name" label="步骤" >
         <template #default="scope">
           {{ scope.row.action_name }}
@@ -63,7 +91,9 @@
 <script setup lang="ts">
 import { ref, defineProps, defineEmits , watch,onMounted, reactive,onBeforeUnmount } from 'vue'
 import EditDialog from "./EditDialog.vue";
-import { ElMessage, ElMessageBox } from 'element-plus';import client from "@/stores/modules/client";
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { Rank, Warning } from '@element-plus/icons-vue';
+import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc';
 const clientStore = client();
 import socket from "@/stores/modules/socket";
@@ -80,10 +110,18 @@ const activeTab = ref({}); // 当前激活的标签页
 const tabs = ref([]); // 所有标签页
 const editId = ref(0); // 当前编辑行的索引
 const selectID = ref(0) //当前默认的ID
+const isSortMode = ref(false); // 是否处于排序模式
+const originalTableData = ref([]); // 保存原始数据用于排序
+const tableRef = ref(null); // 表格引用
+let dragEventHandlers = null; // 拖拽事件处理器
 
 
 onBeforeUnmount(()=>{
     window.removeEventListener('beforeunload', handleBeforeUnload);
+    // 清理排序模式
+    if (isSortMode.value) {
+      exitSortMode();
+    }
 })
 
 onMounted(()=>{
@@ -106,7 +144,9 @@ const handleBeforeUnload = (e)=>{
  * 监听topsTab变化,获取对应标签页的设备配置列表。
  */
 watch(() => topsTab.value, (newTab) => {
-  getTopList();
+  if (!isSortMode.value) {
+    getTopList();
+  }
 });
 
 const getTopList = ()=>{
@@ -140,6 +180,8 @@ const getTopList = ()=>{
 
 //切换tab
 const toggleTab = (item) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   activeTab.value = item
   getList()
 };
@@ -150,6 +192,11 @@ const calibrationId = ref(null) //校准位
  * 获取设备配置列表。
  */
 const getList = () => {
+  // 如果正在排序模式,先退出
+  if (isSortMode.value) {
+    exitSortMode();
+  }
+
   clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
   let params = {
     tab_id: activeTab.value.id
@@ -177,6 +224,7 @@ const getList = () => {
 
 
 const changeSelectId = (id)=>{
+  if (isSortMode.value) return; // 排序模式下禁用
   if(id === selectID.value) return;
   clientStore.ipc.removeAllListeners(icpList.setting.updateLeftRightConfig);
   let params = {
@@ -205,6 +253,8 @@ const changeSelectId = (id)=>{
  * @param {number} index - 当前行的索引
  */
 const editRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   addRowData.value = {}
   dialogVisible.value = true;
   editId.value = row.id
@@ -216,6 +266,8 @@ const editRow = (row, index) => {
  * @param {number} index - 当前行的索引
  */
 const deleteRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.confirm('确定删除该步骤吗?', '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
@@ -242,6 +294,8 @@ const deleteRow = (row, index) => {
  * 重置设备配置。
  */
 const resetConfig = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   console.log(activeTab.value);
   ElMessageBox.confirm(`确定初始化${activeTab.value.mode_name}吗?`, '提示', {
     confirmButtonText: '确定',
@@ -267,6 +321,8 @@ const resetConfig = () => {
 };
 
 const reName = ()=>{
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.prompt('', '重命名配置', {
     confirmButtonText: '保存',
     cancelButtonText: '取消',
@@ -296,7 +352,7 @@ const reName = ()=>{
             ElMessage.success('重命名成功');
           }  else if(result.mssg){
             ElMessage.error(result.mssg);
-          }else {
+            }else {
             ElMessage.error('重命名失败');
           }
           clientStore.ipc.removeAllListeners(icpList.setting.updateTabName);
@@ -308,6 +364,8 @@ const reName = ()=>{
  * 新增一行配置。
  */
 const addRow = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   editId.value = -1
   let length = Number(tableData.value.length)+1
   addRowData.value =   {
@@ -329,6 +387,203 @@ const addRow = () => {
   editTitle.value = '新增步骤';
 };
 
+/**
+ * 切换排序模式
+ */
+const toggleSortMode = () => {
+  if (isSortMode.value) {
+    // 保存排序
+    saveSortOrder();
+  } else {
+    // 进入排序模式
+    enterSortMode();
+  }
+};
+
+/**
+ * 取消排序
+ */
+const cancelSort = () => {
+  exitSortMode();
+  ElMessage.info('已取消排序');
+};
+
+/**
+ * 进入排序模式
+ */
+const enterSortMode = () => {
+  isSortMode.value = true;
+  // 保存原始数据
+  originalTableData.value = JSON.parse(JSON.stringify(tableData.value));
+  // 为每行添加排序值
+  tableData.value.forEach((item, index) => {
+    item.sort = index + 1;
+  });
+
+  // 等待DOM更新后初始化Sortable
+  setTimeout(() => {
+    initSortable();
+  }, 100);
+
+  ElMessage.info('请拖拽行进行排序,完成后点击"保存排序"');
+};
+
+/**
+ * 初始化拖拽排序
+ */
+const initSortable = () => {
+  if (!tableRef.value) return;
+
+  const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody');
+  if (!tbody) return;
+
+  // 使用原生拖拽API实现排序
+  let draggedRow = null;
+  let draggedIndex = -1;
+
+  // 创建事件处理器
+  dragEventHandlers = {
+    handleDragStart: (e) => {
+      if (!isSortMode.value) return;
+      draggedRow = e.target.closest('tr');
+      if (draggedRow) {
+        e.dataTransfer.effectAllowed = 'move';
+        draggedRow.style.opacity = '0.5';
+        const rows = Array.from(tbody.querySelectorAll('tr'));
+        draggedIndex = rows.indexOf(draggedRow);
+      }
+    },
+
+    handleDragEnd: (e) => {
+      if (draggedRow) {
+        draggedRow.style.opacity = '';
+        draggedRow = null;
+        draggedIndex = -1;
+      }
+    },
+
+    handleDragOver: (e) => {
+      if (!isSortMode.value || !draggedRow) return;
+      e.preventDefault();
+      e.dataTransfer.dropEffect = 'move';
+    },
+
+    handleDrop: (e) => {
+      if (!isSortMode.value || !draggedRow) return;
+      e.preventDefault();
+
+      const dropRow = e.target.closest('tr');
+      if (!dropRow || dropRow === draggedRow) return;
+
+      // 获取目标行的索引
+      const rows = Array.from(tbody.querySelectorAll('tr'));
+      const dropIndex = rows.indexOf(dropRow);
+
+      if (draggedIndex !== -1 && dropIndex !== -1 && draggedIndex !== dropIndex) {
+        // 重新排序数据
+        const newData = [...tableData.value];
+        const [draggedItem] = newData.splice(draggedIndex, 1);
+        newData.splice(dropIndex, 0, draggedItem);
+
+        // 更新排序值
+        newData.forEach((item, index) => {
+          item.sort = index + 1;
+        });
+
+        tableData.value = newData;
+      }
+    }
+  };
+
+  // 添加事件监听器
+  tbody.addEventListener('dragstart', dragEventHandlers.handleDragStart);
+  tbody.addEventListener('dragend', dragEventHandlers.handleDragEnd);
+  tbody.addEventListener('dragover', dragEventHandlers.handleDragOver);
+  tbody.addEventListener('drop', dragEventHandlers.handleDrop);
+
+  // 为每行添加拖拽属性
+  const rows = tbody.querySelectorAll('tr');
+  rows.forEach(row => {
+    row.draggable = isSortMode.value;
+  });
+};
+
+/**
+ * 保存排序
+ */
+const saveSortOrder = () => {
+  // 准备排序数据
+  const sortData = tableData.value.map((item, index) => ({
+    id: item.id,
+    sort: index + 1
+  }));
+
+  console.log("sort_data",{
+    tab_id: activeTab.value.id,
+    sort_data: sortData
+  })
+  // 调用后端接口保存排序
+  clientStore.ipc.removeAllListeners(icpList.setting.updateDeviceConfigSort);
+  clientStore.ipc.send(icpList.setting.updateDeviceConfigSort, {
+    tab_id: activeTab.value.id,
+    sort_data: sortData
+  });
+
+  clientStore.ipc.on(icpList.setting.updateDeviceConfigSort, (event, result) => {
+    if (result.code == 0) {
+      ElMessage.success('排序保存成功');
+      isSortMode.value = false;
+      // 重新获取列表以更新数据
+      getList();
+    } else if (result.mssg) {
+      ElMessage.error(result.mssg);
+    } else {
+      ElMessage.error('排序保存失败');
+      // 保存失败时退出排序模式
+      exitSortMode();
+    }
+    clientStore.ipc.removeAllListeners(icpList.setting.updateDeviceConfigSort);
+  });
+};
+
+/**
+ * 获取行类名
+ */
+const getRowClassName = ({ row, rowIndex }) => {
+  if (isSortMode.value) {
+    return 'sortable-row';
+  }
+  return '';
+};
+
+/**
+ * 退出排序模式
+ */
+const exitSortMode = () => {
+  isSortMode.value = false;
+  // 恢复原始数据
+  tableData.value = JSON.parse(JSON.stringify(originalTableData.value));
+
+  // 移除拖拽事件监听器和属性
+  if (tableRef.value && dragEventHandlers) {
+    const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody');
+    if (tbody) {
+      // 移除事件监听器
+      tbody.removeEventListener('dragstart', dragEventHandlers.handleDragStart);
+      tbody.removeEventListener('dragend', dragEventHandlers.handleDragEnd);
+      tbody.removeEventListener('dragover', dragEventHandlers.handleDragOver);
+      tbody.removeEventListener('drop', dragEventHandlers.handleDrop);
+
+      // 移除拖拽属性
+      const rows = tbody.querySelectorAll('tr');
+      rows.forEach(row => {
+        row.draggable = false;
+      });
+    }
+    dragEventHandlers = null;
+  }
+};
+
 </script>
 
 <style lang="scss" scoped>
@@ -410,6 +665,71 @@ const addRow = () => {
   .cursor-pointer{
     cursor: pointer;
   }
+
+  // 排序相关样式
+  .sort-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 100%;
+
+    .sort-number {
+      font-weight: bold;
+      color: #2957FF;
+      font-size: 14px;
+    }
+
+    .sort-handle {
+      cursor: move;
+      color: #909399;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &:hover {
+        color: #2957FF;
+      }
+    }
+  }
+
+  :deep(.sortable-row) {
+    cursor: move;
+
+    &:hover {
+      background-color: #f5f7fa !important;
+    }
+  }
+
+  :deep(.el-table__row.sortable-row) {
+    transition: all 0.3s ease;
+  }
+
+  :deep(.el-table__body-wrapper tbody tr) {
+    &.sortable-row {
+      cursor: move;
+
+      &:hover {
+        background-color: #f5f7fa !important;
+      }
+    }
+  }
+
+  .disabled {
+    pointer-events: none;
+  }
+
+  .sort-tip {
+    background: #EAF3FF;
+    border: 1px solid #CBE1FF;
+    border-radius: 4px;
+    padding: 8px 12px;
+    margin-bottom: 12px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #2957FF;
+    font-size: 14px;
+  }
 }
 .editDialog{
   .el-dialog__body{

+ 2 - 2
public/dist/index.html

@@ -5,8 +5,8 @@
     <link rel="icon" type="image/svg+xml" href="./vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>智惠映AI自动拍照机</title>
-    <script type="module" crossorigin src="./assets/index-CNEyPYnH.js"></script>
-    <link rel="stylesheet" crossorigin href="./assets/index-CWbjMgyJ.css">
+    <script type="module" crossorigin src="./assets/index-B2uDJGOa.js"></script>
+    <link rel="stylesheet" crossorigin href="./assets/index-BPSCl2ER.css">
   </head>
   <body>
     <div id="app"></div>