Ver código fonte

Merge branch 'master' of http://47.107.245.0:3000/xiongxing/kyj-yanglao-web-new

chenjun 3 semanas atrás
pai
commit
ffa529cb4b

+ 50 - 0
src/api/elderly/assess/nursing-record-sheet/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+import { mockCreate, mockDelete, mockDetail, mockPage, mockUpdate } from './mock'
+
+// TODO 后端接口就绪后改为 false,并删除同目录下的 mock.ts
+const USE_MOCK = true
+
+// ====================护理记录单====================
+
+// 获得护理记录单分页
+export const getNursingRecordPage = async (params) => {
+  if (USE_MOCK) return mockPage(params)
+  return request.get({
+    url: 'elderly/nursingRecord/page',
+    params
+  })
+}
+
+// 获得护理记录单详情
+export const getNursingRecord = async (id) => {
+  if (USE_MOCK) return mockDetail(id)
+  return request.get({
+    url: `elderly/nursingRecord/get?id=${id}`
+  })
+}
+
+// 新增护理记录单
+export const createNursingRecord = async (data) => {
+  if (USE_MOCK) return mockCreate(data)
+  return request.post({
+    url: 'elderly/nursingRecord/create',
+    data
+  })
+}
+
+// 修改护理记录单
+export const updateNursingRecord = async (data) => {
+  if (USE_MOCK) return mockUpdate(data)
+  return request.put({
+    url: 'elderly/nursingRecord/update',
+    data
+  })
+}
+
+// 删除护理记录单
+export const deleteNursingRecord = async (id) => {
+  if (USE_MOCK) return mockDelete(id)
+  return request.delete({
+    url: `elderly/nursingRecord/delete?id=${id}`
+  })
+}

+ 195 - 0
src/api/elderly/assess/nursing-record-sheet/mock.ts

