From 988f35b8ae2c9389153701bc2103a2cc3ed58539 Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期四, 19 三月 2026 15:46:04 +0800
Subject: [PATCH] feat(工单管理): 优化工单处理界面及上传组件功能

---
 src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue |  351 +++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 287 insertions(+), 64 deletions(-)

diff --git a/src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue b/src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue
index bde0faa..569005d 100644
--- a/src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue
+++ b/src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { computed, ref, watch } from "vue";
-import type { FormInst, FormRules, UploadFileInfo } from "naive-ui";
+import { computed, nextTick, ref, watch } from "vue";
+import type { FormInst, FormRules, UploadFileInfo, UploadInst } from "naive-ui";
 import { customUploadRequest } from "@/utils/upload";
 
 interface HandleFileItem {
@@ -38,12 +38,24 @@
 const formModel = ref({
   handleLog: "",
   handleUserId: null as number | string | null,
-  handleFile: [] as HandleFileItem[],
+  handleFile: [] as UploadFileInfo[],
 });
-const fileList = ref<UploadFileInfo[]>([]);
+const uploadRef = ref<UploadInst | null>(null);
+
+const isHandleUserSelectOpen = ref(false);
 
 const rules: FormRules = {
   handleLog: [{ required: true, message: "请输入处理情况", trigger: "blur" }],
+  handleFile: [
+    {
+      required: true,
+      validator(_rule, value: UploadFileInfo[]) {
+        return value && value.some((f) => f.status === "finished");
+      },
+      message: "请上传相关资料",
+      trigger: ["change", "blur"],
+    },
+  ],
 };
 
 const disableSave = computed(() => !formModel.value.handleUserId);
@@ -78,65 +90,119 @@
     handleUserId: null,
     handleFile: [],
   };
