liusheng
13 小时以前 9903954c85f651dd5cc6d4b538cfe66b17fc5baa
提交
已修改10个文件
已添加15个文件
1573 ■■■■■ 文件已修改
src/App.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/smartor/pendingTask.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PendingTaskBox/index.vue 568 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/StatisticsCards/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/batch.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/DataOverview.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/outpatientService.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/Probe17.class 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/Probe17.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/Probe18.class 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/Probe18.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/probe17.txt 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe4/probe18.txt 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe9/Probe42.class 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe9/Probe42.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-probe9/probe42.txt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-svy4/Probe41.class 补丁 | 查看 | 原始文档 | blame | 历史
tmp-svy4/Probe41.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp-svy4/probe41.txt 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
骨科中心一病区_出院数据统计_20260430-20261023.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -10,6 +10,8 @@
      :hide-delay="3000"
      primary-color="#1890ff"
    />
    <!-- å³ä¸‹è§’:当前登录人待处理随访任务(按出院年月) -->
    <PendingTaskBox v-if="Assvite" />
    <!-- ä¸´è¿‘超时任务登录后弹窗(仅景宁/新华两家机构触发) -->
    <!-- <NearTimeoutDialog v-if="Assvite" /> -->
  </div>
@@ -23,6 +25,7 @@
  components: {
    ThemePicker,
    Assistant: () => import("./components/Assistant"),
    PendingTaskBox: () => import("./components/PendingTaskBox"),
    NearTimeoutDialog: () => import("./components/NearTimeoutDialog"),
  },
  data() {
src/api/smartor/pendingTask.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
import axios from 'axios'
import { getToken } from '@/utils/auth'
/**
 * å½“前登录人待处理随访任务统计(按出院年月分组)
 *
 * åŽç«¯å£å¾„:service_subtask ä¸­
 *   service_type = '2' å‡ºé™¢éšè®¿ / '3' é—¨è¯Šéšè®¿ï¼ˆåˆ†å¼€è®¡æ•°ï¼‰
 *   current_preachform = '1'(人工)
 *   å¾…随访状态(sendstate in 1,2,3,5,7)
 *   å‡ºé™¢æ—¶é—´ endtime æ—©äºŽç­‰äºŽä»Šå¤© - 7 å¤©
 *   åªç»Ÿè®¡æœ€è¿‘ 4 ä¸ªå‡ºé™¢æœˆä»½ï¼ˆå«å½“月)
 *
 * è¯¥æŽ¥å£ç”¨äºŽå³ä¸‹è§’常驻卡片,属于后台轮询:
 * ä¸èµ° @/utils/request çš„全局拦截器,避免接口未部署/网络异常时反复弹全局错误提示,
 * å¤±è´¥ä¿¡æ¯ç”±å¡ç‰‡å†…部自行展示。
 *
 * @returns {Promise<{userName: string, nickName: string, total: number,
 *   months: Array<{month: string, dischargeCount: number, outpatientCount: number}>}>}
 *   months åªåŒ…含有数据的月份,没数据的月份由卡片补 0
 */
export function getPendingTaskMonthStats() {
  return axios({
    baseURL: process.env.VUE_APP_BASE_API,
    url: '/smartor/serviceSubtask/pendingTaskMonthStats',
    method: 'post',
    timeout: 15000,
    headers: {
      Authorization: 'Bearer ' + getToken()
    }
  }).then(res => res.data)
}
src/components/PendingTaskBox/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,568 @@
<template>
  <div class="ptb-root">
    <!-- æ”¶èµ·æ€ï¼šå³ä¸‹è§’小胶囊 -->
    <button
      v-if="collapsed"
      class="ptb-pill"
      type="button"
      title="展开待处理任务"
      @click="toggle"
    >
      <span class="ptb-pill-dot"></span>
      <span class="ptb-pill-text">待处理</span>
      <span class="ptb-pill-num">{{ total }}</span>
    </button>
    <!-- å±•开态:当前登录人 + è¿‘ 4 ä¸ªæœˆå‡ºé™¢/门诊随访待处理 -->
    <div v-else class="ptb-card">
      <div class="ptb-header">
        <div class="ptb-header-left">
          <svg
            class="ptb-header-icon"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
          >
            <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2" />
            <path d="M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
            <path d="M9 14l2 2 4-4" />
          </svg>
          <span class="ptb-header-title" title="出院满 7 å¤©ã€ä»å¾…随访的出院/门诊任务">
            å¾…处理随访任务
          </span>
        </div>
        <div class="ptb-header-actions">
          <span
            class="ptb-icon-btn"
            :class="{ 'is-loading': loading }"
            title="刷新"
            @click="load"
          >&#8635;</span>
          <span class="ptb-icon-btn" title="收起" @click="toggle">&#8211;</span>
        </div>
      </div>
      <div class="ptb-body">
        <div class="ptb-user">
          <div class="ptb-avatar">{{ userInitial }}</div>
          <div class="ptb-user-meta">
            <div class="ptb-user-name" :title="displayName">{{ displayName }}</div>
            <div class="ptb-user-scope" :title="scopeText">{{ scopeText }}</div>
          </div>
        </div>
        <div class="ptb-months">
          <div
            v-for="row in monthRows"
            :key="row.key"
            class="ptb-month-row"
          >
            <span class="ptb-month-label">{{ row.label }}</span>
            <span
              class="ptb-month-seg is-clickable"
              title="点击查看该月待随访患者"
              @click="goToTasks(row, 2)"
            >出院随访待处理<b class="ptb-num is-discharge">{{ row.dischargeCount }}</b>个</span>
            <span class="ptb-month-comma">,</span>
            <span
              class="ptb-month-seg is-clickable"
              title="点击查看该月待随访患者"
              @click="goToTasks(row, 3)"
            >门诊随访待处理<b class="ptb-num is-outpatient">{{ row.outpatientCount }}</b>个</span>
          </div>
        </div>
        <div v-if="errorMsg" class="ptb-error">{{ errorMsg }}</div>
      </div>
      <div class="ptb-footer">
        <span class="ptb-update-time">更新于 {{ updateTime || '--:--' }}</span>
        <span class="ptb-footer-refresh" @click="load">刷新</span>
      </div>
    </div>
  </div>
</template>
<script>
import { getPendingTaskMonthStats } from '@/api/smartor/pendingTask'
import { getToken } from '@/utils/auth'
const COLLAPSE_KEY = 'pendingTaskBoxCollapsed'
const REFRESH_INTERVAL = 5 * 60 * 1000
// å›ºå®šå±•示:当月 + å¾€å‰ 3 ä¸ªæœˆ
const MONTH_COUNT = 4
export default {
  name: 'PendingTaskBox',
  data() {
    return {
      collapsed: localStorage.getItem(COLLAPSE_KEY) === '1',
      loading: false,
      errorMsg: '',
      userName: '',
      nickName: '',
      total: 0,
      months: [],
      updateTime: '',
      timer: null,
      retryTimer: null,
      retryCount: 0,
      lastLoadAt: 0
    }
  },
  computed: {
    displayName() {
      return (
        this.nickName ||
        this.$store.getters.nickName ||
        this.userName ||
        this.$store.getters.name ||
        '未登录'
      )
    },
    userInitial() {
      const name = this.displayName
      return name ? name.slice(0, 1) : '-'
    },
    scopeText() {
      const depts = (this.$store.getters.belongDepts || [])
        .map(item => item.deptName)
        .filter(Boolean)
      const wards = (this.$store.getters.belongWards || [])
        .map(item => item.districtName)
        .filter(Boolean)
      const scope = wards.length ? wards : depts
      if (!scope.length) {
        return '当前登录人'
      }
      return scope.length > 2 ? scope.slice(0, 2).join('、') + ' ç­‰' : scope.join('、')
    },
    monthMap() {
      const map = {}
      this.months.forEach(item => {
        if (item && item.month) {
          map[item.month] = {
            dischargeCount: Number(item.dischargeCount) || 0,
            outpatientCount: Number(item.outpatientCount) || 0
          }
        }
      })
      return map
    },
    // å½“月 + å‰ 3 ä¸ªæœˆï¼Œæ²¡æœ‰æ•°æ®çš„æœˆä»½è¡¥ 0
    monthRows() {
      const rows = []
      const now = new Date()
      for (let i = 0; i < MONTH_COUNT; i++) {
        const date = new Date(now.getFullYear(), now.getMonth() - i, 1)
        const month = String(date.getMonth() + 1).padStart(2, '0')
        const key = date.getFullYear() + '-' + month
        const data = this.monthMap[key] || { dischargeCount: 0, outpatientCount: 0 }
        rows.push({
          key,
          label: date.getFullYear() + 'å¹´' + month + '月',
          dischargeCount: data.dischargeCount,
          outpatientCount: data.outpatientCount
        })
      }
      return rows
    }
  },
  mounted() {
    this.load()
    this.timer = setInterval(() => this.load(), REFRESH_INTERVAL)
    document.addEventListener('visibilitychange', this.handleVisible)
  },
  beforeDestroy() {
    clearInterval(this.timer)
    this.clearRetry()
    document.removeEventListener('visibilitychange', this.handleVisible)
  },
  methods: {
    toggle() {
      this.collapsed = !this.collapsed
      localStorage.setItem(COLLAPSE_KEY, this.collapsed ? '1' : '0')
      if (!this.collapsed && Date.now() - this.lastLoadAt > 60 * 1000) {
        this.load()
      }
    },
    handleVisible() {
      if (document.visibilityState === 'visible' && Date.now() - this.lastLoadAt > 60 * 1000) {
        this.load()
      }
    },
    async load() {
      if (this.loading) {
        return
      }
      if (!getToken()) {
        // åˆšåˆ·æ–°é¡µé¢æ—¶ç™»å½•态可能还没就绪,稍后重试
        this.scheduleRetry(4000)
        return
      }
      this.loading = true
      try {
        const res = await getPendingTaskMonthStats()
        const payload = res && res.data && res.data.months ? res.data : res || {}
        this.nickName = payload.nickName || this.nickName
        this.userName = payload.userName || this.userName
        this.months = Array.isArray(payload.months) ? payload.months : []
        const sum = this.months.reduce((acc, item) => {
          return acc + (Number(item.dischargeCount) || 0) + (Number(item.outpatientCount) || 0)
        }, 0)
        this.total = Number(payload.total) || sum
        this.errorMsg = ''
        this.updateTime = this.formatTime(new Date())
        this.retryCount = 0
        this.clearRetry()
      } catch (error) {
        // åŽç«¯é‡å¯/网络抖动时自动重试,不用等 5 åˆ†é’Ÿ
        this.errorMsg = '统计接口暂不可用,稍后自动重试'
        this.scheduleRetry(20000)
      } finally {
        this.lastLoadAt = Date.now()
        this.loading = false
      }
    },
    scheduleRetry(delay) {
      if (this.retryCount >= 5) {
        return
      }
      this.retryCount += 1
      this.clearRetry()
      this.retryTimer = setTimeout(() => this.load(), delay)
    },
    clearRetry() {
      if (this.retryTimer) {
        clearTimeout(this.retryTimer)
        this.retryTimer = null
      }
    },
    formatTime(date) {
      const pad = value => String(value).padStart(2, '0')
      return pad(date.getHours()) + ':' + pad(date.getMinutes())
    },
    // ç‚¹å‡»æŸä¸ªæœˆçš„æ•°å­—,跳到随访列表查看对应的待随访患者
    goToTasks(row, serviceType) {
      const path =
        serviceType === 3 ? '/followvisit/outpatientService' : '/followvisit/discharge'
      this.$router
        .push({
          path,
          query: {
            month: row.key,
            serviceType: String(serviceType),
            fromPendingTask: '1',
            _t: String(Date.now())
          }
        })
        .catch(() => {})
    }
  }
}
</script>
<style scoped>
.ptb-root {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 1998;
  color: #1f2d3d;
  font-size: 13px;
  line-height: 1.4;
}
/* æ”¶èµ·æ€ */
.ptb-pill {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  padding: 0 16px 0 14px;
  border: none;
  border-radius: 21px;
  background: linear-gradient(135deg, #1890ff 0%, #4f8cff 100%);
  color: #fff;
  font-size: 13px;
  cursor: pointer;
  outline: none;
  box-shadow: 0 8px 20px rgba(24, 144, 255, 0.35);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.ptb-pill:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 24px rgba(24, 144, 255, 0.42);
}
.ptb-pill-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.22);
}
.ptb-pill-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 22px;
  padding: 0 7px;
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.94);
  color: #1890ff;
  font-weight: 600;
}
/* å±•开态 */
.ptb-card {
  width: 364px;
  max-width: calc(100vw - 24px);
  background: #fff;
  border: 1px solid rgba(24, 144, 255, 0.12);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14), 0 2px 6px rgba(15, 23, 42, 0.06);
  overflow: hidden;
  animation: ptb-fade-in 0.18s ease-out;
}
@keyframes ptb-fade-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.ptb-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 12px 11px 14px;
  background: linear-gradient(135deg, #1890ff 0%, #4f8cff 100%);
  color: #fff;
}
.ptb-header-left {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}
.ptb-header-icon {
  width: 16px;
  height: 16px;
  flex: none;
}
.ptb-header-title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.3px;
  white-space: nowrap;
}
.ptb-header-actions {
  display: flex;
  align-items: center;
  gap: 2px;
}
.ptb-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
  transition: background-color 0.15s ease, opacity 0.15s ease;
}
.ptb-icon-btn:hover {
  background: rgba(255, 255, 255, 0.2);
  opacity: 1;
}
.ptb-icon-btn.is-loading {
  animation: ptb-spin 0.9s linear infinite;
}
@keyframes ptb-spin {
  to {
    transform: rotate(360deg);
  }
}
.ptb-body {
  padding: 12px 14px 8px;
}
.ptb-user {
  display: flex;
  align-items: center;
  gap: 9px;
  padding-bottom: 10px;
  border-bottom: 1px solid #f2f4f8;
}
.ptb-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 50%;
  background: linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%);
  color: #1890ff;
  font-size: 14px;
  font-weight: 600;
}
.ptb-user-meta {
  min-width: 0;
  flex: 1;
}
.ptb-user-name {
  overflow: hidden;
  color: #1f2d3d;
  font-size: 14px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ptb-user-scope {
  overflow: hidden;
  color: #8c98a8;
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 4 è¡Œï¼š2026å¹´10月出院随访待处理109个,门诊随访待处理109个 */
.ptb-months {
  margin-top: 4px;
}
.ptb-month-row {
  display: flex;
  align-items: center;
  padding: 6px 6px;
  margin: 0 -6px;
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.5;
  white-space: nowrap;
  transition: background-color 0.15s ease;
}
.ptb-month-row:hover {
  background: #f5faff;
}
.ptb-month-label {
  color: #3f4d5f;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.ptb-month-seg {
  color: #5a6b80;
}
/* æ•°å­—可点击跳转到随访列表 */
.ptb-month-seg.is-clickable {
  cursor: pointer;
  transition: color 0.15s ease;
}
.ptb-month-seg.is-clickable:hover {
  color: #1890ff;
}
.ptb-month-seg.is-clickable:hover .ptb-num {
  text-decoration: underline;
}
.ptb-month-comma {
  color: #b6c0cc;
}
.ptb-num {
  margin: 0 2px;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ptb-num.is-discharge {
  color: #1890ff;
}
.ptb-num.is-outpatient {
  color: #13a8a8;
}
.ptb-error {
  margin-top: 8px;
  padding: 5px 8px;
  border-radius: 6px;
  background: #fff7e6;
  color: #d46b08;
  font-size: 11px;
}
.ptb-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 14px;
  border-top: 1px solid #f2f4f8;
  background: #fafcff;
}
.ptb-update-time {
  color: #a3adba;
  font-size: 11px;
}
.ptb-footer-refresh {
  color: #1890ff;
  font-size: 11px;
  cursor: pointer;
}
.ptb-footer-refresh:hover {
  text-decoration: underline;
}
@media (max-width: 768px) {
  .ptb-root {
    right: 12px;
    bottom: 12px;
  }
  .ptb-card {
    width: calc(100vw - 24px);
  }
  .ptb-month-row {
    white-space: normal;
    flex-wrap: wrap;
  }
}
</style>
src/components/StatisticsCards/index.vue
@@ -17,10 +17,14 @@
        >
          <el-card
            shadow="hover"
            :body-style="item.router ? 'cursor: pointer' : 'cursor: default'"
            :body-style="isClickable(item) ? 'cursor: pointer' : 'cursor: default'"
            :class="getCardClass(item.name)"
            @click.native="handleCardClick(item)"
          >
            <div class="card-content" @click="handleCardClick(item)">
            <div
              class="card-content"
              :style="{ cursor: isClickable(item) ? 'pointer' : 'default' }"
            >
              <div class="card-label">
                <span class="label-text">{{ item.name }}</span>
              </div>
@@ -64,6 +68,11 @@
      default: 0,
    },
    showWarningCondition: {
      type: Boolean,
      default: false,
    },
    // æ˜¯å¦å…è®¸ç‚¹å‡»å¡ç‰‡ï¼ˆç‚¹å‡»åŽç”±çˆ¶ç»„件决定如何筛选下面的列表)
    clickable: {
      type: Boolean,
      default: false,
    },
@@ -135,6 +144,9 @@
      return tooltips[name] || "";
    },
    isClickable(item) {
      return !!(item && item.router) || this.clickable;
    },
    handleCardClick(item) {
      if (item.router) {
        this.$router.push(item.router);
src/store/modules/user.js
@@ -2,6 +2,23 @@
import { getConfig, getConfigKey, listConfig } from "@/api/system/config";
import { getToken, setToken, removeToken } from "@/utils/auth";
/**
 * sys_user_dept é‡ŒåŒä¸€ç§‘室/病区可能存在大量重复行(HIS åŒæ­¥/重复保存导致),
 * æŽ¥å£ä¼šåŽŸæ ·è¿”å›žï¼Œè¿™é‡ŒæŒ‰ç¼–ç åŽ»é‡ï¼Œé¿å…ä¸ªäººä¸­å¿ƒã€å¾…å¤„ç†å¡ç‰‡ç­‰å¤„é‡å¤å±•ç¤º
 */
function uniqueByCode(list) {
  const seen = {};
  return (list || []).filter((item) => {
    if (!item) return false;
    const code =
      item.districtCode || item.deptCode || item.districtId || item.deptId;
    const key = String(code === undefined || code === null ? "" : code);
    if (!key || seen[key]) return false;
    seen[key] = true;
    return true;
  });
}
const user = {
  state: {
    token: getToken(),
@@ -294,8 +311,8 @@
            commit("SET_Id", user.userId);
            commit("SET_hisUserId", user.hisUserId);
            commit("SET_satisfactionCategories", user.satisfactionCategories);
            commit("SET_leavehospitaldistrictcodes", user.belongWards);
            commit("SET_leaveldeptcodes", user.belongDepts);
            commit("SET_leavehospitaldistrictcodes", uniqueByCode(user.belongWards));
            commit("SET_leaveldeptcodes", uniqueByCode(user.belongDepts));
            commit("SET_AVATAR", avatar);
            // æ­£ç¡®è°ƒç”¨æ–¹å¼ï¼šé€šè¿‡ dispatch
            dispatch("GetConfig")
src/views/Satisfaction/configurationmyd/batch.vue
@@ -126,8 +126,8 @@
                <div class="answer-text">
                  <strong>回答:</strong>{{ row.asrtext || "无回答" }}
                </div>
                <div class="matched-text" v-if="row.matchedtext">
                  <strong>解析值:</strong>{{ row.matchedtext }}
                <div class="matched-text">
                  <strong>备注:</strong>{{ row.answerps || "" }}
                </div>
              </div>
            </template>
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue
@@ -25,8 +25,8 @@
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">解析值:</span>
            <span class="value">{{ currentRecord.matchedtext || '无' }}</span>
            <span class="label">备注:</span>
            <span class="value">{{ currentRecord.answerps || '无' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
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;
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue
@@ -12,7 +12,7 @@
        <el-form-item label="满意度类型" prop="satisfactionScope">
          <el-select
            v-model="satisfactionScope"
            placeholder="默认离院"
            placeholder="默认在院"
            clearable
            style="width: 150px"
            @change="handleSatisfactionScopeChange"
@@ -150,33 +150,67 @@
        @selection-change="handleSelectionChange"
        :row-key="getRowKey"
      >
        <!-- ç—…区列:住院满意度统计在院病区,其余统计出院病区 -->
        <!-- ç—…区统计:在院病区 + ç¦»é™¢ç—…区 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 1"
          :label="wardColumnLabel"
          label="在院病区"
          align="center"
          sortable
          key="hospitaldistrictname"
          prop="hospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortHospitalWard"
          min-width="120"
        >
          <template slot-scope="scope">
            {{ scope.row.hospitaldistrictname || "-" }}
          </template>
        </el-table-column>
        <el-table-column
          v-if="queryParams.statisticaltype == 1"
          label="离院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
          :sort-method="sortLeaveWard"
          min-width="120"
        >
          <template slot-scope="scope">
            {{ getWardName(scope.row) || "-" }}
            {{ scope.row.leavehospitaldistrictname || "-" }}
          </template>
        </el-table-column>
        <!-- ç§‘室列 -->
        <!-- ç§‘室统计:在院科室 + ç¦»é™¢ç§‘室 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          label="在院科室"
          align="center"
          key="hospitaldeptname"
          prop="hospitaldeptname"
          :show-overflow-tooltip="true"
          min-width="120"
        >
          <template slot-scope="scope">
            {{ scope.row.hospitaldeptname || "-" }}
          </template>
        </el-table-column>
        <el-table-column
          v-if="queryParams.statisticaltype == 2"
          label="离院科室"
          align="center"
          key="deptname"
          prop="deptname"
          :show-overflow-tooltip="true"
          min-width="120"
        />
        >
          <template slot-scope="scope">
            {{ scope.row.deptname || "-" }}
          </template>
        </el-table-column>
        <el-table-column
          label="出院人次"
@@ -367,14 +401,16 @@
  data() {
    return {
      // æ»¡æ„åº¦ç±»åž‹ï¼šleave=离院满意度(按出院病区统计),hospital=在院满意度(按在院病区统计)
      satisfactionScope: "leave",
      // é»˜è®¤åœ¨é™¢æ»¡æ„åº¦
      satisfactionScope: "hospital",
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        serviceType: [2],
        // é»˜è®¤æœåŠ¡ç±»åž‹ï¼š6=住院满意度(出入院满意度)
        serviceType: [6],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
@@ -480,15 +516,11 @@
  },
  computed: {
    // æ˜¯å¦æŒ‰ã€Œåœ¨é™¢æ»¡æ„åº¦ã€å£å¾„统计:在院患者按在院病区(hospitaldistrictcodes)统计,
    // ç¦»é™¢æ»¡æ„åº¦ä»æŒ‰å‡ºé™¢ç—…区(leavehospitaldistrictcodes)统计
    // æ˜¯å¦æŒ‰ã€Œåœ¨é™¢æ»¡æ„åº¦ã€å£å¾„统计:在院用 hospitaldistrictcodes / hospitaldeptcodes,
    // ç¦»é™¢ç”¨ leavehospitaldistrictcodes / leaveldeptcodes
    // é»˜è®¤åœ¨é™¢ï¼Œæ‰€ä»¥æ¸…空下拉时仍按在院口径处理
    isInHospitalSatisfaction() {
      return this.satisfactionScope === "hospital";
    },
    // ç—…区列标题
    wardColumnLabel() {
      return this.isInHospitalSatisfaction ? "在院病区" : "出院病区";
      return this.satisfactionScope !== "leave";
    },
  },
@@ -554,7 +586,8 @@
          params.endTime = this.queryParams.dateRange[1];
        }
        // å¤„理病区/科室选择
        // å¤„理病区/科室选择:只保留当前统计类型 + å½“前满意度类型对应的字段,
        // é¿å…åœ¨é™¢/离院两套条件同时拼进 SQL
        if (params.statisticaltype == 1) {
          // ç—…区统计
          let wardCodes = params.leavehospitaldistrictcodes || [];
@@ -564,9 +597,11 @@
              (ward) => ward.districtCode
            );
          }
          // ä½é™¢æ»¡æ„åº¦é’ˆå¯¹åœ¨é™¢æ‚£è€…,按在院病区(hospitaldistrictcodes)统计;
          // å‡ºé™¢éšè®¿ç­‰å…¶ä»–服务类型仍按出院病区(leavehospitaldistrictcodes)统计
          delete params.leavehospitaldistrictcodes;
          delete params.hospitaldistrictcodes;
          delete params.deptcodes;
          delete params.leaveldeptcodes;
          delete params.hospitaldeptcodes;
          if (this.isInHospitalSatisfaction) {
            params.hospitaldistrictcodes = wardCodes;
          } else {
@@ -574,20 +609,33 @@
          }
        } else if (params.statisticaltype == 2) {
          // ç§‘室统计
          if (params.deptcodes.includes("all")) {
          let deptCodes = params.deptcodes || [];
          if (deptCodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.deptcodes = params.deptcodes.filter(
              (item) => item !== "all"
            );
            deptCodes = deptCodes.filter((item) => item !== "all");
            // å¦‚果需要传所有科室代码,可以从store中获取
            params.deptcodes = (this.$store.getters.belongDepts || []).map(
            deptCodes = (this.$store.getters.belongDepts || []).map(
              (dept) => dept.deptCode
            );
          }
          delete params.deptcodes;
          delete params.leaveldeptcodes;
          delete params.hospitaldeptcodes;
          delete params.leavehospitaldistrictcodes;
          delete params.hospitaldistrictcodes;
          if (this.isInHospitalSatisfaction) {
            params.hospitaldeptcodes = deptCodes;
          } else {
            params.leaveldeptcodes = deptCodes;
          }
        }
        const response = await getSfStatisticsJoy(params);
        this.userList = this.customSort(response.data) || [];
        // ç§‘室统计按接口返回顺序,病区统计保持原有的中文序号排序
        this.userList =
          this.queryParams.statisticaltype == 1
            ? this.customSort(response.data) || []
            : response.data || [];
        this.total = response.total || 0;
      } catch (error) {
        console.error("获取统计列表失败:", error);
@@ -597,9 +645,22 @@
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = this.getWardName(aRow);
      const b = this.getWardName(bRow);
      return this.compareWardNames(this.getWardName(aRow), this.getWardName(bRow));
    },
    sortHospitalWard(aRow, bRow) {
      return this.compareWardNames(
        aRow && aRow.hospitaldistrictname,
        bRow && bRow.hospitaldistrictname
      );
    },
    sortLeaveWard(aRow, bRow) {
      return this.compareWardNames(
        aRow && aRow.leavehospitaldistrictname,
        bRow && bRow.leavehospitaldistrictname
      );
    },
    // æ¯”较两个病区名称的中文序号;无序号时按字符串比较
    compareWardNames(a, b) {
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
@@ -793,13 +854,14 @@
      this.getList();
    },
    // é‡ç½®æŸ¥è¯¢
    // é‡ç½®æŸ¥è¯¢ï¼ˆæ¢å¤é»˜è®¤ï¼šåœ¨é™¢æ»¡æ„åº¦ + æœåŠ¡ç±»åž‹ 6)
    resetQuery() {
      this.satisfactionScope = "hospital";
      this.queryParams = {
        statisticaltype: 1,
        leavehospitaldistrictcodes: [],
        deptcodes: [],
        serviceType: [2],
        serviceType: [6],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
@@ -827,17 +889,19 @@
      this.multiple = !selection.length;
    },
    // èŽ·å–è¡Œkey
    // èŽ·å–è¡Œkey(按当前统计类型取对应维度编码)
    getRowKey(row) {
      return row.statisticaltype === 1
      return this.queryParams.statisticaltype == 1
        ? row.hospitaldistrictcode || row.leavehospitaldistrictcode
        : row.deptcode;
        : row.hospitaldeptcode || row.deptcode;
    },
    // ç—…区名称:住院满意度接口返回在院病区字段,其余返回出院病区字段
    // å½“前满意度类型对应的病区名称(用于排序/导出/详情标题)
    getWardName(row) {
      if (!row) return "";
      return row.hospitaldistrictname || row.leavehospitaldistrictname || "";
      return this.isInHospitalSatisfaction
        ? row.hospitaldistrictname || ""
        : row.leavehospitaldistrictname || "";
    },
    // æ ¼å¼åŒ–百分比
@@ -874,22 +938,33 @@
          params.endTime = this.queryParams.dateRange[1];
        }
        // è¯¦æƒ…只按当前分组维度过滤,清掉其余维度,避免在院/离院条件互相干扰
        delete params.deptcodes;
        delete params.leaveldeptcodes;
        delete params.hospitaldeptcodes;
        delete params.leavehospitaldistrictcodes;
        delete params.hospitaldistrictcodes;
        if (this.queryParams.statisticaltype == 1) {
          this.topicvalue.name = this.getWardName(row);
          // ä½é™¢æ»¡æ„åº¦æŒ‰åœ¨é™¢ç—…区统计,其余按出院病区统计
          if (this.isInHospitalSatisfaction) {
            delete params.leavehospitaldistrictcodes;
            params.hospitaldistrictcodes = [
              row.hospitaldistrictcode || row.leavehospitaldistrictcode,
            ];
            this.topicvalue.name = row.hospitaldistrictname || "-";
            params.hospitaldistrictcodes = [row.hospitaldistrictcode].filter(
              Boolean
            );
          } else {
            this.topicvalue.name = row.leavehospitaldistrictname || "-";
            params.leavehospitaldistrictcodes = [
              row.leavehospitaldistrictcode,
            ];
            ].filter(Boolean);
          }
        } else {
          this.topicvalue.name = row.deptname;
          params.deptcodes = [row.deptcode];
          if (this.isInHospitalSatisfaction) {
            this.topicvalue.name = row.hospitaldeptname || "-";
            params.hospitaldeptcodes = [row.hospitaldeptcode].filter(Boolean);
          } else {
            this.topicvalue.name = row.deptname || "-";
            params.leaveldeptcodes = [row.deptcode].filter(Boolean);
          }
        }
        const response = await getSfStatisticsJoyInfo(params);
@@ -1012,16 +1087,18 @@
          },
        };
        // æ·»åŠ æ€»æ ‡é¢˜
        // æ·»åŠ æ€»æ ‡é¢˜ï¼ˆä¸¤åˆ—ç»´åº¦ + å…«ä¸ªæŒ‡æ ‡åˆ—,共 10 åˆ—)
        worksheet.mergeCells(1, 1, 1, 10);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `满意度随访统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
        // æ·»åŠ è¡¨å¤´
        // æ·»åŠ è¡¨å¤´ï¼ˆç§‘å®¤ç»Ÿè®¡å¯¼å‡ºåœ¨é™¢ç§‘å®¤+离院科室,病区统计导出在院病区+离院病区)
        const isWardStatistics = this.queryParams.statisticaltype == 1;
        const headers = [
          this.queryParams.statisticaltype == 1 ? this.wardColumnLabel : "科室",
          isWardStatistics ? "在院病区" : "在院科室",
          isWardStatistics ? "离院病区" : "离院科室",
          "出院人次",
          "无需随访人次",
          "应随访人次",
@@ -1041,9 +1118,12 @@
        // æ·»åŠ æ•°æ®è¡Œ
        this.userList.forEach((item) => {
          const dataRow = worksheet.addRow([
            this.queryParams.statisticaltype == 1
              ? this.getWardName(item)
              : item.deptname,
            isWardStatistics
              ? item.hospitaldistrictname || "-"
              : item.hospitaldeptname || "-",
            isWardStatistics
              ? item.leavehospitaldistrictname || "-"
              : item.deptname || "-",
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
@@ -1063,6 +1143,7 @@
        // è®¾ç½®åˆ—宽
        worksheet.columns = [
          { width: 20 },
          { width: 20 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
src/views/followvisit/discharge/index.vue
@@ -5,6 +5,8 @@
      :ycvalue="ycvalue"
      :jgvalue="jgvalue"
      :show-warning-condition="orgname == '省立同德翠苑院区'"
      clickable
      @card-click="handleCardClick"
    />
    <el-row :gutter="20">
      <!--用户数据-->
@@ -255,6 +257,11 @@
          </span>
        </div>
      </el-row>
      <div v-if="cardFilter" class="card-filter-tip">
        å½“前按顶部卡片筛选:<b>{{ cardFilter }}</b>
        <span class="tip-desc">(不影响上方表单条件,点「搜索」可恢复表单条件)</span>
        <el-button type="text" size="mini" @click="clearCardFilter">清除</el-button>
      </div>
      <div class="selected-info">
        å·²é€‰ä¸­
        <span style="color: #409eff; font-weight: bold">{{
@@ -1278,6 +1285,8 @@
      ],
      lastScopeType: 3, // æ‚£è€…范围上次选择的一级类型(1=科室,2=病区,3=全部)
      loading: false,
      // é¡¶éƒ¨å¡ç‰‡ç‚¹å‡»å¸¦æ¥çš„临时筛选(只作用于下面列表,不动表单、不改卡片数字;点「搜索」时清空)
      cardFilter: null,
      cardlist: [
        {
          name: "患者服务总量",
@@ -1506,6 +1515,15 @@
    // ç›‘听路由参数变化
    "$route.query": {
      handler(newQuery, oldQuery) {
        // å³ä¸‹è§’「待处理随访任务」卡片再次点击(当前已在本页)
        if (
          newQuery?.month &&
          newQuery?.fromPendingTask === "1" &&
          this.taskQueryReady
        ) {
          this.applyPendingTaskQuery();
          return;
        }
        if (newQuery?.errtype !== oldQuery?.errtype) {
          console.log(22);
          this.loadData();
@@ -1536,7 +1554,10 @@
    });
    // å…ç™»ç›´è¾¾ï¼šæ ¹æ®ç§‘室/病区编码预填患者范围
    this.applyFollowUpScope();
    if (this.errtype == 1) {
    // å³ä¸‹è§’「待处理随访任务」卡片跳转:按出院年月 + å¾…随访 + å‡ºé™¢æ»¡ 7 å¤©è¿‡æ»¤
    if (this.applyPendingTaskQuery()) {
      // å·²æŒ‰å¡ç‰‡æ¡ä»¶æŸ¥è¯¢ï¼Œä¸å†èµ°é»˜è®¤åˆ†æ”¯
    } else if (this.errtype == 1) {
      this.toleadExport(2);
    } else if (this.errtype == 2) {
      // å¾…随访
@@ -1556,9 +1577,16 @@
    this.getConfigKey("sys.user.initPassword").then((response) => {
      this.initPassword = response.msg;
    });
    // created åˆå§‹åŒ–完成,之后路由 query å˜åŒ–才按卡片条件重新查询
    this.taskQueryReady = true;
  },
  activated() {
    // ä»Žåˆ«çš„æ ‡ç­¾é¡µåˆ‡å›žæ¥æ—¶ï¼Œä»¥è¡¨å•条件为准,丢弃卡片带来的一次性筛选
    this.cardFilter = null;
    this.errtype = this.$route.query.errtype;
    if (this.applyPendingTaskQuery()) {
      return;
    }
    if (this.errtype == 1) {
      this.toleadExport(2);
    } else if (this.errtype == 2) {
@@ -1613,7 +1641,9 @@
      }
      try {
        const response = await getTaskservelist(this.topqueryParams);
        // é¡¶éƒ¨å¡ç‰‡ç‚¹å‡»çš„临时条件只加在本次请求上,不写回表单
        const query = this.applyCardFilter({ ...this.topqueryParams });
        const response = await getTaskservelist(query);
        // âœ… ç¬¬ä¸€æ­¥ï¼šå…ˆé”å®šæ¢å¤çŠ¶æ€
        this.isRestoring = true;
@@ -1630,15 +1660,10 @@
        // âœ… ç¬¬å››æ­¥ï¼šå¤„理其他数据
        if (refresh) {
          this.cardlist[0].value =
            Number(response.rows[0].wxsf) + Number(response.rows[0].xsf) || 0;
          this.cardlist[1].value = response.rows[0].wxsf || 0;
          this.ycvalue = Number(response.rows[0].yc) || 0;
          this.jgvalue = response.rows[0].jg;
          this.cardlist[2].value = response.rows[0].xsf || 0;
          this.cardlist[3].value = response.rows[0].dsf || 0;
          this.cardlist[4].value = response.rows[0].ywc || 0;
          this.yfsvalue = response.rows[0].yfs;
          // é¡¶éƒ¨å¡ç‰‡çš„æ•°å­—不跟「随访状态/异常」走:单独查一次去掉状态条件的统计,
          // å¦åˆ™ç­›äº†ã€Œå¾…随访」后「已完成」永远是 0,看着像算错了
          this.refreshCardCounts(query);
        }
        // âœ… ç¬¬äº”步:处理 preachform ç­‰å…¶ä»–数据
@@ -1875,6 +1900,8 @@
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery(refresh) {
      // ç‚¹ã€Œæœç´¢ã€æ—¶ä»¥è¡¨å•条件为准,清掉卡片点击带来的临时条件
      this.cardFilter = null;
      this.applyDeptWardScope();
      this.clearAllSelection();
      this.topqueryParams.pageNum = 1;
@@ -1897,6 +1924,60 @@
      if (wardCode) scope.push([2, wardCode]);
      this.topqueryParams.scopetype = scope;
      this.lastScopeType = deptCode ? 1 : 2;
    },
    /**
     * å³ä¸‹è§’「待处理随访任务」卡片点击数字跳转过来:
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/5/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * @returns {boolean} æ˜¯å¦å·²æŒ‰å¡ç‰‡æ¡ä»¶å‘起查询
     */
    applyPendingTaskQuery() {
      const query = this.$route.query || {};
      if (!query.month || query.fromPendingTask !== "1") {
        return false;
      }
      // é¡µé¢è¢« keep-alive å¤ç”¨æ—¶ä¼šé‡å¤è§¦å‘ activated,同一次点击只处理一次
      if (this.pendingTaskToken && this.pendingTaskToken === query._t) {
        return false;
      }
      this.pendingTaskToken = query._t;
      const month = String(query.month);
      const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
      this.errtype = undefined;
      const start = month + "-01";
      let end = this.pendingTaskMonthEnd(month);
      // æœˆåˆ 7 å¤©å†…,"今天-7天" ä¼šæ—©äºŽæœˆåˆï¼Œå…œåº•成月初,避免日期区间倒挂
      if (end < start) {
        end = start;
      }
      this.dateRange = [start, end];
      this.topqueryParams.serviceType = serviceType;
      this.topqueryParams.sendstateView = 1;
      this.topqueryParams.sendstate = null;
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯å…¨éƒ¨å¾…随访任务(不区分首轮/复诊),列表这里同步放开
      this.topqueryParams.visitCount = null;
      this.topqueryParams.pageNum = 1;
      this.handleQuery(1);
      return true;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:
     * å½“月只统计出院满 7 å¤©çš„,所以取「今天 - 7 å¤©ã€ï¼›åŽ†å²æœˆä»½å–å½“æœˆæœ€åŽä¸€å¤©
     */
    pendingTaskMonthEnd(month) {
      const parts = String(month).split("-");
      const year = Number(parts[0]);
      const mon = Number(parts[1]);
      const now = new Date();
      let date;
      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
        date = new Date(year, mon - 1, now.getDate() - 7);
      } else {
        date = new Date(year, mon, 0);
      }
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;
    },
    // æ‚£è€…范围处理:从多选级联 scopetype è§£æžç§‘室/病区
    applyDeptWardScope() {
@@ -2340,6 +2421,74 @@
        this.topqueryParams.scopetype = null;
      }
      this.handleQuery(1);
    },
    // é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ç‚¹å‡»ï¼šæŠŠå¯¹åº”筛选条件下带到下面的列表(数值为 0 çš„卡片同样可点)
    // é¡¶éƒ¨å¡ç‰‡ç»Ÿè®¡ï¼šåŽ»æŽ‰ã€Œéšè®¿çŠ¶æ€ / å¼‚常」条件后再统计,
    // è¿™æ ·å¡ç‰‡æ•°å­—只跟日期、科室/病区等条件走,不会因为筛了「待随访」就把「已完成」算成 0
    async refreshCardCounts(baseQuery) {
      const query = {
        ...(baseQuery || this.topqueryParams),
        pageNum: 1,
        pageSize: 1,
        sendstate: null,
        sendstateView: null,
        sendstates: null,
        excep: null,
      };
      try {
        const res = await getTaskservelist(query);
        const row = (res && res.rows && res.rows[0]) || {};
        this.cardlist[0].value = Number(row.wxsf || 0) + Number(row.xsf || 0);
        this.cardlist[1].value = Number(row.wxsf) || 0;
        this.cardlist[2].value = Number(row.xsf) || 0;
        this.cardlist[3].value = Number(row.dsf) || 0;
        this.cardlist[4].value = Number(row.ywc) || 0;
        this.ycvalue = Number(row.yc) || 0;
        this.jgvalue = Number(row.jg) || 0;
      } catch (e) {
        console.warn("刷新顶部卡片统计失败", e);
      }
    },
    // é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ç‚¹å‡»ï¼šåªç»™ä¸‹é¢çš„列表加临时条件,不动上方表单、也不改卡片数字;
    // å¡ç‰‡æ•°å­—只在点「搜索」时按表单条件重算
    handleCardClick(item) {
      this.cardFilter = item && item.name ? item.name : null;
      this.topqueryParams.pageNum = 1;
      // ä¼  0:刷新列表,但不回写顶部卡片数字
      this.getList(0);
    },
    // æŠŠå¡ç‰‡ç‚¹å‡»çš„æ¡ä»¶å åŠ åˆ°æŸ¥è¯¢å‚æ•°ï¼ˆè¿”å›žæ–°å¯¹è±¡ï¼Œä¸ä¿®æ”¹è¡¨å•æœ¬èº«ï¼‰
    applyCardFilter(query) {
      const name = this.cardFilter;
      if (!name) return query;
      // å¡ç‰‡æ•°å­—是「不含随访状态/异常」的总量,所以点卡片时也把表单里的状态条件去掉,
      // åªæŒ‰å¡ç‰‡è‡ªå·±çš„æ¡ä»¶æŸ¥ï¼Œå¦åˆ™ä¼šå‡ºçŽ°ã€Œå¡ç‰‡ 922、列表 3 æ¡ã€
      query.sendstate = null;
      query.sendstateView = null;
      query.sendstates = null;
      query.excep = null;
      if (name === "无需随访") {
        query.sendstateView = 3;
      } else if (name === "待随访") {
        query.sendstateView = 1;
      } else if (name === "已完成") {
        query.sendstateView = 2;
      } else if (name === "需随访") {
        query.sendstates = [1, 2, 3, 5, 6, 7];
      } else if (name === "异常") {
        query.excep = "1";
      } else if (name === "警告") {
        query.excep = "2";
      }
      // ã€Œæ‚£è€…服务总量」= åªåŽ»æŽ‰çŠ¶æ€æ¡ä»¶ï¼Œä¸åŠ åˆ«çš„
      return query;
    },
    // æ¸…除卡片筛选,回到表单条件
    clearCardFilter() {
      this.cardFilter = null;
      this.topqueryParams.pageNum = 1;
      this.getList(0);
    },
    /** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
    handleExport() {
@@ -2882,6 +3031,20 @@
  font-size: 14px;
  color: #666;
}
.card-filter-tip {
  margin: 2px 0 4px;
  padding: 6px 10px;
  font-size: 13px;
  color: #303133;
  background: #f0f7ff;
  border: 1px solid #d6e9ff;
  border-radius: 4px;
}
.card-filter-tip .tip-desc {
  margin: 0 6px;
  font-size: 12px;
  color: #909399;
}
/* ================= æ¸…新高级简约风 ================= */
.app-container {
  background: #f5f7fb;
src/views/followvisit/discharge/outpatientService.vue
@@ -1117,7 +1117,21 @@
      rules: {},
    };
  },
  watch: {},
  watch: {
    // å³ä¸‹è§’「待处理随访任务」卡片再次点击(当前已在本页)
    "$route.query": {
      handler(newQuery) {
        if (
          newQuery?.month &&
          newQuery?.fromPendingTask === "1" &&
          this.taskQueryReady
        ) {
          this.applyPendingTaskQuery();
        }
      },
      immediate: true,
    },
  },
  created() {
    this.serviceState = store.getters.serviceState;
    this.checkboxlist = store.getters.checkboxlist;
@@ -1136,7 +1150,9 @@
        value: dept.districtCode,
      };
    });
    if (this.errtype) {
    if (this.applyPendingTaskQuery()) {
      // å³ä¸‹è§’「待处理随访任务」卡片跳转:已按年月 + å¾…随访条件查询
    } else if (this.errtype) {
      this.toleadExport(2);
    } else {
      this.getList(1);
@@ -1144,8 +1160,13 @@
    this.getConfigKey("sys.user.initPassword").then((response) => {
      this.initPassword = response.msg;
    });
    // created åˆå§‹åŒ–完成,之后路由 query å˜åŒ–才按卡片条件重新查询
    this.taskQueryReady = true;
  },
  activated() {
    if (this.applyPendingTaskQuery()) {
      return;
    }
    this.getList(1);
  },
  methods: {
@@ -1350,6 +1371,60 @@
      };
      this.resetForm("form");
    },
    /**
     * å³ä¸‹è§’「待处理随访任务」卡片点击数字跳转过来:
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/5/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * @returns {boolean} æ˜¯å¦å·²æŒ‰å¡ç‰‡æ¡ä»¶å‘起查询
     */
    applyPendingTaskQuery() {
      const query = this.$route.query || {};
      if (!query.month || query.fromPendingTask !== "1") {
        return false;
      }
      // é¡µé¢è¢« keep-alive å¤ç”¨æ—¶ä¼šé‡å¤è§¦å‘ activated,同一次点击只处理一次
      if (this.pendingTaskToken && this.pendingTaskToken === query._t) {
        return false;
      }
      this.pendingTaskToken = query._t;
      const month = String(query.month);
      const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
      this.errtype = undefined;
      const start = month + "-01";
      let end = this.pendingTaskMonthEnd(month);
      // æœˆåˆ 7 å¤©å†…,"今天-7天" ä¼šæ—©äºŽæœˆåˆï¼Œå…œåº•成月初,避免日期区间倒挂
      if (end < start) {
        end = start;
      }
      this.dateRange = [start, end];
      this.topqueryParams.serviceType = serviceType;
      this.topqueryParams.sendstateView = 1;
      this.topqueryParams.sendstate = null;
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯å…¨éƒ¨å¾…随访任务(不区分首轮/复诊),列表这里同步放开
      this.topqueryParams.visitCount = null;
      this.topqueryParams.pageNum = 1;
      this.handleQuery(1);
      return true;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:
     * å½“月只统计出院满 7 å¤©çš„,所以取「今天 - 7 å¤©ã€ï¼›åŽ†å²æœˆä»½å–å½“æœˆæœ€åŽä¸€å¤©
     */
    pendingTaskMonthEnd(month) {
      const parts = String(month).split("-");
      const year = Number(parts[0]);
      const mon = Number(parts[1]);
      const now = new Date();
      let date;
      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
        date = new Date(year, mon - 1, now.getDate() - 7);
      } else {
        date = new Date(year, mon, 0);
      }
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery(refresh) {
      if (this.topqueryParams.searchscope == 3) {
src/views/system/user/profile/index.vue
@@ -25,7 +25,7 @@
              </li>
              <!-- ===== æ‰€å±žç—…区 ===== -->
              <li class="list-group-item" v-if="user.belongWards && user.belongWards.length">
              <li class="list-group-item" v-if="uniqueWards && uniqueWards.length">
                <svg-icon icon-class="tree" />所属病区
                <div class="pull-right">
                  <!-- å—华附一:可编辑模式 -->
@@ -44,7 +44,7 @@
                  <!-- å—华附一:只读模式 -->
                  <template v-else-if="isNanhua">
                    <el-tag
                      v-for="ward in user.belongWards"
                      v-for="ward in uniqueWards"
                      :key="ward.districtId"
                      size="small"
                      class="ward-tag"
@@ -60,7 +60,7 @@
                      popper-class="ward-popover"
                    >
                      <div class="popover-content">
                        <p v-for="ward in user.belongWards" :key="ward.districtId" class="popover-item">
                        <p v-for="ward in uniqueWards" :key="ward.districtId" class="popover-item">
                          {{ ward.districtName }}
                        </p>
                      </div>
@@ -72,11 +72,11 @@
                          class="ward-tag"
                        >{{ ward.districtName }}</el-tag>
                        <el-tag
                          v-if="user.belongWards.length > maxTagCount"
                          v-if="uniqueWards.length > maxTagCount"
                          size="small"
                          class="more-tag"
                          @click="showAllWardsPopover = !showAllWardsPopover"
                        >+{{ user.belongWards.length - maxTagCount }}</el-tag>
                        >+{{ uniqueWards.length - maxTagCount }}</el-tag>
                      </div>
                    </el-popover>
                  </template>
@@ -88,7 +88,7 @@
              </li>
              <!-- ===== æ‰€å±žç§‘室 ===== -->
              <li class="list-group-item" v-if="user.belongDepts && user.belongDepts.length">
              <li class="list-group-item" v-if="uniqueDepts && uniqueDepts.length">
                <svg-icon icon-class="tree" />所属科室
                <div class="pull-right">
                  <!-- å—华附一:可编辑模式 -->
@@ -107,7 +107,7 @@
                  <!-- å—华附一:只读模式 -->
                  <template v-else-if="isNanhua">
                    <el-tag
                      v-for="dept in user.belongDepts"
                      v-for="dept in uniqueDepts"
                      :key="dept.deptId"
                      size="small"
                      class="dept-tag"
@@ -123,7 +123,7 @@
                      popper-class="dept-popover"
                    >
                      <div class="popover-content">
                        <p v-for="dept in user.belongDepts" :key="dept.deptId" class="popover-item">
                        <p v-for="dept in uniqueDepts" :key="dept.deptId" class="popover-item">
                          {{ dept.deptName }}
                        </p>
                      </div>
@@ -135,11 +135,11 @@
                          class="dept-tag"
                        >{{ dept.deptName }}</el-tag>
                        <el-tag
                          v-if="user.belongDepts.length > maxTagCount"
                          v-if="uniqueDepts.length > maxTagCount"
                          size="small"
                          class="more-tag"
                          @click="showAllDeptsPopover = !showAllDeptsPopover"
                        >+{{ user.belongDepts.length - maxTagCount }}</el-tag>
                        >+{{ uniqueDepts.length - maxTagCount }}</el-tag>
                      </div>
                    </el-popover>
                  </template>
@@ -223,15 +223,22 @@
    };
  },
  computed: {
    // sys_user_dept é‡ŒåŒä¸€ç—…区可能有几十条重复行,接口原样返回,这里按编码去重
    uniqueWards() {
      return this.uniqueByCode(this.user.belongWards);
    },
    uniqueDepts() {
      return this.uniqueByCode(this.user.belongDepts);
    },
    // è®¡ç®—要显示的病区标签(前maxTagCount个)
    displayWards() {
      if (!this.user.belongWards) return [];
      return this.user.belongWards.slice(0, this.maxTagCount);
      if (!this.uniqueWards) return [];
      return this.uniqueWards.slice(0, this.maxTagCount);
    },
    // è®¡ç®—要显示的科室标签(前maxTagCount个)
    displayDepts() {
      if (!this.user.belongDepts) return [];
      return this.user.belongDepts.slice(0, this.maxTagCount);
      if (!this.uniqueDepts) return [];
      return this.uniqueDepts.slice(0, this.maxTagCount);
    },
    // å—华大学附属第一医院 å…è®¸ç¼–辑科室/病区
    isNanhua() {
@@ -245,6 +252,19 @@
    }
  },
  methods: {
    // æŒ‰ç§‘室/病区编码去重(sys_user_dept å­˜åœ¨é‡å¤è¡Œï¼‰
    uniqueByCode(list) {
      const seen = {};
      return (list || []).filter((item) => {
        if (!item) return false;
        const code =
          item.districtCode || item.deptCode || item.districtId || item.deptId;
        const key = String(code === undefined || code === null ? "" : code);
        if (!key || seen[key]) return false;
        seen[key] = true;
        return true;
      });
    },
    getUser() {
      getUserProfile().then((response) => {
        this.user = response.data;
tmp-probe4/Probe17.class
Binary files differ
tmp-probe4/Probe17.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
public class Probe17 {
    static final String URL = "jdbc:mysql://haiershi.a1.luyouxia.net:23844/smartor_sltd"
            + "?useUnicode=true&characterEncoding=utf8&useSSL=false"
            + "&serverTimezone=GMT%2B8&connectTimeout=15000&socketTimeout=90000";
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection c = DriverManager.getConnection(URL, "smartor", "Smartor.2023");
             PrintWriter out = new PrintWriter(new OutputStreamWriter(
                     new FileOutputStream("probe17.txt"), StandardCharsets.UTF_8))) {
            run(c, out, "select config_key, config_value from sys_config where config_key='sys.authority.campus.all'");
            run(c, out, "select d.dept_code, d.dept_name, d.campusid, count(*) dup from sys_dept d "
                    + "inner join sys_user_dept ud on d.dept_code = ud.dept_code "
                    + "where d.del_flag=0 and ud.del_flag=0 and d.orgid='20001001' and ud.orgid='20001001' "
                    + "and ud.user_id=2609 and d.dept_type='2' group by d.dept_code, d.dept_name, d.campusid");
            run(c, out, "select d.dept_code, d.dept_name, d.campusid from sys_dept d "
                    + "inner join sys_user_dept ud on d.dept_code = ud.dept_code "
                    + "where d.del_flag=0 and ud.del_flag=0 and d.orgid='20001001' and ud.orgid='20001001' "
                    + "and d.campusid='30001002' and ud.user_id=2609 and d.dept_type='2' group by d.dept_code, d.dept_name, d.campusid");
            run(c, out, "select leavehospitaldistrictcode, leavehospitaldistrictname, count(*) cnt from service_subtask "
                    + "where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) "
                    + "and endtime>='2026-10-01' and endtime<'2026-11-01' and endtime <= DATE_SUB(CURDATE(), INTERVAL 7 DAY) "
                    + "and leavehospitaldistrictcode in ('40003047','40003048') group by leavehospitaldistrictcode, leavehospitaldistrictname");
            out.flush();
        }
        System.out.println("done");
    }
    static void run(Connection c, PrintWriter out, String sql) {
        out.println("\n--- " + sql);
        try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(sql)) {
            ResultSetMetaData md = rs.getMetaData();
            int n = md.getColumnCount();
            StringBuilder head = new StringBuilder();
            for (int i = 1; i <= n; i++) head.append(md.getColumnLabel(i)).append(i == n ? "" : " | ");
            out.println(head);
            int rows = 0;
            while (rs.next() && rows < 40) {
                StringBuilder line = new StringBuilder();
                for (int i = 1; i <= n; i++) line.append(rs.getString(i)).append(i == n ? "" : " | ");
                out.println(line);
                rows++;
            }
        } catch (Exception e) {
            out.println("SQL ERROR: " + e.getMessage());
        }
    }
}
tmp-probe4/Probe18.class
Binary files differ
tmp-probe4/Probe18.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
public class Probe18 {
    static final String URL = "jdbc:mysql://haiershi.a1.luyouxia.net:23844/smartor_sltd"
            + "?useUnicode=true&characterEncoding=utf8&useSSL=false"
            + "&serverTimezone=GMT%2B8&connectTimeout=15000&socketTimeout=90000";
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection c = DriverManager.getConnection(URL, "smartor", "Smartor.2023");
             PrintWriter out = new PrintWriter(new OutputStreamWriter(
                     new FileOutputStream("probe18.txt"), StandardCharsets.UTF_8))) {
            String base = "from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) "
                    + "and leavehospitaldistrictcode='40003047' and endtime>='2026-10-01' ";
            run(c, out, "select "
                    + "(select count(*) " + base + "and endtime <= DATE_SUB(CURDATE(), INTERVAL 7 DAY)) old_cut, "
                    + "(select count(*) " + base + "and endtime < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 7 DAY), INTERVAL 1 DAY)) new_cut, "
                    + "(select count(*) " + base + "and endtime >= '2026-10-21' and endtime < '2026-10-22') on_1021");
            run(c, out, "select DATE(endtime) d, count(*) cnt " + base + "and endtime < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 7 DAY), INTERVAL 1 DAY) group by DATE(endtime) order by d desc limit 6");
            out.flush();
        }
        System.out.println("done");
    }
    static void run(Connection c, PrintWriter out, String sql) {
        out.println("\n--- " + sql);
        try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(sql)) {
            ResultSetMetaData md = rs.getMetaData();
            int n = md.getColumnCount();
            StringBuilder head = new StringBuilder();
            for (int i = 1; i <= n; i++) head.append(md.getColumnLabel(i)).append(i == n ? "" : " | ");
            out.println(head);
            int rows = 0;
            while (rs.next() && rows < 20) {
                StringBuilder line = new StringBuilder();
                for (int i = 1; i <= n; i++) line.append(rs.getString(i)).append(i == n ? "" : " | ");
                out.println(line);
                rows++;
            }
        } catch (Exception e) {
            out.println("SQL ERROR: " + e.getMessage());
        }
    }
}
tmp-probe4/probe17.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
--- select config_key, config_value from sys_config where config_key='sys.authority.campus.all'
config_key | config_value
--- select d.dept_code, d.dept_name, d.campusid, count(*) dup from sys_dept d inner join sys_user_dept ud on d.dept_code = ud.dept_code where d.del_flag=0 and ud.del_flag=0 and d.orgid='20001001' and ud.orgid='20001001' and ud.user_id=2609 and d.dept_type='2' group by d.dept_code, d.dept_name, d.campusid
dept_code | dept_name | campusid | dup
40003047 | 2-6东病区(骨伤中心一) | 30001002 | 40
--- select d.dept_code, d.dept_name, d.campusid from sys_dept d inner join sys_user_dept ud on d.dept_code = ud.dept_code where d.del_flag=0 and ud.del_flag=0 and d.orgid='20001001' and ud.orgid='20001001' and d.campusid='30001002' and ud.user_id=2609 and d.dept_type='2' group by d.dept_code, d.dept_name, d.campusid
dept_code | dept_name | campusid
40003047 | 2-6东病区(骨伤中心一) | 30001002
--- select leavehospitaldistrictcode, leavehospitaldistrictname, count(*) cnt from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) and endtime>='2026-10-01' and endtime<'2026-11-01' and endtime <= DATE_SUB(CURDATE(), INTERVAL 7 DAY) and leavehospitaldistrictcode in ('40003047','40003048') group by leavehospitaldistrictcode, leavehospitaldistrictname
leavehospitaldistrictcode | leavehospitaldistrictname | cnt
40003047 | 2-6东病区(骨伤中心一) | 90
40003048 | 2-6西病区(骨伤中心二) | 25
tmp-probe4/probe18.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
--- select (select count(*) from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) and leavehospitaldistrictcode='40003047' and endtime>='2026-10-01' and endtime <= DATE_SUB(CURDATE(), INTERVAL 7 DAY)) old_cut, (select count(*) from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) and leavehospitaldistrictcode='40003047' and endtime>='2026-10-01' and endtime < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 7 DAY), INTERVAL 1 DAY)) new_cut, (select count(*) from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) and leavehospitaldistrictcode='40003047' and endtime>='2026-10-01' and endtime >= '2026-10-21' and endtime < '2026-10-22') on_1021
old_cut | new_cut | on_1021
90 | 99 | 9
--- select DATE(endtime) d, count(*) cnt from service_subtask where del_flag='0' and service_type='2' and sendstate in (1,2,3,5,7) and leavehospitaldistrictcode='40003047' and endtime>='2026-10-01' and endtime < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 7 DAY), INTERVAL 1 DAY) group by DATE(endtime) order by d desc limit 6
d | cnt
2026-10-21 | 9
2026-10-20 | 7
2026-10-19 | 10
2026-10-18 | 2
2026-10-17 | 3
2026-10-16 | 6
tmp-probe9/Probe42.class
Binary files differ
tmp-probe9/Probe42.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
public class Probe42 {
    static final String URL = "jdbc:mysql://haiershi.a1.luyouxia.net:23844/smartor_sltd"
            + "?useUnicode=true&characterEncoding=utf8&useSSL=false"
            + "&serverTimezone=GMT%2B8&connectTimeout=15000&socketTimeout=90000";
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection c = DriverManager.getConnection(URL, "smartor", "Smartor.2023");
             PrintWriter out = new PrintWriter(new OutputStreamWriter(
                     new FileOutputStream("probe42.txt"), StandardCharsets.UTF_8))) {
            String base = "from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047' ";
            run(c, out, "select count(*) db_total " + base);
            run(c, out, "select service_type, visit_count, count(*) cnt " + base + "group by service_type, visit_count order by service_type, visit_count");
            run(c, out, "select count(*) page_like " + base + "and service_type='2' and visit_count=1");
            run(c, out, "select count(*) page_like_no_vc " + base + "and service_type='2'");
            run(c, out, "select count(*) all_status " + base + "and service_type='2' and visit_count=1 and sendstate in (1,2,3,4,5,6,7)");
            out.flush();
        }
        System.out.println("done");
    }
    static void run(Connection c, PrintWriter out, String sql) {
        out.println("\n--- " + sql);
        try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(sql)) {
            ResultSetMetaData md = rs.getMetaData();
            int n = md.getColumnCount();
            StringBuilder head = new StringBuilder();
            for (int i = 1; i <= n; i++) head.append(md.getColumnLabel(i)).append(i == n ? "" : " | ");
            out.println(head);
            int rows = 0;
            while (rs.next() && rows < 20) {
                StringBuilder line = new StringBuilder();
                for (int i = 1; i <= n; i++) line.append(rs.getString(i)).append(i == n ? "" : " | ");
                out.println(line);
                rows++;
            }
        } catch (Exception e) {
            out.println("SQL ERROR: " + e.getMessage());
        }
    }
}
tmp-probe9/probe42.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
--- select count(*) db_total from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047'
db_total
119
--- select service_type, visit_count, count(*) cnt from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047' group by service_type, visit_count order by service_type, visit_count
service_type | visit_count | cnt
2 | 1 | 110
4 | 1 | 9
--- select count(*) page_like from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047' and service_type='2' and visit_count=1
page_like
110
--- select count(*) page_like_no_vc from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047' and service_type='2'
page_like_no_vc
110
--- select count(*) all_status from service_subtask where del_flag='0' and endtime >= '2026-10-01' and endtime < '2026-10-31' and leavehospitaldistrictcode='40003047' and service_type='2' and visit_count=1 and sendstate in (1,2,3,4,5,6,7)
all_status
110
tmp-svy4/Probe41.class
Binary files differ
tmp-svy4/Probe41.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
public class Probe41 {
    static final String URL = "jdbc:mysql://haiershi.a1.luyouxia.net:23844/smartor_sltd"
            + "?useUnicode=true&characterEncoding=utf8&useSSL=false"
            + "&serverTimezone=GMT%2B8&connectTimeout=15000&socketTimeout=90000";
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        try (Connection c = DriverManager.getConnection(URL, "smartor", "Smartor.2023");
             PrintWriter out = new PrintWriter(new OutputStreamWriter(
                     new FileOutputStream("probe41.txt"), StandardCharsets.UTF_8))) {
            run(c, out, "select d.questiontext, sum(case when d.asrtext is null or trim(d.asrtext)='' then 1 else 0 end) blank, count(*) total "
                    + "from service_subtask s join service_subtask_detail d on d.sub_id=s.id "
                    + "where s.del_flag='0' and d.del_flag='0' and s.service_type in ('6','14') and s.sendstate=6 "
                    + "and s.finishtime >= '2026-07-29' and s.finishtime < '2026-10-29' "
                    + "and d.id = (select max(d2.id) from service_subtask_detail d2 where d2.sub_id=d.sub_id and d2.questiontext <=> d.questiontext and d2.del_flag='0') "
                    + "group by d.questiontext order by blank desc");
            out.flush();
        }
        System.out.println("done");
    }
    static void run(Connection c, PrintWriter out, String sql) {
        out.println("\n--- " + sql);
        try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(sql)) {
            ResultSetMetaData md = rs.getMetaData();
            int n = md.getColumnCount();
            StringBuilder head = new StringBuilder();
            for (int i = 1; i <= n; i++) head.append(md.getColumnLabel(i)).append(i == n ? "" : " | ");
            out.println(head);
            int rows = 0;
            while (rs.next() && rows < 30) {
                StringBuilder line = new StringBuilder();
                for (int i = 1; i <= n; i++) line.append(rs.getString(i)).append(i == n ? "" : " | ");
                out.println(line);
                rows++;
            }
        } catch (Exception e) {
            out.println("SQL ERROR: " + e.getMessage());
        }
    }
}
tmp-svy4/probe41.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
--- select d.questiontext, sum(case when d.asrtext is null or trim(d.asrtext)='' then 1 else 0 end) blank, count(*) total from service_subtask s join service_subtask_detail d on d.sub_id=s.id where s.del_flag='0' and d.del_flag='0' and s.service_type in ('6','14') and s.sendstate=6 and s.finishtime >= '2026-07-29' and s.finishtime < '2026-10-29' and d.id = (select max(d2.id) from service_subtask_detail d2 where d2.sub_id=d.sub_id and d2.questiontext <=> d.questiontext and d2.del_flag='0') group by d.questiontext order by blank desc
questiontext | blank | total
请您留下宝贵的意见/建议 | 6 | 25
您对我院投诉满处理满不满意? | 1 | 8
窗口/后勤/保安人员服务 | 0 | 17
您对我院知情告知满不满意? | 0 | 8
您对便民服务是否满意 | 0 | 17
您对我院服务效率满不满意? | 0 | 8
指示牌、导医牌清楚程度 | 0 | 17
您对我院伙食情况满不满意? | 0 | 8
门诊环境卫生/声音(厕所、环境卫生、噪音等) | 0 | 17
您对我院整体环境满不满意? | 0 | 8
挂号预约/专家信息获取 | 0 | 17
看病期间,您的抱怨和不满能否及时得到回应? | 0 | 17
挂号/就诊/检验/检查等环节等候时间 | 0 | 17
总体满意度 | 0 | 17
您是否向亲友推荐这家医院 | 0 | 8
您对医护诊疗过程是否满意*(看诊时间/健康宣教/手卫生/隐私保护) | 0 | 17
您是否会向亲友推荐这家医院 | 0 | 17
医生服务
 | 0 | 17
护士服务 | 0 | 17
您对我院工作人员的服务质量满不满意? | 0 | 8
检查人员服务(B超/心电图/CT/磁共振/X线/内镜等) | 0 | 17
您对我院工作人员的服务态度满不满意? | 0 | 8
药房人员服务 | 0 | 17
您对我院隐私保护满不满意? | 0 | 8
¹Ç¿ÆÖÐÐÄÒ»²¡Çø_³öÔºÊý¾Ýͳ¼Æ_20260430-20261023.xlsx
Binary files differ