@@ -0,0 +1,195 @@
+/**
+ * 护理记录单 —— 临时模拟数据
+ * 后端接口未就绪时使用,接口联调完成后删除本文件,并将 index.ts 中的 USE_MOCK 置为 false
+ */
+
+export interface NursingRecordItem {
+  id: number
+  tenantId: number
+  tenantName: string
+  elderId: number
+  elderName: string
+  elderSex: number
+  bedInfo: string
+  recordDate: string
+  recordTime: string
+  temperature: number
+  bloodOxygen: number
+  pulse: number
+  respiration: number
+  systolicPressure: number
+  diastolicPressure: number
+  observation: string
+  signer: string
+  creator: string
+  createTime: string
+}
+
+/** 长者基础信息 */
+const ELDERS = [
+  {
+    elderId: 1001,
+    elderName: '张桂英',
+    elderSex: 2,
+    bedInfo: '3栋-2楼-201-1床',
+    tenantId: 1,
+    tenantName: '颐年养老院'
+  },
+  {
+    elderId: 1002,
+    elderName: '李秀兰',
+    elderSex: 2,
+    bedInfo: '3栋-2楼-202-1床',
+    tenantId: 1,
+    tenantName: '颐年养老院'
+  },
+  {
+    elderId: 1003,
+    elderName: '王德福',
+    elderSex: 1,
+    bedInfo: '2栋-1楼-105-2床',
+    tenantId: 1,
+    tenantName: '颐年养老院'
+  }
+]
+
+/** 病情观察及护理常用描述 */
+const OBSERVATIONS = [
+  '神志清楚,精神尚可,饮食正常,夜间睡眠好,无跌倒坠床。',
+  '体温偏高,嘱多饮水,已电话告知家属,继续观察体温变化。',
+  '血压较昨日偏高,已遵医嘱服药,建议低盐低脂饮食。',
+  '血氧偏低,已给予低流量吸氧,嘱卧床休息,暂缓下床活动。',
+  '协助翻身拍背,皮肤完整无压红,二便正常。',
+  '长者情绪平稳,主动配合护理,晨间生命体征平稳。',
+  '诉轻微头晕,已卧床休息并测量血压,密切观察病情变化。',
+  '食欲欠佳,改半流质饮食,少量多餐,已告知责任护士。',
+  '夜间入睡困难,予热水泡脚、调暗灯光后入睡。',
+  '康复训练后无不适,四肢活动尚可,继续按计划训练。'
+]
+
+const SIGNERS = ['王护士', '刘护士', '陈护士']
+/** 每日记录时间点 */
+const TIME_POINTS = ['08:00', '14:00', '20:00']
+/** 生成最近 N 天的记录 */
+const DAY_COUNT = 10
+
+/** 伪随机:保证每次生成的数据一致,避免刷新后数据跳动 */
+const rand = (seed: number, min: number, max: number, digits = 0) => {
+  const x = Math.sin(seed * 12.9898) * 43758.5453
+  const rate = x - Math.floor(x)
+  const value = min + rate * (max - min)
+  return digits ? Number(value.toFixed(digits)) : Math.round(value)
+}
+
+const formatDate = (date: Date) =>
+  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
+    date.getDate()
+  ).padStart(2, '0')}`
+
+const buildList = (): NursingRecordItem[] => {
+  const result: NursingRecordItem[] = []
+  let id = 1
+  let seed = 1
+  ELDERS.forEach((elder) => {
+    for (let day = DAY_COUNT - 1; day >= 0; day--) {
+      const date = new Date()
+      date.setDate(date.getDate() - day)
+      const recordDate = formatDate(date)
+      TIME_POINTS.forEach((time) => {
+        result.push({
+          id: id++,
+          tenantId: elder.tenantId,
+          tenantName: elder.tenantName,
+          elderId: elder.elderId,
+          elderName: elder.elderName,
+          elderSex: elder.elderSex,
+          bedInfo: elder.bedInfo,
+          recordDate,
+          recordTime: time,
+          temperature: rand(seed++, 36.2, 37.4, 1),
+          bloodOxygen: rand(seed++, 92, 99),
+          pulse: rand(seed++, 62, 92),
+          respiration: rand(seed++, 16, 22),
+          systolicPressure: rand(seed++, 110, 150),
+          diastolicPressure: rand(seed++, 65, 90),
+          observation: OBSERVATIONS[Math.floor(rand(seed++, 0, OBSERVATIONS.length - 1))],
+          signer: SIGNERS[Math.floor(rand(seed++, 0, SIGNERS.length - 1))],
+          creator: '系统管理员',
+          createTime: `${recordDate} ${time}:00`
+        })
+      })
+    }
+  })
+  return result
+}
+
+/** 内存数据源 */
+let mockData: NursingRecordItem[] = buildList()
+
+let nextId = mockData.length + 1
+
+const sortList = (list: NursingRecordItem[]) =>
+  list.sort((a, b) => {
+    if (a.recordDate === b.recordDate) return b.recordTime.localeCompare(a.recordTime)
+    return b.recordDate.localeCompare(a.recordDate)
+  })
+
+/** 分页查询 */
+export const mockPage = (params: Recordable) => {
+  const pageNo = params?.pageNo || 1
+  const pageSize = params?.pageSize || 10
+  const { elderName, bedInfo, recordDate, elderId } = params || {}
+  let list = [...mockData]
+  if (elderName) list = list.filter((item) => item.elderName.includes(elderName))
+  if (bedInfo) list = list.filter((item) => item.bedInfo.includes(bedInfo))
+  if (elderId) list = list.filter((item) => String(item.elderId) === String(elderId))
+  if (Array.isArray(recordDate) && recordDate[0] && recordDate[1]) {
+    const start = String(recordDate[0]).slice(0, 10)
+    const end = String(recordDate[1]).slice(0, 10)
+    list = list.filter((item) => item.recordDate >= start && item.recordDate <= end)
+  }
+  sortList(list)
+  return {
+    list: list.slice((pageNo - 1) * pageSize, pageNo * pageSize),
+    total: list.length
+  }
+}
+
+/** 详情 */
+export const mockDetail = (id) => {
+  const item = mockData.find((row) => row.id === Number(id))
+  return item ? { ...item } : {}
+}
+
+/** 新增 */
+export const mockCreate = (data) => {
+  const item: NursingRecordItem = {
+    id: nextId++,
+    tenantName: '颐年养老院',
+    creator: '系统管理员',
+    createTime: `${data.recordDate} ${data.recordTime || ''}:00`,
+    ...data
+  }
+  mockData = [item, ...mockData]
+  return item.id
+}
+
+/** 修改 */
+export const mockUpdate = (data) => {
+  const index = mockData.findIndex((row) => row.id === Number(data.id))
+  if (index !== -1) {
+    mockData[index] = { ...mockData[index], ...data }
+    return true
+  }
+  return false
+}
+
+/** 删除 */
+export const mockDelete = (id) => {
+  const index = mockData.findIndex((row) => row.id === Number(id))
+  if (index !== -1) {
+    mockData.splice(index, 1)
+    return true
+  }
+  return false
+}

+ 196 - 0
src/views/elderly/assess/nursing-record-sheet/API.md

@@ -0,0 +1,196 @@
+# 护理记录单 —— 后端接口说明
+
+> 模块位置:`src/views/elderly/assess/nursing-record-sheet/`
+> 当前状态:前端页面已开发完成,接口未就绪,暂时走本地模拟数据 `src/api/elderly/assess/nursing-record-sheet/mock.ts`
+
+## 一、表结构
+
+表名建议:`elderly_nursing_record`
+
+```sql
+CREATE TABLE elderly_nursing_record (
+  id                 BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  tenant_id          BIGINT       NOT NULL DEFAULT 0  COMMENT '机构ID(多租户)',
+  elder_id           BIGINT       NOT NULL            COMMENT '长者ID',
+  elder_name         VARCHAR(64)  NOT NULL            COMMENT '长者姓名(冗余)',
+  elder_sex          TINYINT                          COMMENT '性别(冗余)1-男 2-女',
+  bed_info           VARCHAR(128)                     COMMENT '楼层房号床号(冗余,如 3栋-2楼-201-1床)',
+  record_date        DATE         NOT NULL            COMMENT '记录日期',
+  record_time        VARCHAR(8)   NOT NULL            COMMENT '记录时间 HH:mm',
+  temperature        DECIMAL(4,1)                     COMMENT '体温 ℃',
+  blood_oxygen       SMALLINT                         COMMENT '血氧 Spo2 %',
+  pulse              SMALLINT                         COMMENT '脉搏 次/分',
+  respiration        SMALLINT                         COMMENT '呼吸 次/分',
+  systolic_pressure  SMALLINT                         COMMENT '收缩压 mmHg',
+  diastolic_pressure SMALLINT                         COMMENT '舒张压 mmHg',
+  observation        VARCHAR(500)                     COMMENT '病情观察及护理',
+  signer             VARCHAR(32)                      COMMENT '签名(护理人员姓名)',
+  creator            VARCHAR(64)                      COMMENT '创建人',
+  create_time        DATETIME                         COMMENT '创建时间',
+  updater            VARCHAR(64)                      COMMENT '更新人',
+  update_time        DATETIME                         COMMENT '更新时间',
+  deleted            BIT(1)       NOT NULL DEFAULT 0  COMMENT '是否删除',
+  PRIMARY KEY (id),
+  KEY idx_elder_date (elder_id, record_date),
+  KEY idx_tenant (tenant_id)
+) COMMENT '护理记录单';
+```
+
+## 二、接口清单
+
+接口前缀:`/admin-api/elderly/nursingRecord`(前端 request 里已省略 `/admin-api` 前缀,下面写的是前端调用的完整 url)
+
+| 序号 | 名称 | 方法 | 路径 | 说明 |
+|---|---|---|---|---|
+| 1 | 分页查询 | GET | `elderly/nursingRecord/page` | 列表页数据 |
+| 2 | 详情 | GET | `elderly/nursingRecord/get?id=` | 详情弹窗、编辑回显 |
+| 3 | 新增 | POST | `elderly/nursingRecord/create` | 新增单条记录 |
+| 4 | 修改 | PUT | `elderly/nursingRecord/update` | 修改单条记录 |
+| 5 | 删除 | DELETE | `elderly/nursingRecord/delete?id=` | 单条删除 |
+
+> 说明:
+> - 新增页面支持一次录入多行,前端是按行循环调用 `create` 的,不需要批量接口(如果希望改成批量接口,可另加 `createBatch`)。
+> - 删除是单条删除,前端批量删除时循环调用。
+> - **导出不需要后端接口**,Excel 由前端用 `xlsx-js-style` 本地生成(按纸质护理记录单版式)。
+
+### 1. 分页查询
+
+`GET elderly/nursingRecord/page`
+
+请求参数(Query):
+
+| 参数 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| pageNo | Integer | 是 | 页码,从 1 开始 |
+| pageSize | Integer | 是 | 每页条数 |
+| tenantIds | Long[] | 否 | 机构ID集合(多选),不传表示全部 |
+| elderName | String | 否 | 长者姓名,模糊匹配 |
+| recordDate | String[] | 否 | 记录日期区间,如 `['2026-09-01 00:00:00','2026-09-30 23:59:59']` |
+
+响应:
+
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [ { "id": 1, "elderName": "张桂英", "..." : "..." } ],
+    "total": 90
+  }
+}
+```
+
+> `list` 建议按 `record_date` 倒序、`record_time` 倒序返回(列表最新在前)。
+> 导出时前端会自行按正序重排。
+> 日期区间的数组参数传法与现有「生命体征」模块的 `measuringDate` 保持一致即可。
+
+### 2. 详情
+
+`GET elderly/nursingRecord/get?id=1`
+
+响应 `data` 为单条记录对象(字段见第三节)。
+
+### 3. 新增
+
+`POST elderly/nursingRecord/create`
+
+请求体(JSON):
+
+```json
+{
+  "tenantId": 1,
+  "elderId": 1001,
+  "elderName": "张桂英",
+  "elderSex": 2,
+  "bedInfo": "3栋-2楼-201-1床",
+  "recordDate": "2026-09-17",
+  "recordTime": "08:00",
+  "temperature": 36.5,
+  "bloodOxygen": 97,
+  "pulse": 76,
+  "respiration": 18,
+  "systolicPressure": 128,
+  "diastolicPressure": 78,
+  "observation": "神志清楚,精神尚可,饮食正常。",
+  "signer": "王护士"
+}
+```
+
+响应:返回记录 ID。
+
+> `elderName / elderSex / bedInfo` 前端会根据选中的长者带出并随请求提交,后端可直接存冗余字段(也可只认 elderId 自行反查,两者都有即可)。
+
+### 4. 修改
+
+`PUT elderly/nursingRecord/update`
+
+请求体:同新增,额外必传 `id`。
+
+### 5. 删除
+
+`DELETE elderly/nursingRecord/delete?id=1`
+
+响应:true / false。
+
+## 三、字段说明
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| id | Long | 修改时必填 | 主键 |
+| tenantId | Long | 是 | 机构ID |
+| elderId | Long | 是 | 长者ID |
+| elderName | String | 是 | 长者姓名 |
+| elderSex | Integer | 否 | 性别:1-男,2-女(字典 `system_user_sex`) |
+| bedInfo | String | 否 | 楼层房号床号,示例 `3栋-2楼-201-1床` |
+| recordDate | String | 是 | 记录日期 `yyyy-MM-dd` |
+| recordTime | String | 是 | 记录时间 `HH:mm` |
+| temperature | BigDecimal | 否 | 体温 ℃,1 位小数 |
+| bloodOxygen | Integer | 否 | 血氧 Spo2 % |
+| pulse | Integer | 否 | 脉搏 次/分 |
+| respiration | Integer | 否 | 呼吸 次/分 |
+| systolicPressure | Integer | 否 | 收缩压 mmHg |
+| diastolicPressure | Integer | 否 | 舒张压 mmHg |
+| observation | String | 否 | 病情观察及护理,最长 500 字 |
+| signer | String | 否 | 签名,最长 20 字 |
+| creator | String | 否 | 创建人(后端自动填充) |
+| createTime | String | 否 | 创建时间(详情展示用) |
+
+列表页还需要 `tenantName`(机构名称)用于多机构场景展示。
+
+## 四、校验规则(前端已做,建议后端对齐)
+
+| 字段 | 规则 |
+|---|---|
+| elderId / recordDate / recordTime | 必填 |
+| temperature | 20 ~ 45,最多 1 位小数 |
+| bloodOxygen | 0 ~ 100 |
+| pulse | 0 ~ 300 |
+| respiration | 0 ~ 100 |
+| systolicPressure | 0 ~ 300 |
+| diastolicPressure | 0 ~ 300,且必须小于 systolicPressure |
+| observation | ≤ 500 字 |
+| signer | ≤ 20 字 |
+
+## 五、菜单与权限配置
+
+- 组件路径:`elderly/assess/nursing-record-sheet/index`
+- 建议权限标识:`records:nursing-record-sheet:query / add / edit / delete / export`
+- 前端按钮目前**未挂 `v-hasPermi`**,加权限时只需在按钮上补指令即可。
+
+## 六、联调切换方式
+
+接口开发完成后:
+
+1. 将 `src/api/elderly/assess/nursing-record-sheet/index.ts` 里的 `USE_MOCK` 改为 `false`;
+2. 删除 `src/api/elderly/assess/nursing-record-sheet/mock.ts`。
+
+## 七、导出 Excel 版式(无需后端参与)
+
+```
+护理记录单                                        ← 标题,合并整行,宋体18加粗居中
+楼层房号床号:xxx    姓名:xxx    性别:x          ← 合并整行,左对齐加粗
+日期 | 时间 | 体温 | 血氧 | 脉搏 | 呼吸 | 血压 | 病情观察及护理 | 签名
+     |      | ℃   | Spo2 | 次/分 | 次/分 | mmHg |                |      ← 两行表头,日期/时间/病情观察/签名纵向合并
+数据行(连续不分页)
+护士签名:                                        ← 合并整行,左对齐加粗,留空手签
+第 1 页                                           ← 合并整行,右对齐
+```

