Browse Source

feat(setting): 实现动作配置组件国际化并优化摄影页面UI

- 将动作配置组件中的硬编码文本替换为国际化标签
- 添加英语翻译文件支持动作配置组件国际化
- 修复摄影页面中v-key指令为v-bind:key的语法错误
- 重构摄影页面历史记录部分的UI布局和样式
- 添加批量选择和删除功能到摄影页面
- 优化图像预览和重拍功能的界面展示
- 更新下拉菜单数据源从generate改为generateMenu
- 添加页码控件支持历史记录分页浏览
panqiuyao 1 ngày trước cách đây
mục cha
commit
29287193f9

+ 45 - 0
frontend/src/locales/en.ts

@@ -540,6 +540,51 @@ export default {
     processComplete: 'Done. Click to open output folder',
   },
 
+  // Left/Right Foot Configuration
+  actionConfig: {
+    leftFootProgram: 'Execute Left Foot Program',
+    rightFootProgram: 'Execute Right Foot Program',
+    sortModeTip: 'Sort Mode: Drag rows to reorder, then click "Save Sort"',
+    addRow: 'Add Row',
+    resetInit: 'Reset Init',
+    renameConfig: 'Rename Config',
+    copyConfig: 'Copy Config',
+    saveSort: 'Save Sort',
+    cancelSort: 'Cancel Sort',
+    sort: 'Sort',
+    switchToConfig: 'Switch to Config',
+    sortColumn: 'Sort',
+    step: 'Step',
+    takePhoto: 'Take Photo',
+    photo: 'Photo',
+    noPhoto: 'No Photo',
+    operate: 'Operate',
+    edit: 'Edit',
+    copy: 'Copy',
+    delete: 'Delete',
+    calibration: 'Calibration',
+    confirmDelete: 'Are you sure to delete this step?',
+    tip: 'Tip',
+    confirm: 'Confirm',
+    cancel: 'Cancel',
+    deleteSuccess: 'Delete success',
+    confirmReset: 'Are you sure to reset {configName}?',
+    resetSuccess: 'Reset success',
+    renameTitle: 'Rename Config',
+    save: 'Save',
+    inputConfigName: 'Please enter config name',
+    renameSuccess: 'Rename success',
+    copyTitle: 'Copy Config',
+    copySuccess: 'Copy success',
+    newStep: 'New Step',
+    previewOpenedTip: 'You have opened the live preview popup. Please cancel or save and close the edit popup first, then close this window.',
+    previewOpenedTip2: 'You have opened the live preview popup. Please cancel or save and close the edit popup first, then close this window.',
+    sortModeEnterTip: 'Drag rows to reorder, then click "Save Sort"',
+    sortSuccess: 'Sort success',
+    cancelSortTip: 'Sort cancelled',
+    systemStep: 'System step cannot be operated',
+  },
+
   // 编辑行
   editRow: {
     title: 'Parameter Value Edit',

+ 51 - 0
frontend/src/locales/zh-CN.ts

@@ -28,6 +28,7 @@ export default {
     status: '状态',
     version: '版本',
     download: '下载',
+    selectAll: '全选',
     selected: '已选择',
     of: '共',
     close: '关闭',
@@ -241,6 +242,11 @@ export default {
     imageCount: '张图片',
     advancedGenerate: '高级生成',
     retake: '重拍',
+    retakeSingle: '重拍',
+    delete: '删除',
+    deleteAll: '删除所有',
+    startGenerate: '开始生成',
+    selectedCount: '已选择 {selected} 张图片 共 {total} 张图片',
     leftFootProgram: '执行左脚程序',
     rightFootProgram: '执行右脚程序',
     goodsSuccess: '商品货号{goods}获取成功,请在遥控器上按下左或右脚按键,启动拍摄',
@@ -547,6 +553,51 @@ export default {
     processComplete: '处理完毕,点击打开最终图片目录',
   },
 
+  // 左右脚配置
+  actionConfig: {
+    leftFootProgram: '执行左脚程序',
+    rightFootProgram: '执行右脚程序',
+    sortModeTip: '排序模式:请拖拽行进行排序,完成后点击"保存排序"',
+    addRow: '新增一行',
+    resetInit: '重新初始化',
+    renameConfig: '重命名配置',
+    copyConfig: '复制配置',
+    saveSort: '保存排序',
+    cancelSort: '取消排序',
+    sort: '排序',
+    switchToConfig: '切换成执行配置',
+    sortColumn: '排序',
+    step: '步骤',
+    takePhoto: '是否拍照',
+    photo: '拍照',
+    noPhoto: '不拍照',
+    operate: '操作',
+    edit: '编辑',
+    copy: '复制',
+    delete: '删除',
+    calibration: '校准位',
+    confirmDelete: '确定删除该步骤吗?',
+    tip: '提示',
+    confirm: '确定',
+    cancel: '取消',
+    deleteSuccess: '删除成功',
+    confirmReset: '确定初始化{configName}吗?',
+    resetSuccess: '重置成功',
+    renameTitle: '重命名配置',
+    save: '保存',
+    inputConfigName: '请输入配置名称',
+    renameSuccess: '重命名成功',
+    copyTitle: '复制配置',
+    copySuccess: '复制成功',
+    newStep: '新增步骤',
+    previewOpenedTip: '您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口',
+    previewOpenedTip2: '您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口,',
+    sortModeEnterTip: '请拖拽行进行排序,完成后点击"保存排序"',
+    sortSuccess: '排序成功',
+    cancelSortTip: '已取消排序',
+    systemStep: '系统步骤不可操作',
+  },
+
   // 编辑行
   editRow: {
     title: '参数值编辑',

+ 668 - 51
frontend/src/views/Photography/shot.vue

@@ -46,7 +46,7 @@
         </div>
       </div>
 
-      <div class="last-photo" v-show="showlastPhoto && (lastPhoto as any)?.file_path" v-key="(lastPhoto as any)?.file_path">
+      <div class="last-photo" v-show="showlastPhoto && (lastPhoto as any)?.file_path" v-bind:key="(lastPhoto as any)?.file_path">
         <el-button class="close-btn" type="danger" icon="Close" circle @click="closeLastPhoto" />
         <el-image  :src="getFilePath((lastPhoto as any)?.file_path || '')"  fit="contain" ></el-image>
       </div>
@@ -89,12 +89,12 @@
                   </div>
                 </div>
                 <div class="history-item-right">
-                  <el-dropdown :disabled="runLoading || takePictureLoading" trigger="click">
+                    <el-dropdown :disabled="runLoading || takePictureLoading" trigger="click">
                     <el-button :disabled="runLoading || takePictureLoading" size="small" plain>{{ $t('photoShot.advancedGenerate') }}</el-button>
                     <template #dropdown>
                       <el-dropdown-menu>
                         <el-dropdown-item
-                            v-for="menuItem in generate.children"
+                            v-for="menuItem in generateMenu.children"
                             @click.native="onGenerateCLick(menuItem, item)">{{ menuItem.name }}</el-dropdown-item>
                       </el-dropdown-menu>
                     </template>
@@ -111,62 +111,180 @@
                   class="history-item_image"
                   v-loading="!image.PhotoRecord.image_path && runAction.goods_art_no == item.goods_art_no"
                 >
-                  <span class="tag" v-if="!image.PhotoRecord.image_path">{{ image.action_name }}</span>
-                  <el-image
-                    v-if="image.PhotoRecord.image_path"
-                    :src="thumbnailMap[image.PhotoRecord.image_path] || getFilePath(image.PhotoRecord.image_path)"
-                    :preview-src-list="getPreviewImageList(item)"
-                    hide-on-click-modal
-                    :initial-index="getPreviewIndex(item, index)"
-                    class="preview-image"
-                    fit="contain"
-                    :preview-teleported="true"
-                    lazy
-                  >
-                    <template #placeholder>
-                      <span class="tag">{{ image.action_name }}</span>
-                    </template>
-                    <template #error>
-                      <div class="image-slot">
+                  <div class="el-image_view">
+                    <el-image
+                      v-if="image.PhotoRecord.image_path"
+                      :src="thumbnailMap[image.PhotoRecord.image_path] || getFilePath(image.PhotoRecord.image_path)"
+                      :preview-src-list="getPreviewImageList(item)"
+                      hide-on-click-modal
+                      :initial-index="getPreviewIndex(item, index)"
+                      class="preview-image"
+                      fit="contain"
+                      :preview-teleported="true"
+                      lazy
+                    >
+                      <template #placeholder>
                         <span class="tag">{{ image.action_name }}</span>
-                      </div>
-                    </template>
-                  </el-image>
-                  <div v-else class="image-placeholder">
-                    <span class="tag">{{ image.action_name }}</span>
+                      </template>
+                      <template #error>
+                        <div class="image-slot">
+                          <span class="tag">{{ image.action_name }}</span>
+                        </div>
+                      </template>
+                    </el-image>
+                    <el-button v-if="image.action_name && !runLoading && !takePictureLoading" :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)" v-log="{ describe: { action: 'retake single image', goods_art_no: image.PhotoRecord.goods_art_no, action_name: image.action_name } }">{{ $t('photoShot.retakeSingle') }}</el-button>
                   </div>
                 </div>
               </div>
             </div>
           </div>
       </div>
+
+      <div class="footer-controls">
+        <div class="footer-left">
+          <el-checkbox
+            :model-value="isSelectAll"
+            :indeterminate="isIndeterminate"
+            @change="toggleSelectAll"
+            class="select-all-checkbox"
+          >
+            {{ $t('common.selectAll') }}
+          </el-checkbox>
+          <span class="image-count-text">
+            {{ $t('photoShot.selectedCount', { selected: selectedImageCount, total: totalImageCount }) }}
+          </span>
+        </div>
+        <div class="footer-right">
+          <div class="pagination-container" style="padding: 10px 0; text-align: center;">
+            <el-pagination
+                :page-count="totalPages"
+                :current-page="currentPage"
+                @current-change="(page) => getPhotoRecords({ page })"
+                layout="prev, pager, next"
+                :page-size="pageSize"
+            />
+          </div>
+          <el-button
+            :disabled="selectedGoods.size === 0 || runLoading || takePictureLoading"
+            @click="deleteSelected"
+            v-log="{ describe: { action: 'delete selected goods' } }"
+          >
+            {{ $t('photoShot.delete') }}
+          </el-button>
+          <el-button
+            :disabled="!goodsList.length || runLoading || takePictureLoading"
+            @click="handleDeleteAll"
+            style="color: #FF4C00"
+            v-log="{ describe: { action: 'delete all goods' } }"
+          >
+            {{ $t('photoShot.deleteAll') }}
+          </el-button>
+          <el-button
+            type="primary"
+            :disabled="!goodsList.length || runLoading || takePictureLoading"
+            @click="openPhotographyDetail()"
+            v-log="{ describe: { action: 'start generate' } }"
+          >
+            <img src="@/assets/images/processImage.vue/sc.png" />
+            {{ $t('photoShot.startGenerate') }}
+            <img src="@/assets/images/processImage.vue/go.png"  class="go"/>
+          </el-button>
+        </div>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, onBeforeUnmount } from 'vue'
+import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { ElMessageBox } from 'element-plus'
 import RemoteControl from '@/views/RemoteControl/index.vue'
 import headerBar from '@/components/header-bar/index.vue'
 import hardwareCheck from '@/components/check/index.vue'
 import usePhotography from './mixin/usePhotography'
+import generateMenu from '@/utils/menus/generate'
 
 const { t } = useI18n()
 
 const {
   loading, runLoading, takePictureLoading, goodsList, pageSize, currentPage,
-  totalPages, goods_art_no_tpl, goods_art_no, runAction, lastPhoto,
-  showlastPhoto, goodsArtNo, searchGoodsArtNo, menu, generate,
-  getTime, getFilePath, getPhotoRecords, delGoods, importDirs, deleteAllGoods,
-  reTakePictureNos, onRemoteControl, initEventListeners, cleanupEventListeners,
+  totalPages, goods_art_no, runAction, lastPhoto,
+  showlastPhoto, goodsArtNo, searchGoodsArtNo, menu,
+  getTime, getFilePath, getPhotoRecords, delGoods, del, deleteAllGoods,
+  reTakePicture, reTakePictureNos, onRemoteControl, initEventListeners, cleanupEventListeners,
+  openPhotographyDetail, onGenerateCLick,
 } = usePhotography()
 
 const containerRef = ref<HTMLElement | null>(null)
 const selectedGoods = ref<Set<string>>(new Set())
 const thumbnailMap = ref<Record<string, string>>({})
 
+// 全选状态
+const isSelectAll = computed(() => {
+  return goodsList.value.length > 0 && selectedGoods.value.size === goodsList.value.length
+})
+
+// 是否半选状态
+const isIndeterminate = computed(() => {
+  return selectedGoods.value.size > 0 && selectedGoods.value.size < goodsList.value.length
+})
+
+// 计算已选择的图片数量
+const selectedImageCount = computed(() => {
+  let count = 0
+  goodsList.value.forEach((item: any) => {
+    if (selectedGoods.value.has(item.goods_art_no)) {
+      count += item.items?.length || 0
+    }
+  })
+  return count
+})
+
+// 计算总图片数量
+const totalImageCount = computed(() => {
+  let count = 0
+  goodsList.value.forEach((item: any) => {
+    count += item.items?.length || 0
+  })
+  return count
+})
+
+// 全选/取消全选
+const toggleSelectAll = () => {
+  if (isSelectAll.value) {
+    selectedGoods.value.clear()
+  } else {
+    goodsList.value.forEach((item: any) => {
+      selectedGoods.value.add(item.goods_art_no)
+    })
+  }
+}
+
+// 删除选中的货号
+const deleteSelected = async () => {
+  if (selectedGoods.value.size === 0) {
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      t('message.confirmDeleteSelected', { count: selectedGoods.value.size }),
+      t('common.tips'),
+      {
+        confirmButtonText: t('common.confirm'),
+        cancelButtonText: t('common.cancel'),
+      }
+    )
+
+    const goodsArtNos = Array.from(selectedGoods.value)
+    await del({ goods_art_nos: goodsArtNos })
+    selectedGoods.value.clear()
+  } catch (e) {
+    // 用户取消
+  }
+}
+
 const toggleGoods = (goodsArtNo: string) => {
   if (selectedGoods.value.has(goodsArtNo)) {
     selectedGoods.value.delete(goodsArtNo)
@@ -228,29 +346,528 @@ onBeforeUnmount(() => {
   top: 100px !important;
   height: calc(100vh - 170px) !important;
   transform: translate(0px, 0px) !important;
-  .el-image { width: 100%; height: 100%; display: block; .el-image__inner { width: 100%; height: 100%; display: block; } }
+
+  .el-image {
+    width: 100%;
+    height:100%;
+    display: block;
+
+    .el-image__inner {
+      width: 100%;
+      height:100%;
+      display: block;
+
+    }
+  }
 }
 </style>
 
 <style scoped lang="scss">
-.photography-page { position: relative; }
-.main-container { position: relative; display: flex; }
-.history-section { width: 100%; min-height: calc(100vh - 30px); display: flex; flex-direction: column; padding: 20px; }
-.history-warp { flex: 1; }
-.history-item { background: #FFFFFF; box-shadow: 0px 2px 4px 0px rgba(23,33,71,0.1); border-radius: 10px; border: 1px solid #D9DEE6; margin-bottom: 20px; }
-.history-item-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
-.history-item-left { display: flex; align-items: center; gap: 12px; }
-.goods-art-no { font-weight: 600; font-size: 16px; color: #333; }
-.history-item-meta { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #8C92A7; }
-.history-item-right { display: flex; align-items: center; gap: 8px; }
-.history-item-images { display: flex; flex-wrap: wrap; padding: 12px 16px; gap: 12px; }
-.history-item_image { width: 120px; height: 120px; border-radius: 8px; overflow: hidden; background: #f5f5f5; display: flex; align-items: center; justify-content: center; position: relative; }
-.preview-image { width: 120px; height: 120px; }
-.image-placeholder { width: 120px; height: 120px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; }
-.tag { position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.5); color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
-.image-count { display: flex; align-items: center; gap: 4px; }
-.last-photo { position: fixed; top: 50px; right: 20px; z-index: 9999; width: 300px; background: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 8px; }
-.close-btn { position: absolute; top: 8px; right: 8px; z-index: 1; }
-::v-deep .el-checkbox__input { transform: scale(1.4); }
-.search-bar { margin-bottom: 15px; display: flex; justify-content: flex-start; }
+.photography-page {
+  background-color: rgba(255, 255, 255, 1);
+  position: relative;
+  .main-container {
+    position: relative;
+    display: flex;
+    .content-wrapper {
+      flex-grow: 1;
+      position: relative;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      width: 510px;
+      padding: 0 50px;
+      height: calc(100vh - 30px);
+      margin:  auto;
+      justify-content: flex-start;
+      flex-direction: column;
+      overflow: hidden;
+      border-right:1px solid rgba(0,0,0,.1);
+
+      .step-section {
+        display: flex;
+        margin-bottom: 20px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+
+      .step-number {
+        background-color: rgba(22, 119, 255, 1);
+        border-radius: 50%;
+        height: 32px;
+        margin-top: 51px;
+        width: 32px;
+
+        .text_22 {
+          width: 6px;
+          height: 22px;
+          overflow-wrap: break-word;
+          color: rgba(255, 255, 255, 1);
+          font-size: 14px;
+          font-weight: NaN;
+          text-align: right;
+          white-space: nowrap;
+          line-height: 22px;
+          margin: 5px 0 0 13px;
+        }
+      }
+
+      .step-one {
+        width: 350px;
+        height: auto;
+        margin: 55px 0 0 5px;
+
+        .step-header {
+          width: 391px;
+          height: 24px;
+          margin-left: 3px;
+
+          .step-title {
+            width: 160px;
+            height: 24px;
+            overflow-wrap: break-word;
+            color: rgba(0, 0, 0, 0.85);
+            font-size: 16px;
+            font-family: PingFangSC-Medium;
+            font-weight: 500;
+            text-align: left;
+            white-space: nowrap;
+            line-height: 24px;
+          }
+
+          .step-icon {
+            width: 32px;
+            height: 20px;
+            margin-top: 4px;
+          }
+
+          .step-divider {
+            width: 191px;
+            height: 1px;
+            margin: 13px 0 0 8px;
+          }
+        }
+
+        .step-content {
+          width: 350px;
+
+          .input-container {
+            width: calc(100% - 20px );
+            height: 36px;
+            margin: 10px 10px 0;
+
+            .input-item {
+              :deep(.el-input__inner){
+                  height: 36px;
+                  line-height: 36px;
+                }
+            }
+          }
+
+          .auto-method {
+            width: 253px;
+            height: 24px;
+            margin: 28px 0 0 14px;
+
+            .text-method-tag {
+              background-color: rgba(0, 174, 30, 1);
+              height: 24px;
+              width: 65px;
+
+              .text_4 {
+                width: 56px;
+                height: 20px;
+                overflow-wrap: break-word;
+                color: rgba(255, 255, 255, 1);
+                font-size: 14px;
+                font-family: PingFangSC-Semibold;
+                font-weight: 600;
+                text-align: left;
+                white-space: nowrap;
+                line-height: 20px;
+                margin: 2px 0 0 4px;
+              }
+            }
+
+            .method-description {
+              width: 182px;
+              height: 20px;
+              overflow-wrap: break-word;
+              color: rgba(71, 71, 71, 1);
+              font-size: 14px;
+              font-family: PingFangSC-Semibold;
+              font-weight: 600;
+              text-align: left;
+              white-space: nowrap;
+              line-height: 20px;
+              margin-top: 2px;
+            }
+          }
+        }
+      }
+
+      .step-two {
+        width: 350px;
+        height: auto;
+        margin: 55px 0 0 5px;
+
+        .step-title {
+          width: 350px;
+          height: 24px;
+          overflow-wrap: break-word;
+          color: rgba(0, 0, 0, 0.85);
+          font-size: 16px;
+          font-family: PingFangSC-Medium;
+          font-weight: 500;
+          text-align: left;
+          white-space: nowrap;
+          line-height: 24px;
+        }
+
+        .shooting-container {
+          width: 353px;
+          height: auto;
+
+
+          .remote-control-wrap {
+            width: 353px;
+            height: 300px;
+          }
+
+          .shooting-tips {
+            width: 325px;
+            height: 40px;
+            margin: 12px 0 0 15px;
+
+            .info-icon {
+              width: 16px;
+              height: 16px;
+              margin-top: 2px;
+            }
+
+            .tips-text {
+              width: 302px;
+              height: 40px;
+              overflow-wrap: break-word;
+              color: rgba(255, 76, 0, 1);
+              font-size: 14px;
+              font-weight: NaN;
+              text-align: left;
+              line-height: 20px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.history-section {
+        width:  calc(100vw - 510px);
+        height: calc(100vh - 30px);
+        display: flex;
+        flex-direction: column;
+        padding: 20px;
+        overflow-y: auto;
+         background:#F5F6F7;
+
+        .search-bar {
+          margin-bottom: 15px;
+          display: flex;
+          justify-content: flex-start;
+        }
+
+        ::v-deep {
+          .el-checkbox__input {
+            transform: scale(1.4);
+          }
+        }
+        .history-warp {
+          flex: 1;
+
+          .history-item {
+            background: #FFFFFF;
+            box-shadow: 0px 2px 4px 0px rgba(23,33,71,0.1);
+            border-radius: 10px;
+            border: 1px solid #D9DEE6;
+            margin-bottom: 20px;
+
+            .history-item-header {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              height: 40px;
+              padding: 0 10px;
+
+              background: linear-gradient( 90deg, #F4ECFF 0%, #DFEDFF 100%);
+              border-radius: 10px 10px 0px 0px;
+
+              .history-item-left {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+
+                .goods-checkbox {
+                  margin-right: 0;
+                }
+
+                .goods-art-no {
+                  font-size: 16px;
+                  font-weight: 500;
+                  color: #333;
+                }
+              }
+
+              .history-item-right {
+                display: flex;
+                align-items: center;
+                ::v-deep {
+                  .el-button { height: 30px; line-height: 30px;}
+                }
+              }
+            }
+
+            .history-item-meta {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              font-size: 12px;
+              color: #666;
+
+              img {
+                height: 14px;
+                margin-right: 2px;
+              }
+
+              .action-time {
+                color: #666;
+              }
+
+              .image-count {
+                color: #666;
+              }
+            }
+
+            .history-item-images {
+              display: grid;
+              grid-template-columns: repeat(5, 1fr);
+              gap: 10px;
+              padding: 15px;
+              border-top: 1px solid #f0f0f0;
+              overflow-x: auto;
+            }
+
+            .history-item_image_wrap {
+              padding-bottom: 0;
+              border-bottom: none;
+            }
+            .history-item_image {
+              position: relative;
+              width: 100%;
+              aspect-ratio: 1;
+              background: #F7F7F7;
+              border-radius: 10px;
+              overflow: hidden;
+              cursor: pointer;
+              border: 1px solid #D9DEE6;
+              transition: all 0.3s;
+
+
+              .tag {
+                color: #bbb;
+                position: absolute;
+                left: 0;
+                right: 0;
+                top: 50%;
+                margin-top: -10px;
+                line-height: 20px;
+                text-align: center;
+                font-size: 12px;
+                z-index: 1;
+                pointer-events: none;
+              }
+
+              .preview-image {
+                width: 100%;
+                height: 100%;
+
+                :deep(.el-image__inner) {
+                  width: 100%;
+                  height: 100%;
+                  object-fit: cover;
+                }
+              }
+
+              .image-placeholder {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: #F7F7F7;
+              }
+
+              .image-slot {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: #F7F7F7;
+              }
+
+              &:hover {
+                border-color: #409eff;
+                transform: scale(1.02);
+                box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+              }
+
+              &.el-loading-parent--relative{
+                ::v-deep {
+                  .el-loading-mask { display: none}
+                }
+              }
+            }
+
+
+            .el-image_view {
+              display: flex;
+              width: 100%;
+              height: 100%;
+
+              .reset-button {
+                width: 40px;
+                text-align: center;
+                height: 20px;
+                position: absolute;
+                left:50%;
+                top:50%;
+                padding: 0px;
+                margin-left:-20px;
+                margin-top:-10px;
+                color: #ffffff;
+                font-size: 14px;
+                background: rgba(0,0,0,0.6);
+                border-radius: 12px;
+                display: none;
+                cursor: pointer;
+              }
+              &:hover {
+                .reset-button {
+                  display: block;
+                }
+              }
+            }
+            p:first-of-type {
+              ::v-deep {
+                .el-loading-mask { display: block !important;}
+              }
+            }
+
+
+          }
+        }
+
+        .footer-controls {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          padding: 0px 20px;
+          border-top: 1px solid #D9DEE6;
+          background-color: #fff;
+          min-height: 50px;
+          flex-shrink: 0;
+          position: fixed;
+          bottom:0;
+          left: 510px;
+          right: 0;
+          font-size: 14px;
+          z-index: 100;
+          img {
+            height: 12px;
+            margin: 0 5px;
+          }
+          .go {
+            height: 12px;
+            opacity: .8;
+          }
+          ::v-deep {
+            .el-button {
+              border-radius: 10px;
+              height: 40px;
+              line-height: 40px;
+            }
+          }
+
+          .footer-left {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+
+            .select-all-checkbox {
+              margin-right: 0;
+              ::v-deep {
+                .el-checkbox__label {
+
+                  font-size: 14px;
+                  color: #666;
+                }
+              }
+            }
+
+            .image-count-text {
+              font-size: 14px;
+              color: #333;
+              margin-left: 0;
+            }
+          }
+
+          .footer-right {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+          }
+        }
+
+      }
+
+.last-photo{
+  position: fixed;
+  padding: 10px;
+  box-shadow: 0 0 5px rgb(0 0 0 / 50%);
+  left: 510px;
+  top: 30px;
+  bottom: 0px;
+  right: 10px;
+  z-index: 1000;
+  background-color: rgba(0,0,0,.5);
+
+  .close-btn {
+    position: absolute;
+    top: 20px;
+    right: 20px;
+    z-index: 1001;
+    width: 32px;
+    height: 32px;
+
+    ::v-deep(.el-icon) {
+      font-size: 16px;
+    }
+  }
+
+  .el-image {
+    width: 100%;
+    height:100%;
+    display: block;
+
+    .el-image__inner {
+      width: 100%;
+      height:100%;
+      display: block;
+    }
+  }
+}
 </style>

+ 47 - 57
frontend/src/views/Setting/components/action_config.vue

@@ -1,9 +1,9 @@
 <template>
 
   <el-tabs v-model="topsTab" type="card" class="top_tabs" :disabled="isSortMode">
-    <el-tab-pane label="执行左脚程序" name="left">
+    <el-tab-pane :label="$t('actionConfig.leftFootProgram')" name="left">
     </el-tab-pane>
-    <el-tab-pane label="执行右脚程序" name="right"></el-tab-pane>
+    <el-tab-pane :label="$t('actionConfig.rightFootProgram')" name="right"></el-tab-pane>
   </el-tabs>
 
   <div class="two_tabs">
@@ -19,21 +19,21 @@
   <div class="form-table">
     <div v-if="isSortMode" class="sort-tip">
       <el-icon><Warning /></el-icon>
-      <span>排序模式:请拖拽行进行排序,完成后点击"保存排序"</span>
+      <span>{{ $t('actionConfig.sortModeTip') }}</span>
     </div>
     <div class="btnBox">
-      <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="copySetting" v-log="{ describe: { action: '点击复制配置' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">复制配置</div>
+      <div class="primary-btn" @click="addRow" v-log="{ describe: { action: '点击新增一行' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">{{ $t('actionConfig.addRow') }}</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' }">{{ $t('actionConfig.resetInit') }}</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' }">{{ $t('actionConfig.renameConfig') }}</div>
+      <div class="primary-btn" @click="copySetting" v-log="{ describe: { action: '点击复制配置' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">{{ $t('actionConfig.copyConfig') }}</div>
       <div class="primary-btn" @click="toggleSortMode" v-log="{ describe: { action: isSortMode ? '点击保存排序' : '点击排序' } }">
-        {{ isSortMode ? '保存排序' : '排序' }}
+        {{ isSortMode ? $t('actionConfig.saveSort') : $t('actionConfig.sort') }}
       </div>
       <div v-if="isSortMode" class="normal-btn" @click="cancelSort" v-log="{ describe: { action: '点击取消排序' } }">
-        取消排序
+        {{ $t('actionConfig.cancelSort') }}
       </div>
       <el-radio-group style="margin-left: 10px" v-model="selectID" @click.native.stop="changeSelectId($event,activeTab.id)"  :disabled="isSortMode">
-        <el-radio :label="activeTab.id">切换成执行配置</el-radio>
+        <el-radio :label="activeTab.id">{{ $t('actionConfig.switchToConfig') }}</el-radio>
       </el-radio-group>
     </div>
     <el-table
@@ -45,7 +45,7 @@
       :row-class-name="getRowClassName"
       ref="tableRef"
     >
-      <el-table-column prop="sort" label="排序" width="80" v-if="isSortMode">
+      <el-table-column prop="sort" :label="$t('actionConfig.sortColumn')" width="80" v-if="isSortMode">
         <template #default="scope">
           <div class="sort-content">
             <span class="sort-number">{{ scope.row.sort }}</span>
@@ -55,26 +55,26 @@
           </div>
         </template>
       </el-table-column>
-      <el-table-column prop="action_name" label="步骤" >
+      <el-table-column prop="action_name" :label="$t('actionConfig.step')" >
         <template #default="scope">
           {{ scope.row.action_name }}
-          <el-tag type="success" size="small" v-if="calibrationId === scope.row.id">校准位</el-tag>
+          <el-tag type="success" size="small" v-if="calibrationId === scope.row.id">{{ $t('actionConfig.calibration') }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column prop="take_picture" label="是否拍照" width="200px">
+      <el-table-column prop="take_picture" :label="$t('actionConfig.takePhoto')" width="200px">
         <template #default="scope">
           <div v-if="!scope.row.is_system">
-            {{ scope.row.take_picture ? '拍照' : '不拍照' }}
+            {{ scope.row.take_picture ? $t('actionConfig.photo') : $t('actionConfig.noPhoto') }}
           </div>
           <span v-else></span>
 
         </template>
       </el-table-column>
-      <el-table-column prop="value" label="操作" >
+      <el-table-column prop="value" :label="$t('actionConfig.operate')" >
         <template #default="{row, $index}">
-          <a class="mar-right-10 cursor-pointer" @click="editRow(row, $index)" v-log="{ describe: { action: '点击编辑步骤', id: row.id, action_name: row.action_name } }">编辑</a>
-          <a class="mar-right-10 cursor-pointer" v-if="!row.is_system" @click="copyRow(row, $index)" v-log="{ describe: { action: '点击复制步骤', id: row.id, action_name: row.action_name } }">复制</a>
-          <a class="cursor-pointer" v-if="!row.is_system" @click="deleteRow(row, $index)" v-log="{ describe: { action: '点击删除步骤', id: row.id, action_name: row.action_name } }">删除</a>
+          <a class="mar-right-10 cursor-pointer" @click="editRow(row, $index)" v-log="{ describe: { action: '点击编辑步骤', id: row.id, action_name: row.action_name } }">{{ $t('actionConfig.edit') }}</a>
+          <a class="mar-right-10 cursor-pointer" v-if="!row.is_system" @click="copyRow(row, $index)" v-log="{ describe: { action: '点击复制步骤', id: row.id, action_name: row.action_name } }">{{ $t('actionConfig.copy') }}</a>
+          <a class="cursor-pointer" v-if="!row.is_system" @click="deleteRow(row, $index)" v-log="{ describe: { action: '点击删除步骤', id: row.id, action_name: row.action_name } }">{{ $t('actionConfig.delete') }}</a>
         </template>
       </el-table-column>
     </el-table>
@@ -106,6 +106,7 @@ const clientStore = client();
 import socket from "@/stores/modules/socket";
 const socketStore = socket(); // WebSocket状态管理实例
 const tokenInfoStore = tokenInfo();
+import i18n from '@/locales';
 
 import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,sortDeviceConfig,setTabName,delDviceConfig, copyDeviceConfig } from '@/apis/setting'
 
@@ -294,7 +295,7 @@ const copyRow = (row, index) => {
   editId.value = -1
   let length = Number(tableData.value.length) + 1
   addRowData.value = {
-    mode_type: topsTab.value === 'left' ? '执行左脚程序' : '执行右脚程序',
+    mode_type: topsTab.value === 'left' ? i18n.global.t('actionConfig.leftFootProgram') : i18n.global.t('actionConfig.rightFootProgram'),
     tab_id: activeTab.value.id,
     action_name: row.action_name + '_副本',
     take_picture: row.take_picture,
@@ -309,7 +310,7 @@ const copyRow = (row, index) => {
     after_delay: row.after_delay,
   }
   dialogVisible.value = true;
-  editTitle.value = '新增步骤';
+  editTitle.value = i18n.global.t('actionConfig.newStep');
 };
 
 /**
@@ -320,20 +321,17 @@ const copyRow = (row, index) => {
 const deleteRow = (row, index) => {
   if (isSortMode.value) return; // 排序模式下禁用
 
-  ElMessageBox.confirm('确定删除该步骤吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
+  ElMessageBox.confirm(i18n.global.t('actionConfig.confirmDelete'), i18n.global.t('actionConfig.tip'), {
+    confirmButtonText: i18n.global.t('actionConfig.confirm'),
+    cancelButtonText: i18n.global.t('actionConfig.cancel'),
     type: 'warning'
   }).then(async () => {
-
-
     const result =  await delDviceConfig({
       id: row.id
     })
-
     if (result.code == 0) {
       getList();
-      ElMessage.success('删除成功');
+      ElMessage.success(i18n.global.t('actionConfig.deleteSuccess'));
     }
   });
 };
@@ -345,42 +343,37 @@ const resetConfig = () => {
   if (isSortMode.value) return; // 排序模式下禁用
 
   console.log(activeTab.value);
-  ElMessageBox.confirm(`确定初始化${activeTab.value.mode_name}吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
+  ElMessageBox.confirm(i18n.global.t('actionConfig.confirmReset', { configName: activeTab.value.mode_name }), i18n.global.t('actionConfig.tip'), {
+    confirmButtonText: i18n.global.t('actionConfig.confirm'),
+    cancelButtonText: i18n.global.t('actionConfig.cancel'),
     type: 'warning'
   }).then(async () => {
-
     const result =  await restConfig({
       tab_id: activeTab.value.id
     })
     if (result.code == 0) {
       getList();
-      ElMessage.success('重置成功');
+      ElMessage.success(i18n.global.t('actionConfig.resetSuccess'));
     }
-
   });
 };
 
 const reName = ()=>{
   if (isSortMode.value) return; // 排序模式下禁用
 
-  ElMessageBox.prompt('', '重命名配置', {
-    confirmButtonText: '保存',
-    cancelButtonText: '取消',
+  ElMessageBox.prompt('', i18n.global.t('actionConfig.renameTitle'), {
+    confirmButtonText: i18n.global.t('actionConfig.save'),
+    cancelButtonText: i18n.global.t('actionConfig.cancel'),
     inputValue:  activeTab.value.mode_name,
-    inputPlaceholder:'请输入配置名称',
+    inputPlaceholder: i18n.global.t('actionConfig.inputConfigName'),
     inputValidator: (value) => {
       if (value === '') {
-        return '请输入配置名称';
+        return i18n.global.t('actionConfig.inputConfigName');
       }
       return true;
     },
   })
       .then(async ({ value }) => {
-
-
-
         const result =  await setTabName({
           id: activeTab.value.id,
           mode_name:value,
@@ -392,29 +385,26 @@ const reName = ()=>{
               item.mode_name   =  activeTab.value.mode_name
             }
           })
-          ElMessage.success('重命名成功');
+          ElMessage.success(i18n.global.t('actionConfig.renameSuccess'));
         }
       })
 }
 const copySetting = ()=>{
   if (isSortMode.value) return; // 排序模式下禁用
 
-  ElMessageBox.prompt('', '复制配置', {
-    confirmButtonText: '保存',
-    cancelButtonText: '取消',
+  ElMessageBox.prompt('', i18n.global.t('actionConfig.copyTitle'), {
+    confirmButtonText: i18n.global.t('actionConfig.save'),
+    cancelButtonText: i18n.global.t('actionConfig.cancel'),
     inputValue: "",
-    inputPlaceholder:'请输入配置名称',
+    inputPlaceholder: i18n.global.t('actionConfig.inputConfigName'),
     inputValidator: (value) => {
       if (value === '') {
-        return '请输入配置名称';
+        return i18n.global.t('actionConfig.inputConfigName');
       }
       return true;
     },
   })
       .then(async ({ value }) => {
-
-
-
         const result =  await copyDeviceConfig({
           tab_id: activeTab.value.id,
           tab_name:value,
@@ -455,9 +445,9 @@ const addRow = () => {
   editId.value = -1
   let length = Number(tableData.value.length)+1
   addRowData.value =   {
-    mode_type: topsTab.value === 'left' ? '执行左脚程序' : '执行右脚程序',
+    mode_type: topsTab.value === 'left' ? i18n.global.t('actionConfig.leftFootProgram') : i18n.global.t('actionConfig.rightFootProgram'),
     tab_id: activeTab.value.id,
-    action_name: '新增步骤'+length,
+    action_name: i18n.global.t('actionConfig.newStep')+length,
     take_picture: false,
     camera_height: 0,
     camera_angle: 0,
@@ -470,7 +460,7 @@ const addRow = () => {
     after_delay: 0,
   }
   dialogVisible.value = true;
-  editTitle.value = '新增步骤';
+  editTitle.value = i18n.global.t('actionConfig.newStep');
 };
 
 /**
@@ -491,7 +481,7 @@ const toggleSortMode = () => {
  */
 const cancelSort = () => {
   exitSortMode();
-  ElMessage.info('已取消排序');
+  ElMessage.info(i18n.global.t('actionConfig.cancelSortTip'));
 };
 
 /**
@@ -511,7 +501,7 @@ const enterSortMode = () => {
     initSortable();
   }, 100);
 
-  ElMessage.info('请拖拽行进行排序,完成后点击"保存排序"');
+  ElMessage.info(i18n.global.t('actionConfig.sortModeEnterTip'));
 };
 
 /**
@@ -610,7 +600,7 @@ const saveSortOrder = async () => {
   })
   if (result.code == 0) {
     getList();
-    ElMessage.success('排序成功');
+    ElMessage.success(i18n.global.t('actionConfig.sortSuccess'));
   }
 };