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