WXL (wul)
5 天以前 70e4d2c33cc2e3af590d1816c0c703da341538cf
src/views/Satisfaction/configurationmyd/batch.vue
@@ -211,7 +211,7 @@
                </div>
                <div class="info-item">
                  <span class="label">处理说明:</span>
                  <span class="value">{{ row.handledesc }}</span>
                  <span class="value">{{ formatHandledesc(row.handledesc) }}</span>
                </div>
              </div>
              <span v-else class="no-data">未处理</span>
@@ -238,7 +238,7 @@
                size="small"
                icon="el-icon-edit"
                @click="handleProcess(row)"
                :disabled="row.handleFlag === '1'"
                :disabled="row.handleresult === 'resolved'"
              >
                处理
              </el-button>
@@ -266,9 +266,13 @@
    <el-dialog
      title="处理异常反馈"
      :visible.sync="processDialogVisible"
      width="600px"
      width="60%"
      top="6vh"
      center
    >
      <div class="flow-dialog-body">
        <!-- 左半部分:处理信息 -->
        <div class="flow-left">
      <el-form
        :model="processForm"
        :rules="processRules"
@@ -276,6 +280,16 @@
        label-width="100px"
        size="medium"
      >
            <el-form-item label="是否流转" prop="isFlow">
              <el-switch
                v-model="processForm.isFlow"
                active-value="1"
                inactive-value="0"
                active-text="流转"
                inactive-text="不流转"
              />
            </el-form-item>
        <el-form-item label="处理状态" prop="handleFlag">
          <el-select
            v-model="processForm.handleFlag"
