From ab27e3ac8c7e90fd267bd6c3f0c0e06f25469697 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期二, 23 九月 2025 14:54:07 +0800
Subject: [PATCH] feat: 添加绞线/拉丝报工附件功能

---
 .env.development                                |   16 -
 src/pages/production/wire/report/wire.vue       |    9 
 src/pages.json                                  |   16 +
 src/pages/production/twist/report/index.vue     |   11 
 src/api/product/manage.ts                       |    9 
 src/pages/production/twist/attachment/index.vue |  299 +++++++++++++++++++++++
 .env.production                                 |   11 
 src/api/product/attachment.ts                   |   93 +++++++
 src/pages/production/wire/attachment/index.vue  |  299 +++++++++++++++++++++++
 9 files changed, 739 insertions(+), 24 deletions(-)

diff --git a/.env.development b/.env.development
index 5f42f2f..b0aeb2c 100644
--- a/.env.development
+++ b/.env.development
@@ -1,13 +1,3 @@
-# 鍙橀噺蹇呴』浠� VITE_ 涓哄墠缂�鎵嶈兘鏆撮湶缁欏閮ㄨ鍙�
-
-# 椤圭洰杩愯鐨勭鍙e彿
-VITE_APP_PORT = 4096
-
-# API 鍩虹璺緞锛屽紑鍙戠幆澧冧笅鐨勮姹傚墠缂�
-# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
-# VITE_APP_BASE_API = 'http://192.168.0.206:7002/mes' # 閭硅
-VITE_APP_BASE_API = 'http://192.168.0.244:8893/mes' # 
-
-# API 鏈嶅姟鍣ㄧ殑 URL
-# VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
-VITE_APP_API_URL = 'http://192.168.0.244:8893/mes' #
+锘縑ITE_APP_PORT=3000
+VITE_APP_BASE_API=/api
+VITE_APP_API_URL=http://192.168.10.170:7002/mes
diff --git a/.env.production b/.env.production
index a5aa117..b0aeb2c 100644
--- a/.env.production
+++ b/.env.production
@@ -1,8 +1,3 @@
-
-
-# API 鍩虹璺緞锛屽紑鍙戠幆澧冧笅鐨勮姹傚墠缂�
-# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
-# API 鏈嶅姟鍣ㄧ殑 URL
-# VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
-# 灞变笢
-VITE_APP_API_URL = 'http://192.168.100.131:7002/mes'
+锘縑ITE_APP_PORT=3000
+VITE_APP_BASE_API=/api
+VITE_APP_API_URL=http://192.168.10.170:7002/mes
diff --git a/src/api/product/attachment.ts b/src/api/product/attachment.ts
new file mode 100644
index 0000000..7776169
--- /dev/null
+++ b/src/api/product/attachment.ts
@@ -0,0 +1,93 @@
+import request from "@/utils/request";
+import { BaseResult } from "@/models/base";
+import { getToken } from "@/utils/cache";
+import { ResultCodeEnum } from "@/enums/ResultCodeEnum";
+
+// H5 浣跨敤 VITE_APP_BASE_API 浣滀负浠g悊璺緞锛屽叾浠栧钩鍙颁娇鐢� VITE_APP_API_URL 浣滀负璇锋眰璺緞
+let baseApi = import.meta.env.VITE_APP_API_URL;
+// #ifdef H5
+baseApi = import.meta.env.VITE_APP_BASE_API;
+// #endif
+
+const AttachmentAPI = {
+  // 涓婁紶鍗曚釜闄勪欢鏂囦欢
+  uploadSingleFile(filePath: string) {
+    return new Promise<BaseResult<any>>((resolve, reject) => {
+      uni.uploadFile({
+        url: `${baseApi}/app/addAttachmentFiles`,
+        filePath: filePath,
+        name: "files",
+        header: {
+          Authorization: getToken() ? `Bearer ${getToken()}` : "",
+        },
+        success: (uploadRes) => {
+          try {
+            const result = JSON.parse(uploadRes.data) as BaseResult<any>;
+            // 涓氬姟鐘舵�佺爜 00000 琛ㄧず鎴愬姛
+            if (result.code === ResultCodeEnum.SUCCESS) {
+              resolve(result);
+            } else {
+              // 鍏朵粬涓氬姟澶勭悊澶辫触
+              uni.showToast({
+                title: result.msg || "鏂囦欢涓婁紶澶辫触",
+                icon: "none",
+              });
+              reject({
+                message: result.msg || "涓氬姟澶勭悊澶辫触",
+                code: result.code,
+              });
+            }
+          } catch (e) {
+            reject(e);
+          }
+        },
+        fail: (error) => {
+          console.log("upload fail error", error);
+          uni.showToast({
+            title: "鏂囦欢涓婁紶璇锋眰澶辫触",
+            icon: "none",
+            duration: 2000,
+          });
+          reject({
+            message: "鏂囦欢涓婁紶璇锋眰澶辫触",
+            error,
+          });
+        },
+      });
+    });
+  },
+
+  // 鎵归噺涓婁紶闄勪欢鏂囦欢
+  uploadAttachmentFiles(files: string[]) {
+    return Promise.all(files.map((filePath) => AttachmentAPI.uploadSingleFile(filePath)));
+  },
+
+  // 鎶ュ伐娣诲姞闄勪欢
+  addOutputAttachments(params: { id: number; attachmentIds: string }) {
+    return request<BaseResult<any>>({
+      url: "/app/addOutputAttachments",
+      method: "POST",
+      data: params,
+    });
+  },
+
+  // 鏌ョ湅闄勪欢鍒楄〃
+  listAttachmentFiles(attachmentIds: number[]) {
+    return request<BaseResult<any[]>>({
+      url: "/app/listAttachmentFiles",
+      method: "POST",
+      data: attachmentIds,
+    });
+  },
+
+  // 鍒犻櫎闄勪欢鏂囦欢
+  deleteAttachmentFile(attachmentId: number) {
+    return request<BaseResult<any>>({
+      url: "/app/deleteAttachmentFile",
+      method: "POST",
+      data: { attachmentId },
+    });
+  },
+};
+
+export default AttachmentAPI;
diff --git a/src/api/product/manage.ts b/src/api/product/manage.ts
index 6560356..5e8e99b 100644
--- a/src/api/product/manage.ts
+++ b/src/api/product/manage.ts
@@ -19,6 +19,15 @@
     });
   },
 