-  fileList.value = [];
   formRef.value?.restoreValidation();
 }
 
 async function handleUploadFile({ file, onFinish, onError, onProgress }: any) {
-  const res = await customUploadRequest({
-    file,
-    onFinish,
-    onError,
-    onProgress,
-  });
-  if (res) {
-    file.id = res.id;
-    file.url = res.url;
-    file.status = "finished";
-    const nextItem = {
-      id: String(res.id || file.id || Date.now()),
-      name: file.name || "附件",
-      url: res.url,
-    };
-    formModel.value.handleFile = [
-      ...formModel.value.handleFile.filter((item) => item.id !== nextItem.id),
-      nextItem,
-    ];
-    onFinish();
+  try {
+    const res = await customUploadRequest({
+      file,
+      onFinish: () => {},
+      onError,
+      onProgress,
+    });
+    if (res) {
+      // 保持 file.id 不变,供 Naive UI 内部追踪
+      file.url = res.url;
+      // 存储后端返回的真正的 ID 到自定义属性上
+      (file as any).serverId = String(res.id);
+      // 显式标记完成
+      file.status = "finished";
+
+      // 调用 Naive UI 的完成回调
+      onFinish();
+
+      // 触发视图刷新
+      formModel.value.handleFile = [...formModel.value.handleFile];
+    } else {
+      onError();
+    }
+  } catch (err) {
+    onError();
   }
 }
 
+
+
 function handleRemoveFile(
   _file: UploadFileInfo,
-  fileListValue: UploadFileInfo[]
-) {
-  const ids = new Set(
-    fileListValue
-      .map((item) => String(item.id))
-      .filter((id) => id && id !== "undefined")
-  );
-  formModel.value.handleFile = formModel.value.handleFile.filter((item) =>
-    ids.has(String(item.id))
-  );
+  _fileListValue: UploadFileInfo[]
+) {}
+
+function getFileIcon(file: UploadFileInfo) {
+  if (file.status === "error") return "/src/assets/public/error-icon.png";
+  const name = file.name.toLowerCase();
+  if (name.endsWith(".ppt") || name.endsWith(".pptx"))
+    return "/src/assets/public/ppt-icon.png";
+  if (name.endsWith(".doc") || name.endsWith(".docx") || name.endsWith(".pdf"))
+    return "/src/assets/public/word-icon.png";
+
+  const imgExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
+  if (imgExts.some((ext) => name.endsWith(ext)))
+    return "/src/assets/public/img-icon.png";
+
+  return "/src/assets/public/img-icon.png";
+}
+
+function formatFileSize(bytes?: number) {
+  if (!bytes) return "";
+  if (bytes < 1024) return bytes + "B";
+  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + "KB";
+  return (bytes / 1024 / 1024).toFixed(1) + "MB";
+}
+
+function handleDownload(file: UploadFileInfo) {
+  if (file.url) {
+    window.open(file.url);
+  }
+}
+
+function handleRetry(file: UploadFileInfo) {
+  uploadRef.value?.submit(file.id);
+}
+
+function removeFile(file: UploadFileInfo) {
+  const index = formModel.value.handleFile.findIndex((f) => f.id === file.id);
+  if (index !== -1) {
+    formModel.value.handleFile.splice(index, 1);
+  }
 }
 
 async function handleSave() {
   await formRef.value?.validate();
+
+  const files = formModel.value.handleFile
+    .filter((f) => f.status === "finished" && (f as any).serverId)
+    .map((f) => ({
+      id: (f as any).serverId,
+      name: f.name,
+      url: f.url || "",
+    }));
+
   emit("submit", {
     handleType: 1,
     handleLog: formModel.value.handleLog,
     handleUserId: formModel.value.handleUserId,
-    handleFile: JSON.stringify(formModel.value.handleFile),
+    handleFile: JSON.stringify(files),
   });
 }
 
 async function handleClose() {
   await formRef.value?.validate();
+
+  const files = formModel.value.handleFile
+    .filter((f) => f.status === "finished" && (f as any).serverId)
+    .map((f) => ({
+      id: (f as any).serverId,
+      name: f.name,
+      url: f.url || "",
+    }));
+
   emit("submit", {
     handleType: 2,
     handleLog: formModel.value.handleLog,
     handleUserId: null,
-    handleFile: JSON.stringify(formModel.value.handleFile),
+    handleFile: JSON.stringify(files),
   });
 }
 
@@ -156,24 +222,31 @@
     preset="card"
     title="处理工单"
     :mask-closable="false"
-    :style="{ width: '860px' }"
+    :style="{ width: '600px' }"
+    :auto-focus="false"
     :segmented="{ content: true, action: true }"
   >
-    <div class="space-y-4">
-      <div class="rounded border border-[#E6E6E6] p-3">
-        <div class="grid grid-cols-2 gap-4 text-sm text-[#1D2129]">
-          <div>问题:{{ props.rowData?.problem || "-" }}</div>
-          <div>反馈用户:{{ props.rowData?.feedbackUserPhone || "-" }}</div>
-          <div>来源:{{ displayProblemSource }}</div>
-          <div>类型:{{ displayType }}</div>
-          <div>优先级:{{ displayPriority }}</div>
-        </div>
-        <div class="mt-3 flex text-sm text-[#1D2129]">
-          <div>问题描述:</div>
+    <div class="space-y-10">
+      <div class="space-y-5">
+        <n-flex :size="[8, 0]" align="center" class="mb-2">
+          <div class="bg-[#0068FF] w-3px h-14px"></div>
+          <div class="text-[#171F26] text-sm font-medium">基础信息</div>
+        </n-flex>
+        <div class="space-y-4">
+          <!-- 问题 -->
           <div>
-            <div class="whitespace-pre-wrap text-[#4E5969]">
+            <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">问题</div>
+            <div class="text-[#171F26] text-sm">
+              {{ props.rowData?.problem || "-" }}
+            </div>
+          </div>
+          <!-- 问题描述 -->
+          <div>
+            <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">问题描述</div>
+            <div class="text-[#171F26] text-sm whitespace-pre-wrap">
               {{ props.rowData?.problemDescription || "-" }}
             </div>
+            <!-- 图片列表 -->
             <div v-if="displayImages.length" class="mt-3">
               <n-image-group>
                 <div class="flex flex-wrap gap-2">
@@ -181,11 +254,52 @@
                     v-for="item in displayImages"
                     :key="item.id || item.url"
                     :src="item.url"
-                    width="80"
-                    height="80"
+                    width="100"
+                    height="100"
+                    class="rounded-lg"
                   />
                 </div>
               </n-image-group>
+            </div>
+          </div>
+
+          <!-- 网格信息 -->
+          <div class="grid grid-cols-2 gap-y-4">
+            <div>
+              <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">反馈用户</div>
+              <div class="text-[#171F26] text-sm">
+                {{ props.rowData?.feedbackUserPhone || "-" }}
+              </div>
+            </div>
+            <div>
+              <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">来源</div>
+              <div class="text-[#171F26] text-sm">
+                {{ displayProblemSource }}
+              </div>
+            </div>
+            <div>
+              <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">类型</div>
+              <div class="text-[#171F26] text-sm">{{ displayType }}</div>
+            </div>
+            <div>
+              <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">优先级</div>
+              <div>
+                <n-tag
+                  v-if="props.rowData?.priority"
+                  :type="
+                    props.rowData.priority === 3
+                      ? 'error'
+                      : props.rowData.priority === 2
+                      ? 'warning'
+                      : 'success'
+                  "
+                  :bordered="false"
+                  size="small"
+                >
+                  {{ displayPriority }}
+                </n-tag>
+                <span v-else class="text-[#171F26] text-sm">-</span>
+              </div>
             </div>
           </div>
         </div>
@@ -195,11 +309,14 @@
         ref="formRef"
         :model="formModel"
         :rules="rules"
-        label-placement="left"
+        label-placement="top"
         label-width="90"
         require-mark-placement="left"
       >
-        <div class="text-sm font-medium text-[#1D2129] mb-2">处理日志</div>
+        <n-flex :size="[8, 0]" align="center" class="mb-4">
+          <div class="bg-[#0068FF] w-3px h-14px"></div>
+          <div class="text-[#171F26] text-sm font-medium">处理日志</div>
+        </n-flex>
         <n-form-item label="处理情况" path="handleLog">
           <n-input
             v-model:value="formModel.handleLog"
@@ -209,20 +326,108 @@
             clearable
           />
         </n-form-item>
-        <n-form-item label="相关资料" path="handleFile" :show-feedback="false">
+        <n-form-item path="handleFile">
+          <template #label>
+            <n-flex>
+              <div>相关资料</div>
+              <div style="color: rgba(23, 31, 38, 0.6)">
+                上传通话录音、处理结果等资料
+              </div>
+            </n-flex>
+          </template>
           <div class="w-full">
             <n-upload
-              v-model:file-list="fileList"
-              list-type="text"
+              ref="uploadRef"
+              v-model:file-list="formModel.handleFile"
+              :show-file-list="false"
               :max="10"
-              accept="image/*,audio/*,video/*"
+              accept="image/*,audio/*,video/*,.doc,.docx,.ppt,.pptx,.pdf"
               :custom-request="handleUploadFile"
-              @remove="handleRemoveFile"
+              @on-remove="handleRemoveFile"
+              directory-dnd
             >
-              <n-button>上传文件</n-button>
+              <n-upload-dragger>
+                <n-flex justify="center">
+                  <img
+                    src="/src/assets/public/upload-folder-icon.png"
+                    class="w-32px h-32px"
+                  />
+                </n-flex>
+                <n-text style="font-size: 16px; margin-top: 18px">
+                  点击或拖拽文件到此处上传
+                </n-text>
+                <n-p
+                  depth="3"
+                  style="margin-top: 12px; color: rgba(23, 31, 38, 0.6)"
+                >
+                  支持图片、音频、视频等文件,大小≤60M
+                </n-p>
+              </n-upload-dragger>
             </n-upload>
-            <div class="mt-2 text-xs text-[#86909C]">
-              上传通话录音、处理结果等资料。上传文件类型可选图片、音频、视频
+
+            <div v-if="formModel.handleFile.length" class="mt-4 flex flex-col gap-3 w-full">
+              <div
+                v-for="file in formModel.handleFile"
+                :key="file.id"
+                class="relative flex items-center justify-between p-3 rounded bg-[#F4F6F8] overflow-hidden"
+              >
+                <div
+                  v-if="file.status === 'uploading' || file.status === 'error'"
+                  class="absolute bottom-0 left-0 h-[2px] transition-all duration-300"
+                  :class="
+                    file.status === 'error' ? 'bg-[#F53F3F]' : 'bg-[#165DFF]'
+                  "
+                  :style="{ width: `${file.percentage || 0}%` }"
+                ></div>
+
+                <div class="flex items-center gap-3 z-10">
+                  <img
+                    :src="getFileIcon(file)"
+                    class="w-6 h-6 object-contain"
+                  />
+                  <div class="flex items-center gap-3">
+                    <span
+                      v-if="file.status === 'error'"
+                      class="text-[#F53F3F] text-sm"
+                      >上传失败</span
+                    >
+                    <span
+                      v-else-if="file.status === 'uploading'"
+                      class="text-[#165DFF] text-sm"
+                      >上传中</span
+                    >
+                    <template v-else>
+                      <span
+                        class="text-[#1D2129] text-sm truncate max-w-[200px]"
+                        >{{ file.name }}</span
+                      >
+                      <span class="text-[#86909C] text-sm">{{
+                        formatFileSize(file.file?.size)
+                      }}</span>
+                    </template>
+                  </div>
+                </div>
+
+                <div class="flex items-center gap-4 pr-1 z-10">
+                  <img
+                    src="/src/assets/public/delete-file-icon.png"
+                    class="w-11px h-13px cursor-pointer hover:opacity-80"
+                    @click="removeFile(file)"
+                  />
+                  <img
+                    v-if="file.status === 'error'"
+                    src="/src/assets/public/reload-upload-icon.png"
+                    class="w-12px h-12px cursor-pointer hover:opacity-80"
+                    @click="handleRetry(file)"
+                  />
+                  <img
+                    v-if="file.status === 'finished'"
+                    src="/src/assets/public/download-file-icon.png"
+                    class="w-12px h-12px cursor-pointer hover:opacity-80"
+                    @click="handleDownload(file)"
+                  />
+                </div>
+              </div>
             </div>
           </div>
         </n-form-item>
@@ -232,7 +437,20 @@
             :options="handleUserOptions"
             placeholder="请选择"
             clearable
-          />
+            @update:show="(val) => (isHandleUserSelectOpen = val)"
+          >
+            <template #arrow>
+              <img
+                src="/src/assets/element-img/select-down.png"
+                :class="[
+                  'arrow-icon',
+                  isHandleUserSelectOpen ? 'arrow-icon--open' : '',
+                ]"
+                style="width: 16px; height: 16px"
+                alt="arrow"
+              />
+            </template>
+          </n-select>
         </n-form-item>
       </n-form>
     </div>
@@ -251,4 +469,9 @@
   </n-modal>
 </template>
 
-<style scoped></style>
+<style scoped lang="scss">
+::v-deep .n-upload-dragger {
+  padding: 40px 0;
+  background-color: transparent;
+}
+</style>

--
Gitblit v1.7.1