+ 408 - 0
src/views/elderly/assess/nursing-record-sheet/AddForm.vue

@@ -0,0 +1,408 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="formTitle" width="92%">
+    <el-form
+      ref="formRef"
+      :model="dataForm"
+      :rules="dataRule"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-row :gutter="20">
+        <el-col :xs="24" :sm="24" :md="12" :lg="6" :xl="6">
+          <el-form-item label="长者姓名" prop="elderId">
+            <SelectElder
+              v-model="dataForm.elderId"
+              :tId="dataForm.tenantId"
+              :disabled="isEdit"
+              @elder="handleSelectElder"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="12" :lg="6" :xl="6">
+          <el-form-item label="楼层房号床号">
+            <el-input v-model="dataForm.bedInfo" disabled placeholder="选择长者后自动带出" />
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="12" :lg="6" :xl="6">
+          <el-form-item label="性别">
+            <el-input v-model="sexText" disabled />
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="12" :lg="6" :xl="6">
+          <el-form-item label="记录日期" prop="recordDate">
+            <el-date-picker
+              v-model="dataForm.recordDate"
+              type="date"
+              style="width: 100%"
+              value-format="YYYY-MM-DD"
+              placeholder="请选择记录日期"
+              :disabled-date="disabledDate"
+              :disabled="isEdit"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="mb-10px">
+        <el-button type="primary" @click="handleAdd" :disabled="isEdit">
+          <Icon icon="ep:plus" class="mr-5px" />添加记录
+        </el-button>
+        <el-button type="danger" @click="handleClear" :disabled="isEdit">
+          <Icon icon="ep:delete" class="mr-5px" />清空
+        </el-button>
+      </div>
+
+      <el-table
+        :data="dataForm.list"
+        :header-cell-style="tableHeaderColor"
+        max-height="440"
+        border
+      >
+        <el-table-column
+          label="序号"
+          type="index"
+          align="center"
+          header-align="center"
+          width="60"
+          fixed
+        />
+        <el-table-column label="时间" align="center" header-align="center" width="150">
+          <template #default="scope">
+            <el-time-picker
+              v-model="scope.row.recordTime"
+              style="width: 100%"
+              value-format="HH:mm"
+              format="HH:mm"
+              placeholder="选择时间"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="体温℃" align="center" header-align="center" width="120">
+          <template #default="scope">
+            <el-input-number
+              v-model="scope.row.temperature"
+              :controls="false"
+              :precision="1"
+              :min="20"
+              :max="45"
+              style="width: 100%"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="血氧Spo2" align="center" header-align="center" width="120">
+          <template #default="scope">
+            <el-input-number
+              v-model="scope.row.bloodOxygen"
+              :controls="false"
+              :min="0"
+              :max="100"
+              style="width: 100%"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="脉搏(次/分)" align="center" header-align="center" width="130">
+          <template #default="scope">
+            <el-input-number
+              v-model="scope.row.pulse"
+              :controls="false"
+              :min="0"
+              :max="300"
+              style="width: 100%"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="呼吸(次/分)" align="center" header-align="center" width="130">
+          <template #default="scope">
+            <el-input-number
+              v-model="scope.row.respiration"
+              :controls="false"
+              :min="0"
+              :max="100"
+              style="width: 100%"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="血压mmHg" align="center" header-align="center" width="180">
+          <template #default="scope">
+            <el-input-number
+              v-model="scope.row.systolicPressure"
+              :controls="false"
+              :min="0"
+              :max="300"
+              style="width: 45%"
+            />
+            <span style="width: 10%; display: inline-block; text-align: center">/</span>
+            <el-input-number
+              v-model="scope.row.diastolicPressure"
+              :controls="false"
+              :min="0"
+              :max="300"
+              style="width: 45%"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="病情观察及护理" header-align="center" min-width="220">
+          <template #default="scope">
+            <el-input
+              v-model="scope.row.observation"
+              type="textarea"
+              :rows="1"
+              :autosize="{ minRows: 1, maxRows: 3 }"
+              maxlength="500"
+              show-word-limit
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="签名" align="center" header-align="center" width="130">
+          <template #default="scope">
+            <el-input v-model="scope.row.signer" maxlength="20" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          align="center"
+          header-align="center"
+          width="80"
+          fixed="right"
+          v-if="!isEdit"
+        >
+          <template #default="scope">
+            <el-button link type="danger" @click="handleDelete(scope.$index)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">取消</el-button>
+      <el-button type="primary" v-loading="formLoading" @click="submitForm">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { FormRules } from 'element-plus'
+import {
+  createNursingRecord,
+  updateNursingRecord,
+  getNursingRecord
+} from '@/api/elderly/assess/nursing-record-sheet'
+import { tableHeaderColor } from '@/utils/table'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'NursingRecordForm' })
+
+const userStore = useUserStore()
+const nickname = userStore.getUser.nickname
+const message = useMessage()
+const { t } = useI18n()
+
+const dialogVisible = ref(false) // 弹窗
+const formRef = ref() // 表单 Ref
+const formLoading = ref(false) // 提交中
+const isEdit = ref(false) // 是否编辑
+
+const getToday = () => {
+  const now = new Date()
+  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(
+    now.getDate()
+  ).padStart(2, '0')}`
+}
+
+const getCurrentTime = () => {
+  const now = new Date()
+  return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
+}
+
+/** 生成一行空记录 */
+const buildRow = (row?: Recordable) => ({
+  id: row?.id,
+  recordTime: row?.recordTime || getCurrentTime(),
+  temperature: row?.temperature ?? undefined,
+  bloodOxygen: row?.bloodOxygen ?? undefined,
+  pulse: row?.pulse ?? undefined,
+  respiration: row?.respiration ?? undefined,
+  systolicPressure: row?.systolicPressure ?? undefined,
+  diastolicPressure: row?.diastolicPressure ?? undefined,
+  observation: row?.observation || '',
+  signer: row?.signer || nickname
+})
+
+const dataForm = ref<any>({
+  id: undefined,
+  tenantId: undefined,
+  elderId: '',
+  elderName: '',
+  elderSex: undefined,
+  bedInfo: '',
+  recordDate: getToday(),
+  list: []
+})
+
+const dataRule = reactive<FormRules>({
+  elderId: [{ required: true, message: '长者姓名不能为空', trigger: 'change' }],
+  recordDate: [{ required: true, message: '记录日期不能为空', trigger: 'change' }]
+})
+
+const formTitle = computed(() => (isEdit.value ? '编辑-护理记录单' : '新增-护理记录单'))
+
+const sexText = computed(() => {
+  const sex = dataForm.value.elderSex
+  if (sex === 1 || sex === '1') return '男'
+  if (sex === 2 || sex === '2') return '女'
+  return ''
+})
+
+const disabledDate = (time) => time > Date.now()
+
+/** 选择长者后自动带出床位与性别 */
+const handleSelectElder = (item) => {
+  if (!item) return
+  dataForm.value.elderName = item.elderName
+  dataForm.value.elderSex = item.elderSex
+  dataForm.value.bedInfo =
+    [item.buildName, item.floorName, item.roomName, item.bedName].filter(Boolean).join('-') ||
+    item.bedName ||
+    ''
+  if (item.tenantId) dataForm.value.tenantId = item.tenantId
+}
+
+/** 打开弹窗 */
+const open = async (tenantId, id?) => {
+  resetForm()
+  dialogVisible.value = true
+  dataForm.value.tenantId = tenantId
+  isEdit.value = !!id
+  if (id) {
+    try {
+      formLoading.value = true
+      const res = await getNursingRecord(id)
+      dataForm.value.id = res.id
+      dataForm.value.elderId = res.elderId
+      dataForm.value.elderName = res.elderName
+      dataForm.value.elderSex = res.elderSex
+      dataForm.value.bedInfo = res.bedInfo
+      dataForm.value.recordDate = res.recordDate
+      dataForm.value.list = [buildRow(res)]
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    dataForm.value.list = [buildRow()]
+  }
+}
+defineExpose({ open })
+
+const emit = defineEmits(['success'])
+
+/** 校验体征数值范围 */
+const validateRows = () => {
+  if (dataForm.value.list.length === 0) {
+    message.warning('请至少添加一条记录')
+    return false
+  }
+  for (const row of dataForm.value.list) {
+    if (!row.recordTime) {
+      message.warning('请填写记录时间')
+      return false
+    }
+    const checks = [
+      [row.temperature, 20, 45, '体温'],
+      [row.bloodOxygen, 0, 100, '血氧'],
+      [row.pulse, 0, 300, '脉搏'],
+      [row.respiration, 0, 100, '呼吸'],
+      [row.systolicPressure, 0, 300, '收缩压'],
+      [row.diastolicPressure, 0, 300, '舒张压']
+    ]
+    for (const [val, min, max, label] of checks) {
+      if (val === undefined || val === null || val === '') continue
+      if (Number(val) < min || Number(val) > max) {
+        message.error(`${label}应在 ${min} ~ ${max} 之间`)
+        return false
+      }
+    }
+    if (
+      row.systolicPressure !== undefined &&
+      row.systolicPressure !== null &&
+      row.diastolicPressure !== undefined &&
+      row.diastolicPressure !== null &&
+      Number(row.diastolicPressure) >= Number(row.systolicPressure)
+    ) {
+      message.error('舒张压应小于收缩压')
+      return false
+    }
+  }
+  return true
+}
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (formLoading.value) return
+  if (!formRef.value) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  if (!validateRows()) return
+
+  formLoading.value = true
+  try {
+    const base = {
+      tenantId: dataForm.value.tenantId,
+      elderId: dataForm.value.elderId,
+      elderName: dataForm.value.elderName,
+      elderSex: dataForm.value.elderSex,
+      bedInfo: dataForm.value.bedInfo,
+      recordDate: dataForm.value.recordDate
+    }
+    if (dataForm.value.id) {
+      await updateNursingRecord({
+        id: dataForm.value.id,
+        ...base,
+        ...dataForm.value.list[0]
+      })
+    } else {
+      for (const row of dataForm.value.list) {
+        await createNursingRecord({ ...base, ...row })
+      }
+    }
+    message.success(t('common.updateSuccess'))
+    handleClosed()
+    emit('success')
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 300)
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  dataForm.value = {
+    id: undefined,
+    tenantId: undefined,
+    elderId: '',
+    elderName: '',
+    elderSex: undefined,
+    bedInfo: '',
+    recordDate: getToday(),
+    list: []
+  }
+  formRef.value?.resetFields()
+}
+
+const handleClosed = () => {
+  resetForm()
+  dialogVisible.value = false
+}
+
+const handleAdd = () => {
+  dataForm.value.list.push(buildRow())
+}
+
+const handleClear = () => {
+  dataForm.value.list = []
+}
+
+const handleDelete = (index: number) => {
+  dataForm.value.list.splice(index, 1)
+}
+</script>

+ 64 - 0
src/views/elderly/assess/nursing-record-sheet/DetailForm.vue

@@ -0,0 +1,64 @@
+<template>
+  <Dialog v-model="dialogVisible" title="详情-护理记录单" width="60%">
+    <el-descriptions :column="2" border v-loading="formLoading">
+      <el-descriptions-item label="所属机构">{{ dataForm.tenantName }}</el-descriptions-item>
+      <el-descriptions-item label="长者姓名">{{ dataForm.elderName }}</el-descriptions-item>
+      <el-descriptions-item label="性别">
+        <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="dataForm.elderSex" />
+      </el-descriptions-item>
+      <el-descriptions-item label="楼层房号床号">{{ dataForm.bedInfo }}</el-descriptions-item>
+      <el-descriptions-item label="日期">
+        {{ dataForm.recordDate ? formatTime(dataForm.recordDate, 'yyyy-MM-dd') : '' }}
+      </el-descriptions-item>
+      <el-descriptions-item label="时间">{{ dataForm.recordTime }}</el-descriptions-item>
+      <el-descriptions-item label="体温℃">{{ dataForm.temperature }}</el-descriptions-item>
+      <el-descriptions-item label="血氧Spo2">{{ dataForm.bloodOxygen }}</el-descriptions-item>
+      <el-descriptions-item label="脉搏(次/分)">{{ dataForm.pulse }}</el-descriptions-item>
+      <el-descriptions-item label="呼吸(次/分)">{{ dataForm.respiration }}</el-descriptions-item>
+      <el-descriptions-item label="血压mmHg">
+        {{
+          dataForm.systolicPressure || dataForm.diastolicPressure
+            ? `${dataForm.systolicPressure ?? ''}/${dataForm.diastolicPressure ?? ''}`
+            : ''
+        }}
+      </el-descriptions-item>
+      <el-descriptions-item label="签名">{{ dataForm.signer }}</el-descriptions-item>
+      <el-descriptions-item label="病情观察及护理" :span="2">
+        {{ dataForm.observation }}
+      </el-descriptions-item>
+      <el-descriptions-item label="创建人">{{ dataForm.creator }}</el-descriptions-item>
+      <el-descriptions-item label="创建时间">
+        {{ dataForm.createTime ? formatTime(dataForm.createTime, 'yyyy-MM-dd HH:mm:ss') : '' }}
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <template #footer>
+      <el-button @click="dialogVisible = false">关闭</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { getNursingRecord } from '@/api/elderly/assess/nursing-record-sheet'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatTime } from '@/utils'
+
+defineOptions({ name: 'NursingRecordDetail' })
+
+const dialogVisible = ref(false) // 弹窗
+const formLoading = ref(false)
+const dataForm = ref<any>({})
+
+/** 打开弹窗 */
+const open = async (id) => {
+  dialogVisible.value = true
+  dataForm.value = {}
+  try {
+    formLoading.value = true
+    dataForm.value = await getNursingRecord(id)
+  } finally {
+    formLoading.value = false
+  }
+}
+defineExpose({ open })
+</script>

+ 207 - 0
src/views/elderly/assess/nursing-record-sheet/exportSheet.ts

@@ -0,0 +1,207 @@
+import * as XLSX from 'xlsx-js-style'
+
+/** 护理记录单导出的行数据 */
+export interface NursingRecordExportRow {
+  /** 日期 yyyy-MM-dd */
+  recordDate?: string
+  /** 时间 HH:mm */
+  recordTime?: string
+  /** 体温 ℃ */
+  temperature?: number | string
+  /** 血氧 Spo2 % */
+  bloodOxygen?: number | string
+  /** 脉搏 次/分 */
+  pulse?: number | string
+  /** 呼吸 次/分 */
+  respiration?: number | string
+  /** 收缩压 mmHg */
+  systolicPressure?: number | string
+  /** 舒张压 mmHg */
+  diastolicPressure?: number | string
+  /** 病情观察及护理 */
+  observation?: string
+  /** 签名 */
+  signer?: string
+  /** 长者姓名 */
+  elderName?: string
+  /** 性别 1男 2女 */
+  elderSex?: number | string
+  /** 楼层房号床号 */
+  bedInfo?: string
+}
+
+/** 表头(第一行:名称;第二行:单位) */
+const HEADER_LABEL = ['日期', '时间', '体温', '血氧', '脉搏', '呼吸', '血压', '病情观察及护理', '签名']
+const HEADER_UNIT = ['', '', '℃', 'Spo2', '次/分', '次/分', 'mmHg', '', '']
+/** 表头中纵向合并两行的列下标 */
+const SPAN_HEADER_COLS = [0, 1, 7, 8]
+const COLUMN_COUNT = HEADER_LABEL.length
+/** 列宽 */
+const COLUMN_WIDTH = [
+  { wch: 12 },
+  { wch: 8 },
+  { wch: 8 },
+  { wch: 9 },
+  { wch: 9 },
+  { wch: 9 },
+  { wch: 12 },
+  { wch: 42 },
+  { wch: 10 }
+]
+
+const FULL_BORDER = {
+  top: { style: 'thin', color: { rgb: 'FF000000' } },
+  bottom: { style: 'thin', color: { rgb: 'FF000000' } },
+  left: { style: 'thin', color: { rgb: 'FF000000' } },
+  right: { style: 'thin', color: { rgb: 'FF000000' } }
+}
+
+/** 行角色,用于后续统一设置样式 */
+type RowRole = 'title' | 'info' | 'header1' | 'header2' | 'data' | 'nurseSign' | 'pageNo'
+
+const emptyRow = (): string[] => new Array(COLUMN_COUNT).fill('')
+
+const sexText = (sex?: number | string) => {
+  if (sex === 1 || sex === '1') return '男'
+  if (sex === 2 || sex === '2') return '女'
+  return sex ?? ''
+}
+
+const val = (v?: number | string) => (v === null || v === undefined ? '' : String(v))
+
+/** 后端可能返回 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss,统一取日期部分 */
+const dateText = (d?: string) => (d ? String(d).slice(0, 10) : '')
+
+const toDataRow = (row: NursingRecordExportRow): string[] => [
+  dateText(row.recordDate),
+  row.recordTime || '',
+  val(row.temperature),
+  val(row.bloodOxygen),
+  val(row.pulse),
+  val(row.respiration),
+  [val(row.systolicPressure), val(row.diastolicPressure)].filter((i) => i !== '').join('/'),
+  row.observation || '',
+  row.signer || ''
+]
+
+/**
+ * 导出护理记录单(单人表格版式)
+ * 版式:标题行 + 基本信息行 + 两行表头 + 全部数据行 + 页码,所有记录连续排在一张表内
+ * @param records 护理记录列表(同一位长者,按日期时间升序)
+ * @param fileName 导出文件名
+ */
+export const exportNursingRecordSheet = (
+  records: NursingRecordExportRow[],
+  fileName = '护理记录单'
+) => {
+  const list = Array.isArray(records) ? records : []
+  const first = list[0] || {}
+  const aoa: string[][] = []
+  const rowRoles: RowRole[] = []
+  const rowHeights: { hpt: number }[] = []
+
+  // 标题行
+  const titleRow = emptyRow()
+  titleRow[0] = '护理记录单'
+  aoa.push(titleRow)
+  rowRoles.push('title')
+  rowHeights.push({ hpt: 36 })
+
+  // 基本信息行:楼层房号床号 / 姓名 / 性别
+  const infoRow = emptyRow()
+  infoRow[0] = `楼层房号床号:${first.bedInfo || ''}        姓名:${
+    first.elderName || ''
+  }        性别:${sexText(first.elderSex)}`
+  aoa.push(infoRow)
+  rowRoles.push('info')
+  rowHeights.push({ hpt: 26 })
+
+  // 表头(两行)
+  aoa.push([...HEADER_LABEL])
+  rowRoles.push('header1')
+  rowHeights.push({ hpt: 24 })
+  aoa.push([...HEADER_UNIT])
+  rowRoles.push('header2')
+  rowHeights.push({ hpt: 22 })
+
+  // 数据行(连续排列,不分页)
+  list.forEach((row) => {
+    aoa.push(toDataRow(row))
+    rowRoles.push('data')
+    rowHeights.push({ hpt: 26 })
+  })
+
+  // 护士签名:所有记录签名人一致时自动带出,否则留空由护士打印后手签
+  const signers = Array.from(new Set(list.map((row) => row.signer).filter(Boolean)))
+  const nurseSignName = signers.length === 1 ? String(signers[0]) : ''
+
+  // 护士签名行:位于表格下方整行合并
+  const nurseSignRowIndex = aoa.length
+  const nurseSignRow = emptyRow()
+  nurseSignRow[0] = `护士签名:${nurseSignName}`
+  aoa.push(nurseSignRow)
+  rowRoles.push('nurseSign')
+  rowHeights.push({ hpt: 34 })
+
+  // 页码行
+  const pageNoRow = emptyRow()
+  pageNoRow[COLUMN_COUNT - 1] = '第 1 页'
+  aoa.push(pageNoRow)
+  rowRoles.push('pageNo')
+  rowHeights.push({ hpt: 26 })
+
+  // 合并单元格
+  const lastRowIndex = aoa.length - 1
+  const merges: Recordable[] = [
+    { s: { r: 0, c: 0 }, e: { r: 0, c: COLUMN_COUNT - 1 } },
+    { s: { r: 1, c: 0 }, e: { r: 1, c: COLUMN_COUNT - 1 } },
+    ...SPAN_HEADER_COLS.map((col) => ({ s: { r: 2, c: col }, e: { r: 3, c: col } })),
+    { s: { r: nurseSignRowIndex, c: 0 }, e: { r: nurseSignRowIndex, c: COLUMN_COUNT - 1 } },
+    { s: { r: lastRowIndex, c: 0 }, e: { r: lastRowIndex, c: COLUMN_COUNT - 1 } }
+  ]
+
+  const ws = XLSX.utils.aoa_to_sheet(aoa)
+  ws['!merges'] = merges
+  ws['!cols'] = COLUMN_WIDTH
+  ws['!rows'] = rowHeights
+
+  const range = XLSX.utils.decode_range(ws['!ref'] || 'A1')
+  for (let R = range.s.r; R <= range.e.r; R++) {
+    const role = rowRoles[R]
+    for (let C = range.s.c; C <= range.e.c; C++) {
+      const cellRef = XLSX.utils.encode_cell({ r: R, c: C })
+      if (!ws[cellRef]) ws[cellRef] = { v: '', t: 's' }
+      const cell: Recordable = ws[cellRef]
+      cell.s = cell.s || {}
+      cell.s.font = { sz: 11, name: '宋体' }
+      cell.s.alignment = { vertical: 'center', horizontal: 'center', wrapText: true }
+
+      if (role === 'title') {
+        cell.s.font = { sz: 18, bold: true, name: '宋体' }
+      } else if (role === 'info') {
+        cell.s.font = { sz: 11, bold: true, name: '宋体' }
+        cell.s.alignment = { vertical: 'center', horizontal: 'left', wrapText: true }
+      } else if (role === 'header1' || role === 'header2') {
+        cell.s.border = FULL_BORDER
+        cell.s.font = { sz: 11, bold: true, name: '宋体' }
+      } else if (role === 'data') {
+        cell.s.border = FULL_BORDER
+        // 病情观察及护理内容较长,左对齐并自动换行
+        if (C === 7) {
+          cell.s.alignment = { vertical: 'center', horizontal: 'left', wrapText: true }
+        }
+      } else if (role === 'nurseSign') {
+        // 护士签名行:位于表格下方,无边框、左对齐、加粗
+        cell.s.alignment = { vertical: 'center', horizontal: 'left' }
+        cell.s.font = { sz: 11, bold: true, name: '宋体' }
+      } else {
+        // 页码行:无边框,右对齐
+        cell.s.alignment = { vertical: 'center', horizontal: 'right' }
+      }
+    }
+  }
+
+  const wb = XLSX.utils.book_new()
+  XLSX.utils.book_append_sheet(wb, ws, '护理记录单')
+  XLSX.writeFile(wb, `${fileName}.xlsx`)
+}

+ 300 - 0
src/views/elderly/assess/nursing-record-sheet/index.vue

@@ -0,0 +1,300 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="90px"
+    >
+      <TenantSelect
+        v-model="queryParams.tenantIds"
+        placeholder="请选择机构名称"
+        prop="tenantIds"
+      />
+      <el-form-item label="长者姓名" prop="elderName">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="请输入长者姓名"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="记录日期" prop="recordDate">
+        <el-date-picker
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          v-model="queryParams.recordDate"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <div class="mb-10px">
+      <ButtonAdd @click="openForm()" />
+      <el-button type="warning" plain @click="handleSingleExport">
+        <Icon icon="ep:download" class="mr-5px" /> 单人导出
+      </el-button>
+      <el-button type="danger" plain :disabled="ids.length === 0" @click="handleDelete">
+        <Icon icon="ep:delete" class="mr-5px" /> 删除
+      </el-button>
+    </div>
+
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :header-cell-style="tableHeaderColor"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="40" fixed />
+      <el-table-column
+        label="序号"
+        type="index"
+        align="center"
+        header-align="center"
+        width="60"
+        fixed
+      />
+      <el-table-column
+        v-if="userStore.getOrgTenant.multiple"
+        prop="tenantName"
+        label="所属机构"
+        align="center"
+        header-align="center"
+        min-width="160"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="elderName"
+        label="长者姓名"
+        align="center"
+        header-align="center"
+        min-width="100"
+        show-overflow-tooltip
+      />
+      <el-table-column label="性别" align="center" header-align="center" width="70">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.elderSex" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="bedInfo"
+        label="楼层房号床号"
+        align="center"
+        header-align="center"
+        min-width="160"
+        show-overflow-tooltip
+      />
+      <el-table-column label="日期" align="center" header-align="center" width="110">
+        <template #default="scope">
+          {{ scope.row.recordDate ? formatTime(scope.row.recordDate, 'yyyy-MM-dd') : '' }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="recordTime"
+        label="时间"
+        align="center"
+        header-align="center"
+        width="90"
+      />
+      <el-table-column label="体温℃" align="center" header-align="center" width="90">
+        <template #default="scope">{{ scope.row.temperature }}</template>
+      </el-table-column>
+      <el-table-column label="血氧Spo2" align="center" header-align="center" width="100">
+        <template #default="scope">{{ scope.row.bloodOxygen }}</template>
+      </el-table-column>
+      <el-table-column
+        prop="pulse"
+        label="脉搏(次/分)"
+        align="center"
+        header-align="center"
+        width="110"
+      />
+      <el-table-column
+        prop="respiration"
+        label="呼吸(次/分)"
+        align="center"
+        header-align="center"
+        width="110"
+      />
+      <el-table-column label="血压mmHg" align="center" header-align="center" width="110">
+        <template #default="scope">
+          {{
+            scope.row.systolicPressure || scope.row.diastolicPressure
+              ? `${scope.row.systolicPressure ?? ''}/${scope.row.diastolicPressure ?? ''}`
+              : ''
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="observation"
+        label="病情观察及护理"
+        align="center"
+        header-align="center"
+        min-width="220"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="signer"
+        label="签名"
+        align="center"
+        header-align="center"
+        width="100"
+        show-overflow-tooltip
+      />
+      <el-table-column label="操作" align="center" header-align="center" width="180" fixed="right">
+        <template #default="scope">
+          <el-button link type="primary" @click="openDetail(scope.row.id)"> 详情 </el-button>
+          <el-button link type="primary" @click="openForm(scope.row)">编辑</el-button>
+          <el-button link type="danger" @click="handleDeleteOne(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <AddForm ref="formRef" @success="getList" />
+  <DetailForm ref="detailRef" />
+</template>
+
+<script lang="ts" setup>
+import { getNursingRecordPage, deleteNursingRecord } from '@/api/elderly/assess/nursing-record-sheet'
+import { exportNursingRecordSheet } from './exportSheet'
+import { DICT_TYPE } from '@/utils/dict'
+import { tableHeaderColor } from '@/utils/table'
+import { formatTime } from '@/utils'
+import ButtonAdd from '@/components/ButtonAdd/src/ButtonAdd.vue'
+import AddForm from './AddForm.vue'
+import DetailForm from './DetailForm.vue'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'NursingRecord' })
+
+const userStore = useUserStore()
+const message = useMessage()
+const { t } = useI18n()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总条数
+const list = ref([]) // 列表的数据
+const queryParams = reactive<any>({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: undefined,
+  recordDate: undefined,
+  tenantIds: userStore.orgTenantId
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await getNursingRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 新增/编辑操作 */
+const formRef = ref()
+const openForm = (row?: any) => {
+  if (queryParams.tenantIds.length !== 1 && !row?.tenantId) {
+    message.error('请选择且仅选择一个机构')
+    return
+  }
+  formRef.value.open(row?.tenantId || queryParams.tenantIds[0], row?.id)
+}
+
+/** 详情操作 */
+const detailRef = ref()
+const openDetail = (id) => {
+  detailRef.value.open(id)
+}
+
+/** 勾选 */
+const ids = ref<number[]>([])
+const handleSelectionChange = (val) => {
+  ids.value = val.map((item) => item.id)
+}
+
+/** 删除单条 */
+const handleDeleteOne = async (row) => {
+  try {
+    await message.delConfirm()
+    await deleteNursingRecord(row.id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 批量删除 */
+const handleDelete = async () => {
+  try {
+    await message.delConfirm()
+    for (const id of ids.value) {
+      await deleteNursingRecord(id)
+    }
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 单人导出:按纸质护理记录单版式导出 */
+const handleSingleExport = async () => {
+  if (!queryParams.elderName) {
+    message.warning('请先输入长者姓名')
+    return
+  }
+  const data = await getNursingRecordPage({ ...queryParams, pageNo: 1, pageSize: 99999 })
+  const records = Array.isArray(data?.list) ? data.list : []
+  if (records.length === 0) {
+    message.warning(`未查询到长者「${queryParams.elderName}」的护理记录,无法导出`)
+    return
+  }
+  // 表格按时间正序展示,与纸质记录单一致
+  const sorted = [...records].sort((a, b) =>
+    a.recordDate === b.recordDate
+      ? String(a.recordTime || '').localeCompare(String(b.recordTime || ''))
+      : String(a.recordDate).localeCompare(String(b.recordDate))
+  )
+  exportNursingRecordSheet(sorted, `护理记录单_${queryParams.elderName}`)
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>