WXL (wul)
5 天以前 39c8b288ec8294e026459dfb1955f0bbc49ebb52
src/views/patient/archives/maternalHealth/PSQIScoreTable.vue
@@ -1,37 +1,386 @@
<template>
  <div>
    <el-table :data="items" border size="small">
      <el-table-column prop="item" label="条目" width="280" />
      <el-table-column label="评分" align="center" width="120">
        <template slot-scope="scope">
          <el-select v-if="editing" v-model="scope.row.score" size="small">
            <el-option v-for="s in [0,1,2,3]" :key="s" :label="s" :value="s" />
          </el-select>
          <span v-else>{{ scope.row.score }}</span>
        </template>
  <div class="scale-wrap">
    <div class="scale-title">匹兹堡睡眠质量指数量表(PSQI)</div>
    <!-- 纸质表表头:患者基本信息 -->
    <el-descriptions :column="3" border size="small" class="scale-head">
      <el-descriptions-item label="姓名">{{ patient.name || "-" }}</el-descriptions-item>
      <el-descriptions-item label="性别">{{ patient.sex || "-" }}</el-descriptions-item>
      <el-descriptions-item label="年龄">{{ patient.age || "-" }}</el-descriptions-item>
      <el-descriptions-item label="科室">{{ patient.dept || "-" }}</el-descriptions-item>
      <el-descriptions-item label="临床诊断">{{ patient.diagnosis || "-" }}</el-descriptions-item>
      <el-descriptions-item label="评估日期">
        <el-date-picker v-if="editing" v-model="local.date" value-format="yyyy-MM-dd" size="mini" style="width:160px" placeholder="选择日期" />
        <span v-else>{{ local.date || "-" }}</span>
      </el-descriptions-item>
    </el-descriptions>
    <div class="scale-tip">指导语:下面一些问题与您近 1 个月的睡眠状况有关,请选择或填写最符合您近 1 个月实际情况的答案。</div>
    <div class="q-list">
      <div class="q-item">
        <div class="q-text">1. 近1个月,晚上上床睡觉通常在 ____ 点钟</div>
        <div class="q-body">
          <el-time-picker v-if="editing" v-model="local.q1BedTime" value-format="HH:mm" format="HH:mm" size="small" placeholder="选择时间" style="width:130px" />
          <span v-else class="q-value">{{ local.q1BedTime || "—" }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">2. 近1个月,从上床到入睡通常需要 ____ 分钟</div>
        <div class="q-body">
          <el-radio-group v-if="editing" v-model="local.q2Latency">
            <el-radio :label="0">≤15分钟</el-radio>
            <el-radio :label="1">16~30分钟</el-radio>
            <el-radio :label="2">31~60分钟</el-radio>
            <el-radio :label="3">≥61分钟</el-radio>
          </el-radio-group>
          <span v-else class="q-value">{{ latencyLabel }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">3. 近1个月,通常早上 ____ 点起床</div>
        <div class="q-body">
          <el-time-picker v-if="editing" v-model="local.q3WakeTime" value-format="HH:mm" format="HH:mm" size="small" placeholder="选择时间" style="width:130px" />
          <span v-else class="q-value">{{ local.q3WakeTime || "—" }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">4. 近1个月,每晚实际睡眠时间 ____ 小时</div>
        <div class="q-body">
          <el-input-number v-if="editing" v-model="local.q4Hours" :min="0" :max="24" :step="0.5" :precision="1" size="small" controls-position="right" style="width:130px" />
          <span v-else class="q-value">{{ local.q4Hours === null || local.q4Hours === "" ? "—" : local.q4Hours + " 小时" }}</span>
          <span class="q-hint">(&gt;7h 记0分,6~7h 记1分,5~6h 记2分,&lt;5h 记3分)</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">5. 近1个月,因下列情况影响睡眠而烦恼</div>
        <div class="q-body">
          <el-table :data="q5Items" border size="small" class="q-sub-table">
            <el-table-column prop="label" label="情况" min-width="220" />
            <el-table-column label="无" align="center" width="90">
              <template slot-scope="scope"><el-radio v-if="editing" v-model="local[scope.row.key]" :label="0">&nbsp;</el-radio><span v-else>{{ local[scope.row.key] === 0 ? "✓" : "" }}</span></template>
            </el-table-column>
            <el-table-column label="不足1次/周" align="center" width="110">
              <template slot-scope="scope"><el-radio v-if="editing" v-model="local[scope.row.key]" :label="1">&nbsp;</el-radio><span v-else>{{ local[scope.row.key] === 1 ? "✓" : "" }}</span></template>
            </el-table-column>
            <el-table-column label="1~2次/周" align="center" width="110">
              <template slot-scope="scope"><el-radio v-if="editing" v-model="local[scope.row.key]" :label="2">&nbsp;</el-radio><span v-else>{{ local[scope.row.key] === 2 ? "✓" : "" }}</span></template>
            </el-table-column>
            <el-table-column label="≥3次/周" align="center" width="100">
              <template slot-scope="scope"><el-radio v-if="editing" v-model="local[scope.row.key]" :label="3">&nbsp;</el-radio><span v-else>{{ local[scope.row.key] === 3 ? "✓" : "" }}</span></template>
            </el-table-column>
            <el-table-column label="得分" align="center" width="70">
              <template slot-scope="scope">{{ local[scope.row.key] || 0 }}</template>
      </el-table-column>
    </el-table>
    <div style="margin-top:10px;font-size:14px">PSQI总分:{{ totalScore }} &nbsp;&nbsp; 评价:{{ evaluation }}</div>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">6. 近1个月,总的来说您认为自己的睡眠质量</div>
        <div class="q-body">
          <el-radio-group v-if="editing" v-model="local.q6Quality">
            <el-radio :label="0">很好</el-radio>
            <el-radio :label="1">较好</el-radio>
            <el-radio :label="2">较差</el-radio>
            <el-radio :label="3">很差</el-radio>
          </el-radio-group>
          <span v-else class="q-value">{{ qualityLabel }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">7. 近1个月,您使用催眠药物的情况</div>
        <div class="q-body">
          <el-radio-group v-if="editing" v-model="local.q7Medication">
            <el-radio :label="0">无</el-radio>
            <el-radio :label="1">少于1次/周</el-radio>
            <el-radio :label="2">1~2次/周</el-radio>
            <el-radio :label="3">3次或以上/周</el-radio>
          </el-radio-group>
          <span v-else class="q-value">{{ medicationLabel }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">8. 近1个月,您常感到困倦吗</div>
        <div class="q-body">
          <el-radio-group v-if="editing" v-model="local.q8Sleepy">
            <el-radio :label="0">没有</el-radio>
            <el-radio :label="1">偶尔(少于1次/周)</el-radio>
            <el-radio :label="2">有时(1~2次/周)</el-radio>
            <el-radio :label="3">经常(3次或以上/周)</el-radio>
          </el-radio-group>
          <span v-else class="q-value">{{ freqLabel(local.q8Sleepy) }}</span>
        </div>
      </div>
      <div class="q-item">
        <div class="q-text">9. 近1个月,您做事情的精力不足吗</div>
        <div class="q-body">
          <el-radio-group v-if="editing" v-model="local.q9Energy">
            <el-radio :label="0">没有</el-radio>
            <el-radio :label="1">偶尔(少于1次/周)</el-radio>
            <el-radio :label="2">有时(1~2次/周)</el-radio>
            <el-radio :label="3">经常(3次或以上/周)</el-radio>
          </el-radio-group>
          <span v-else class="q-value">{{ freqLabel(local.q9Energy) }}</span>
        </div>
      </div>
    </div>
    <!-- 计分表 -->
    <div class="sec-title">计分表</div>
    <el-table :data="components" border size="small" class="score-table">
      <el-table-column prop="name" label="成分" width="70" align="center" />
      <el-table-column prop="title" label="内容" min-width="240" />
      <el-table-column prop="score" label="评分" width="70" align="center" />
    </el-table>
    <!-- 结果 -->
    <el-descriptions :column="2" border size="small" class="scale-result">
      <el-descriptions-item label="PSQI 总分"><b :class="totalClass">{{ totalScore }}</b> 分</el-descriptions-item>
      <el-descriptions-item label="评价">{{ evaluation }}</el-descriptions-item>
      <el-descriptions-item label="睡眠效率">
        <span v-if="efficiency !== null">{{ efficiency }}%(在床 {{ fmtHours(hoursInBed) }} 小时 / 实际睡眠 {{ sleepHours }} 小时)</span>
        <span v-else class="muted">填写条目 1、3、4 后自动计算</span>
      </el-descriptions-item>
      <el-descriptions-item label="评分人员">
        <el-input v-if="editing" v-model="local.evaluator" size="mini" style="width:160px" placeholder="请输入" />
        <span v-else>{{ local.evaluator || "-" }}</span>
      </el-descriptions-item>
    </el-descriptions>
  </div>
</template>
<script>
const FREQ_OPTIONS = [
  { v: 0, l: "无" },
  { v: 1, l: "不足1次/周" },
  { v: 2, l: "1~2次/周" },
  { v: 3, l: "≥3次/周" },
];
const LATENCY_OPTIONS = [
  { v: 0, l: "≤15分钟" },
  { v: 1, l: "16~30分钟" },
  { v: 2, l: "31~60分钟" },
  { v: 3, l: "≥61分钟" },
];
const QUALITY_OPTIONS = [
  { v: 0, l: "很好" },
  { v: 1, l: "较好" },
  { v: 2, l: "较差" },
  { v: 3, l: "很差" },
];
const MEDICATION_OPTIONS = [
  { v: 0, l: "无" },
  { v: 1, l: "少于1次/周" },
  { v: 2, l: "1~2次/周" },
  { v: 3, l: "3次或以上/周" },
];
const Q5_ITEMS = [
  { key: "q5a", label: "a. 入睡困难(30分钟内不能入睡)" },
  { key: "q5b", label: "b. 夜间易醒或早醒" },
  { key: "q5c", label: "c. 夜间去厕所" },
  { key: "q5d", label: "d. 呼吸不畅" },
  { key: "q5e", label: "e. 咳嗽或鼾声高" },
  { key: "q5f", label: "f. 感觉冷" },
  { key: "q5g", label: "g. 感觉热" },
  { key: "q5h", label: "h. 做噩梦" },
  { key: "q5i", label: "i. 疼痛不适" },
  { key: "q5j", label: "j. 其它影响睡眠的事情" },
];
// 条目4 手填小时数 → 睡眠时长成分 C 的分档阈值
const SCORE_C_RULES = [
  { test: (h) => h > 7, score: 0 },
  { test: (h) => h > 6, score: 1 },
  { test: (h) => h >= 5, score: 2 },
  { test: () => true, score: 3 },
];
const FIELDS = [
  "date", "q1BedTime", "q2Latency", "q3WakeTime", "q4Hours",
  "q5a", "q5b", "q5c", "q5d", "q5e", "q5f", "q5g", "q5h", "q5i", "q5j",
  "q6Quality", "q7Medication", "q8Sleepy", "q9Energy",
];
export default {
  name:"PSQIScoreTable",
  props:{ value:{ type:Object, default:()=>({}) }, editing:Boolean },
  computed:{
    items(){
      const names=["主观睡眠质量","入睡时间(分钟)","睡眠时长(小时)","睡眠效率(%)","睡眠障碍","催眠药物使用","日间功能障碍"];
      return names.map((n,i)=>({ item:n, key:'psqi_'+i, score:this.value['psqi_'+i]||0 }));
  props: {
    value: { type: Object, default: () => ({}) },
    patient: { type: Object, default: () => ({}) },
    editing: Boolean,
    },
    totalScore(){ return this.items.reduce((s,i)=>s+i.score,0); },
    evaluation(){ const t=this.totalScore; return t<=5?"睡眠质量良好":t<=10?"睡眠质量一般":t<=15?"睡眠质量较差":"睡眠质量很差"; },
  data() {
    return { q5Items: Q5_ITEMS, local: this.normalize(this.value) };
  },
  computed: {
    hoursInBed() {
      const bed = this.parseTime(this.local.q1BedTime);
      const wake = this.parseTime(this.local.q3WakeTime);
      if (bed === null || wake === null) return null;
      let diff = wake - bed;
      if (diff <= 0) diff += 24;
      return diff;
    },
    // 实际睡眠小时数(条目4 手填)
    sleepHours() {
      const h = Number(this.local.q4Hours);
      return h > 0 ? h : null;
    },
    efficiency() {
      if (this.hoursInBed === null || this.hoursInBed <= 0) return null;
      const sh = this.sleepHours;
      if (sh === null) return null;
      return Math.round((sh / this.hoursInBed) * 100);
    },
    scoreA() { return this.num("q6Quality"); },
    scoreB() { return this.mapSum(this.num("q2Latency") + this.num("q5a"), [1, 3, 5]); },
    scoreC() {
      const sh = this.sleepHours;
      if (sh === null) return null;
      const rule = SCORE_C_RULES.find((r) => r.test(sh));
      return rule ? rule.score : 3;
    },
    scoreD() {
      const e = this.efficiency;
      if (e === null) return null;
      if (e > 85) return 0;
      if (e >= 75) return 1;
      if (e >= 65) return 2;
      return 3;
    },
    scoreE() {
      const sum = ["q5b", "q5c", "q5d", "q5e", "q5f", "q5g", "q5h", "q5i", "q5j"]
        .reduce((s, k) => s + this.num(k), 0);
      return this.mapSum(sum, [1, 10, 19]);
    },
    scoreF() { return this.num("q7Medication"); },
    scoreG() { return this.mapSum(this.num("q8Sleepy") + this.num("q9Energy"), [1, 3, 5]); },
    components() {
      return [
        { name: "A", title: "主观睡眠质量(条目6)", score: this.disp(this.scoreA) },
        { name: "B", title: "入睡时间(条目2 + 条目5a)", score: this.disp(this.scoreB) },
        { name: "C", title: "睡眠时长(条目4 实际睡眠小时数)", score: this.disp(this.scoreC) },
        { name: "D", title: "睡眠效率(实际睡眠时间 ÷ 在床时间)", score: this.disp(this.scoreD) },
        { name: "E", title: "睡眠障碍(条目5b~5j 累计)", score: this.disp(this.scoreE) },
        { name: "F", title: "催眠药物使用(条目7)", score: this.disp(this.scoreF) },
        { name: "G", title: "日间功能障碍(条目8 + 条目9)", score: this.disp(this.scoreG) },
      ];
    },
    totalScore() {
      return [this.scoreA, this.scoreB, this.scoreC, this.scoreD, this.scoreE, this.scoreF, this.scoreG]
        .reduce((s, v) => s + (Number(v) || 0), 0);
    },
    evaluation() {
      const t = this.totalScore;
      if (t <= 5) return "睡眠质量很好";
      if (t <= 10) return "睡眠质量还行";
      if (t <= 15) return "睡眠质量一般";
      return "睡眠质量很差";
    },
    totalClass() {
      const t = this.totalScore;
      return t <= 5 ? "lv-good" : t <= 10 ? "lv-mid" : t <= 15 ? "lv-warn" : "lv-bad";
    },
    latencyLabel() { return this.labelOf(LATENCY_OPTIONS, this.local.q2Latency); },
    qualityLabel() { return this.labelOf(QUALITY_OPTIONS, this.local.q6Quality); },
    medicationLabel() { return this.labelOf(MEDICATION_OPTIONS, this.local.q7Medication); },
  },
  watch:{
    items:{ deep:true, handler(v){
      const obj={...this.value, psqiTotal:this.totalScore, psqiEvaluation:this.evaluation};
      v.forEach(i=>{ obj[i.key]=i.score; });
      this.$emit('input',obj);
    }},
    value: {
      deep: true,
      handler(v) {
        const n = this.normalize(v);
        if (JSON.stringify(n) !== JSON.stringify(this.local)) this.local = n;
      },
    },
    local: {
      deep: true,
      handler(v) {
        const out = this.buildOut(v);
        if (JSON.stringify(out) !== JSON.stringify(this.value)) this.$emit("input", out);
      },
    },
  },
  methods: {
    num(key) { return Number(this.local[key]) || 0; },
    // 未填写的成分显示占位符,不参与总分
    disp(v) { return v === null || v === undefined ? "-" : v; },
    // 按阈值把累计分转换为 0~3 的成分分
    mapSum(sum, thresholds) {
      if (sum < thresholds[0]) return 0;
      if (sum < thresholds[1]) return 1;
      if (sum < thresholds[2]) return 2;
      return 3;
    },
    parseTime(str) {
      if (!str) return null;
      const m = String(str).match(/^(\d{1,2}):(\d{2})$/);
      if (!m) return null;
      const h = Number(m[1]), mi = Number(m[2]);
      if (h > 23 || mi > 59) return null;
      return h + mi / 60;
    },
    fmtHours(v) {
      if (v === null || v === undefined) return "-";
      return Number.isInteger(v) ? String(v) : v.toFixed(1);
    },
    labelOf(list, v) {
      const o = list.find((x) => x.v === (Number(v) || 0));
      return o ? o.l : "-";
    },
    freqLabel(v) { return this.labelOf(FREQ_OPTIONS, v); },
    normalize(v) {
      const src = v || {};
      const out = { evaluator: src.evaluator || "" };
      FIELDS.forEach((k) => {
        if (k === "date" || k === "q1BedTime" || k === "q3WakeTime") {
          out[k] = src[k] === undefined || src[k] === null ? "" : src[k];
        } else {
          out[k] = src[k] === undefined || src[k] === null ? 0 : src[k];
        }
      });
      return out;
    },
    buildOut(v) {
      return Object.assign({}, v, {
        psqiA: this.scoreA, psqiB: this.scoreB, psqiC: this.scoreC, psqiD: this.scoreD,
        psqiE: this.scoreE, psqiF: this.scoreF, psqiG: this.scoreG,
        psqiEfficiency: this.efficiency,
        psqiTotal: this.totalScore,
        psqiEvaluation: this.evaluation,
      });
    },
  },
};
</script>
<style scoped>
.scale-title { margin-bottom: 10px; font-size: 16px; font-weight: 700; text-align: center; color: #303133; }
.scale-head { margin-bottom: 10px; }
.scale-tip { margin-bottom: 10px; padding: 6px 10px; font-size: 13px; color: #606266; line-height: 1.6; background: #f8f9fb; border-left: 3px solid #c0c4cc; }
.q-item { padding: 8px 0; border-bottom: 1px dashed #ebeef5; }
.q-text { font-size: 13px; color: #303133; font-weight: 600; }
.q-body { margin-top: 6px; padding-left: 12px; }
.q-value { font-size: 13px; color: #409eff; font-weight: 600; }
.q-hint { margin-left: 10px; font-size: 12px; color: #909399; }
.q-sub-table { margin: 4px 0 6px; }
.sec-title { margin: 14px 0 8px; font-size: 14px; font-weight: 700; color: #303133; border-left: 3px solid #409eff; padding-left: 8px; }
.score-table { margin-bottom: 12px; }
.scale-result { margin-top: 6px; }
.muted { color: #909399; font-size: 12px; }
.lv-good { color: #67c23a; }
.lv-mid { color: #409eff; }
.lv-warn { color: #e6a23c; }
.lv-bad { color: #f56c6c; }
</style>