WXL (wul)
11 小时以前 e9f35f9782f3d99d742c294fe503fc1294f203c4
src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -1,6 +1,54 @@
<template>
  <div class="data-overview">
    <!-- 表格一:各服务项目满意度统计 -->
    <!-- 顶部筛选:时间范围 + 科室/病区,控制本页所有数据 -->
    <el-card shadow="never" class="overview-card filter-card">
      <el-form inline class="filter-form">
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="queryParams.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
            size="small"
          />
        </el-form-item>
        <el-form-item label="科室/病区">
          <el-select
            v-model="queryParams.deptWard"
            clearable
            filterable
            placeholder="全部"
            size="small"
            class="filter-select"
          >
            <el-option-group label="科室">
              <el-option
                v-for="d in deptOptions"
                :key="'dept:' + d"
                :label="d"
                :value="'dept:' + d"
              />
            </el-option-group>
            <el-option-group label="病区">
              <el-option
                v-for="w in wardOptions"
                :key="'ward:' + w"
                :label="w"
                :value="'ward:' + w"
              />
            </el-option-group>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" size="small" icon="el-icon-search" @click="fetchData">查询</el-button>
          <el-button size="small" icon="el-icon-refresh" @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- 表格一:各服务项目满意度统计(固定五项选项的题目) -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">各服务项目满意度统计</span>
@@ -32,31 +80,48 @@
      </el-table>
    </el-card>
    <!-- 表格二:单选题/多选题统计 -->
    <!-- 表格二:单选题/多选题统计(其余题目) -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">单选题/多选题统计</span>
        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table2')">导出Excel</el-button>
      </div>
      <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
        <h4 class="q-title">{{ q.title }}</h4>
        <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
          <el-table-column prop="option" label="选项" min-width="180" />
          <el-table-column prop="count" label="数量" width="100" align="center" />
          <el-table-column prop="rate" label="比例" width="100" align="center" />
        </el-table>
        <div v-if="q.total" class="q-footer">本题有效填写人次:<b>{{ q.total }}</b></div>
      <div id="table2">
        <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
          <h4 class="q-title">{{ q.title }}</h4>
          <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
            <el-table-column prop="option" label="选项" min-width="180" />
            <el-table-column prop="count" label="数量" width="100" align="center" />
            <el-table-column prop="rate" label="比例" width="100" align="center" />
          </el-table>
          <div v-if="q.total" class="q-footer">本题有效填写人次:<b>{{ q.total }}</b></div>
        </div>
      </div>
    </el-card>
    <!-- 表格三:各科室满意度评分对比 -->
    <!-- 表格三:各科室/病区满意度评分对比 -->
    <el-card shadow="never" class="overview-card">
      <div slot="header" class="card-header">
        <span class="card-title">各科室满意度评分对比</span>
        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">导出Excel</el-button>
        <span class="card-title">各科室/病区满意度评分对比</span>
        <div class="header-right">
          <el-radio-group v-model="statDimension" size="small" @change="onStatDimensionChange">
            <el-radio-button label="dept">按科室</el-radio-button>
            <el-radio-button label="ward">按病区</el-radio-button>
          </el-radio-group>
          <el-select v-model="statDeptWard" size="small" class="stat-select" placeholder="沿用顶部条件">
            <el-option label="沿用顶部条件" value="inherit" />
            <template v-if="statDimension === 'dept'">
              <el-option v-for="d in deptOptions" :key="'dept:' + d" :label="d" :value="'dept:' + d" />
            </template>
            <template v-else>
              <el-option v-for="w in wardOptions" :key="'ward:' + w" :label="w" :value="'ward:' + w" />
            </template>
          </el-select>
          <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">导出Excel</el-button>
        </div>
      </div>
      <el-table id="table3" :data="deptComparison" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
        <el-table-column prop="dept" label="科室"fixed="left" align="center" />
      <el-table id="table3" :data="table3Rows" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
        <el-table-column prop="name" :label="statDimension === 'dept' ? '科室' : '病区'" fixed="left" align="center" />
        <el-table-column label="第1题" align="center">
          <el-table-column prop="q1VeryGood" label="非常满意" width="100" />
          <el-table-column prop="q1Good" label="满意" width="90" />
