| | |
| | | <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> |
| | | </el-table-column> |
| | | <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">(>7h 记0分,6~7h 记1分,5~6h 记2分,<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"> </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"> </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"> </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"> </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> |
| | | </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> |
| | | <div style="margin-top:10px;font-size:14px">PSQI总分:{{ totalScore }} 评价:{{ evaluation }}</div> |
| | | |
| | | <!-- 结果 --> |
| | | <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 })); |
| | | }, |
| | | totalScore(){ return this.items.reduce((s,i)=>s+i.score,0); }, |
| | | evaluation(){ const t=this.totalScore; return t<=5?"睡眠质量良好":t<=10?"睡眠质量一般":t<=15?"睡眠质量较差":"睡眠质量很差"; }, |
| | | name: "PSQIScoreTable", |
| | | props: { |
| | | value: { type: Object, default: () => ({}) }, |
| | | patient: { type: Object, default: () => ({}) }, |
| | | editing: Boolean, |
| | | }, |
| | | 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); |
| | | }}, |
| | | 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: { |
| | | 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> |