From 3682ad63b5bdb47228325dea1efe2bb9069254a5 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期一, 11 五月 2026 15:53:18 +0800
Subject: [PATCH] 合格出库扫销售二维码进行发货

---
 src/pages/sales/salesAccount/goOut.vue |  570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 567 insertions(+), 3 deletions(-)

diff --git a/src/pages/sales/salesAccount/goOut.vue b/src/pages/sales/salesAccount/goOut.vue
index 35e2a3f..05e3f90 100644
--- a/src/pages/sales/salesAccount/goOut.vue
+++ b/src/pages/sales/salesAccount/goOut.vue
@@ -20,7 +20,78 @@
                    @click="showPicker = true"></up-icon>
         </template>
       </u-form-item>
+      <u-form-item v-if="typeValue === '璐ц溅'"
+                   prop="shippingCarNumber"
+                   label="杞︾墝鍙�"
+                   required>
+        <u-input v-model="form.shippingCarNumber"
+                 placeholder="璇疯緭鍏ヨ溅鐗屽彿"
+                 clearable />
+      </u-form-item>
+      <u-form-item v-if="typeValue === '蹇��'"
+                   prop="expressNumber"
+                   label="蹇�掑崟鍙�"
+                   required>
+        <u-input v-model="form.expressNumber"
+                 placeholder="璇疯緭鍏ュ揩閫掑崟鍙�"
+                 clearable />
+      </u-form-item>
     </u-form>
+    <!-- 鍙戣揣鍥剧墖锛氱浉鍐屾垨鐩告満銆�/file/upload銆侀瑙堜笌鍒楄〃鏍峰紡 -->
+    <view class="ship-images-card">
+      <view class="ship-images-header">
+        <text class="ship-images-title">鍙戣揣鍥剧墖</text>
+        <text class="ship-images-hint">鏈�澶� {{ uploadConfig.limit }} 寮�</text>
+      </view>
+      <view class="simple-upload-area">
+        <view class="upload-buttons">
+          <u-button type="primary"
+                    @click="chooseShipImage"
+                    :loading="shipUploading"
+                    :disabled="shipFiles.length >= uploadConfig.limit"
+                    :customStyle="{ width: '100%' }">
+            <u-icon name="camera"
+                    size="18"
+                    color="#fff"
+                    style="margin-right: 5px;"></u-icon>
+            {{ shipUploading ? '涓婁紶涓�...' : '娣诲姞鍥剧墖' }}
+          </u-button>
+        </view>
+        <view v-if="shipUploading"
+              class="upload-progress">
+          <u-line-progress :percentage="shipUploadProgress"
+                           :showText="true"
+                           activeColor="#409eff"></u-line-progress>
+        </view>
+        <view v-if="shipFiles.length > 0"
+              class="file-list">
+          <view v-for="(file, index) in shipFiles"
+                :key="file.uid || index"
+                class="file-item">
+            <view class="file-preview-container"
+                  @click="previewShipImage(file)">
+              <image :src="getFileAccessUrl(file)"
+                     class="file-preview"
+                     mode="aspectFill" />
+              <view class="delete-btn"
+                    @click.stop="removeShipFile(index)">
+                <u-icon name="close"
+                        size="12"
+                        color="#fff"></u-icon>
+              </view>
+            </view>
+            <view class="file-info">
+              <text class="file-name">{{ file.bucketFilename || file.name || '鍥剧墖' }}</text>
+              <text class="file-size">{{ formatFileSize(file.size) }}</text>
+            </view>
+          </view>
+        </view>
+        <view v-else
+              class="empty-state">
+          <text>璇蜂粠鐩稿唽閫夋嫨鎴栨媿鐓т笂浼犲彂璐у浘鐗�</text>
+        </view>
+      </view>
+    </view>
     <!-- 閫夋嫨鍣ㄥ脊绐� -->
     <up-action-sheet :show="showPicker"
                      :actions="productOptions"
@@ -87,9 +158,11 @@
 </template>
 
 <script setup>
-  import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
+  import { ref, computed, onMounted, onUnmounted, reactive, toRefs } from "vue";
   import PageHeader from "@/components/PageHeader.vue";
   import { addShippingInfo } from "@/api/salesManagement/salesLedger";
+  import config from "@/config";
+  import { getToken } from "@/utils/auth";
   const showToast = message => {
     uni.showToast({
       title: message,
@@ -97,6 +170,8 @@
     });
   };
   import { userListNoPageByTenantId } from "@/api/system/user";