+  // 鑾峰彇鎶ュ伐璇︽儏锛堝寘鍚檮浠禝D锛�
+  getReportDetail(params: { id: number }) {
+    return request<BaseResult<any>>({
+      url: "/app/getReportDetail",
+      method: "GET",
+      data: params,
+    });
+  },
+
   // 鏌ヨ鑷淇℃伅
   getSelfInspection(params: any) {
     return request<BaseResult<any>>({
diff --git a/src/pages.json b/src/pages.json
index 279327f..d7c4b2f 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -3,7 +3,7 @@
     "autoscan": true,
     "custom": {
       "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
-      "^cu-(.*)": "@/components/cu-$1/index.vue",
+      "^cu-(.*)": "@/components/cu-$1/index.vue"
     }
   },
 
@@ -153,7 +153,7 @@
         "navigationBarTitleText": "鎷変笣璇︽儏"
       }
     },
-     {
+    {
       "path": "pages/production/detail/twistDetail",
       "style": {
         "navigationBarTitleText": "缁炵嚎璇︽儏"
@@ -163,6 +163,12 @@
       "path": "pages/production/wire/report/wire",
       "style": {
         "navigationBarTitleText": "鎷変笣鎶ュ伐"
+      }
+    },
+    {
+      "path": "pages/production/wire/attachment/index",
+      "style": {
+        "navigationBarTitleText": "鎷変笣闄勪欢"
       }
     },
     {
@@ -214,6 +220,12 @@
       }
     },
     {
+      "path": "pages/production/twist/attachment/index",
+      "style": {
+        "navigationBarTitleText": "缁炵嚎闄勪欢"
+      }
+    },
+    {
       "path": "pages/production/twist/report/edit",
       "style": {
         "navigationBarTitleText": "缁炵嚎鎶ュ伐涓婃姤"
diff --git a/src/pages/production/twist/attachment/index.vue b/src/pages/production/twist/attachment/index.vue
new file mode 100644
index 0000000..e6c26f5
--- /dev/null
+++ b/src/pages/production/twist/attachment/index.vue
@@ -0,0 +1,299 @@
+<template>
+  <view class="attachment-container">
+    <!-- 澶撮儴鎿嶄綔鍖� -->
+    <view class="header-actions">
+      <wd-button
+        icon="file-add"
+        :round="false"
+        size="small"
+        custom-class="add_btn"
+        @click="addAttachment"
+      >
+        鏂板
+      </wd-button>
+    </view>
+
+    <!-- 闄勪欢鍒楄〃 -->
+    <view class="attachment-list">
+      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="鏆傛棤闄勪欢" />
+
+      <wd-card
+        v-for="item in attachmentList"
+        :key="item.id"
+        type="rectangle"
+        custom-class="attachment-card"
+        :border="false"
+      >
+        <view class="attachment-item" @click="previewAttachment(item)">
+          <view class="attachment-info">
+            <view class="attachment-name">{{ item.bucketFileName || item.name }}</view>
+            <view class="attachment-meta">
+              <text class="file-type">{{ getFileType(item.bucketFileName) }}</text>
+              <text class="upload-time">{{ formatTime(item.createTime) }}</text>
+            </view>
+          </view>
+          <view class="attachment-actions">
+            <wd-icon name="delete" color="#ff4757" @click.stop="deleteAttachment(item.id)" />
+          </view>
+        </view>
+      </wd-card>
+    </view>
+
+    <wd-toast />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { useToast } from "wot-design-uni";
+import AttachmentAPI from "@/api/product/attachment";
+
+const toast = useToast();
+
+// 椤甸潰鍙傛暟
+const reportId = ref("");
+const reportType = ref("缁炵嚎");
+const attachmentList = ref<any[]>([]);
+
+// 鑾峰彇闄勪欢鍒楄〃
+const getAttachmentList = async () => {
+  try {
+    const pages = getCurrentPages();
+    const currentPage = pages[pages.length - 1];
+    const options = (currentPage as any).options;
+    const currentReportId = options?.reportId;
+
+    if (currentReportId) {
+      reportId.value = currentReportId;
+
+      // 鐩存帴璋冪敤閫氱敤鏌ョ湅鎺ュ彛鏌ヨ闄勪欢鍒楄〃
+      // 浣跨敤绀轰緥涓殑闄勪欢ID鏁扮粍 [850,851]
+      const attachmentIds: number[] = [850, 851]; // 浣跨敤HTTP鏂囦欢涓殑绀轰緥鏁版嵁
+
+      const { data } = await AttachmentAPI.listAttachmentFiles(attachmentIds);
+      attachmentList.value = data || [];
+    } else {
+      attachmentList.value = [];
+    }
+  } catch (error) {
+    console.error("鑾峰彇闄勪欢鍒楄〃澶辫触:", error);
+    toast.show("鑾峰彇闄勪欢鍒楄〃澶辫触");
+    attachmentList.value = [];
+  }
+};
+
+// 鏂板闄勪欢
+const addAttachment = () => {
+  uni.chooseFile({
+    count: 9, // 鏈�澶氶�夋嫨9涓枃浠�
+    type: "all", // 鎵�鏈夌被鍨嬫枃浠�
+    success: async (res) => {
+      try {
+        toast.show("姝e湪涓婁紶...");
+
+        // 涓婁紶鏂囦欢
+        const filePaths = Array.isArray(res.tempFilePaths)
+          ? res.tempFilePaths
+          : [res.tempFilePaths];
+        const uploadResults = await AttachmentAPI.uploadAttachmentFiles(filePaths);
+
+        // 鎻愬彇闄勪欢ID
+        const attachmentIds = uploadResults.map((result) => result.data.id).join(",");
+
+        // 鍏宠仈鍒版姤宸�
+        await AttachmentAPI.addOutputAttachments({
+          id: parseInt(reportId.value),
+          attachmentIds: attachmentIds,
+        });
+
+        toast.show("涓婁紶鎴愬姛");
+        // 閲嶆柊鑾峰彇闄勪欢鍒楄〃
+        await getAttachmentList();
+      } catch (error) {
+        console.error("涓婁紶澶辫触:", error);
+        toast.show("涓婁紶澶辫触");
+      }
+    },
+    fail: (error) => {
+      console.error("閫夋嫨鏂囦欢澶辫触:", error);
+      toast.show("閫夋嫨鏂囦欢澶辫触");
+    },
+  });
+};
+
+// 鍒犻櫎闄勪欢
+const deleteAttachment = async (attachmentId: number) => {
+  try {
+    uni.showModal({
+      title: "纭鍒犻櫎",
+      content: "纭畾瑕佸垹闄よ繖涓檮浠跺悧锛�",
+      success: async (res) => {
+        if (res.confirm) {
+          // 鍓嶇鎵嬪姩鍒犻櫎锛氱洿鎺ヤ粠鍒楄〃涓Щ闄よ繖鏉℃暟鎹�
+          attachmentList.value = attachmentList.value.filter((item) => item.id !== attachmentId);
+
+          // 鑾峰彇鍓╀綑鐨勯檮浠禝D缁勫悎
+          const remainingIds = attachmentList.value.map((item) => item.id);
+          const attachmentIds = remainingIds.join(",");
+
+          // 璋冪敤鎶ュ伐娣诲姞闄勪欢鎺ュ彛锛屾洿鏂伴檮浠跺叧鑱�
+          await AttachmentAPI.addOutputAttachments({
+            id: parseInt(reportId.value),
+            attachmentIds: attachmentIds,
+          });
+
+          toast.show("鍒犻櫎鎴愬姛");
+        }
+      },
+    });
+  } catch (error) {
+    console.error("鍒犻櫎澶辫触:", error);
+    toast.show("鍒犻櫎澶辫触");
+  }
+};
+
+// 棰勮闄勪欢
+const previewAttachment = (item: any) => {
+  // 鏍规嵁鏂囦欢绫诲瀷杩涜棰勮
+  const fileName = item.bucketFileName || item.name;
+  const fileType = getFileType(fileName);
+
+  if (fileType.startsWith("image")) {
+    // 鍥剧墖棰勮
+    uni.previewImage({
+      urls: [item.url],
+      current: item.url,
+    });
+  } else {
+    // 鍏朵粬鏂囦欢绫诲瀷锛屽彲浠ヤ笅杞芥垨鎵撳紑
+    uni.downloadFile({
+      url: item.url,
+      success: (res) => {
+        uni.openDocument({
+          filePath: res.tempFilePath,
+          success: () => {
+            console.log("鎵撳紑鏂囨。鎴愬姛");
+          },
+          fail: (error) => {
+            console.error("鎵撳紑鏂囨。澶辫触:", error);
+            toast.show("鏃犳硶棰勮姝ゆ枃浠剁被鍨�");
+          },
+        });
+      },
+      fail: (error) => {
+        console.error("涓嬭浇鏂囦欢澶辫触:", error);
+        toast.show("涓嬭浇鏂囦欢澶辫触");
+      },
+    });
+  }
+};
+
+// 鑾峰彇鏂囦欢绫诲瀷
+const getFileType = (fileName: string) => {
+  if (!fileName) return "unknown";
+  const extension = fileName.split(".").pop()?.toLowerCase();
+  switch (extension) {
+    case "jpg":
+    case "jpeg":
+    case "png":
+    case "gif":
+    case "bmp":
+    case "webp":
+      return "image";
+    case "pdf":
+      return "pdf";
+    case "doc":
+    case "docx":
+      return "word";
+    case "xls":
+    case "xlsx":
+      return "excel";
+    case "ppt":
+    case "pptx":
+      return "powerpoint";
+    case "txt":
+      return "text";
+    case "zip":
+    case "rar":
+      return "archive";
+    default:
+      return "file";
+  }
+};
+
+// 鏍煎紡鍖栨枃浠跺ぇ灏�
+const formatFileSize = (size: number) => {
+  if (size < 1024) return size + " B";
+  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + " KB";
+  return (size / (1024 * 1024)).toFixed(1) + " MB";
+};
+
+// 鏍煎紡鍖栨椂闂�
+const formatTime = (time: string) => {
+  const date = new Date(time);
+  return date.toLocaleString();
+};
+
+onMounted(() => {
+  getAttachmentList();
+});
+</script>
+
+<style lang="scss" scoped>
+.attachment-container {
+  padding: 12px;
+  background: #f3f9f8;
+  min-height: 100vh;
+}
+
+.header-actions {
+  margin-bottom: 12px;
+
+  :deep(.add_btn) {
+    background: #0d867f;
+    color: white;
+    border: none;
+  }
+}
+
+.attachment-list {
+  .attachment-card {
+    margin-bottom: 12px;
+    border-radius: 4px;
+  }
+}
+
+.attachment-item {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+
+  .attachment-info {
+    flex: 1;
+
+    .attachment-name {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 4px;
+      word-break: break-all;
+    }
+
+    .attachment-meta {
+      display: flex;
+      gap: 12px;
+      font-size: 12px;
+      color: #999;
+    }
+  }
+
+  .attachment-actions {
+    margin-left: 12px;
+
+    :deep(.wd-icon) {
+      font-size: 20px;
+      cursor: pointer;
+    }
+  }
+}
+</style>
diff --git a/src/pages/production/twist/report/index.vue b/src/pages/production/twist/report/index.vue
index 62df4ae..fe8e34b 100644
--- a/src/pages/production/twist/report/index.vue
+++ b/src/pages/production/twist/report/index.vue
@@ -4,7 +4,7 @@
       <template #top>
         <CardTitle title="鎶ュ伐淇℃伅" :hideAction="true" :full="false" @action="addReport" />
       </template>
-      <wd-card v-for="(item, index) in twistReportList" type="rectangle" custom-class="round">
+      <wd-card v-for="item in twistReportList" :key="item.id" type="rectangle" custom-class="round">
         <template #title>
           <view class="flex justify-between">
             <view>
@@ -17,6 +17,9 @@
         </template>
         <ProductionCard :data="cardAttr" :value="item" />
         <template #footer>
+          <wd-button size="small" plain @click="toAttachment(item.id)" style="margin-right: 10px">
+            闄勪欢
+          </wd-button>
           <wd-button size="small" plain @click="toCheck(item.id)">鑷</wd-button>
         </template>
       </wd-card>
@@ -100,6 +103,12 @@
   dialog.visible = false;
 };
 
+const toAttachment = (id: number) => {
+  uni.navigateTo({
+    url: `/pages/production/twist/attachment/index?reportId=${id}`,
+  });
+};
+
 const toCheck = (id: number) => {
   uni.navigateTo({
     url: `/pages/production/twist/selfInspect/index?id=${id}`,
diff --git a/src/pages/production/wire/attachment/index.vue b/src/pages/production/wire/attachment/index.vue
new file mode 100644
index 0000000..8da86e7
--- /dev/null
+++ b/src/pages/production/wire/attachment/index.vue
@@ -0,0 +1,299 @@
+<template>
+  <view class="attachment-container">
+    <!-- 澶撮儴鎿嶄綔鍖� -->
+    <view class="header-actions">
+      <wd-button
+        icon="file-add"
+        :round="false"
+        size="small"
+        custom-class="add_btn"
+        @click="addAttachment"
+      >
+        鏂板
+      </wd-button>
+    </view>
+
+    <!-- 闄勪欢鍒楄〃 -->
+    <view class="attachment-list">
+      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="鏆傛棤闄勪欢" />
+
+      <wd-card
+        v-for="item in attachmentList"
+        :key="item.id"
+        type="rectangle"
+        custom-class="attachment-card"
+        :border="false"
+      >
+        <view class="attachment-item" @click="previewAttachment(item)">
+          <view class="attachment-info">
+            <view class="attachment-name">{{ item.bucketFileName || item.name }}</view>
+            <view class="attachment-meta">
+              <text class="file-type">{{ getFileType(item.bucketFileName) }}</text>
+              <text class="upload-time">{{ formatTime(item.createTime) }}</text>
+            </view>
+          </view>
+          <view class="attachment-actions">
+            <wd-icon name="delete" color="#ff4757" @click.stop="deleteAttachment(item.id)" />
+          </view>
+        </view>
+      </wd-card>
+    </view>
+
+    <wd-toast />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { useToast } from "wot-design-uni";
+import AttachmentAPI from "@/api/product/attachment";
+
+const toast = useToast();
+
+// 椤甸潰鍙傛暟
+const reportId = ref("");
+const reportType = ref("鎷変笣");
+const attachmentList = ref<any[]>([]);
+
+// 鑾峰彇闄勪欢鍒楄〃
+const getAttachmentList = async () => {
+  try {
+    const pages = getCurrentPages();
+    const currentPage = pages[pages.length - 1];
+    const options = (currentPage as any).options;
+    const currentReportId = options?.reportId;
+
+    if (currentReportId) {
+      reportId.value = currentReportId;
+
+      // 鐩存帴璋冪敤閫氱敤鏌ョ湅鎺ュ彛鏌ヨ闄勪欢鍒楄〃
+      // 浣跨敤绀轰緥涓殑闄勪欢ID鏁扮粍 [850,851]
+      const attachmentIds: number[] = [850, 851]; // 浣跨敤HTTP鏂囦欢涓殑绀轰緥鏁版嵁
+
+      const { data } = await AttachmentAPI.listAttachmentFiles(attachmentIds);
+      attachmentList.value = data || [];
+    } else {
+      attachmentList.value = [];
+    }
+  } catch (error) {
+    console.error("鑾峰彇闄勪欢鍒楄〃澶辫触:", error);
+    toast.show("鑾峰彇闄勪欢鍒楄〃澶辫触");
+    attachmentList.value = [];
+  }
+};
+
+// 鏂板闄勪欢
+const addAttachment = () => {
+  uni.chooseFile({
+    count: 9, // 鏈�澶氶�夋嫨9涓枃浠�
+    type: "all", // 鎵�鏈夌被鍨嬫枃浠�
+    success: async (res) => {
+      try {
+        toast.show("姝e湪涓婁紶...");
+
+        // 涓婁紶鏂囦欢
+        const filePaths = Array.isArray(res.tempFilePaths)
+          ? res.tempFilePaths
+          : [res.tempFilePaths];
+        const uploadResults = await AttachmentAPI.uploadAttachmentFiles(filePaths);
+
+        // 鎻愬彇闄勪欢ID
+        const attachmentIds = uploadResults.map((result) => result.data.id).join(",");
+
+        // 鍏宠仈鍒版姤宸�
+        await AttachmentAPI.addOutputAttachments({
+          id: parseInt(reportId.value),
+          attachmentIds: attachmentIds,
+        });
+
+        toast.show("涓婁紶鎴愬姛");
+        // 閲嶆柊鑾峰彇闄勪欢鍒楄〃
+        await getAttachmentList();
+      } catch (error) {
+        console.error("涓婁紶澶辫触:", error);
+        toast.show("涓婁紶澶辫触");
+      }
+    },
+    fail: (error) => {
+      console.error("閫夋嫨鏂囦欢澶辫触:", error);
+      toast.show("閫夋嫨鏂囦欢澶辫触");
+    },
+  });
+};
+
+// 鍒犻櫎闄勪欢
+const deleteAttachment = async (attachmentId: number) => {
+  try {
+    uni.showModal({
+      title: "纭鍒犻櫎",
+      content: "纭畾瑕佸垹闄よ繖涓檮浠跺悧锛�",
+      success: async (res) => {
+        if (res.confirm) {
+          // 鍓嶇鎵嬪姩鍒犻櫎锛氱洿鎺ヤ粠鍒楄〃涓Щ闄よ繖鏉℃暟鎹�
+          attachmentList.value = attachmentList.value.filter((item) => item.id !== attachmentId);
+
+          // 鑾峰彇鍓╀綑鐨勯檮浠禝D缁勫悎
+          const remainingIds = attachmentList.value.map((item) => item.id);
+          const attachmentIds = remainingIds.join(",");
+
+          // 璋冪敤鎶ュ伐娣诲姞闄勪欢鎺ュ彛锛屾洿鏂伴檮浠跺叧鑱�
+          await AttachmentAPI.addOutputAttachments({
+            id: parseInt(reportId.value),
+            attachmentIds: attachmentIds,
+          });
+
+          toast.show("鍒犻櫎鎴愬姛");
+        }
+      },
+    });
+  } catch (error) {
+    console.error("鍒犻櫎澶辫触:", error);
+    toast.show("鍒犻櫎澶辫触");
+  }
+};
+
+// 棰勮闄勪欢
+const previewAttachment = (item: any) => {
+  // 鏍规嵁鏂囦欢绫诲瀷杩涜棰勮
+  const fileName = item.bucketFileName || item.name;
+  const fileType = getFileType(fileName);
+
+  if (fileType.startsWith("image")) {
+    // 鍥剧墖棰勮
+    uni.previewImage({
+      urls: [item.url],
+      current: item.url,
+    });
+  } else {
+    // 鍏朵粬鏂囦欢绫诲瀷锛屽彲浠ヤ笅杞芥垨鎵撳紑
+    uni.downloadFile({
+      url: item.url,
+      success: (res) => {
+        uni.openDocument({
+          filePath: res.tempFilePath,
+          success: () => {
+            console.log("鎵撳紑鏂囨。鎴愬姛");
+          },
+          fail: (error) => {
+            console.error("鎵撳紑鏂囨。澶辫触:", error);
+            toast.show("鏃犳硶棰勮姝ゆ枃浠剁被鍨�");
+          },
+        });
+      },
+      fail: (error) => {
+        console.error("涓嬭浇鏂囦欢澶辫触:", error);
+        toast.show("涓嬭浇鏂囦欢澶辫触");
+      },
+    });
+  }
+};
+
+// 鑾峰彇鏂囦欢绫诲瀷
+const getFileType = (fileName: string) => {
+  if (!fileName) return "unknown";
+  const extension = fileName.split(".").pop()?.toLowerCase();
+  switch (extension) {
+    case "jpg":
+    case "jpeg":
+    case "png":
+    case "gif":
+    case "bmp":
+    case "webp":
+      return "image";
+    case "pdf":
+      return "pdf";
+    case "doc":
+    case "docx":
+      return "word";
+    case "xls":
+    case "xlsx":
+      return "excel";
+    case "ppt":
+    case "pptx":
+      return "powerpoint";
+    case "txt":
+      return "text";
+    case "zip":
+    case "rar":
+      return "archive";
+    default:
+      return "file";
+  }
+};
+
+// 鏍煎紡鍖栨枃浠跺ぇ灏�
+const formatFileSize = (size: number) => {
+  if (size < 1024) return size + " B";
+  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + " KB";
+  return (size / (1024 * 1024)).toFixed(1) + " MB";
+};
+
+// 鏍煎紡鍖栨椂闂�
+const formatTime = (time: string) => {
+  const date = new Date(time);
+  return date.toLocaleString();
+};
+
+onMounted(() => {
+  getAttachmentList();
+});
+</script>
+
+<style lang="scss" scoped>
+.attachment-container {
+  padding: 12px;
+  background: #f3f9f8;
+  min-height: 100vh;
+}
+
+.header-actions {
+  margin-bottom: 12px;
+
+  :deep(.add_btn) {
+    background: #0d867f;
+    color: white;
+    border: none;
+  }
+}
+
+.attachment-list {
+  .attachment-card {
+    margin-bottom: 12px;
+    border-radius: 4px;
+  }
+}
+
+.attachment-item {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+
+  .attachment-info {
+    flex: 1;
+
+    .attachment-name {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 4px;
+      word-break: break-all;
+    }
+
+    .attachment-meta {
+      display: flex;
+      gap: 12px;
+      font-size: 12px;
+      color: #999;
+    }
+  }
+
+  .attachment-actions {
+    margin-left: 12px;
+
+    :deep(.wd-icon) {
+      font-size: 20px;
+      cursor: pointer;
+    }
+  }
+}
+</style>
diff --git a/src/pages/production/wire/report/wire.vue b/src/pages/production/wire/report/wire.vue
index 76db8fb..b57634b 100644
--- a/src/pages/production/wire/report/wire.vue
+++ b/src/pages/production/wire/report/wire.vue
@@ -22,6 +22,9 @@
         </template>
         <ProductionCard :data="cardAttr" :value="item" />
         <template #footer>
+          <wd-button size="small" plain @click="toAttachment(item.id)" style="margin-right: 10px">
+            闄勪欢
+          </wd-button>
           <wd-button size="small" plain @click="toCheck(item.id)">鑷</wd-button>
         </template>
       </wd-card>
@@ -120,6 +123,12 @@
   dialog.visible = false;
 };
 
+const toAttachment = (id: number) => {
+  uni.navigateTo({
+    url: `/pages/production/wire/attachment/index?reportId=${id}`,
+  });
+};
+
 const toCheck = (id: number) => {
   uni.navigateTo({
     url: `/pages/production/wire/selfInspect/index?id=${id}`,

--
Gitblit v1.9.3