@@ -348,6 +362,162 @@
          />
        </el-form-item>
      </el-form>
        </div>
        <!-- 右半部分:流转时间线(选中流转后展开) -->
        <div class="flow-right" v-if="processForm.isFlow === '1'">
          <el-timeline>
            <el-timeline-item type="primary" placement="top" color="#5788fe">
              <div class="flow-step-title">第一步 · 流转发起</div>
              <el-form label-width="90px" size="small" class="flow-step-form">
                <el-form-item label="流转人">
                  <el-input
                    v-model="processForm.flowPerson"
                    placeholder="流转人(默认当前用户)"
                  />
                </el-form-item>
                <el-form-item label="流转人编号">
                  <el-input
                    v-model="processForm.flowPersonCode"
                    placeholder="流转人编号(默认当前用户工号)"
                  />
                </el-form-item>
                <el-form-item label="流转时间">
                  <el-date-picker
                    v-model="processForm.flowTime"
                    type="datetime"
                    value-format="yyyy-MM-dd HH:mm:ss"
                    placeholder="流转时间"
                    style="width: 100%"
                  />
                </el-form-item>
                <el-form-item label="流转说明">
                  <el-input
                    v-model="processForm.flowDesc"
                    type="textarea"
                    :rows="2"
                    placeholder="请输入流转说明"
                  />
                </el-form-item>
                <el-form-item label="流转科室">
                  <el-select
                    v-model="processForm.flowDepts"
                    multiple
                    filterable
                    collapse-tags
                    placeholder="请选择流转科室"
                    style="width: 100%"
                    @change="onFlowDeptChange"
                  >
                    <el-option
                      v-for="dept in deptList"
                      :key="dept.deptCode"
                      :label="dept.label"
                      :value="dept.deptCode"
                    />
                  </el-select>
                </el-form-item>
                <el-form-item label="流转科室code">
                  <el-select
                    v-model="processForm.flowDeptCodes"
                    multiple
                    disabled
                    placeholder="选择科室后自动生成"
                    style="width: 100%"
                  >
                    <el-option
                      v-for="code in processForm.flowDeptCodes"
                      :key="code"
                      :label="code"
                      :value="code"
                    />
                  </el-select>
                </el-form-item>
                <el-form-item label="推送负责人">
                  <el-select
                    v-model="processForm.pushUsers"
                    multiple
                    filterable
                    collapse-tags
                    placeholder="请选择推送负责人"
                    style="width: 100%"
                    @change="onPushUserChange"
                  >
                    <el-option
                      v-for="u in userList"
                      :key="u.userId"
                      :label="u.nickName || u.userName"
                      :value="u.userId"
                    />
                  </el-select>
                </el-form-item>
              </el-form>
            </el-timeline-item>
            <el-timeline-item type="primary" placement="top" color="#e6a23c">
              <div class="flow-step-title">第二步 · 科室反馈</div>
              <el-table
                :data="processForm.flowFeedbacks"
                border
                size="mini"
                v-if="processForm.flowFeedbacks.length > 0"
              >
                <el-table-column
                  label="科室"
                  prop="deptName"
                  width="120"
                  align="center"
                />
                <el-table-column label="是否同意" width="130" align="center">
                  <template slot-scope="scope">
                    <el-select
                      v-model="scope.row.agree"
                      placeholder="请选择"
                      size="mini"
                    >
                      <el-option label="同意" value="1" />
                      <el-option label="不同意" value="0" />
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column label="具体说明" min-width="200" align="center">
                  <template slot-scope="scope">
                    <el-input
                      v-model="scope.row.desc"
                      placeholder="请输入具体说明"
                      size="mini"
                    />
                  </template>
                </el-table-column>
              </el-table>
              <div v-else class="flow-empty-tip">
                请先在第一步选择流转科室
              </div>
            </el-timeline-item>
            <el-timeline-item type="primary" placement="top" color="#67c23a">
              <div class="flow-step-title">第三步 · 处理结果</div>
              <el-form label-width="90px" size="small" class="flow-step-form">
                <el-form-item label="处理人">
                  <el-input
                    v-model="processForm.handleBy"
                    placeholder="处理人"
                  />
                </el-form-item>
                <el-form-item label="处理时间">
                  <el-date-picker
                    v-model="processForm.handleTime"
                    type="datetime"
                    value-format="yyyy-MM-dd HH:mm:ss"
                    placeholder="处理时间"
                    style="width: 100%"
                  />
                </el-form-item>
              </el-form>
            </el-timeline-item>
          </el-timeline>
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="processDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitProcess" :loading="processing">
@@ -483,7 +653,7 @@
import DetailsAnomaly from "./components/DetailsAnomaly.vue";
import { tracelist, traceedit } from "@/api/AiCentre/index";
import dayjs from "dayjs";
import { deptTreeSelect } from "@/api/system/user";
import { deptTreeSelect, listUser } from "@/api/system/user";
export default {
  name: "BatchProcess",
@@ -527,6 +697,8 @@
      // 科室列表
      deptList: [],
      // 用户列表(流转负责人候选)
      userList: [],
      // 异常列表数据
      exceptionList: [],
@@ -540,6 +712,21 @@
        handleresult: "",
        handledesc: "",
        finaloption: "",
        // 流转相关
        isFlow: "0",
        flowPerson: "",
        flowPersonCode: "",
        flowTime: "",
        flowDesc: "",
        flowDepts: [],
        flowDeptNames: [],
        flowDeptCodes: [],
        pushUsers: [],
        pushUserNames: [],
        pushUserNos: [],
        flowFeedbacks: [],
        handleBy: "",
        handleTime: "",
      },
      batchProgress: {
        visible: false,
@@ -616,6 +803,7 @@
  mounted() {
    this.loadExceptionList();
    this.getDeptOptions();
    this.getUserOptions();
  },
  methods: {
@@ -648,6 +836,19 @@
        .catch((error) => {
          console.error("获取科室列表失败:", error);
          this.$message.error("获取科室列表失败");
        });
    },
    /** 查询用户列表(流转负责人候选) */
    getUserOptions() {
      listUser({ pageNum: 1, pageSize: 999 })
        .then((res) => {
          if (res.code == 200) {
            this.userList = res.rows || [];
          }
        })
        .catch((error) => {
          console.error("获取用户列表失败:", error);
          this.$message.error("获取用户列表失败");
        });
    },
    flattenArray(multiArray) {
@@ -870,22 +1071,212 @@
    handleProcess(row) {
      this.currentExceptionId = row.id;
      const nickName = this.$store.state.user.nickName || "";
      const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
      // 是否流转(后端 circulationStatus:0 不流转 / 1 流转)
      const isFlow = String(row.circulationStatus) === "1" ? "1" : "0";
      // 报备科室(兼容数组或逗号分隔字符串)
      const ccdepts = Array.isArray(row.ccdepts)
        ? row.ccdepts
        : row.ccdepts
        ? row.ccdepts.split(",")
        : [];
      // 流转科室 code / 名称
      const flowDeptCodes = row.circulationDeptCode
        ? row.circulationDeptCode.split(",")
        : [];
      const flowDeptNames = row.circulationDeptName
        ? row.circulationDeptName.split(",")
        : [];
      // 推送负责人:后端存工号(userName),下拉 value 用 userId,需回映
      const pushUserNos = row.circulationDeptPersonCode
        ? row.circulationDeptPersonCode.split(",")
        : [];
      const pushUserNames = row.circulationDeptPersonName
        ? row.circulationDeptPersonName.split(",")
        : [];
      const pushUsers = pushUserNos.map((no) => {
        const u = this.userList.find((item) => item.userName === no);
        return u ? u.userId : no;
      });
      // 科室反馈:从 handledesc JSON 中还原
      let flowFeedbacks = [];
      try {
        const parsed =
          typeof row.handledesc === "string" &&
          row.handledesc.trim().startsWith("{")
            ? JSON.parse(row.handledesc)
            : null;
        if (parsed && parsed.flow && Array.isArray(parsed.flow.feedbacks)) {
          flowFeedbacks = parsed.flow.feedbacks;
        }
      } catch (e) {
        // handledesc 非 JSON,忽略
      }
      // 流转时间:后端 Date 字段,统一转成 yyyy-MM-dd HH:mm:ss 供日期选择器回显
      let flowTime = now;
      if (row.circulationTime) {
        const d = new Date(
          typeof row.circulationTime === "number"
            ? row.circulationTime
            : String(row.circulationTime).replace(" ", "T")
        );
        if (!isNaN(d.getTime())) {
          const pad = (n) => String(n).padStart(2, "0");
          flowTime =
            d.getFullYear() +
            "-" +
            pad(d.getMonth() + 1) +
            "-" +
            pad(d.getDate()) +
            " " +
            pad(d.getHours()) +
            ":" +
            pad(d.getMinutes()) +
            ":" +
            pad(d.getSeconds());
        }
      }
      // 初始化表单数据
      this.processForm = {
        handleFlag: row.handleFlag === "0" ? "1" : "0",
        ccdepts: row.ccdepts ? row.ccdepts.split(",") : [],
        handleFlag: "1",
        ccdepts,
        handleresult: row.handleresult || "",
        handledesc: row.handledesc || "",
        handledesc: this.formatHandledesc(row.handledesc),
        finaloption: row.finaloption || "",
        // 流转相关
        isFlow,
        flowPerson: row.circulationPersonName || nickName,
        flowPersonCode:
          row.circulationPersonCode || this.$store.state.user.userName || "",
        flowTime,
        flowDesc: row.circulationExplain || "",
        flowDepts: [...flowDeptCodes],
        flowDeptNames: [...flowDeptNames],
        flowDeptCodes: [...flowDeptCodes],
        pushUsers,
        pushUserNames: [...pushUserNames],
        pushUserNos: [...pushUserNos],
        flowFeedbacks,
        handleBy: nickName,
        handleTime: now,
      };
      this.processDialogVisible = true;
    },
    // 流转科室变化:同步科室名称、科室code,并生成反馈表格
    onFlowDeptChange(codes) {
      this.processForm.flowDeptCodes = [...codes];
      this.processForm.flowDeptNames = codes.map((code) => {
        const dept = this.deptList.find((item) => item.deptCode === code);
        return dept ? dept.label : code;
      });
      // 生成/维护第二步反馈表格(保留已填写的反馈)
      this.processForm.flowFeedbacks = codes.map((code) => {
        const existing = this.processForm.flowFeedbacks.find(
          (item) => item.deptCode === code
        );
        if (existing) return existing;
        const dept = this.deptList.find((item) => item.deptCode === code);
        return {
          deptCode: code,
          deptName: dept ? dept.label : code,
          agree: "",
          desc: "",
        };
      });
    },
    // 推送负责人变化:同步负责人昵称、工号
    onPushUserChange(ids) {
      this.processForm.pushUserNames = ids.map((id) => {
        const user = this.userList.find((u) => String(u.userId) === String(id));
        return user ? user.nickName || user.userName : "";
      });
      this.processForm.pushUserNos = ids.map((id) => {
        const user = this.userList.find((u) => String(u.userId) === String(id));
        return user ? user.userName || "" : "";
      });
    },
    // 组装 handledesc:流转时以 JSON 形式保存原处理说明与流转说明、科室反馈
    buildHandledesc() {
      if (this.processForm.isFlow !== "1") {
        return this.processForm.handledesc;
      }
      const flow = {
        feedbacks: this.processForm.flowFeedbacks.map((f) => ({
          deptCode: f.deptCode,
          deptName: f.deptName,
          agree: f.agree,
          desc: f.desc,
        })),
      };
      return JSON.stringify({
        desc: this.processForm.handledesc,
        flow,
      });
    },
    // 解析 handledesc:若是流转 JSON 则取其中的处理说明,否则原样返回
    formatHandledesc(val) {
      if (!val) return "";
      if (typeof val !== "string") return val;
      const trimmed = val.trim();
      if (trimmed.startsWith("{")) {
        try {
          const obj = JSON.parse(trimmed);
          if (obj && obj.desc != null) return obj.desc;
        } catch (e) {
          // 不是合法 JSON,按原文本返回
        }
      }
      return val;
    },
    // 流转时间转为带时区的 ISO 格式,供后端 Date 类型字段反序列化
    toISODateTime(str) {
      if (!str) return null;
      const d = new Date(String(str).replace(" ", "T"));
      if (isNaN(d.getTime())) return null;
      const pad = (n) => String(n).padStart(2, "0");
      const offset = -d.getTimezoneOffset();
      const sign = offset >= 0 ? "+" : "-";
      const abs = Math.abs(offset);
      const tz = sign + pad(Math.floor(abs / 60)) + ":" + pad(abs % 60);
      return (
        d.getFullYear() +
        "-" + pad(d.getMonth() + 1) +
        "-" + pad(d.getDate()) +
        "T" + pad(d.getHours()) +
        ":" + pad(d.getMinutes()) +
        ":" + pad(d.getSeconds()) +
        "." + String(d.getMilliseconds()).padStart(3, "0") +
        tz
      );
    },
    // 提交处理
    async submitProcess() {
      this.$refs.processForm.validate(async (valid) => {
        if (!valid) {
          return;
        }
        // 流转时校验必须选择流转科室
        if (
          this.processForm.isFlow === "1" &&
          (!this.processForm.flowDepts || this.processForm.flowDepts.length === 0)
        ) {
          this.$message.warning("请先选择流转科室");
          return;
        }
@@ -897,14 +1288,30 @@
            id: this.currentExceptionId,
            handleFlag: this.processForm.handleFlag,
            handleresult: this.processForm.handleresult,
            handledesc: this.processForm.handledesc,
            handledesc: this.buildHandledesc(),
            finaloption: this.processForm.finaloption,
            handleBy: this.$store.state.user.nickName,
            handleTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
            handleBy:
              this.processForm.handleBy || this.$store.state.user.nickName,
            handleTime:
              this.processForm.handleTime ||
              dayjs().format("YYYY-MM-DD HH:mm:ss"),
            // 将数组转换为逗号分隔的字符串
            ccdepts: Array.isArray(this.processForm.ccdepts)
              ? this.processForm.ccdepts.join(",")
              : this.processForm.ccdepts,
            // 流转相关主表字段(后端已定稿)
            circulationPersonName: this.processForm.flowPerson,
            circulationPersonCode:
              this.processForm.flowPersonCode ||
              this.$store.state.user.userName ||
              "",
            circulationStatus: this.processForm.isFlow,
            circulationDeptName: this.processForm.flowDeptNames.join(","),
            circulationDeptCode: this.processForm.flowDeptCodes.join(","),
            circulationDeptPersonName: this.processForm.pushUserNames.join(","),
            circulationDeptPersonCode: this.processForm.pushUserNos.join(","),
            circulationExplain: this.processForm.flowDesc,
            circulationTime: this.toISODateTime(this.processForm.flowTime),
          };
          // TODO: 这里需要调用实际的处理接口
          await traceedit(submitData);
@@ -1227,6 +1634,47 @@
  }
}
// 处理弹框左右布局
.flow-dialog-body {
  display: flex;
  align-items: stretch;
  .flow-left {
    flex: 1;
    min-width: 320px;
    padding-right: 24px;
    border-right: 1px solid #ebeef5;
  }
  .flow-right {
    flex: 1;
    min-width: 320px;
    padding-left: 24px;
    max-height: 60vh;
    overflow-y: auto;
    .flow-step-title {
      font-weight: 600;
      color: #303133;
      margin-bottom: 12px;
      font-size: 14px;
    }
    .flow-step-form {
      ::v-deep .el-form-item {
        margin-bottom: 12px;
      }
    }
    .flow-empty-tip {
      padding: 20px;
      text-align: center;
      color: #909399;
      font-size: 13px;
    }
  }
}
@media (max-width: 768px) {
  .batch-process {
    padding: 10px;