hejianhao
2026-03-23 df0c1ba23ed6d76819146811ae46dc28a0975aa4
feat(工单): 新增工单管理模块并修复上传组件问题

- 新增工单管理功能,包括列表、创建、编辑、处理和详情页面
- 添加工单相关中英文国际化文案
- 在工单模态框和详情页中使用国际化文本
- 添加上传状态相关翻译(上传中、上传失败)
6个文件已修改
557 ■■■■■ 已修改文件
locales/en_US.json 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locales/zh_CN.json 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/workOrder/components/WorkOrderModal.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/workOrder/detail.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/workOrder/index.vue 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
locales/en_US.json
@@ -9,7 +9,9 @@
    "inputPlaceholder": "please enter",
    "selectPlaceholder": "please choose",
    "search": "Search",
    "reset": "Reset"
    "reset": "Reset",
    "uploading": "Uploading",
    "uploadFailed": "Upload Failed"
  },
  "app": {
    "loginOut": "Login out",
@@ -256,6 +258,112 @@
      "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": {
locales/zh_CN.json
@@ -9,11 +9,13 @@
    "navigate": "切换",
    "inputPlaceholder": "请输入",
    "selectPlaceholder": "请选择",
    "search": "查询",
    "search": "搜索",
    "reset": "重置",
    "export": "导出",
    "expand": "展开",
    "collapse": "收起"
    "collapse": "收起",
    "uploading": "上传中",
    "uploadFailed": "上传失败"
  },
  "ticket": {
    "feedback": {
@@ -157,6 +159,112 @@
    "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": "首页",
src/views/service/ticket/workOrder/components/WorkOrderHandleModal.vue
@@ -1,7 +1,10 @@
<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;
@@ -45,14 +48,16 @@
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"],
    },
  ],
@@ -217,7 +222,7 @@
  <n-modal
    v-model:show="showModal"
    preset="card"
    title="处理工单"
    :title="t('workOrder.handleModal.title')"
    :mask-closable="false"
    :style="{ width: '600px' }"
    :auto-focus="false"
@@ -227,19 +232,19 @@
      <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>
@@ -263,23 +268,23 @@
          <!-- 网格信息 -->
          <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"
@@ -312,13 +317,13 @@
      >
        <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
          />
@@ -326,9 +331,9 @@
        <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>
@@ -351,13 +356,13 @@
                  />
                </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>
@@ -389,12 +394,12 @@
                    <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
@@ -431,11 +436,11 @@
            </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)"
          >
@@ -457,12 +462,12 @@
    <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>
src/views/service/ticket/workOrder/components/WorkOrderModal.vue
@@ -1,7 +1,10 @@
<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;
@@ -62,18 +65,20 @@
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"],
    },
@@ -82,7 +87,7 @@
    {
      validator: (_rule, value) =>
        value === null || value === undefined || value === ""
          ? new Error("请选择类型")
          ? new Error(t("workOrder.modal.validation.type"))
          : true,
      trigger: ["change", "blur"],
    },
@@ -91,7 +96,7 @@
    {
      validator: (_rule, value) =>
        value === null || value === undefined || value === ""
          ? new Error("请选择优先级")
          ? new Error(t("workOrder.modal.validation.priority"))
          : true,
      trigger: ["change", "blur"],
    },
@@ -100,7 +105,7 @@
    {
      validator: (_rule, value) =>
        value === null || value === undefined || value === ""
          ? new Error("请选择处理人")
          ? new Error(t("workOrder.modal.validation.handler"))
          : true,
      trigger: ["change", "blur"],
    },
@@ -108,7 +113,7 @@
};
const modalTitle = computed(() =>
  props.mode === "add" ? "创建工单" : "编辑工单"
  props.mode === "add" ? t("workOrder.modal.add") : t("workOrder.modal.edit")
);
function resetForm() {
@@ -228,25 +233,25 @@
      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"
@@ -281,22 +286,22 @@
              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)"
        >
@@ -314,11 +319,11 @@
        </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)"
          >
@@ -335,11 +340,11 @@
            </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)"
          >
@@ -357,11 +362,11 @@
          </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)"
        >