+
+  const typeValue = ref("璐ц溅");
 
   const data = reactive({
     form: {
@@ -114,9 +189,43 @@
       endDate: "",
       location: "",
       price: "",
+      shippingCarNumber: "",
+      expressNumber: "",
     },
     rules: {
       typeValue: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+      shippingCarNumber: [
+        {
+          validator: (rule, value, callback) => {
+            if (typeValue.value !== "璐ц溅") {
+              callback();
+              return;
+            }
+            if (!value || !String(value).trim()) {
+              callback(new Error("璇疯緭鍏ヨ溅鐗屽彿"));
+              return;
+            }
+            callback();
+          },
+          trigger: ["blur", "change"],
+        },
+      ],
+      expressNumber: [
+        {
+          validator: (rule, value, callback) => {
+            if (typeValue.value !== "蹇��") {
+              callback();
+              return;
+            }
+            if (!value || !String(value).trim()) {
+              callback(new Error("璇疯緭鍏ュ揩閫掑崟鍙�"));
+              return;
+            }
+            callback();
+          },
+          trigger: ["blur", "change"],
+        },
+      ],
     },
   });
   const { form, rules } = toRefs(data);
@@ -138,6 +247,329 @@
   const formRef = ref(null);
   const approveType = ref(0);
   const goOutData = ref({});
