Sfoglia il codice sorgente

巡房项目日志模块需求修改

xiongxing 1 settimana fa
parent
commit
d0be83f785

+ 3 - 3
src/api/elderly/nursing/index.ts

@@ -383,10 +383,10 @@ export const getElderlyItemsRoundPage = (params) => {
   })
 }
 
-/** 长者巡房项目详情 */
-export const getElderlyItemsRound = (id) => {
+/** 长者巡房明细分页(GET elderly-items-round/get,返回 ElderlyItemsRoundRespVO:长者信息 + pageResult) */
+export const getElderlyItemsRoundDetail = (params: Recordable) => {
   return request.get({
     url: 'elderly-items-round/get',
-    params: { id }
+    params
   })
 }

+ 0 - 19
src/views/elderly/nursing/column.ts

@@ -191,24 +191,5 @@ export const ElderlyItemsRoundLogColumns = reactive([
   {
     label: '角色',
     field: 'roleLabel',
-  },
-  {
-    label: '巡房时间',
-    field: 'roundTime',
-    type: '9',
-    width: 170
-  },
-  {
-    label: '巡视项目组',
-    field: 'itemsLabel',
-  },
-  {
-    label: '备注',
-    field: 'remark',
-  },
-  {
-    label: '创建时间',
-    field: 'createTime',
-    type: '9',
   }
 ])

+ 310 - 75
src/views/elderly/nursing/room-Inspection-project-log/Detail.vue

@@ -1,51 +1,136 @@
 <template>
   <Dialog
     v-model="dialogVisible"
-    width="800px"
+    width="90%"
     title="巡房项目详情"
     class="form-tag-dialog"
     scroll
     @close="handleClosed"
   >
-    <el-descriptions v-loading="loading" :column="2" border>
-      <el-descriptions-item label="记录人">{{ detail.recorder || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="角色">{{ getRoleSubmitLabel(detail.role) }}</el-descriptions-item>
-      <el-descriptions-item label="长者姓名">{{ detail.elderName || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="房间号">{{ detail.roomName ?? '-' }}</el-descriptions-item>
-      <el-descriptions-item label="巡房时间" :span="2">{{
-        formatBackendDateTime(detail.roundTime) || '-'
-      }}</el-descriptions-item>
-      <el-descriptions-item label="巡视项目组" :span="2">
-        <template v-if="formatItems(detail.items).length">
-          <el-tag
-            v-for="(it, i) in formatItems(detail.items)"
-            :key="i"
-            class="mr-5px mb-5px"
-            size="small"
-            effect="plain"
+    <div v-loading="detailLoading">
+      <div class="info-title">长者信息</div>
+      <div class="info-wrap mb-15px">
+        <el-row :gutter="20">
+          <el-col :span="8" :xs="24" class="header-item"
+            >长者姓名:{{ header.elderName || '-' }}</el-col
           >
-            {{ it }}
-          </el-tag>
-        </template>
-        <span v-else>-</span>
-      </el-descriptions-item>
-      <el-descriptions-item label="语音" :span="2">
-        <template v-if="voiceParts.hasAny">
-          <div v-for="(url, i) in voiceParts.audios" :key="'audio-' + i" class="voice-row">
-            <audio :src="url" controls class="voice-audio"></audio>
-          </div>
-          <div v-for="(t, i) in voiceParts.texts" :key="'text-' + i" class="voice-text">
-            {{ t }}
-          </div>
-        </template>
-        <span v-else>-</span>
-      </el-descriptions-item>
-      <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="创建时间">{{ formatBackendDateTime(detail.createTime) || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="更新时间">{{ formatBackendDateTime(detail.updateTime) || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="创建人">{{ detail.creator || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="更新人">{{ detail.updater || '-' }}</el-descriptions-item>
-    </el-descriptions>
+          <el-col :span="8" :xs="24" class="header-item"
+            >楼栋:{{ header.buildName || '-' }}</el-col
+          >
+          <el-col :span="8" :xs="24" class="header-item"
+            >楼层:{{ header.floorName || '-' }}</el-col
+          >
+          <el-col :span="8" :xs="24" class="header-item"
+            >房间号:{{ header.roomName || '-' }}</el-col
+          >
+          <el-col :span="8" :xs="24" class="header-item"
+            >床位号:{{ header.bedName || '-' }}</el-col
+          >
+        </el-row>
+      </div>
+
+      <div class="info-title">明细查询</div>
+      <el-form class="detail-query-form mb-15px" :inline="true" @submit.prevent>
+        <el-form-item label="巡房时间">
+          <el-date-picker
+            v-model="detailRoundTime"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            clearable
+            class="!w-280px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="detailLoading" @click="handleDetailQuery">
+            查询
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table
+        :data="records"
+        :header-cell-style="(data) => tableHeaderColor(data) || {}"
+        max-height="58vh"
+      >
+        <el-table-column label="巡房时间" prop="roundTime" min-width="160" show-overflow-tooltip>
+          <template #default="scope">
+            {{ formatBackendDateTime(scope.row.roundTime) || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="记录人" prop="recorder" width="100" show-overflow-tooltip />
+        <el-table-column label="角色" prop="role" width="100" show-overflow-tooltip>
+          <template #default="scope">
+            {{ getRoleSubmitLabel(scope.row.role) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" prop="status" width="96" align="center">
+          <template #default="scope">
+            <el-tag v-if="isAbnormalStatus(scope.row.status)" type="danger" size="small"
+              >异常</el-tag
+            >
+            <el-tag v-else-if="isNormalStatus(scope.row.status)" type="success" size="small"
+              >正常</el-tag
+            >
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="巡视项目组" min-width="160">
+          <template #default="scope">
+            <template v-if="splitItemLabels(scope.row.items).length">
+              <el-tag
+                v-for="(t, i) in splitItemLabels(scope.row.items)"
+                :key="i"
+                class="mr-5px mb-5px"
+                size="small"
+                effect="plain"
+              >
+                {{ t }}
+              </el-tag>
+            </template>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="语音" min-width="200">
+          <template #default="scope">
+            <template v-if="getVoiceParts(scope.row.voice).hasAny">
+              <div
+                v-for="(url, i) in getVoiceParts(scope.row.voice).audios"
+                :key="'a-' + scope.$index + '-' + i"
+                class="voice-row"
+              >
+                <audio :src="url" controls class="voice-audio"></audio>
+              </div>
+              <div
+                v-for="(t, i) in getVoiceParts(scope.row.voice).texts"
+                :key="'t-' + scope.$index + '-' + i"
+                class="voice-text"
+              >
+                {{ t }}
+              </div>
+            </template>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
+        <el-table-column label="创建时间" prop="createTime" min-width="160" show-overflow-tooltip>
+          <template #default="scope">
+            {{ formatBackendDateTime(scope.row.createTime) || '-' }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-if="detailTotal > 0" class="flex justify-end">
+        <Pagination
+          :total="detailTotal"
+          v-model:page="detailPageNo"
+          v-model:limit="detailPageSize"
+          @pagination="onDetailPagination"
+        />
+      </div>
+    </div>
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
     </template>
@@ -53,43 +138,69 @@
 </template>
 
 <script setup lang="ts">
-import { getElderlyItemsRound } from '@/api/elderly/nursing'
+import { getElderlyItemsRoundDetail } from '@/api/elderly/nursing'
 import { formatBackendDateTime } from '@/utils/formatTime'
+import { tableHeaderColor } from '@/utils/table'
 import { getRoleSubmitLabel } from './roleSubmitLabel'
+import dayjs from 'dayjs'
 
 defineOptions({ name: 'ElderlyItemsRoundDetail' })
 
-export type ElderlyItemsRoundDetailVO = {
+/** 巡房记录明细(PageResultRecord.list 单项) */
+export type ElderlyItemsRoundRecordVO = Recordable & {
   id?: number
   recorder?: string
   role?: string
-  elderId?: number
-  elderName?: string
-  buildId?: number
-  floorId?: number
-  roomId?: number
-  roomName?: string
-  items?: string | string[]
+  items?: string
   voice?: string
   remark?: string
-  tenantId?: number
-  /** 巡房时间(时间戳或与后端约定格式) */
+  status?: number | string
   roundTime?: string | number
   createTime?: string | number
-  updateTime?: string | number
-  creator?: string
-  updater?: string
+}
+
+const message = useMessage()
+
+/** 详情内默认巡房时间:本月起止 */
+function createDetailDefaultRoundTime(): string[] {
+  const start = dayjs().startOf('month')
+  const end = dayjs().endOf('month')
+  return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')]
 }
 
 const dialogVisible = ref(false)
-const loading = ref(false)
-const detail = reactive<ElderlyItemsRoundDetailVO>({})
+const detailLoading = ref(false)
+/** 传给接口 roundTime:[开始,结束],与列表页同款 YYYY-MM-DD */
+const detailRoundTime = ref<string[] | undefined>(undefined)
+
+const header = reactive({
+  elderId: undefined as number | undefined,
+  elderName: '',
+  buildName: '',
+  floorName: '',
+  roomName: '',
+  bedName: ''
+})
+
+const records = ref<ElderlyItemsRoundRecordVO[]>([])
+const detailPageNo = ref(1)
+const detailPageSize = ref(10)
+const detailTotal = ref(0)
+/** 详情 total 缺失时服务端一次返回全集,前端切片分页 */
+const detailRecordsAll = ref<ElderlyItemsRoundRecordVO[]>([])
 
-function emptyDetail() {
-  Object.keys(detail).forEach((k) => delete (detail as Recordable)[k])
+let listFiltersSnapshot: Recordable = {}
+let elderIdSnapshot: number | undefined
+
+function resetHeader() {
+  header.elderId = undefined
+  header.elderName = ''
+  header.buildName = ''
+  header.floorName = ''
+  header.roomName = ''
+  header.bedName = ''
 }
 
-/** items:按中英文逗号拆分;数组则每项再拆分。voice 仍用本函数(同分隔规则) */
 function splitDelimited(raw: unknown): string[] {
   if (raw == null || raw === '') return []
   if (Array.isArray(raw)) {
@@ -101,12 +212,12 @@ function splitDelimited(raw: unknown): string[] {
     .filter(Boolean)
 }
 
-function formatItems(items: string | string[] | undefined) {
+function splitItemLabels(items: unknown) {
   return splitDelimited(items)
 }
 
-const voiceParts = computed(() => {
-  const parts = splitDelimited(detail.voice)
+function getVoiceParts(raw: unknown) {
+  const parts = splitDelimited(raw)
   const audios: string[] = []
   const texts: string[] = []
   for (const p of parts) {
@@ -117,41 +228,165 @@ const voiceParts = computed(() => {
     }
   }
   return { audios, texts, hasAny: audios.length > 0 || texts.length > 0 }
-})
+}
 
-const open = async (row: Recordable) => {
-  const id = row?.id as number | undefined
-  if (id == null) return
-  dialogVisible.value = true
-  loading.value = true
-  emptyDetail()
+/** 接口约定:status 0 异常,1 正常 */
+function isAbnormalStatus(s: unknown) {
+  return s !== undefined && s !== '' && Number(s) === 0
+}
+
+function isNormalStatus(s: unknown) {
+  return s !== undefined && s !== '' && Number(s) === 1
+}
+
+function syncDetailPagedSlice() {
+  const all = detailRecordsAll.value
+  if (!all.length) {
+    records.value = []
+    return
+  }
+  const start = (detailPageNo.value - 1) * detailPageSize.value
+  records.value = all.slice(start, start + detailPageSize.value)
+}
+
+/** 解析 elderly-items-round/get 返回 ElderlyItemsRoundRespVO */
+function parseDetailResponse(res: Recordable) {
+  const pageResult = res.pageResult as Recordable | undefined
+  let list: ElderlyItemsRoundRecordVO[] = []
+  let total: number | undefined
+  if (pageResult && typeof pageResult === 'object' && !Array.isArray(pageResult)) {
+    const rawList = pageResult.list
+    list = Array.isArray(rawList) ? (rawList as ElderlyItemsRoundRecordVO[]) : []
+    const t = pageResult.total
+    if (t !== undefined && t !== null && t !== '') {
+      total = Number(t)
+    }
+  }
+  return {
+    elderId: res.elderId as number | undefined,
+    elderName: (res.elderName as string) || '',
+    buildName: (res.buildName as string) || '',
+    floorName: (res.floorName as string) || '',
+    roomName: (res.roomName as string) || '',
+    bedName: (res.bedName as string) || '',
+    list,
+    total
+  }
+}
+
+function buildDetailRequestParams(): Recordable {
+  const params: Recordable = {
+    ...listFiltersSnapshot,
+    elderId: elderIdSnapshot,
+    pageNo: detailPageNo.value,
+    pageSize: detailPageSize.value
+  }
+  const rt = detailRoundTime.value
+  if (Array.isArray(rt) && rt.length >= 2 && rt[0] && rt[1]) {
+    params.roundTime = [rt[0], rt[1]]
+  }
+  return params
+}
+
+async function fetchDetailList() {
+  if (elderIdSnapshot == null) return
+  detailLoading.value = true
   try {
-    const data = (await getElderlyItemsRound(id)) as ElderlyItemsRoundDetailVO
-    Object.assign(detail, data ?? {})
+    const raw = (await getElderlyItemsRoundDetail(buildDetailRequestParams())) as Recordable
+    const parsed = parseDetailResponse(raw ?? {})
+
+    header.elderId = parsed.elderId
+    header.elderName = parsed.elderName
+    header.buildName = parsed.buildName
+    header.floorName = parsed.floorName
+    header.roomName = parsed.roomName
+    header.bedName = parsed.bedName
+
+    if (parsed.total !== undefined && !Number.isNaN(parsed.total)) {
+      detailRecordsAll.value = []
+      records.value = parsed.list
+      detailTotal.value = parsed.total
+    } else {
+      detailRecordsAll.value = parsed.list
+      detailTotal.value = detailRecordsAll.value.length
+      syncDetailPagedSlice()
+    }
   } finally {
-    loading.value = false
+    detailLoading.value = false
+  }
+}
+
+function onDetailPagination() {
+  if (detailRecordsAll.value.length > 0) {
+    syncDetailPagedSlice()
+  } else {
+    fetchDetailList()
   }
 }
 
+function handleDetailQuery() {
+  detailPageNo.value = 1
+  detailRecordsAll.value = []
+  fetchDetailList()
+}
+
+const open = async (row: Recordable, listFilters: Recordable) => {
+  const eid = row?.elderId as number | undefined
+  if (eid == null) {
+    message.warning('该行缺少长者 id,无法查询巡房明细')
+    return
+  }
+  elderIdSnapshot = eid
+  listFiltersSnapshot = { ...(listFilters || {}) }
+  dialogVisible.value = true
+  detailRoundTime.value = createDetailDefaultRoundTime()
+  detailPageNo.value = 1
+  detailPageSize.value = 10
+  detailTotal.value = 0
+  detailRecordsAll.value = []
+  records.value = []
+  resetHeader()
+
+  await fetchDetailList()
+}
+
 defineExpose({ open })
 
 const handleClosed = () => {
-  emptyDetail()
   dialogVisible.value = false
+  elderIdSnapshot = undefined
+  listFiltersSnapshot = {}
+  detailRoundTime.value = undefined
+  detailRecordsAll.value = []
+  records.value = []
+  detailTotal.value = 0
+  detailPageNo.value = 1
+  resetHeader()
 }
 </script>
 
 <style lang="scss" scoped>
+.info-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+.header-item {
+  margin-bottom: 6px;
+  font-size: 14px;
+  color: var(--el-text-color-regular);
+}
 .voice-audio {
-  max-width: 100%;
+  max-width: 220px;
   height: 32px;
   vertical-align: middle;
 }
 .voice-row + .voice-row {
-  margin-top: 8px;
+  margin-top: 6px;
 }
 .voice-text {
-  margin-top: 6px;
+  margin-top: 4px;
+  font-size: 13px;
   color: var(--el-text-color-regular);
   word-break: break-all;
 }

+ 11 - 39
src/views/elderly/nursing/room-Inspection-project-log/index.vue

@@ -29,17 +29,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="巡房时间" prop="roundTime">
-        <el-date-picker
-          v-model="queryParams.roundTime"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          value-format="YYYY-MM-DD"
-          class="!w-280px"
-        />
-      </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>
@@ -74,7 +63,6 @@ import Detail from './Detail.vue'
 import { ElderlyItemsRoundLogColumns } from '../column'
 import { useUserStore } from '@/store/modules/user'
 import { ROLE_SUBMIT_LABEL, getRoleSubmitLabel } from './roleSubmitLabel'
-import dayjs from 'dayjs'
 
 const userStore = useUserStore()
 
@@ -85,19 +73,11 @@ const roleSubmitOptions = Object.entries(ROLE_SUBMIT_LABEL).map(([enumKey, label
 
 defineOptions({ name: 'RoomInspectionProjectLog' })
 
-/** 默认巡房时间:[今天, 今天起往后一个月](按日历月加一个月) */
-function createDefaultRoundTimeRange(): string[] {
-  const start = dayjs().startOf('day')
-  const end = start.add(1, 'month')
-  return [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')]
-}
-
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   elderName: '',
   role: '',
-  roundTime: createDefaultRoundTimeRange(),
   tenantIds: userStore.orgTenantId
 })
 
@@ -113,33 +93,17 @@ const handleQuery = () => {
 
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
-  queryParams.roundTime = createDefaultRoundTimeRange()
   handleQuery()
 }
 
 const buildPageParams = () => {
   const p = { ...queryParams } as Recordable
-  if (!Array.isArray(p.roundTime) || p.roundTime.length < 2) {
-    delete p.roundTime
-  }
   if (p.role === '' || p.role == null) {
     delete p.role
   }
   return p
 }
 
-/** 巡视项目组:按中英文逗号拆分;若为数组则对每一项再拆分后合并(兼容 ["a,b","c"]) */
-function splitItemsDelimited(raw: unknown): string[] {
-  if (raw == null || raw === '') return []
-  if (Array.isArray(raw)) {
-    return raw.flatMap((x) => splitItemsDelimited(String(x)))
-  }
-  return String(raw)
-    .split(/[,,]+/)
-    .map((s) => s.trim())
-    .filter(Boolean)
-}
-
 const getList = async () => {
   loading.value = true
   try {
@@ -147,7 +111,6 @@ const getList = async () => {
     const raw = data.list ?? []
     list.value = raw.map((r: Recordable) => ({
       ...r,
-      itemsLabel: splitItemsDelimited(r.items).join('、'),
       roleLabel: getRoleSubmitLabel(r.role)
     }))
     total.value = data.total ?? 0
@@ -156,9 +119,18 @@ const getList = async () => {
   }
 }
 
-const detailRef = ref<{ open: (row: Recordable) => void }>()
+/** 详情分页:与列表查询对齐(不传列表 pageNo/pageSize),支持文档中的可选条件 */
+const buildDetailListFilters = () => {
+  const { tenantIds, role, elderName } = queryParams as Recordable
+  const f: Recordable = { tenantIds }
+  if (role !== '' && role != null) f.role = role
+  if (elderName !== '' && elderName != null) f.elderName = elderName
+  return f
+}
+
+const detailRef = ref<{ open: (row: Recordable, listFilters: Recordable) => void }>()
 const openDetail = (row: Recordable) => {
-  detailRef.value?.open(row)
+  detailRef.value?.open(row, buildDetailListFilters())
 }
 
 onMounted(() => {