feat(工单): 新增工单管理模块并修复上传组件问题
- 新增工单管理功能,包括列表、创建、编辑、处理和详情页面
- 添加工单相关中英文国际化文案
- 在工单模态框和详情页中使用国际化文本
- 添加上传状态相关翻译(上传中、上传失败)
| | |
| | | "inputPlaceholder": "please enter", |
| | | "selectPlaceholder": "please choose", |
| | | "search": "Search", |
| | | "reset": "Reset" |
| | | "reset": "Reset", |
| | | "uploading": "Uploading", |
| | | "uploadFailed": "Upload Failed" |
| | | }, |
| | | "app": { |
| | | "loginOut": "Login out", |
| | |
| | | "index": "No.", |
| | | "audit": "Review", |
| | | "searchName": "Employee name" |
| | | }, |
| | | "workOrder": { |
| | | "index": "Index", |
| | | "submitUser": "Submit User", |
| | | "source": "Source", |
| | | "type": "Type", |
| | | "problem": "Problem", |
| | | "priority": "Priority", |
| | | "status": "Status", |
| | | "handler": "Handler", |
| | | "addTime": "Add Time", |
| | | "actions": "Actions", |
| | | "detail": "Detail", |
| | | "handle": "Handle", |
| | | "edit": "Edit", |
| | | "delete": "Delete", |
| | | "create": "Create", |
| | | "confirmDelete": "Confirm Delete", |
| | | "confirmDeleteContent": "Are you sure you want to delete this work order?", |
| | | "search": { |
| | | "user": "User", |
| | | "source": "Problem Source", |
| | | "type": "Type", |
| | | "priority": "Priority", |
| | | "status": "Status", |
| | | "placeholder": "Please enter", |
| | | "selectPlaceholder": "Please select" |
| | | }, |
| | | "modal": { |
| | | "add": "Create Work Order", |
| | | "edit": "Edit Work Order", |
| | | "problem": "Problem", |
| | | "problemDesc": "Problem Description", |
| | | "image": "Image", |
| | | "feedbackUser": "Feedback User", |
| | | "source": "Source", |
| | | "type": "Type", |
| | | "priority": "Priority", |
| | | "handler": "Handler", |
| | | "placeholder": "Please enter", |
| | | "selectPlaceholder": "Please select", |
| | | "uploadTip": "Support jpg/png/gif, size ≤ 10MB, up to 9 images", |
| | | "validation": { |
| | | "problem": "Please enter problem", |
| | | "problemDesc": "Please enter problem description", |
| | | "feedbackUser": "Please enter feedback user", |
| | | "source": "Please select source", |
| | | "type": "Please select type", |
| | | "priority": "Please select priority", |
| | | "handler": "Please select handler" |
| | | } |
| | | }, |
| | | "handleModal": { |
| | | "title": "Handle Work Order", |
| | | "baseInfo": "Basic Info", |
| | | "problem": "Problem", |
| | | "problemDesc": "Problem Description", |
| | | "feedbackUser": "Feedback User", |
| | | "source": "Source", |
| | | "type": "Type", |
| | | "priority": "Priority", |
| | | "handleLog": "Handle Log", |
| | | "handleInfo": "Handle Description", |
| | | "relatedFiles": "Related Files", |
| | | "relatedFilesTip": "Upload call recordings, results, etc.", |
| | | "uploadDraggerText": "Click or drag files here to upload", |
| | | "uploadDraggerSubText": "Support images, audio, video, etc., size ≤ 60M", |
| | | "transfer": "Transfer", |
| | | "close": "Finish", |
| | | "save": "Save", |
| | | "confirmClose": "Are you sure you want to finish?", |
| | | "validation": { |
| | | "handleInfo": "Please enter handle description", |
| | | "relatedFiles": "Please upload related files" |
| | | } |
| | | }, |
| | | "priorityMap": { |
| | | "low": "Low", |
| | | "medium": "Medium", |
| | | "high": "High" |
| | | }, |
| | | "statusMap": { |
| | | "pending": "Pending", |
| | | "processing": "Processing", |
| | | "finished": "Finished" |
| | | }, |
| | | "sourceMap": { |
| | | "phone": "Phone" |
| | | }, |
| | | "detailPage": { |
| | | "title": "Work Order Base Info", |
| | | "processTitle": "Processing Flow", |
| | | "createBy": "Created By", |
| | | "createTime": "Creation Time", |
| | | "handleContent": "Handling Content", |
| | | "transfer": "Transfer", |
| | | "close": "Close", |
| | | "preview": "Preview", |
| | | "download": "Download", |
| | | "fileNotExist": "File address does not exist", |
| | | "editSuccess": "Edit success", |
| | | "editFailed": "Edit failed", |
| | | "sourcePhone": "Phone Customer Service", |
| | | "createWorkOrder": "Create Work Order", |
| | | "handle": "Process" |
| | | } |
| | | } |
| | | }, |
| | | "accountManagement": { |
| | |
| | | "navigate": "切换", |
| | | "inputPlaceholder": "请输入", |
| | | "selectPlaceholder": "请选择", |
| | | "search": "查询", |
| | | "search": "搜索", |
| | | "reset": "重置", |
| | | "export": "导出", |
| | | "expand": "展开", |
| | | "collapse": "收起" |
| | | "collapse": "收起", |
| | | "uploading": "上传中", |
| | | "uploadFailed": "上传失败" |
| | | }, |
| | | "ticket": { |
| | | "feedback": { |
| | |
| | | "resetPasswordRuleTip": "请输入账号/手机号码", |
| | | "resetPassword": "重置密码" |
| | | }, |
| | | "workOrder": { |
| | | "index": "序号", |
| | | "submitUser": "提交用户", |
| | | "source": "来源", |
| | | "type": "类型", |
| | | "problem": "问题", |
| | | "priority": "优先级", |
| | | "status": "处理状态", |
| | | "handler": "处理人", |
| | | "addTime": "添加时间", |
| | | "actions": "操作", |
| | | "detail": "详情", |
| | | "handle": "处理", |
| | | "edit": "编辑", |
| | | "delete": "删除", |
| | | "create": "创建工单", |
| | | "confirmDelete": "确认删除", |
| | | "confirmDeleteContent": "确定要删除该工单吗?", |
| | | "search": { |
| | | "user": "用户", |
| | | "source": "问题来源", |
| | | "type": "类型", |
| | | "priority": "优先级", |
| | | "status": "状态", |
| | | "placeholder": "请输入", |
| | | "selectPlaceholder": "请选择" |
| | | }, |
| | | "modal": { |
| | | "add": "创建工单", |
| | | "edit": "编辑工单", |
| | | "problem": "问题", |
| | | "problemDesc": "问题描述", |
| | | "image": "图片", |
| | | "feedbackUser": "反馈用户", |
| | | "source": "来源", |
| | | "type": "类型", |
| | | "priority": "优先级", |
| | | "handler": "处理人", |
| | | "placeholder": "请输入", |
| | | "selectPlaceholder": "请选择", |
| | | "uploadTip": "支持jpg/png/gif ,大小≤10MB,最多上传9张", |
| | | "validation": { |
| | | "problem": "请输入问题", |
| | | "problemDesc": "请输入问题描述", |
| | | "feedbackUser": "请输入反馈用户", |
| | | "source": "请选择来源", |
| | | "type": "请选择类型", |
| | | "priority": "请选择优先级", |
| | | "handler": "请选择处理人" |
| | | } |
| | | }, |
| | | "handleModal": { |
| | | "title": "处理工单", |
| | | "baseInfo": "基础信息", |
| | | "problem": "问题", |
| | | "problemDesc": "问题描述", |
| | | "feedbackUser": "反馈用户", |
| | | "source": "来源", |
| | | "type": "类型", |
| | | "priority": "优先级", |
| | | "handleLog": "处理日志", |
| | | "handleInfo": "处理情况", |
| | | "relatedFiles": "相关资料", |
| | | "relatedFilesTip": "上传通话录音、处理结果等资料", |
| | | "uploadDraggerText": "点击或拖拽文件到此处上传", |
| | | "uploadDraggerSubText": "支持图片、音频、视频等文件,大小≤60M", |
| | | "transfer": "移交", |
| | | "close": "结办", |
| | | "save": "保存", |
| | | "confirmClose": "是否确认结办?", |
| | | "validation": { |
| | | "handleInfo": "请输入处理情况", |
| | | "relatedFiles": "请上传相关资料" |
| | | } |
| | | }, |
| | | "detailPage": { |
| | | "title": "工单基础信息", |
| | | "processTitle": "处理流程", |
| | | "createBy": "创建人", |
| | | "createTime": "创建时间", |
| | | "handleContent": "处理内容", |
| | | "transfer": "移交", |
| | | "close": "结办", |
| | | "preview": "预览", |
| | | "download": "下载", |
| | | "fileNotExist": "文件地址不存在", |
| | | "editSuccess": "编辑成功", |
| | | "editFailed": "编辑失败", |
| | | "sourcePhone": "电话客服", |
| | | "createWorkOrder": "创建工单", |
| | | "handle": "处理" |
| | | }, |
| | | "priorityMap": { |
| | | "low": "低", |
| | | "medium": "中", |
| | | "high": "高" |
| | | }, |
| | | "statusMap": { |
| | | "pending": "未处理", |
| | | "processing": "处理中", |
| | | "finished": "已完结" |
| | | }, |
| | | "sourceMap": { |
| | | "phone": "电话客服" |
| | | } |
| | | }, |
| | | "route": { |
| | | "appRoot": "首页", |
| | | "dashboard": "首页", |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, nextTick, ref, watch } from "vue"; |
| | | import { useI18n } from "vue-i18n"; |
| | | import type { FormInst, FormRules, UploadFileInfo, UploadInst } from "naive-ui"; |
| | | import { customUploadRequest } from "@/utils/upload"; |
| | | |
| | | const { t } = useI18n(); |
| | | |
| | | interface HandleFileItem { |
| | | id: string; |
| | |
| | | const isHandleUserSelectOpen = ref(false); |
| | | |
| | | const rules: FormRules = { |
| | | handleLog: [{ required: true, message: "请输入处理情况", trigger: "blur" }], |
| | | handleLog: [ |
| | | { required: true, message: t("workOrder.handleModal.validation.handleInfo"), trigger: "blur" }, |
| | | ], |
| | | handleFile: [ |
| | | { |
| | | required: true, |
| | | validator(_rule, value: UploadFileInfo[]) { |
| | | return value && value.some((f) => f.status === "finished"); |
| | | }, |
| | | message: "请上传相关资料", |
| | | message: t("workOrder.handleModal.validation.relatedFiles"), |
| | | trigger: ["change", "blur"], |
| | | }, |
| | | ], |
| | |
| | | <n-modal |
| | | v-model:show="showModal" |
| | | preset="card" |
| | | title="处理工单" |
| | | :title="t('workOrder.handleModal.title')" |
| | | :mask-closable="false" |
| | | :style="{ width: '600px' }" |
| | | :auto-focus="false" |
| | |
| | | <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> |
| | | <div class="text-[#171F26] text-sm font-medium">{{ t("workOrder.handleModal.baseInfo") }}</div> |
| | | </n-flex> |
| | | <div class="space-y-4"> |
| | | <!-- 问题 --> |
| | | <div> |
| | | <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">问题</div> |
| | | <div class="text-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.problem") }}</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-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.problemDesc") }}</div> |
| | | <div class="text-[#171F26] text-sm whitespace-pre-wrap"> |
| | | {{ props.rowData?.problemDescription || "-" }} |
| | | </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-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.feedbackUser") }}</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-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.source") }}</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-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.type") }}</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 class="text-[rgba(23,31,38,0.6)] text-sm mb-1">{{ t("workOrder.handleModal.priority") }}</div> |
| | | <div> |
| | | <n-tag |
| | | v-if="props.rowData?.priority" |
| | |
| | | > |
| | | <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> |
| | | <div class="text-[#171F26] text-sm font-medium">{{ t("workOrder.handleModal.handleLog") }}</div> |
| | | </n-flex> |
| | | <n-form-item label="处理情况" path="handleLog"> |
| | | <n-form-item :label="t('workOrder.handleModal.handleInfo')" path="handleLog"> |
| | | <n-input |
| | | v-model:value="formModel.handleLog" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :placeholder="t('workOrder.handleModal.placeholder')" |
| | | :rows="4" |
| | | clearable |
| | | /> |
| | |
| | | <n-form-item path="handleFile"> |
| | | <template #label> |
| | | <n-flex> |
| | | <div>相关资料</div> |
| | | <div>{{ t("workOrder.handleModal.relatedFiles") }}</div> |
| | | <div style="color: rgba(23, 31, 38, 0.6)"> |
| | | 上传通话录音、处理结果等资料 |
| | | {{ t("workOrder.handleModal.relatedFilesTip") }} |
| | | </div> |
| | | </n-flex> |
| | | </template> |
| | |
| | | /> |
| | | </n-flex> |
| | | <n-text style="font-size: 16px; margin-top: 18px"> |
| | | 点击或拖拽文件到此处上传 |
| | | {{ t("workOrder.handleModal.uploadDraggerText") }} |
| | | </n-text> |
| | | <n-p |
| | | depth="3" |
| | | style="margin-top: 12px; color: rgba(23, 31, 38, 0.6)" |
| | | > |
| | | 支持图片、音频、视频等文件,大小≤60M |
| | | {{ t("workOrder.handleModal.uploadDraggerSubText") }} |
| | | </n-p> |
| | | </n-upload-dragger> |
| | | </n-upload> |
| | |
| | | <span |
| | | v-if="file.status === 'error'" |
| | | class="text-[#F53F3F] text-sm" |
| | | >上传失败</span |
| | | >{{ t("common.uploadFailed") }}</span |
| | | > |
| | | <span |
| | | v-else-if="file.status === 'uploading'" |
| | | class="text-[#165DFF] text-sm" |
| | | >上传中</span |
| | | >{{ t("common.uploading") }}</span |
| | | > |
| | | <template v-else> |
| | | <span |
| | |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="移交" path="handleUserId"> |
| | | <n-form-item :label="t('workOrder.handleModal.transfer')" path="handleUserId"> |
| | | <n-select |
| | | v-model:value="formModel.handleUserId" |
| | | :options="handleUserOptions" |
| | | placeholder="请选择" |
| | | :placeholder="t('workOrder.handleModal.selectPlaceholder')" |
| | | clearable |
| | | @update:show="(val) => (isHandleUserSelectOpen = val)" |
| | | > |
| | |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="showModal = false">取消</n-button> |
| | | <n-button @click="showModal = false">{{ t("common.cancel") }}</n-button> |
| | | <n-button v-if="!disableClose" type="primary" @click="handleClose"> |
| | | 结办 |
| | | {{ t("workOrder.handleModal.close") }} |
| | | </n-button> |
| | | <n-button v-if="!disableSave" type="primary" @click="handleSave"> |
| | | 保存 |
| | | {{ t("workOrder.handleModal.save") }} |
| | | </n-button> |
| | | </n-space> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from "vue"; |
| | | import { useI18n } from "vue-i18n"; |
| | | import type { FormInst, FormRules, UploadFileInfo } from "naive-ui"; |
| | | import { customUploadRequest } from "@/utils/upload"; |
| | | |
| | | const { t } = useI18n(); |
| | | |
| | | interface WorkOrderFormModel { |
| | | id?: number | null; |
| | |
| | | const isHandleUserSelectOpen = ref(false); |
| | | |
| | | const rules: FormRules = { |
| | | problem: [{ required: true, message: "请输入问题", trigger: "blur" }], |
| | | problem: [ |
| | | { required: true, message: t("workOrder.modal.validation.problem"), trigger: "blur" }, |
| | | ], |
| | | problemDescription: [ |
| | | { required: true, message: "请输入问题描述", trigger: "blur" }, |
| | | { required: true, message: t("workOrder.modal.validation.problemDesc"), trigger: "blur" }, |
| | | ], |
| | | feedbackUserPhone: [ |
| | | { required: true, message: "请输入反馈用户", trigger: "blur" }, |
| | | { required: true, message: t("workOrder.modal.validation.feedbackUser"), trigger: "blur" }, |
| | | ], |
| | | problemSource: [ |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择来源") |
| | | ? new Error(t("workOrder.modal.validation.source")) |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择类型") |
| | | ? new Error(t("workOrder.modal.validation.type")) |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择优先级") |
| | | ? new Error(t("workOrder.modal.validation.priority")) |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | |
| | | { |
| | | validator: (_rule, value) => |
| | | value === null || value === undefined || value === "" |
| | | ? new Error("请选择处理人") |
| | | ? new Error(t("workOrder.modal.validation.handler")) |
| | | : true, |
| | | trigger: ["change", "blur"], |
| | | }, |
| | |
| | | }; |
| | | |
| | | const modalTitle = computed(() => |
| | | props.mode === "add" ? "创建工单" : "编辑工单" |
| | | props.mode === "add" ? t("workOrder.modal.add") : t("workOrder.modal.edit") |
| | | ); |
| | | |
| | | function resetForm() { |
| | |
| | | label-width="90" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="问题" path="problem"> |
| | | <n-form-item :label="t('workOrder.modal.problem')" path="problem"> |
| | | <n-input |
| | | v-model:value="formModel.problem" |
| | | placeholder="请输入" |
| | | :placeholder="t('workOrder.modal.placeholder')" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="问题描述" path="problemDescription"> |
| | | <n-form-item :label="t('workOrder.modal.problemDesc')" path="problemDescription"> |
| | | <n-input |
| | | v-model:value="formModel.problemDescription" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :placeholder="t('workOrder.modal.placeholder')" |
| | | :rows="4" |
| | | maxlength="200" |
| | | clearable |
| | | show-count |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="图片" path="problemImage"> |
| | | <n-form-item :label="t('workOrder.modal.image')" path="problemImage"> |
| | | <div class="flex flex-col"> |
| | | <n-upload |
| | | v-model:file-list="fileList" |
| | |
| | | src="/src/assets/public/info-icon.png" |
| | | style="width: 12px; height: 12px; margin-right: 4px" |
| | | /> |
| | | 支持jpg/png/gif ,大小≤10MB,最多上传9张 |
| | | {t("workOrder.modal.uploadTip")} |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | <n-form-item label="反馈用户" path="feedbackUserPhone"> |
| | | <n-form-item :label="t('workOrder.modal.feedbackUser')" path="feedbackUserPhone"> |
| | | <n-input |
| | | v-model:value="formModel.feedbackUserPhone" |
| | | placeholder="请输入" |
| | | :placeholder="t('workOrder.modal.placeholder')" |
| | | clearable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="来源" path="problemSource" required> |
| | | <n-form-item :label="t('workOrder.modal.source')" path="problemSource" required> |
| | | <n-select |
| | | v-model:value="formModel.problemSource" |
| | | :options="problemSourceOptions" |
| | | placeholder="请选择" |
| | | :placeholder="t('workOrder.modal.selectPlaceholder')" |
| | | clearable |
| | | @update:show="(val) => (isProblemSourceSelectOpen = val)" |
| | | > |
| | |
| | | </n-select> |
| | | </n-form-item> |
| | | <div class="grid grid-cols-2 gap-4"> |
| | | <n-form-item label="类型" path="feedbackTypeId" required> |
| | | <n-form-item :label="t('workOrder.modal.type')" path="feedbackTypeId" required> |
| | | <n-select |
| | | v-model:value="formModel.feedbackTypeId" |
| | | :options="typeOptions" |
| | | placeholder="请选择" |
| | | :placeholder="t('workOrder.modal.selectPlaceholder')" |
| | | clearable |
| | | @update:show="(val) => (isFeedbackTypeSelectOpen = val)" |
| | | > |
| | |
| | | </template> |
| | | </n-select> |
| | | </n-form-item> |
| | | <n-form-item label="优先级" path="priority" required> |
| | | <n-form-item :label="t('workOrder.modal.priority')" path="priority" required> |
| | | <n-select |
| | | v-model:value="formModel.priority" |
| | | :options="priorityOptions" |
| | | placeholder="请选择" |
| | | :placeholder="t('workOrder.modal.selectPlaceholder')" |
| | | clearable |
| | | @update:show="(val) => (isPrioritySelectOpen = val)" |
| | | > |
| | |
| | | </n-select> |
| | | </n-form-item> |
| | | </div> |
| | | <n-form-item label="处理人" path="handleUserId" required> |
| | | <n-form-item :label="t('workOrder.modal.handler')" path="handleUserId" required> |
| | | <n-select |
| | | v-model:value="formModel.handleUserId" |
| | | :options="handleUserOptions" |
| | | placeholder="请选择" |
| | | :placeholder="t('workOrder.modal.selectPlaceholder')" |
| | | clearable |
| | | @update:show="(val) => (isHandleUserSelectOpen = val)" |
| | | > |
| | |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="showModal = false">取消</n-button> |
| | | <n-button type="primary" @click="handleSave">确定</n-button> |
| | | <n-button @click="showModal = false">{{ t("common.cancel") }}</n-button> |
| | | <n-button type="primary" @click="handleSave">{{ t("common.confirm") }}</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, onMounted } from "vue"; |
| | | import { useI18n } from "vue-i18n"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { NTag } from "naive-ui"; |
| | | import { useBoolean } from "@/hooks"; |
| | |
| | | import WorkOrderModal from "./components/WorkOrderModal.vue"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const { t } = useI18n(); |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | const modalVisible = ref(false); |
| | |
| | | const typeOptions = ref<{ label: string; value: number }[]>([]); |
| | | const handleUserOptions = ref<{ label: string; value: number | string }[]>([]); |
| | | |
| | | const problemSourceLabelMap: Record<number, string> = { |
| | | 1: "电话客服", |
| | | }; |
| | | const priorityLabelMap: Record<number, string> = { |
| | | 1: "低", |
| | | 2: "中", |
| | | 3: "高", |
| | | }; |
| | | const problemSourceLabelMap = computed<Record<number, string>>(() => ({ |
| | | 1: t("workOrder.detailPage.sourcePhone"), |
| | | })); |
| | | const priorityLabelMap = computed<Record<number, string>>(() => ({ |
| | | 1: t("workOrder.priorityMap.low"), |
| | | 2: t("workOrder.priorityMap.medium"), |
| | | 3: t("workOrder.priorityMap.high"), |
| | | })); |
| | | |
| | | const displayProblemSource = computed( |
| | | () => problemSourceLabelMap[detailData.value.problemSource] || "-" |
| | | () => problemSourceLabelMap.value[detailData.value.problemSource] || "-" |
| | | ); |
| | | const displayPriority = computed( |
| | | () => priorityLabelMap[detailData.value.priority] || "-" |
| | | () => priorityLabelMap.value[detailData.value.priority] || "-" |
| | | ); |
| | | const tagType = computed(() => |
| | | detailData.value.priority === 3 |
| | |
| | | const previewUrl = ref(""); |
| | | const previewName = ref(""); |
| | | |
| | | const problemSourceOptions = [{ label: "电话客服", value: 1 }]; |
| | | const problemSourceOptions = computed(() => [ |
| | | { label: t("workOrder.detailPage.sourcePhone"), value: 1 }, |
| | | ]); |
| | | |
| | | const priorityOptions = [ |
| | | { label: "低", value: 1 }, |
| | | { label: "中", value: 2 }, |
| | | { label: "高", value: 3 }, |
| | | ]; |
| | | const priorityOptions = computed(() => [ |
| | | { label: t("workOrder.priorityMap.low"), value: 1 }, |
| | | { label: t("workOrder.priorityMap.medium"), value: 2 }, |
| | | { label: t("workOrder.priorityMap.high"), value: 3 }, |
| | | ]); |
| | | |
| | | async function getTypeOptions() { |
| | | const res = await fetchFeedbackTypeList({ pageNum: 1, pageSize: 9999 }); |
| | |
| | | items.push({ |
| | | title: detailData.value.createTime, |
| | | handleUserName: detailData.value.createBy, |
| | | typeTxt: "创建工单", |
| | | typeTxt: t("workOrder.detailPage.createWorkOrder"), |
| | | }); |
| | | } |
| | | const logs = Array.isArray(detailData.value.workOrderLogs) |
| | |
| | | items.push({ |
| | | title: log.handleTime || log.createTime || "-", |
| | | handleUserName: log.handleUserName || "-", |
| | | typeTxt: "处理", |
| | | typeTxt: t("workOrder.detailPage.handle"), |
| | | log, |
| | | }); |
| | | }); |
| | |
| | | } |
| | | |
| | | function handleTypeLabel(value: number) { |
| | | if (value === 1) return "移交"; |
| | | if (value === 2) return "结办"; |
| | | if (value === 1) return t("workOrder.detailPage.transfer"); |
| | | if (value === 2) return t("workOrder.detailPage.close"); |
| | | return "-"; |
| | | } |
| | | |
| | |
| | | |
| | | function openNewTab(url: string) { |
| | | if (!url) { |
| | | window.$message?.warning("文件地址不存在"); |
| | | window.$message?.warning(t("workOrder.detailPage.fileNotExist")); |
| | | return; |
| | | } |
| | | window.open(url, "_blank", "noopener"); |
| | |
| | | async function handleDownload(file: any) { |
| | | const url = file?.url; |
| | | if (!url) { |
| | | window.$message?.warning("文件地址不存在"); |
| | | window.$message?.warning(t("workOrder.detailPage.fileNotExist")); |
| | | return; |
| | | } |
| | | try { |
| | |
| | | }; |
| | | const { code } = await updateWorkOrder(savePayload); |
| | | if (code === 200) { |
| | | window.$message?.success("编辑成功"); |
| | | window.$message?.success(t("workOrder.detailPage.editSuccess")); |
| | | modalVisible.value = false; |
| | | getDetail(); |
| | | } else { |
| | | window.$message?.error("编辑失败"); |
| | | window.$message?.error(t("workOrder.detailPage.editFailed")); |
| | | } |
| | | } |
| | | |
| | |
| | | class="text-[#171F26]" |
| | | style="line-height: 24px; font-size: 16px; font-weight: bold" |
| | | > |
| | | 工单基础信息 |
| | | {{ t("workOrder.detailPage.title") }} |
| | | </div> |
| | | </div> |
| | | <n-button type="primary" secondary @click="handleEdit" class="w-84px" |
| | | >编辑</n-button |
| | | <n-button |
| | | v-permission="80215" |
| | | type="primary" |
| | | secondary |
| | | @click="handleEdit" |
| | | class="w-84px" |
| | | > |
| | | {{ t("workOrder.edit") }} |
| | | </n-button> |
| | | </div> |
| | | <n-spin :show="loading"> |
| | | <div class="grid grid-cols-1 gap-16px text-sm text-[#1D2129]"> |
| | | <div> |
| | | <span class="label-txt-color">问题:</span |
| | | <span class="label-txt-color">{{ t("workOrder.problem") }}:</span |
| | | >{{ detailData.problem || "-" }} |
| | | </div> |
| | | <div class="flex text-sm label-txt-color"> |
| | | <div class="shrink-0">问题描述:</div> |
| | | <div class="shrink-0">{{ t("workOrder.problemDesc") }}:</div> |
| | | <div> |
| | | <div class="whitespace-pre-wrap text-[#171F26]"> |
| | | {{ detailData.problemDescription || "-" }} |
| | |
| | | </div> |
| | | <div class="grid grid-cols-3 gap-16px text-sm text-[#1D2129] mt-16px"> |
| | | <div> |
| | | <span class="label-txt-color">反馈用户:</span |
| | | <span class="label-txt-color">{{ t("workOrder.feedbackUser") }}:</span |
| | | >{{ detailData.feedbackUserPhone || "-" }} |
| | | </div> |
| | | <div> |
| | | <span class="label-txt-color">来源:</span |
| | | <span class="label-txt-color">{{ t("workOrder.source") }}:</span |
| | | >{{ displayProblemSource }} |
| | | </div> |
| | | <div> |
| | | <span class="label-txt-color">类型:</span>{{ displayType }} |
| | | <span class="label-txt-color">{{ t("workOrder.type") }}:</span>{{ displayType }} |
| | | </div> |
| | | <div> |
| | | <span class="label-txt-color">优先级:</span> |
| | | <span class="label-txt-color">{{ t("workOrder.priority") }}:</span> |
| | | <NTag :type="tagType" :bordered="false" size="small"> |
| | | {{ displayPriority }} |
| | | </NTag> |
| | | </div> |
| | | <div> |
| | | <span class="label-txt-color">创建人:</span |
| | | <span class="label-txt-color">{{ t("workOrder.detailPage.createBy") }}:</span |
| | | >{{ detailData.createBy }} |
| | | </div> |
| | | <div> |
| | | <span class="label-txt-color">创建时间:</span |
| | | <span class="label-txt-color">{{ t("workOrder.detailPage.createTime") }}:</span |
| | | >{{ |
| | | detailData.createTime |
| | | ? dayjs(detailData.createTime).format("YYYY/MM/DD HH:mm:ss") || |
| | |
| | | class="text-[#171F26]" |
| | | style="line-height: 24px; font-size: 16px; font-weight: bold" |
| | | > |
| | | 处理流程 |
| | | {{ t("workOrder.detailPage.processTitle") }} |
| | | </div> |
| | | </div> |
| | | <n-spin :show="loading"> |
| | |
| | | > |
| | | <div class="flex mb-15px"> |
| | | <div class="shrink-0" style="color: rgba(23, 31, 38, 0.6)"> |
| | | 处理内容: |
| | | {{ t("workOrder.detailPage.handleContent") }}: |
| | | </div> |
| | | <div>{{ item.log.handleLog || "-" }}</div> |
| | | </div> |
| | |
| | | :text="true" |
| | | @click="handlePreview(file)" |
| | | > |
| | | 预览 |
| | | {{ t("workOrder.detailPage.preview") }} |
| | | </n-button> |
| | | <n-button |
| | | type="primary" |
| | | :text="true" |
| | | @click="handleDownload(file)" |
| | | > |
| | | 下载 |
| | | {{ t("workOrder.detailPage.download") }} |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | |
| | | <n-modal |
| | | v-model:show="previewVisible" |
| | | preset="card" |
| | | title="预览" |
| | | :title="t('workOrder.detailPage.preview')" |
| | | :style="{ width: '720px' }" |
| | | :bordered="false" |
| | | :segmented="{ content: true }" |
| | |
| | | mode="edit" |
| | | :row-data="detailData" |
| | | :type-options="typeOptions" |
| | | :problem-source-options="problemSourceOptions" |
| | | :priority-options="priorityOptions" |
| | | :problem-source-options="problemSourceOptions.value" |
| | | :priority-options="priorityOptions.value" |
| | | :handle-user-options="handleUserOptions" |
| | | @submit="handleSave" |
| | | /> |
| | |
| | | <script setup lang="tsx"> |
| | | import { ref, h, onMounted } from "vue"; |
| | | import { ref, h, onMounted, computed } from "vue"; |
| | | import type { DataTableColumns } from "naive-ui"; |
| | | import { NButton, NTag } from "naive-ui"; |
| | | import { useRouter } from "vue-router"; |
| | |
| | | const typeOptions = ref<{ label: string; value: number }[]>([]); |
| | | const handleUserOptions = ref<{ label: string; value: number | string }[]>([]); |
| | | |
| | | const problemSourceOptions = [{ label: "电话客服", value: 1 }]; |
| | | const problemSourceOptions = computed(() => [{ label: t("workOrder.sourceMap.phone"), value: 1 }]); |
| | | |
| | | const priorityOptions = [ |
| | | { label: "低", value: 1 }, |
| | | { label: "中", value: 2 }, |
| | | { label: "高", value: 3 }, |
| | | ]; |
| | | const priorityOptions = computed(() => [ |
| | | { label: t("workOrder.priorityMap.low"), value: 1 }, |
| | | { label: t("workOrder.priorityMap.medium"), value: 2 }, |
| | | { label: t("workOrder.priorityMap.high"), value: 3 }, |
| | | ]); |
| | | |
| | | const statusOptions = [ |
| | | { label: "待处理", value: 1 }, |
| | | { label: "处理中", value: 2 }, |
| | | { label: "已处理", value: 3 }, |
| | | ]; |
| | | const statusOptions = computed(() => [ |
| | | { label: t("workOrder.statusMap.pending"), value: 1 }, |
| | | { label: t("workOrder.statusMap.processing"), value: 2 }, |
| | | { label: t("workOrder.statusMap.finished"), value: 3 }, |
| | | ]); |
| | | |
| | | const priorityLabelMap: Record<number, string> = { |
| | | 1: "低", |
| | | 2: "中", |
| | | 3: "高", |
| | | 1: t("workOrder.priorityMap.low"), |
| | | 2: t("workOrder.priorityMap.medium"), |
| | | 3: t("workOrder.priorityMap.high"), |
| | | }; |
| | | |
| | | const statusLabelMap: Record<number, string> = { |
| | | 1: "未处理", |
| | | 2: "处理中", |
| | | 3: "已完结", |
| | | 1: t("workOrder.statusMap.pending"), |
| | | 2: t("workOrder.statusMap.processing"), |
| | | 3: t("workOrder.statusMap.finished"), |
| | | }; |
| | | |
| | | const problemSourceLabelMap: Record<number, string> = { |
| | | 1: "电话客服", |
| | | 1: t("workOrder.sourceMap.phone"), |
| | | }; |
| | | |
| | | const columns: DataTableColumns<WorkOrderItem> = [ |
| | | { |
| | | title: "序号", |
| | | title: t("workOrder.index"), |
| | | key: "index", |
| | | width: 80, |
| | | resizable: true, |
| | |
| | | ), |
| | | }, |
| | | { |
| | | title: "提交用户", |
| | | title: t("workOrder.submitUser"), |
| | | key: "feedbackUserPhone", |
| | | width: 100, |
| | | resizable: true, |
| | | render: (row) => row.feedbackUserPhone || "-", |
| | | }, |
| | | { |
| | | title: "来源", |
| | | title: t("workOrder.source"), |
| | | key: "problemSource", |
| | | width: 120, |
| | | resizable: true, |
| | | render: (row) => problemSourceLabelMap[row.problemSource || 0] || "-", |
| | | }, |
| | | { |
| | | title: "类型", |
| | | title: t("workOrder.type"), |
| | | key: "feedbackTypeName", |
| | | width: 100, |
| | | resizable: true, |
| | | render: (row) => row.feedbackTypeName || "-", |
| | | }, |
| | | { |
| | | title: "问题", |
| | | title: t("workOrder.problem"), |
| | | key: "problem", |
| | | width: 100, |
| | | resizable: true, |
| | | render: (row) => row.problem || "-", |
| | | }, |
| | | { |
| | | title: "优先级", |
| | | title: t("workOrder.priority"), |
| | | key: "priority", |
| | | width: 80, |
| | | resizable: true, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | title: "处理状态", |
| | | title: t("workOrder.status"), |
| | | key: "handleStatus", |
| | | width: 80, |
| | | resizable: true, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | title: "处理人", |
| | | title: t("workOrder.handler"), |
| | | key: "handleUserName", |
| | | width: 80, |
| | | resizable: true, |
| | | render: (row) => row.handleUserName || "-", |
| | | }, |
| | | { |
| | | title: "添加时间", |
| | | title: t("workOrder.addTime"), |
| | | key: "createTime", |
| | | width: 150, |
| | | resizable: true, |
| | | render: (row) => dayjs(row.createTime).format("YYYY/MM/DD HH:mm") || "-", |
| | | }, |
| | | { |
| | | title: "操作", |
| | | title: t("workOrder.actions"), |
| | | key: "actions", |
| | | width: 140, |
| | | resizable: true, |
| | |
| | | type="primary" |
| | | onClick={() => handleView(row)} |
| | | > |
| | | 详情 |
| | | {t("workOrder.detail")} |
| | | </NButton> |
| | | {[1, 2].includes(row.handleStatus || 0) ? ( |
| | | <NButton |
| | |
| | | type="primary" |
| | | onClick={() => handleStart(row)} |
| | | > |
| | | 处理 |
| | | {t("workOrder.handle")} |
| | | </NButton> |
| | | ) : null} |
| | | <NButton |
| | | v-permission="80215" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleEdit(row)} |
| | | > |
| | | 编辑 |
| | | </NButton> |
| | | <NButton |
| | | v-permission="80216" |
| | | text |
| | | type="primary" |
| | | onClick={() => handleDelete(row)} |
| | | > |
| | | 删除 |
| | | {t("workOrder.delete")} |
| | | </NButton> |
| | | </div> |
| | | ); |
| | |
| | | if (modalMode.value === "add") { |
| | | const { code } = await saveWorkOrder(savePayload); |
| | | if (code === 200) { |
| | | window.$message?.success("新增成功"); |
| | | window.$message?.success(t("feedbackType.addSuccess")); |
| | | modalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("新增失败"); |
| | | window.$message?.error(t("feedbackType.addFail")); |
| | | } |
| | | return; |
| | | } |
| | |
| | | ...savePayload, |
| | | }); |
| | | if (code === 200) { |
| | | window.$message?.success("编辑成功"); |
| | | window.$message?.success(t("feedbackType.editSuccess")); |
| | | modalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("编辑失败"); |
| | | window.$message?.error(t("feedbackType.editFail")); |
| | | } |
| | | } |
| | | |
| | |
| | | const execSubmit = async () => { |
| | | const { code } = await handleWorkOrder(submitData); |
| | | if (code === 200) { |
| | | window.$message?.success("操作成功"); |
| | | window.$message?.success(t("serviceReview.common.reviewSuccess")); |
| | | handleModalVisible.value = false; |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("操作失败"); |
| | | window.$message?.error(t("serviceReview.common.reviewFail")); |
| | | } |
| | | }; |
| | | if (payload.handleType === 2) { |
| | | window.$dialog?.warning({ |
| | | title: "是否确认结办?", |
| | | title: t("workOrder.handleModal.confirmClose"), |
| | | content: "", |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | positiveText: t("common.confirm"), |
| | | negativeText: t("common.cancel"), |
| | | onPositiveClick: execSubmit, |
| | | }); |
| | | return; |
| | |
| | | |
| | | function handleDelete(row: WorkOrderItem) { |
| | | window.$dialog?.warning({ |
| | | title: "确认删除", |
| | | content: "确定要删除该工单吗?", |
| | | positiveText: "确定", |
| | | negativeText: "取消", |
| | | title: t("workOrder.confirmDelete"), |
| | | content: t("workOrder.confirmDeleteContent"), |
| | | positiveText: t("common.confirm"), |
| | | negativeText: t("common.cancel"), |
| | | onPositiveClick: async () => { |
| | | const res = await deleteWorkOrder(row.id); |
| | | if (res.code === 200) { |
| | | window.$message?.success("删除成功"); |
| | | window.$message?.success(t("feedbackType.deleteSuccess")); |
| | | getWorkOrderList(); |
| | | } else { |
| | | window.$message?.error("删除失败"); |
| | | window.$message?.error(t("feedbackType.deleteFail")); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | }); |
| | | |
| | | |
| | | import { useI18n } from "vue-i18n"; |
| | | const { t } = useI18n(); |
| | | |
| | | const isProblemSourceSelectOpen = ref(false); |
| | | const isFeedbackTypeSelectOpen = ref(false); |
| | | const isPrioritySelectOpen = ref(false); |
| | |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '用户', |
| | | label: t("workOrder.search.user"), |
| | | path: 'feedbackUserPhone', |
| | | render: () => <n-input v-model:value={searchForm.value.feedbackUserPhone} placeholder='请输入' />, |
| | | render: () => <n-input v-model:value={searchForm.value.feedbackUserPhone} placeholder={t("workOrder.search.placeholder")} />, |
| | | }, |
| | | { |
| | | label: '问题来源', |
| | | label: t("workOrder.search.source"), |
| | | path: 'problemSource', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchForm.value.problemSource} |
| | | options={problemSourceOptions} |
| | | placeholder='请选择' |
| | | options={problemSourceOptions.value} |
| | | placeholder={t("workOrder.search.selectPlaceholder")} |
| | | clearable |
| | | onUpdate:show={val => isProblemSourceSelectOpen.value = val} |
| | | > |
| | |
| | | ), |
| | | }, |
| | | { |
| | | label: '类型', |
| | | label: t("workOrder.search.type"), |
| | | path: 'feedbackTypeId', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchForm.value.feedbackTypeId} |
| | | options={typeOptions.value} |
| | | placeholder='请选择' |
| | | placeholder={t("workOrder.search.selectPlaceholder")} |
| | | clearable |
| | | onUpdate:show={val => isFeedbackTypeSelectOpen.value = val} |
| | | > |
| | |
| | | ), |
| | | }, |
| | | { |
| | | label: '优先级', |
| | | label: t("workOrder.search.priority"), |
| | | path: 'priority', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchForm.value.priority} |
| | | options={priorityOptions} |
| | | placeholder='请选择' |
| | | options={priorityOptions.value} |
| | | placeholder={t("workOrder.search.selectPlaceholder")} |
| | | clearable |
| | | onUpdate:show={val => isPrioritySelectOpen.value = val} |
| | | > |
| | |
| | | ), |
| | | }, |
| | | { |
| | | label: '状态', |
| | | label: t("workOrder.search.status"), |
| | | path: 'handleStatus', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchForm.value.handleStatus} |
| | | options={statusOptions} |
| | | placeholder='请选择' |
| | | options={statusOptions.value} |
| | | placeholder={t("workOrder.search.selectPlaceholder")} |
| | | clearable |
| | | onUpdate:show={val => isHandleStatusSelectOpen.value = val} |
| | | > |