liusheng
13 小时以前 9903954c85f651dd5cc6d4b538cfe66b17fc5baa
src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -114,6 +114,7 @@
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">各服务项目满意度统计</span>
        <span class="card-subtitle">(六档评分题:很满意/满意/一般/不满意/很不满意/未经历)</span>
        <el-button
          type="warning"
          size="small"
@@ -174,7 +175,8 @@
    <!-- 表格二:单选题/多选题统计(其余题目) -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">单选题/多选题统计</span>
        <span class="card-title">其他题目统计</span>
        <span class="card-subtitle">(六档满意度题已在上表,这里只列其余题型:推荐、抱怨回应、意见等)</span>
        <div class="header-right">
          <el-input
            v-model="questionSearch"
@@ -333,6 +335,7 @@
<script>
import { sltdMydTotalByScore,statQuestionOptionByKsOrBq } from "@/api/AiCentre/satisfaction";
import { deptTreeSelect } from "@/api/system/user";
import XLSX from "xlsx";
// 满意度题的固定选项(顺序即表格列顺序)——这些选项的题目进表一,其余进表二
@@ -360,9 +363,10 @@
// 服务类型默认选中项(按 label 匹配,适配不同字典的 value 差异)
const DEFAULT_SERVICE_LABELS = ["住院满意度", "门诊满意度"];
// 兜底:store 中无科室/病区数据时使用(表三对比 + 筛选下拉)
const DEPTS = ["内分泌科", "皮肤科", "肿瘤内科", "消化内科", "肾内科"];
const WARDS = ["一病区", "二病区", "三病区", "四病区", "五病区"];
// 注意:这里以前用 DEPTS / WARDS 两个「演示用」名称数组兜底,
// 结果被当成编码塞进 leavehospitaldistrictcodes / leaveldeptcodes,
// 后端按 leavehospitaldistrictcode IN ('一病区','二病区'…) 过滤 → 一条也匹配不上。
// 科室/病区一律只用真实编码:有归属取 store,无归属(如管理员)取 deptTreeSelect 字典。
/**
 * 接口数据 → 页面内部题目结构
@@ -537,22 +541,27 @@
      // 表三:科室/病区评分对比(rows=行数据,questions=动态列)
      deptComparison: { rows: [], questions: [] },
      wardComparison: { rows: [], questions: [] },
      // 科室/病区字典:当前账号没有归属科室/病区时(如管理员)从 deptTreeSelect 拉取,
      // 保证下拉里放的是真实编码(deptCode / districtCode),而不是名称
      deptDict: [],
      wardDict: [],
    };
  },
  computed: {
    // 科室选项:优先取当前用户所属科室,无则兜底
    // 科室选项:优先取当前用户所属科室;没有归属时用科室字典(真实 deptCode)
    deptOptions() {
      const list = this.$store.getters.belongDepts || [];
      return list.length
        ? list.map((d) => ({ label: d.deptName, value: d.deptCode }))
        : DEPTS.map((d) => ({ label: d, value: d }));
      const list = (this.$store.getters.belongDepts || [])
        .map((d) => ({ label: d.deptName, value: d.deptCode }))
        .filter((d) => d.label && d.value);
      return list.length ? list : this.deptDict;
    },
    // 病区选项:优先取当前用户所属病区,无则兜底
    // 病区选项:优先取当前用户所属病区;没有归属时用病区字典(真实 districtCode)
    wardOptions() {
      const list = this.$store.getters.belongWards || [];
      return list.length
        ? list.map((w) => ({ label: w.districtName, value: w.districtCode }))
        : WARDS.map((w) => ({ label: w, value: w }));
      const list = (this.$store.getters.belongWards || [])
        .map((w) => ({ label: w.districtName, value: w.districtCode }))
        .filter((w) => w.label && w.value);
      return list.length ? list : this.wardDict;
    },
    // 服务类型下拉:取 store 的随访类型字典 tasktypes,取不到时兜底本地 SERVICE_TYPES
    serviceTypeOptions() {
@@ -566,18 +575,22 @@
      const taskTypes = toOptions(this.$store.getters.tasktypes);
      return taskTypes.length ? taskTypes : SERVICE_TYPES;
    },
    // 未选病区时的默认查询范围:全部病区编码
    // 未选病区时的默认查询范围:全部病区编码(空数组 = 不限,后端不拼过滤条件)
    defaultWardCodes() {
      return Array.from(new Set(this.wardOptions.map((w) => w.value)));
      return Array.from(
        new Set(this.wardOptions.map((w) => w.value).filter(Boolean))
      );
    },
    // 是否按「在院满意度」口径查询:在院满意度用 deptcodes/hospitaldistrictcodes,
    // 离院满意度(以及清空下拉时的默认口径)用 leaveldeptcodes/leavehospitaldistrictcodes
    isInHospitalSatisfaction() {
      return this.satisfactionScope === "hospital";
    },
    // 未选科室时的默认查询范围:全部科室编码
    // 未选科室时的默认查询范围:全部科室编码(空数组 = 不限)
    defaultDeptCodes() {
      return Array.from(new Set(this.deptOptions.map((d) => d.value)));
      return Array.from(
        new Set(this.deptOptions.map((d) => d.value).filter(Boolean))
      );
    },
    // 固定六项选项的题目 => 表一
    satisfactionQuestions() {
@@ -714,7 +727,9 @@
    }
    this.queryParams.serviceTypeList = this.defaultServiceTypes();
  },
  mounted() {
  async mounted() {
    // 先把科室/病区字典补齐,保证首次查询用的就是真实编码
    await this.ensureOrgDict();
    this.fetchData();
  },
  methods: {
@@ -725,6 +740,52 @@
        return item ? item.value : null;
      }).filter((v) => v != null);
      return Array.from(new Set(hit));
    },
    // 归一化科室/病区编码:选中项优先,其次默认全部;去重、去空
    // 返回空数组表示「不限」(后端 mapper 用 size() > 0 判断,空集合不拼过滤条件)
    codeList(selected, fallback) {
      const list = (selected && selected.length ? selected : fallback) || [];
      return Array.from(
        new Set(
          list.filter((v) => v !== null && v !== undefined && String(v).trim() !== "")
        )
      );
    },
    // 补全科室/病区字典:仅当 store 里没有归属数据时(典型是管理员账号)才请求
    async ensureOrgDict() {
      const tasks = [];
      if (!(this.$store.getters.belongDepts || []).length) {
        tasks.push(this.loadOrgDict(1));
      }
      if (!(this.$store.getters.belongWards || []).length) {
        tasks.push(this.loadOrgDict(2));
      }
      if (tasks.length) await Promise.all(tasks);
    },
    // deptType:1=科室,2=病区;结果既作下拉选项,也作「全部」时的编码来源
    async loadOrgDict(deptType) {
      let flat = [];
      try {
        const res = await deptTreeSelect(deptType);
        const walk = (nodes) => {
          (Array.isArray(nodes) ? nodes : []).forEach((n) => {
            // deptTreeSelect 返回 TreeSelect:{ id, label, deptCode, children }
            // 只认 deptCode,缺失就跳过——用 deptId 冒充编码同样查不到数据
            if (n.label && n.deptCode) {
              flat.push({ label: n.label, value: String(n.deptCode) });
            }
            if (n.children && n.children.length) walk(n.children);
          });
        };
        walk(res && res.data);
      } catch (e) {
        console.warn("[DataOverview] 获取科室/病区字典失败,将按“全部”查询", e);
      }
      if (deptType === 1) {
        this.deptDict = flat;
      } else {
        this.wardDict = flat;
      }
    },
    // 表三独立条件:'inherit' 时沿用顶部所选;返回选中的名称数组(接口只回名称),空数组表示不限
    statFilterNames() {
@@ -862,22 +923,26 @@
        endFinishTime: range[1],
      };
      if (this.queryParams.statMode === "dept") {
        const deptCodes = depts.length
          ? Array.from(new Set(depts))
          : this.defaultDeptCodes;
        if (this.isInHospitalSatisfaction) {
          payload.deptcodes = deptCodes;
        } else {
          payload.leaveldeptcodes = deptCodes;
        const deptCodes = this.codeList(depts, this.defaultDeptCodes);
        // 有编码才传;空数组等价于「全部」(后端 mapper 对空集合不拼条件)
        if (deptCodes.length) {
          if (this.isInHospitalSatisfaction) {
            payload.deptcodes = deptCodes;
          } else {
            payload.leaveldeptcodes = deptCodes;
          }
        }
      } else {
        const wardCodes = wards.length
          ? Array.from(new Set(wards))
          : this.defaultWardCodes;
        if (this.isInHospitalSatisfaction) {
          payload.hospitaldistrictcodes = wardCodes;
        } else {
          payload.leavehospitaldistrictcodes = wardCodes;
        // 只传「用户真正选中的病区」:没选(= 全部病区)就不传,后端按不限处理。
        // 门诊满意度等任务的 leavehospitaldistrictcode 为空,
        // 原来把「全部病区编码」传下去会让这些任务被 IN 条件整段过滤掉(25 题只剩 10 题)。
        const wardCodes = this.codeList(wards, []);
        if (wardCodes.length) {
          if (this.isInHospitalSatisfaction) {
            payload.hospitaldistrictcodes = wardCodes;
          } else {
            payload.leavehospitaldistrictcodes = wardCodes;
          }
        }
      }
      // 服务类型多选:逗号分隔(接口字段为 serviceType 字符串)
@@ -927,12 +992,25 @@
      try {
        // 按科室统计、按病区统计各查一次,条件随「满意度类型」切换:
        // 在院满意度 => deptcodes / hospitaldistrictcodes;离院满意度 => leaveldeptcodes / leavehospitaldistrictcodes
        const deptParam = this.isInHospitalSatisfaction
          ? { deptcodes: this.defaultDeptCodes }
          : { leaveldeptcodes: this.defaultDeptCodes };
        const wardParam = this.isInHospitalSatisfaction
          ? { hospitaldistrictcodes: this.defaultWardCodes }
          : { leavehospitaldistrictcodes: this.defaultWardCodes };
        // 没有真实编码时不传该字段(= 全部),绝不把名称当编码传
        const deptParam = {};
        const deptCodes = this.codeList([], this.defaultDeptCodes);
        if (deptCodes.length) {
          if (this.isInHospitalSatisfaction) {
            deptParam.deptcodes = deptCodes;
          } else {
            deptParam.leaveldeptcodes = deptCodes;
          }
        }
        const wardParam = {};
        const wardCodes = this.codeList([], this.defaultWardCodes);
        if (wardCodes.length) {
          if (this.isInHospitalSatisfaction) {
            wardParam.hospitaldistrictcodes = wardCodes;
          } else {
            wardParam.leavehospitaldistrictcodes = wardCodes;
          }
        }
        const [deptRes, wardRes] = await Promise.all([
          statQuestionOptionByKsOrBq({ ...base, ...deptParam }),
          statQuestionOptionByKsOrBq({ ...base, ...wardParam }),
@@ -1053,7 +1131,7 @@
    exportTable(tableId) {
      const tableNames = {
        table1: "各服务项目满意度统计",
        table2: "单选题多选题统计",
        table2: "其他题目统计",
        table3: "各科室满意度评分对比",
      };
      const name = tableNames[tableId] || "统计数据";
@@ -1137,6 +1215,12 @@
  font-weight: 600;
  color: #303133;
}
.card-subtitle {
  margin-left: 6px;
  font-size: 12px;
  font-weight: 400;
  color: #909399;
}
.header-right {
  display: flex;
  align-items: center;