@@ -71,7 +136,7 @@
          <el-table-column prop="q2Bad" label="不满意" width="90" />
          <el-table-column prop="q2Score" label="得分" width="100" />
        </el-table-column>
        <el-table-column prop="totalScore" label="综合得分"  align="center" />
        <el-table-column prop="totalScore" label="综合得分" align="center" />
      </el-table>
    </el-card>
  </div>
@@ -80,68 +145,312 @@
<script>
import XLSX from 'xlsx';
// 满意度题的固定选项(顺序即表格列顺序)
const FIXED_OPTIONS = ['非常满意', '满意', '一般', '不满意', '不适用'];
const DEPTS = ['内分泌科', '皮肤科', '肿瘤内科', '消化内科', '肾内科'];
const WARDS = ['一病区', '二病区', '三病区', '四病区', '五病区'];
// 满意度题(固定五项选项,counts 顺序对应 FIXED_OPTIONS)
const SATISFACTION_BASE = [
  ['诊疗过程中,医务人员是否注意保护您的隐私?', [3219, 1351, 142, 33, 0]],
  ['您对医院服务设施的总体印象如何?', [3110, 1369, 217, 49, 0]],
  ['医院的厕所是否清洁无异味?', [2973, 1504, 235, 33, 0]],
  ['医院内的路标和指示是否明确?', [3234, 1289, 196, 26, 0]],
  ['医院的空间布局是否便利?', [3037, 1460, 209, 39, 0]],
  ['看病期间,您的抱怨和不满能否及时得到回应?', [3019, 1317, 294, 115, 0]],
  ['医生是否用您听得懂的方式解释问题?', [3402, 967, 254, 122, 0]],
  ['医生对您是否尊重?', [3495, 897, 249, 104, 0]],
  ['医生是否仔细倾听您讲话?', [3422, 937, 251, 135, 0]],
  ['对挂号服务人员服务态度是否满意', [3458, 1205, 52, 30, 0]],
  ['您对预约挂号是否满意?', [3527, 1088, 72, 58, 0]],
  ['护士对您是否尊重?', [3420, 1184, 113, 28, 0]],
  ['护士是否用您听得懂的方式解释问题?', [3346, 1250, 122, 27, 0]],
  ['护士是否仔细倾听您讲话?', [3378, 1205, 134, 28, 0]],
];
// 其余题目(单选/多选,非固定满意度选项)
const OTHER_BASE = [
  {
    title: '您对我院工作人员的服务质量满意不满意?【分值单选题】',
    type: 'single',
    options: [['非常满意', 1084], ['满意', 1959], ['一般', 167], ['不满意', 153]],
  },
  {
    title: '您对哪部分服务不满意?【多选题】',
    type: 'multiple',
    total: 408,
    options: [['医生诊断和诊疗', 131], ['护理服务', 47], ['其他医务人员技术水平', 30], ['工人', 72]],
  },
  {
    title: '医生诊断和治疗中哪些服务质量不满意?【多选题】',
    type: 'multiple',
    total: 131,
    options: [['医生诊断错误', 23], ['对治疗效果不满意', 72], ['其他', 54]],
  },
];
// 统一接口返回的题目(模拟):固定五项选项 => 表一,其余 => 表二
const RAW_QUESTIONS = [
  ...SATISFACTION_BASE.map(([title, counts]) => ({
    title,
    type: 'single',
    options: FIXED_OPTIONS.map((opt, i) => ({ option: opt, count: counts[i] })),
  })),
  ...OTHER_BASE.map((q) => ({
    title: q.title,
    type: q.type,
    total: q.total,
    options: q.options.map(([option, count]) => ({ option, count })),
  })),
];
// 确定性伪随机(便于筛选条件固定时结果稳定)
function hashString(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}
function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
export default {
  name: "DataOverview",
  name: 'DataOverview',
  data() {
    return {
      serviceSatisfaction: [
        { category: "诊疗过程中,医务人员是否注意保护您的隐私?", veryGoodCount: 3219, veryGoodRate: "67.84%", goodCount: 1351, goodRate: "28.47%", normalCount: 142, normalRate: "2.99%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "87.82" },
        { category: "您对医院服务设施的总体印象如何?", veryGoodCount: 3110, veryGoodRate: "65.54%", goodCount: 1369, goodRate: "28.85%", normalCount: 217, normalRate: "4.57%", badCount: 49, badRate: "1.03%", naCount: 0, naRate: "0.00%", score: "86.30" },
        { category: "医院的厕所是否清洁无异味?", veryGoodCount: 2973, veryGoodRate: "62.66%", goodCount: 1504, goodRate: "31.70%", normalCount: 235, normalRate: "4.95%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "85.44" },
        { category: "医院内的路标和指示是否明确?", veryGoodCount: 3234, veryGoodRate: "68.16%", goodCount: 1289, goodRate: "27.17%", normalCount: 196, normalRate: "4.13%", badCount: 26, badRate: "0.55%", naCount: 0, naRate: "0.00%", score: "87.64" },
        { category: "医院的空间布局是否便利?", veryGoodCount: 3037, veryGoodRate: "64.00%", goodCount: 1460, goodRate: "30.77%", normalCount: 209, normalRate: "4.40%", badCount: 39, badRate: "0.82%", naCount: 0, naRate: "0.00%", score: "85.99" },
        { category: "看病期间,您的抱怨和不满能否及时得到回应?", veryGoodCount: 3019, veryGoodRate: "63.62%", goodCount: 1317, goodRate: "27.76%", normalCount: 294, normalRate: "6.20%", badCount: 115, badRate: "2.42%", naCount: 0, naRate: "0.00%", score: "84.19" },
        { category: "医生是否用您听得懂的方式解释问题?", veryGoodCount: 3402, veryGoodRate: "71.70%", goodCount: 967, goodRate: "20.38%", normalCount: 254, normalRate: "5.35%", badCount: 122, badRate: "2.57%", naCount: 0, naRate: "0.00%", score: "87.07" },
        { category: "医生对您是否尊重?", veryGoodCount: 3495, veryGoodRate: "73.66%", goodCount: 897, goodRate: "18.90%", normalCount: 249, normalRate: "5.25%", badCount: 104, badRate: "2.19%", naCount: 0, naRate: "0.00%", score: "88.01" },
        { category: "医生是否仔细倾听您讲话?", veryGoodCount: 3422, veryGoodRate: "72.12%", goodCount: 937, goodRate: "19.75%", normalCount: 251, normalRate: "5.29%", badCount: 135, badRate: "2.85%", naCount: 0, naRate: "0.00%", score: "87.05" },
        { category: "对挂号服务人员服务态度是否满意", veryGoodCount: 3458, veryGoodRate: "72.88%", goodCount: 1205, goodRate: "25.40%", normalCount: 52, normalRate: "1.10%", badCount: 30, badRate: "0.63%", naCount: 0, naRate: "0.00%", score: "90.17" },
        { category: "您对预约挂号是否满意?", veryGoodCount: 3527, veryGoodRate: "74.33%", goodCount: 1088, goodRate: "22.93%", normalCount: 72, normalRate: "1.52%", badCount: 58, badRate: "1.22%", naCount: 0, naRate: "0.00%", score: "90.12" },
        { category: "护士对您是否尊重?", veryGoodCount: 3420, veryGoodRate: "72.08%", goodCount: 1184, goodRate: "24.95%", normalCount: 113, normalRate: "2.38%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.50" },
        { category: "护士是否用您听得懂的方式解释问题?", veryGoodCount: 3346, veryGoodRate: "70.52%", goodCount: 1250, goodRate: "26.34%", normalCount: 122, normalRate: "2.57%", badCount: 27, badRate: "0.57%", naCount: 0, naRate: "0.00%", score: "88.94" },
        { category: "护士是否仔细倾听您讲话?", veryGoodCount: 3378, veryGoodRate: "71.19%", goodCount: 1205, goodRate: "25.40%", normalCount: 134, normalRate: "2.82%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.06" },
        { category: "总计", veryGoodCount: 54863, veryGoodRate: "69.02%", goodCount: 20800, goodRate: "26.17%", normalCount: 2920, normalRate: "3.67%", badCount: 908, badRate: "1.14%", naCount: 0, naRate: "0.00%", score: "87.66" },
      ],
      questionStats: [
        {
          title: "第1题:您对我院工作人员的服务质量满意不满意?【分值单选题】- 综合得分",
          options: [
            { option: "非常满意", count: 1084, rate: "75.66%" },
            { option: "满意", count: 1959, rate: "20.92%" },
            { option: "一般", count: 167, rate: "1.78%" },
            { option: "不满意", count: 153, rate: "1.63%" },
          ],
          total: 3363
        },
        {
          title: "第2题:您对哪部分服务不满意?【多选题】",
          options: [
            { option: "医生诊断和诊疗", count: 131, rate: "32.11%" },
            { option: "护理服务", count: 47, rate: "11.52%" },
            { option: "其他医务人员技术水平", count: 30, rate: "7.33%" },
            { option: "工人", count: 72, rate: "17.65%" },
          ],
          total: 408
        },
        {
          title: "第3题:医生诊断和治疗中哪些服务质量不满意?【多选题】",
          options: [
            { option: "医生诊断错误", count: 23, rate: "15.44%" },
            { option: "对治疗效果不满意", count: 72, rate: "48.32%" },
            { option: "其他", count: 54, rate: "36.24%" },
          ],
          total: 131
        },
      ],
      deptComparison: [
        { dept: "内分泌科", q1VeryGood: 103, q1Good: 34, q1Normal: 0, q1Bad: 0, q1Score: "377.00", q2VeryGood: 101, q2Good: 35, q2Normal: 1, q2Bad: 0, q2Score: "374.00", totalScore: "75.25" },
        { dept: "皮肤科", q1VeryGood: 112, q1Good: 25, q1Normal: 2, q1Bad: 3, q1Score: "388.00", q2VeryGood: 111, q2Good: 27, q2Normal: 3, q2Bad: 1, q2Score: "390.00", totalScore: "76.12" },
        { dept: "肿瘤内科", q1VeryGood: 24, q1Good: 12, q1Normal: 2, q1Bad: 0, q1Score: "98.00", q2VeryGood: 25, q2Good: 12, q2Normal: 1, q2Bad: 0, q2Score: "100.00", totalScore: "70.22" },
        { dept: "消化内科", q1VeryGood: 180, q1Good: 52, q1Normal: 1, q1Bad: 0, q1Score: "645.00", q2VeryGood: 168, q2Good: 62, q2Normal: 3, q2Bad: 0, q2Score: "631.00", totalScore: "73.56" },
        { dept: "肾内科", q1VeryGood: 21, q1Good: 4, q1Normal: 0, q1Bad: 0, q1Score: "71.00", q2VeryGood: 19, q2Good: 6, q2Normal: 0, q2Bad: 0, q2Score: "65.00", totalScore: "68.50" },
      ],
      // 顶部全局筛选
      queryParams: {
        dateRange: [],
        deptWard: '',
      },
      deptOptions: DEPTS,
      wardOptions: WARDS,
      // 表格三:统计维度 + 独立科室/病区条件('inherit' = 沿用顶部)
      statDimension: 'dept',
      statDeptWard: 'inherit',
      // 统一接口返回的数据(模拟)
      questionList: [],
      deptComparison: [],
      wardComparison: [],
    };
  },
  computed: {
    // 固定五项选项的题目 => 表一
    satisfactionQuestions() {
      return this.questionList.filter((q) => this.isSatisfactionQuestion(q));
    },
    // 其余题目 => 表二
    otherQuestions() {
      return this.questionList.filter((q) => !this.isSatisfactionQuestion(q));
    },
    // 表一:满意度统计(含总计行)
    serviceSatisfaction() {
      const rows = this.satisfactionQuestions.map((q) => {
        const m = {};
        q.options.forEach((o) => (m[o.option] = o.count));
        const veryGood = m['非常满意'] || 0;
        const good = m['满意'] || 0;
        const normal = m['一般'] || 0;
        const bad = m['不满意'] || 0;
        const na = m['不适用'] || 0;
        const total = veryGood + good + normal + bad + na;
        return {
          category: q.title,
          veryGoodCount: veryGood,
          veryGoodRate: this.rateOf(veryGood, total),
          goodCount: good,
          goodRate: this.rateOf(good, total),
          normalCount: normal,
          normalRate: this.rateOf(normal, total),
          badCount: bad,
          badRate: this.rateOf(bad, total),
          naCount: na,
          naRate: this.rateOf(na, total),
          score: this.scoreOf([veryGood, good, normal, bad, na]),
        };
      });
      // 总计行
      const sum = { veryGood: 0, good: 0, normal: 0, bad: 0, na: 0 };
      rows.forEach((r) => {
        sum.veryGood += r.veryGoodCount;
        sum.good += r.goodCount;
        sum.normal += r.normalCount;
        sum.bad += r.badCount;
        sum.na += r.naCount;
      });
      const total = sum.veryGood + sum.good + sum.normal + sum.bad + sum.na;
      rows.push({
        category: '总计',
        veryGoodCount: sum.veryGood,
        veryGoodRate: this.rateOf(sum.veryGood, total),
        goodCount: sum.good,
        goodRate: this.rateOf(sum.good, total),
        normalCount: sum.normal,
        normalRate: this.rateOf(sum.normal, total),
        badCount: sum.bad,
        badRate: this.rateOf(sum.bad, total),
        naCount: sum.na,
        naRate: this.rateOf(sum.na, total),
        score: this.scoreOf([sum.veryGood, sum.good, sum.normal, sum.bad, sum.na]),
      });
      return rows;
    },
    // 表二:单选/多选统计
    questionStats() {
      return this.otherQuestions.map((q) => {
        const total = this.questionTotal(q);
        return {
          title: q.title,
          total,
          options: q.options.map((o) => ({
            option: o.option,
            count: o.count,
            rate: this.rateOf(o.count, total),
          })),
        };
      });
    },
    // 表三:按维度 + 独立条件过滤后的行
    table3Rows() {
      const list = this.statDimension === 'dept' ? this.deptComparison : this.wardComparison;
      const f = this.statFilter();
      if (!f) return list;
      const [type, name] = f.split(':');
      if (type !== this.statDimension) return list;
      return list.filter((r) => r.name === name);
    },
  },
  mounted() {
    if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
      this.queryParams.dateRange = this.defaultRange();
    }
    this.fetchData();
  },
  methods: {
    // 表三独立条件:'inherit' 时沿用顶部条件
    statFilter() {
      const v = this.statDeptWard === 'inherit' ? this.queryParams.deptWard : this.statDeptWard;
      return v || '';
    },
    onStatDimensionChange() {
      // 切换维度后,若当前已选条件不属于新维度,则恢复“沿用顶部”
      if (this.statDeptWard !== 'inherit' && !this.statDeptWard.startsWith(this.statDimension + ':')) {
        this.statDeptWard = 'inherit';
      }
    },
    handleReset() {
      this.queryParams.dateRange = this.defaultRange();
      this.queryParams.deptWard = '';
      this.fetchData();
    },
    // 判断题目选项是否为固定满意度五项
    isSatisfactionQuestion(q) {
      if (q.options.length !== FIXED_OPTIONS.length) return false;
      const set = q.options.map((o) => o.option);
      return FIXED_OPTIONS.every((o) => set.includes(o));
    },
    questionTotal(q) {
      if (q.total != null) return q.total;
      return q.options.reduce((s, o) => s + o.count, 0);
    },
    rateOf(count, total) {
      if (!total) return '0.00%';
      return ((count / total) * 100).toFixed(2) + '%';
    },
    // 综合得分:非常满意5分、满意4分、一般3分、不满意2分,换算百分制(不适用不计入)——占位公式,接口就绪后可按后端口径替换
    scoreOf(counts) {
      const [a, b, c, d] = counts;
      const total = a + b + c + d;
      if (!total) return '0.00';
      return (((a * 5 + b * 4 + c * 3 + d * 2) / total) * 20).toFixed(2);
    },
    defaultRange() {
      const end = new Date();
      const start = new Date();
      start.setDate(start.getDate() - 29);
      const f = (d) => {
        const y = d.getFullYear();
        const m = String(d.getMonth() + 1).padStart(2, '0');
        const day = String(d.getDate()).padStart(2, '0');
        return `${y}-${m}-${day}`;
      };
      return [f(start), f(end)];
    },
    dateRangeDays(range) {
      if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
        const ms = new Date(range[1]).getTime() - new Date(range[0]).getTime();
        return Math.max(1, Math.round(ms / 86400000));
      }
      return 30;
    },
    // 拉取数据(当前为模拟数据,接口就绪后替换为真实请求)
    fetchData() {
      const data = this.mockFetch(this.queryParams);
      this.questionList = data.questionList;
      this.deptComparison = data.deptComparison;
      this.wardComparison = data.wardComparison;
    },
    mockFetch(params) {
      const seed = hashString(JSON.stringify(params));
      const rnd = mulberry32(seed);
      const days = this.dateRangeDays(params.dateRange);
      const timeFactor = Math.max(0.05, days / 30);
      const scopeFactor = params.deptWard ? 0.18 : 1;
      const jitter = () => 0.9 + rnd() * 0.2;
      const questionList = RAW_QUESTIONS.map((q) => {
        const options = q.options.map((o) => ({
          option: o.option,
          count: Math.round(o.count * timeFactor * scopeFactor * jitter()),
        }));
        const item = { title: q.title, type: q.type, options };
        if (q.type === 'multiple' && q.total != null) {
          item.total = Math.round(q.total * timeFactor * scopeFactor);
        }
        return item;
      });
      const deptComparison = DEPTS.map((d) => this.genCompareRow(d, rnd));
      const wardComparison = WARDS.map((w) => this.genCompareRow(w, rnd));
      return { questionList, deptComparison, wardComparison };
    },
    genCompareRow(name, rnd) {
      const r = (lo, hi) => Math.round(lo + rnd() * (hi - lo));
      const q1VeryGood = r(20, 180);
      const q1Good = r(5, 55);
      const q1Normal = r(0, 4);
      const q1Bad = r(0, 3);
      const q2VeryGood = r(18, 175);
      const q2Good = r(5, 55);
      const q2Normal = r(0, 4);
      const q2Bad = r(0, 3);
      return {
        name,
        q1VeryGood, q1Good, q1Normal, q1Bad,
        q1Score: this.scoreOf([q1VeryGood, q1Good, q1Normal, q1Bad]),
        q2VeryGood, q2Good, q2Normal, q2Bad,
        q2Score: this.scoreOf([q2VeryGood, q2Good, q2Normal, q2Bad]),
        totalScore: this.scoreOf([
          q1VeryGood + q2VeryGood,
          q1Good + q2Good,
          q1Normal + q2Normal,
          q1Bad + q2Bad,
        ]),
      };
    },
    exportTable(tableId) {
      const tableNames = { table1: '各服务项目满意度统计', table2: '单选题多选题统计', table3: '各科室满意度评分对比' };
      const name = tableNames[tableId] || '统计数据';
@@ -152,9 +461,9 @@
        // 从表格提取数据构建带样式的 Excel
        const wb = XLSX.utils.book_new();
        const data = [];
        table.querySelectorAll('tr').forEach(tr => {
        table.querySelectorAll('tr').forEach((tr) => {
          const row = [];
          tr.querySelectorAll('th,td').forEach(td => {
          tr.querySelectorAll('th,td').forEach((td) => {
            row.push((td.innerText || '').trim());
          });
          if (row.length) data.push(row);
@@ -170,14 +479,16 @@
      } catch (e) {
        // fallback: CSV
        let csv = '';
        table.querySelectorAll('tr').forEach(tr => {
        table.querySelectorAll('tr').forEach((tr) => {
          const row = [];
          tr.querySelectorAll('th,td').forEach(td => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
          tr.querySelectorAll('th,td').forEach((td) => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
          csv += row.join(',') + '\n';
        });
        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob); link.download = name + '.csv'; link.click();
        link.href = URL.createObjectURL(blob);
        link.download = name + '.csv';
        link.click();
      }
    },
  },
@@ -186,8 +497,10 @@
<style lang="scss" scoped>
.data-overview { .overview-card { margin-bottom: 20px; } }
.filter-card { .filter-form { margin-bottom: -10px; } .filter-select { width: 200px; } }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.card-title { font-size: 16px; font-weight: 600; color: #303133; }
.header-right { display: flex; align-items: center; .stat-select { width: 180px; margin: 0 12px; } }
.question-block { margin-bottom: 24px;
  .q-title { margin: 0 0 12px 0; font-size: 14px; color: #606266; }
  .q-footer { margin-top: 8px; font-size: 13px; color: #909399; text-align: right; }