index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <template>
  2. <ContentWrap>
  3. <el-form
  4. class="-mb-15px"
  5. :model="queryParams"
  6. ref="queryFormRef"
  7. :inline="true"
  8. label-width="80px"
  9. >
  10. <el-form-item prop="tenantIds">
  11. <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
  12. </el-form-item>
  13. <el-form-item label="长者名称" prop="elderName">
  14. <TgInput @keyup.enter="handleQuery" v-model="queryParams.elderName" class="!w-160px" />
  15. </el-form-item>
  16. <el-form-item label="检查日期" prop="checkDate">
  17. <el-date-picker
  18. v-model="queryParams.checkDate"
  19. type="daterange"
  20. range-separator="至"
  21. start-placeholder="开始日期"
  22. end-placeholder="结束日期"
  23. value-format="YYYY-MM-DD"
  24. clearable
  25. class="!w-280px"
  26. />
  27. </el-form-item>
  28. <el-form-item>
  29. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  30. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  31. </el-form-item>
  32. </el-form>
  33. </ContentWrap>
  34. <ContentWrap>
  35. <Table2 v-loading="loading" :list="list" :columns="columns" :queryParams="queryParams">
  36. <template #pre="{ scope }">
  37. <el-button link type="primary" @click="openDetail(scope)">查看</el-button>
  38. </template>
  39. </Table2>
  40. <Pagination
  41. :total="total"
  42. v-model:page="queryParams.pageNo"
  43. v-model:limit="queryParams.pageSize"
  44. @pagination="getList"
  45. />
  46. <Dialog
  47. v-model="detailVisible"
  48. width="90%"
  49. title="安全检查日志详情"
  50. class="form-tag-dialog"
  51. scroll
  52. @close="closeDetail"
  53. >
  54. <div v-loading="detailLoading">
  55. <div class="info-title">长者信息</div>
  56. <div class="info-wrap mb-15px">
  57. <el-row :gutter="20">
  58. <el-col :span="8" :xs="24" class="header-item">长者姓名:{{ detailHeader.elderName || '-' }}</el-col>
  59. <el-col :span="8" :xs="24" class="header-item">房间号:{{ detailHeader.roomName || '-' }}</el-col>
  60. <el-col :span="8" :xs="24" class="header-item">床位号:{{ detailHeader.bedName || '-' }}</el-col>
  61. </el-row>
  62. </div>
  63. <div class="info-title">历史记录查询</div>
  64. <el-form class="detail-query-form mb-15px" :inline="true" @submit.prevent>
  65. <el-form-item label="检查日期">
  66. <el-date-picker
  67. v-model="detailCheckDateRange"
  68. type="daterange"
  69. range-separator="至"
  70. start-placeholder="开始日期"
  71. end-placeholder="结束日期"
  72. value-format="YYYY-MM-DD"
  73. clearable
  74. class="!w-280px"
  75. />
  76. </el-form-item>
  77. <el-form-item>
  78. <el-button type="primary" :loading="detailLoading" @click="handleDetailQuery">查询</el-button>
  79. </el-form-item>
  80. </el-form>
  81. <el-table :data="detailRecords" max-height="58vh">
  82. <el-table-column type="expand">
  83. <template #default="scope">
  84. <div class="expand-wrap">
  85. <div class="expand-title">安全检查明细</div>
  86. <el-descriptions :column="4" border size="small" class="mb-10px" label-width="130px">
  87. <el-descriptions-item label="被检查人签字">
  88. {{ scope.row.beCheckSign || '-' }}
  89. </el-descriptions-item>
  90. <el-descriptions-item label="长者自带电器设备">
  91. {{ scope.row.elderSelfEquipment || '-' }}
  92. </el-descriptions-item>
  93. <el-descriptions-item label="存在问题及整改要求">
  94. {{ scope.row.safeRemark || '-' }}
  95. </el-descriptions-item>
  96. <el-descriptions-item label="其他">
  97. {{ scope.row.other || '-' }}
  98. </el-descriptions-item>
  99. </el-descriptions>
  100. <el-table :data="scope.row.items || []" size="small" class="inner-table">
  101. <el-table-column label="检查项" prop="label" min-width="220" show-overflow-tooltip />
  102. <el-table-column label="结果" prop="status" width="96" align="center">
  103. <template #default="s">
  104. <el-tag v-if="Number(s.row.status) === 1" type="success" size="small">无问题</el-tag>
  105. <el-tag v-else-if="Number(s.row.status) === 0" type="danger" size="small">有问题</el-tag>
  106. <span v-else>-</span>
  107. </template>
  108. </el-table-column>
  109. </el-table>
  110. </div>
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="检查时间" prop="checkTime" min-width="160" show-overflow-tooltip>
  114. <template #default="scope">
  115. {{ formatBackendDateTime(scope.row.checkTime, 'YYYY-MM-DD HH:mm') || '-' }}
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="状态" prop="overallStatus" align="center">
  119. <template #default="scope">
  120. <el-tag v-if="Number(scope.row.overallStatus) === 1" type="success" size="small">正常</el-tag>
  121. <el-tag v-else-if="Number(scope.row.overallStatus) === 0" type="danger" size="small">异常</el-tag>
  122. <span v-else>-</span>
  123. </template>
  124. </el-table-column>
  125. <el-table-column label="异常项数" prop="failCount" align="center" />
  126. <el-table-column label="记录人" prop="recorder" show-overflow-tooltip />
  127. <!-- <el-table-column label="问题与要求" prop="question" min-width="180" show-overflow-tooltip /> -->
  128. <el-table-column label="被检查人签字" prop="beCheckSign" show-overflow-tooltip />
  129. <el-table-column label="创建时间" prop="createTime" min-width="160" show-overflow-tooltip>
  130. <template #default="scope">
  131. {{ formatBackendDateTime(scope.row.createTime, 'YYYY-MM-DD HH:mm') || '-' }}
  132. </template>
  133. </el-table-column>
  134. </el-table>
  135. <div v-if="detailTotal > 0" class="flex justify-end">
  136. <Pagination
  137. :total="detailTotal"
  138. v-model:page="detailPageNo"
  139. v-model:limit="detailPageSize"
  140. @pagination="onDetailPagination"
  141. />
  142. </div>
  143. </div>
  144. <template #footer>
  145. <el-button @click="closeDetail">关闭</el-button>
  146. </template>
  147. </Dialog>
  148. </ContentWrap>
  149. </template>
  150. <script setup lang="ts">
  151. import { formatBackendDateTime } from '@/utils/formatTime'
  152. import dayjs from 'dayjs'
  153. import { useUserStore } from '@/store/modules/user'
  154. import { getElderlySafeCheckElderPage, getElderlySafeCheckRecordPage } from '@/api/elderly/nursing'
  155. defineOptions({ name: 'SafetyCheckLog' })
  156. type SafetyChecklistItem = {
  157. key?: string
  158. label?: string
  159. status?: number | string
  160. }
  161. type SafetyCheckElderRow = {
  162. elderId: number | string
  163. elderName: string
  164. roomName: string
  165. bedName: string
  166. }
  167. type ElderlySafeCheckRespVO = {
  168. id?: number | string
  169. elderId?: number | string
  170. elderName?: string
  171. roomName?: string
  172. bedName?: string
  173. safeSituation?: string
  174. safeRemark?: string
  175. elderSelfEquipment?: string
  176. question?: string
  177. beCheckSign?: string
  178. failCount?: number | string
  179. creator?: string
  180. createTime?: number | string
  181. }
  182. type SafetyCheckDetailRow = ElderlySafeCheckRespVO & {
  183. checkTime?: number | string
  184. items?: SafetyChecklistItem[]
  185. other?: string
  186. overallStatus: 0 | 1
  187. }
  188. const userStore = useUserStore()
  189. const message = useMessage()
  190. const columns = reactive([
  191. { label: '长者姓名', field: 'elderName' },
  192. { label: '房间号', field: 'roomName' },
  193. { label: '床位号', field: 'bedName' }
  194. ])
  195. const queryParams = reactive({
  196. pageNo: 1,
  197. pageSize: 10,
  198. elderName: '',
  199. checkDate: undefined as string[] | undefined,
  200. tenantIds: userStore.orgTenantId
  201. })
  202. const loading = ref(true)
  203. const total = ref(0)
  204. const list = ref<SafetyCheckElderRow[]>([])
  205. const queryFormRef = ref()
  206. function buildPageParams() {
  207. const p = { ...queryParams } as Recordable
  208. if (!p.elderName) delete p.elderName
  209. const cd = p.checkDate as string[] | undefined
  210. if (!Array.isArray(cd) || cd.length < 2 || !cd[0] || !cd[1]) {
  211. delete p.checkDate
  212. }
  213. return p
  214. }
  215. const handleQuery = () => {
  216. queryParams.pageNo = 1
  217. getList()
  218. }
  219. const resetQuery = () => {
  220. queryFormRef.value?.resetFields?.()
  221. handleQuery()
  222. }
  223. const getList = async () => {
  224. loading.value = true
  225. try {
  226. const data = await getElderlySafeCheckElderPage(buildPageParams())
  227. list.value = (data?.list ?? []) as SafetyCheckElderRow[]
  228. total.value = Number(data?.total ?? 0)
  229. } finally {
  230. loading.value = false
  231. }
  232. }
  233. const detailVisible = ref(false)
  234. const detailLoading = ref(false)
  235. const detailHeader = reactive({
  236. elderId: undefined as number | string | undefined,
  237. elderName: '',
  238. roomName: '',
  239. bedName: ''
  240. })
  241. const detailCheckDateRange = ref<string[] | undefined>(undefined)
  242. const detailRecords = ref<SafetyCheckDetailRow[]>([])
  243. const detailPageNo = ref(1)
  244. const detailPageSize = ref(10)
  245. const detailTotal = ref(0)
  246. function createDetailDefaultDateRange(): string[] {
  247. const start = dayjs().startOf('month')
  248. const end = dayjs().endOf('month')
  249. return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')]
  250. }
  251. function safeParseJson<T>(raw: unknown): T | undefined {
  252. if (raw == null || raw === '') return undefined
  253. if (typeof raw === 'object') return raw as T
  254. const s = String(raw)
  255. try {
  256. return JSON.parse(s) as T
  257. } catch {
  258. return undefined
  259. }
  260. }
  261. function parseSafeSituation(raw: unknown): { checkTime?: number | string; items: SafetyChecklistItem[]; other?: string } {
  262. const parsed = safeParseJson<any>(raw) ?? {}
  263. const items = Array.isArray(parsed.items) ? (parsed.items as SafetyChecklistItem[]) : []
  264. return { checkTime: parsed.checkTime, items, other: parsed.other }
  265. }
  266. function computeOverallStatusByFailCountAndItems(failCount: unknown, items: SafetyChecklistItem[]): 0 | 1 {
  267. if (failCount !== undefined && failCount !== null && failCount !== '') {
  268. const n = Number(failCount)
  269. if (!Number.isNaN(n)) return n > 0 ? 0 : 1
  270. }
  271. return items.some((x) => Number((x as any)?.status) === 0) ? 0 : 1
  272. }
  273. async function fetchDetailList() {
  274. if (detailHeader.elderId == null) return
  275. detailLoading.value = true
  276. try {
  277. const params: Recordable = {
  278. tenantIds: queryParams.tenantIds,
  279. elderId: detailHeader.elderId,
  280. pageNo: detailPageNo.value,
  281. pageSize: detailPageSize.value
  282. }
  283. const cd = detailCheckDateRange.value
  284. if (Array.isArray(cd) && cd.length >= 2 && cd[0] && cd[1]) {
  285. params.checkDate = [cd[0], cd[1]]
  286. }
  287. const data = await getElderlySafeCheckRecordPage(params)
  288. detailTotal.value = Number(data?.total ?? 0)
  289. const rawList = (data?.list ?? []) as ElderlySafeCheckRespVO[]
  290. detailRecords.value = rawList.map((r) => {
  291. const parsed = parseSafeSituation(r.safeSituation)
  292. return {
  293. ...r,
  294. checkTime: parsed.checkTime,
  295. items: parsed.items,
  296. other: parsed.other,
  297. overallStatus: computeOverallStatusByFailCountAndItems(r.failCount, parsed.items)
  298. } as SafetyCheckDetailRow
  299. })
  300. } finally {
  301. detailLoading.value = false
  302. }
  303. }
  304. function onDetailPagination() {
  305. fetchDetailList()
  306. }
  307. function handleDetailQuery() {
  308. detailPageNo.value = 1
  309. fetchDetailList()
  310. }
  311. function resetDetailHeader() {
  312. detailHeader.elderId = undefined
  313. detailHeader.elderName = ''
  314. detailHeader.roomName = ''
  315. detailHeader.bedName = ''
  316. }
  317. function openDetail(row: SafetyCheckElderRow) {
  318. if (!row?.elderId && row?.elderId !== 0) {
  319. message.warning('该行缺少长者 id,无法查询详情')
  320. return
  321. }
  322. detailVisible.value = true
  323. detailPageNo.value = 1
  324. detailPageSize.value = 10
  325. detailCheckDateRange.value = createDetailDefaultDateRange()
  326. detailRecords.value = []
  327. detailHeader.elderId = row.elderId
  328. detailHeader.elderName = row.elderName || ''
  329. detailHeader.roomName = row.roomName || ''
  330. detailHeader.bedName = row.bedName || ''
  331. fetchDetailList()
  332. }
  333. function closeDetail() {
  334. detailVisible.value = false
  335. resetDetailHeader()
  336. detailCheckDateRange.value = undefined
  337. detailRecords.value = []
  338. detailTotal.value = 0
  339. detailPageNo.value = 1
  340. detailPageSize.value = 10
  341. }
  342. onMounted(() => {
  343. getList()
  344. })
  345. </script>
  346. <style lang="scss" scoped>
  347. .info-title {
  348. margin-bottom: 8px;
  349. font-weight: 600;
  350. color: var(--el-text-color-primary);
  351. }
  352. .header-item {
  353. margin-bottom: 6px;
  354. font-size: 14px;
  355. color: var(--el-text-color-regular);
  356. }
  357. .expand-wrap {
  358. padding: 10px 10px 6px;
  359. }
  360. .expand-title {
  361. margin-bottom: 8px;
  362. font-weight: 600;
  363. color: var(--el-text-color-primary);
  364. }
  365. .inner-table {
  366. width: 100%;
  367. }
  368. </style>