+
+  // 涓庤澶囧贰妫� inspectionUpload/index.vue 涓� uploadConfig 涓�鑷�
+  const uploadConfig = {
+    action: "/file/upload",
+    limit: 10,
+    fileSize: 50,
+    fileType: ["jpg", "jpeg", "png", "gif", "webp"],
+  };
+
+  /** 涓庤澶囧贰妫�涓�鑷达細鐢熶骇鍓� type=10 浼犵粰 /file/upload 鐨� formData.type */
+  const shipUploadFormType = 10;
+
+  const uploadFileUrl = computed(() => (config.baseUrl || "").replace(/\/$/, "") + uploadConfig.action);
+
+  const shipFiles = ref([]);
+  const shipUploading = ref(false);
+  const shipUploadProgress = ref(0);
+
+  const isImageFile = file => {
+    if (file?.contentType && String(file.contentType).startsWith("image/")) return true;
+    if (file?.type === "image" || file?.mediaType === "image") return true;
+    const name = file?.bucketFilename || file?.originalFilename || file?.name || "";
+    const ext = name.split(".").pop()?.toLowerCase();
+    return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
+  };
+
+  const filePreviewBase = config.fileUrl;
+
+  const normalizeFileUrl = (rawUrl = "") => {
+    let fileUrl = rawUrl || "";
+    if (typeof fileUrl === "string") {
+      fileUrl = fileUrl.trim().replace(/^['"]|['"]$/g, "");
+    }
+    const javaApi = filePreviewBase;
+    const localPrefixes = ["wxfile://", "file://", "content://", "blob:", "data:"];
+
+    if (localPrefixes.some(prefix => fileUrl.startsWith(prefix))) {
+      return fileUrl;
+    }
+
+    if (fileUrl && fileUrl.indexOf("\\") > -1) {
+      const lowerPath = fileUrl.toLowerCase();
+      const uploadPathIndex = lowerPath.indexOf("uploadpath");
+      const prodIndex = lowerPath.indexOf("\\prod\\");
+
+      if (uploadPathIndex > -1) {
+        fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/");
+      } else if (prodIndex > -1) {
+        fileUrl = fileUrl
+          .substring(prodIndex + "\\prod\\".length)
+          .replace(/\\/g, "/");
+      } else {
+        fileUrl = fileUrl.replace(/\\/g, "/");
+      }
+    }
+    const normalizedLower = String(fileUrl).toLowerCase();
+    const fileProdIdx = normalizedLower.indexOf("/file/prod/");
+    if (fileProdIdx > -1) {
+      fileUrl = `/profile/prod/${fileUrl.substring(fileProdIdx + "/file/prod/".length)}`;
+    }
+    const fileTempIdx = normalizedLower.indexOf("/file/temp/");
+    if (fileTempIdx > -1) {
+      fileUrl = `/profile/temp/${fileUrl.substring(fileTempIdx + "/file/temp/".length)}`;
+    }
+
+    if (/^\/?uploadPath/i.test(fileUrl)) {
+      fileUrl = fileUrl.replace(/^\/?uploadPath/i, "/profile");
+    }
+
+    if (fileUrl && !fileUrl.startsWith("http")) {
+      if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl;
+      fileUrl = javaApi + fileUrl;
+    }
+
+    return fileUrl;
+  };
+
+  const getFileAccessUrl = (file = {}) => {
+    if (file?.link) {
+      if (String(file.link).startsWith("http")) return file.link;
+      return normalizeFileUrl(file.link);
+    }
+    const remoteUrl = normalizeFileUrl(
+      file?.url || file?.downloadUrl || file?.tempPath || ""
+    );
+    if (remoteUrl) return remoteUrl;
+    if (file?._localPreviewUrl) return file._localPreviewUrl;
+    return normalizeFileUrl(file?.tempFilePath || file?.path || "");
+  };
+
+  const formatFileSize = size => {
+    if (!size) return "";
+    if (size < 1024) return size + "B";
+    if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
+    return (size / (1024 * 1024)).toFixed(1) + "MB";
+  };
+
+  const handleShipUploadError = (message = "涓婁紶鏂囦欢澶辫触") => {
+    shipUploading.value = false;
+    shipUploadProgress.value = 0;
+    uni.showToast({ title: message, icon: "error" });
+  };
+
+  const handleShipUploadSuccess = (res, file) => {
+    const uploadedFile = res.data;
+    if (!uploadedFile) {
+      handleShipUploadError("涓婁紶鍝嶅簲鏁版嵁鏍煎紡閿欒");
+      return;
+    }
+    const fileData = {
+      ...file,
+      id: uploadedFile.id,
+      tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
+      url:
+        uploadedFile.url ||
+        uploadedFile.downloadUrl ||
+        uploadedFile.tempPath ||
+        file.tempFilePath ||
+        file.path,
+      tempPath: uploadedFile.tempPath || file.tempPath || "",
+      _localPreviewUrl: file.tempFilePath || file.path || "",
+      bucketFilename:
+        uploadedFile.bucketFilename ||
+        uploadedFile.originalFilename ||
+        uploadedFile.originalName ||
+        file.name,
+      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
+      size: uploadedFile.size || uploadedFile.byteSize || file.size,
+      createTime: uploadedFile.createTime || new Date().getTime(),
+      mediaType: file.type || uploadedFile.mediaType,
+    };
+    shipFiles.value.push(fileData);
+    uni.showToast({ title: "涓婁紶鎴愬姛", icon: "success" });
+  };
+
+  const uploadShipWithUniUploadFile = (file, filePath, token) => {
+    if (!filePath) {
+      handleShipUploadError("鏂囦欢璺緞涓嶅瓨鍦�");
+      return;
+    }
+    shipUploading.value = true;
+    shipUploadProgress.value = 0;
+
+    const uploadTask = uni.uploadFile({
+      url: uploadFileUrl.value,
+      filePath,
+      name: "file",
+      formData: {
+        type: shipUploadFormType,
+      },
+      header: {
+        Authorization: `Bearer ${token}`,
+      },
+      success: res => {
+        try {
+          if (res.statusCode === 200) {
+            const response = JSON.parse(res.data || "{}");
+            if (response.code === 200) {
+              handleShipUploadSuccess(response, file);
+            } else {
+              handleShipUploadError(response.msg || "鏈嶅姟鍣ㄨ繑鍥為敊璇�");
+            }
+          } else {
+            handleShipUploadError(`鏈嶅姟鍣ㄩ敊璇紝鐘舵�佺爜: ${res.statusCode}`);
+          }
+        } catch (e) {
+          console.error("瑙f瀽鍝嶅簲澶辫触:", e);
+          handleShipUploadError("鍝嶅簲鏁版嵁瑙f瀽澶辫触");
+        }
+      },
+      fail: err => {
+        let errorMessage = "涓婁紶澶辫触";
+        if (err.errMsg) {
+          if (err.errMsg.includes("statusCode: null")) {
+            errorMessage = "缃戠粶杩炴帴澶辫触锛岃妫�鏌ョ綉缁滆缃�";
+          } else if (err.errMsg.includes("timeout")) {
+            errorMessage = "涓婁紶瓒呮椂锛岃閲嶈瘯";
+          } else if (err.errMsg.includes("fail")) {
+            errorMessage = "涓婁紶澶辫触锛岃妫�鏌ョ綉缁滆繛鎺�";
+          } else {
+            errorMessage = err.errMsg;
+          }
+        }
+        handleShipUploadError(errorMessage);
+      },
+      complete: () => {
+        shipUploading.value = false;
+        shipUploadProgress.value = 0;
+      },
+    });
+
+    if (uploadTask && uploadTask.onProgressUpdate) {
+      uploadTask.onProgressUpdate(r => {
+        shipUploadProgress.value = r.progress;
+      });
+    }
+  };
+
+  const uploadShipFile = file => {
+    const token = getToken();
+    if (!token) {
+      handleShipUploadError("鐢ㄦ埛鏈櫥褰�");
+      return;
+    }
+    uploadShipWithUniUploadFile(file, file.tempFilePath || file.path || "", token);
+  };
+
+  const handleBeforeShipUpload = async file => {
+    if (uploadConfig.fileType?.length) {
+      const fileName = file.name || "";
+      const fileExtension = fileName ? fileName.split(".").pop().toLowerCase() : "";
+      const expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
+      if (fileExtension && expectedTypes.length > 0) {
+        const isAllowed = expectedTypes.some(
+          t => uploadConfig.fileType.includes(t) && t === fileExtension
+        );
+        if (!isAllowed) {
+          uni.showToast({
+            title: `鏂囦欢鏍煎紡涓嶆敮鎸侊紝璇烽�夋嫨 ${expectedTypes.join("/")} 鏍煎紡鐨勫浘鐗嘸,
+            icon: "none",
+          });
+          return false;
+        }
+      }
+    }
+    uploadShipFile(file);
+    return true;
+  };
+
+  const chooseShipImage = () => {
+    if (shipFiles.value.length >= uploadConfig.limit) {
+      uni.showToast({
+        title: `鏈�澶氬彧鑳戒笂浼�${uploadConfig.limit}寮犲浘鐗嘸,
+        icon: "none",
+      });
+      return;
+    }
+
+    const remaining = uploadConfig.limit - shipFiles.value.length;
+
+    if (typeof uni.chooseMedia === "function") {
+      uni.chooseMedia({
+        count: Math.min(remaining, 1),
+        mediaType: ["image"],
+        sizeType: ["compressed", "original"],
+        sourceType: ["album", "camera"],
+        success: res => {
+          try {
+            const files = res?.tempFiles || [];
+            if (!files.length) throw new Error("鏈幏鍙栧埌鏂囦欢");
+            files.forEach((tf, idx) => {
+              const filePath = tf.tempFilePath || tf.path || "";
+              const file = {
+                tempFilePath: filePath,
+                path: filePath,
+                type: "image",
+                name: `image_${Date.now()}_${idx}.jpg`,
+                size: tf.size || 0,
+                createTime: Date.now(),
+                uid: Date.now() + Math.random() + idx,
+              };
+              handleBeforeShipUpload(file);
+            });
+          } catch (e) {
+            console.error("澶勭悊鍥剧墖閫夋嫨缁撴灉澶辫触:", e);
+            uni.showToast({ title: "澶勭悊鏂囦欢澶辫触", icon: "error" });
+          }
+        },
+        fail: () => uni.showToast({ title: "閫夋嫨鍥剧墖澶辫触", icon: "error" }),
+      });
+      return;
+    }
+
+    uni.chooseImage({
+      count: 1,
+      sizeType: ["compressed", "original"],
+      sourceType: ["album", "camera"],
+      success: res => {
+        const tempFilePath = res?.tempFilePaths?.[0];
+        const tempFile = res?.tempFiles?.[0] || {};
+        if (!tempFilePath) return;
+        handleBeforeShipUpload({
+          tempFilePath,
+          path: tempFilePath,
+          type: "image",
+          name: `photo_${Date.now()}.jpg`,
+          size: tempFile.size || 0,
+          createTime: Date.now(),
+          uid: Date.now() + Math.random(),
+        });
+      },
+      fail: () => uni.showToast({ title: "閫夋嫨鍥剧墖澶辫触", icon: "none" }),
+    });
+  };
+
+  const previewShipImage = file => {
+    if (!file || !isImageFile(file)) return;
+    const imageUrls = shipFiles.value
+      .filter(f => isImageFile(f))
+      .map(f => getFileAccessUrl(f))
+      .filter(Boolean);
+    const current = getFileAccessUrl(file);
+    if (!imageUrls.length || !current) return;
+    uni.previewImage({ urls: imageUrls, current });
+  };
+
+  const removeShipFile = index => {
+    uni.showModal({
+      title: "纭鍒犻櫎",
+      content: "纭畾瑕佸垹闄よ繖涓枃浠跺悧锛�",
+      success: res => {
+        if (res.confirm) {
+          shipFiles.value.splice(index, 1);
+          uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+        }
+      },
+    });
+  };
+
+  const getShipTempFileIds = () =>
+    shipFiles.value
+      .map(it => it.tempId ?? it.tempFileId ?? it.id)
+      .filter(v => v !== undefined && v !== null && v !== "");
   onMounted(async () => {
     try {
       userListNoPageByTenantId().then(res => {
@@ -161,11 +593,14 @@
     // 绉婚櫎浜嬩欢鐩戝惉
     uni.$off("selectContact", handleSelectContact);
   });
-  const typeValue = ref("璐ц溅");
   const onConfirm = item => {
-    // 璁剧疆閫変腑鐨勯儴闂�
     typeValue.value = item.name;
     showPicker.value = false;
+    if (item.name === "璐ц溅") {
+      form.value.expressNumber = "";
+    } else {
+      form.value.shippingCarNumber = "";
+    }
   };
 
   const goBack = () => {
@@ -183,6 +618,10 @@
       showToast("璇蜂负姣忎釜瀹℃壒姝ラ閫夋嫨瀹℃壒浜�");
       return;
     }
+    if (shipUploading.value) {
+      showToast("鍥剧墖姝e湪涓婁紶锛岃绋嶅��");
+      return;
+    }
     formRef.value
       .validate()
       .then(valid => {
@@ -198,6 +637,11 @@
             salesLedgerProductId: goOutData.value.id,
             type: typeValue.value,
             approveUserIds,
+            shippingCarNumber:
+              typeValue.value === "璐ц溅" ? String(form.value.shippingCarNumber || "").trim() : "",
+            expressNumber:
+              typeValue.value === "蹇��" ? String(form.value.expressNumber || "").trim() : "",
+            tempFileIds: getShipTempFileIds(),
           };
           console.log(params, "params");
 
@@ -273,6 +717,126 @@
 <style scoped lang="scss">
   @import "@/static/scss/form-common.scss";
 
+  .ship-images-card {
+    background: #fff;
+    margin: 16px;
+    border-radius: 16px;
+    padding: 16px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  }
+
+  .ship-images-header {
+    margin-bottom: 12px;
+  }
+
+  .ship-images-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+    display: block;
+    margin-bottom: 4px;
+  }
+
+  .ship-images-hint {
+    font-size: 12px;
+    color: #999;
+  }
+
+  /* 浠ヤ笅涓� inspectionUpload/index.vue 绠�鍖栦笂浼犲尯銆佹枃浠跺垪琛ㄣ�佽棰戝脊绐椾竴鑷� */
+  .simple-upload-area {
+    padding: 0;
+  }
+
+  .upload-buttons {
+    display: flex;
+    gap: 10px;
+    margin-bottom: 15px;
+  }
+
+  .upload-progress {
+    margin-bottom: 12px;
+  }
+
+  .file-list {
+    margin-top: 15px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .file-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: #fff;
+    border-radius: 12px;
+    padding: 8px;
+    border: 1px solid #e9ecef;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+    width: calc(50% - 6px);
+    min-width: 120px;
+  }
+
+  .file-preview-container {
+    position: relative;
+    margin-bottom: 8px;
+  }
+
+  .file-preview {
+    width: 80px;
+    height: 80px;
+    border-radius: 8px;
+    object-fit: cover;
+    border: 2px solid #f0f0f0;
+  }
+
+  .delete-btn {
+    position: absolute;
+    top: -6px;
+    right: -6px;
+    width: 20px;
+    height: 20px;
+    background: #ff4757;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
+  }
+
+  .file-info {
+    text-align: center;
+    width: 100%;
+  }
+
+  .file-name {
+    font-size: 12px;
+    color: #333;
+    font-weight: 500;
+    display: block;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 100px;
+  }
+
+  .file-size {
+    font-size: 10px;
+    color: #999;
+    margin-top: 2px;
+    display: block;
+  }
+
+  .empty-state {
+    text-align: center;
+    padding: 24px 16px;
+    color: #999;
+    font-size: 14px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 2px dashed #ddd;
+  }
+
   .approval-process {
     background: #fff;
     margin: 16px;

--
Gitblit v1.9.3