@@ -382,8 +387,8 @@
    <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>
src/views/service/ticket/workOrder/detail.vue
@@ -1,5 +1,6 @@
<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";
@@ -12,6 +13,7 @@
import WorkOrderModal from "./components/WorkOrderModal.vue";
import dayjs from "dayjs";
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const modalVisible = ref(false);
@@ -26,20 +28,20 @@
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
@@ -57,13 +59,15 @@
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 });
@@ -99,7 +103,7 @@
    items.push({
      title: detailData.value.createTime,
      handleUserName: detailData.value.createBy,
      typeTxt: "创建工单",
      typeTxt: t("workOrder.detailPage.createWorkOrder"),
    });
  }
  const logs = Array.isArray(detailData.value.workOrderLogs)
@@ -109,7 +113,7 @@
    items.push({
      title: log.handleTime || log.createTime || "-",
      handleUserName: log.handleUserName || "-",
      typeTxt: "处理",
      typeTxt: t("workOrder.detailPage.handle"),
      log,
    });
  });
@@ -131,8 +135,8 @@
}
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 "-";
}
@@ -169,7 +173,7 @@
function openNewTab(url: string) {
  if (!url) {
    window.$message?.warning("文件地址不存在");
    window.$message?.warning(t("workOrder.detailPage.fileNotExist"));
    return;
  }
  window.open(url, "_blank", "noopener");
@@ -202,7 +206,7 @@
async function handleDownload(file: any) {
  const url = file?.url;
  if (!url) {
    window.$message?.warning("文件地址不存在");
    window.$message?.warning(t("workOrder.detailPage.fileNotExist"));
    return;
  }
  try {
@@ -280,11 +284,11 @@
  };
  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"));
  }
}
@@ -315,21 +319,27 @@
            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 || "-" }}
@@ -352,28 +362,28 @@
        </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") ||
@@ -398,7 +408,7 @@
          class="text-[#171F26]"
          style="line-height: 24px; font-size: 16px; font-weight: bold"
        >
          处理流程
          {{ t("workOrder.detailPage.processTitle") }}
        </div>
      </div>
      <n-spin :show="loading">
@@ -428,7 +438,7 @@
              >
                <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>
@@ -457,14 +467,14 @@
                          :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>
@@ -489,7 +499,7 @@
    <n-modal
      v-model:show="previewVisible"
      preset="card"
      title="预览"
      :title="t('workOrder.detailPage.preview')"
      :style="{ width: '720px' }"
      :bordered="false"
      :segmented="{ content: true }"
@@ -516,8 +526,8 @@
      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"
    />
src/views/service/ticket/workOrder/index.vue
@@ -1,5 +1,5 @@
<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";
@@ -65,39 +65,39 @@
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,
@@ -108,35 +108,35 @@
      ),
  },
  {
    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,
@@ -152,7 +152,7 @@
    },
  },
  {
    title: "处理状态",
    title: t("workOrder.status"),
    key: "handleStatus",
    width: 80,
    resizable: true,
@@ -168,21 +168,21 @@
    },
  },
  {
    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,
@@ -195,7 +195,7 @@
            type="primary"
            onClick={() => handleView(row)}
          >
            详情
            {t("workOrder.detail")}
          </NButton>
          {[1, 2].includes(row.handleStatus || 0) ? (
            <NButton
@@ -204,24 +204,16 @@
              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>
      );
@@ -288,11 +280,11 @@
  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;
  }
@@ -301,11 +293,11 @@
    ...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"));
  }
}
@@ -329,19 +321,19 @@
  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;
@@ -351,17 +343,17 @@
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"));
      }
    },
  });
@@ -426,6 +418,9 @@
});
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const isProblemSourceSelectOpen = ref(false);
const isFeedbackTypeSelectOpen = ref(false);
const isPrioritySelectOpen = ref(false);
@@ -433,18 +428,18 @@
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}
      >
@@ -462,13 +457,13 @@
    ),
  },
  {
    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}
      >
@@ -486,13 +481,13 @@
    ),
  },
  {
    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}
      >
@@ -510,13 +505,13 @@
    ),
  },
  {
    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}
      >