<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">(>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>
|
|
<!-- 结果 -->
|
<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: () => ({}) },
|
patient: { type: Object, default: () => ({}) },
|
editing: Boolean,
|
},
|
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>
|