浏览代码

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

xiongxing 4 周之前
父节点
当前提交
14a2e23529

+ 227 - 14
src/api/elderly/elder/discharge-summary/index.ts

@@ -10,16 +10,29 @@ import request from '@/config/axios'
 
 // ==================== 枚举 ====================
 
-/** 出院原因 */
+/**
+ * 出院原因
+ * 与「退住原因」字典 back_cause_arr 合并(全部项),键值与字典一致:
+ *   1 正常退住  2 死亡退住  3 转院退住  4 试住结束
+ *   5 居家照护  6 医院就诊  7 死亡退住(院内)  8 死亡退住(院外)  9 其他
+ */
 export const DISCHARGE_REASON = {
-  HOME_CARE: 1, // 居家照护
-  HOSPITAL: 2, // 医院就诊
-  DEATH_IN: 3, // 死亡退住(院内)
-  DEATH_OUT: 4, // 死亡退住(院外)
-  OTHER: 5 // 其他
+  NORMAL: 1, // 正常退住
+  DEATH: 2, // 死亡退住(仅有该大类,无院内/院外细分)
+  TRANSFER: 3, // 转院退住
+  TRIAL_END: 4, // 试住结束
+  HOME_CARE: 5, // 居家照护
+  HOSPITAL: 6, // 医院就诊
+  DEATH_IN: 7, // 死亡退住(院内)
+  DEATH_OUT: 8, // 死亡退住(院外)
+  OTHER: 9 // 其他
 } as const
 
 export const DISCHARGE_REASON_OPTIONS = [
+  { value: DISCHARGE_REASON.NORMAL, label: '正常退住' },
+  { value: DISCHARGE_REASON.DEATH, label: '死亡退住' },
+  { value: DISCHARGE_REASON.TRANSFER, label: '转院退住' },
+  { value: DISCHARGE_REASON.TRIAL_END, label: '试住结束' },
   { value: DISCHARGE_REASON.HOME_CARE, label: '居家照护' },
   { value: DISCHARGE_REASON.HOSPITAL, label: '医院就诊' },
   { value: DISCHARGE_REASON.DEATH_IN, label: '死亡退住(院内)' },
@@ -27,7 +40,7 @@ export const DISCHARGE_REASON_OPTIONS = [
   { value: DISCHARGE_REASON.OTHER, label: '其他' }
 ]
 
-/** 死亡出院(用药情况 / 出院指导 不需填写) */
+/** 死亡出院(用药情况 / 出院指导 不需填写):命中「死亡退住(院内/院外)」即触发 */
 export const isDeathDischarge = (reason: any): boolean =>
   Number(reason) === DISCHARGE_REASON.DEATH_IN || Number(reason) === DISCHARGE_REASON.DEATH_OUT
 
@@ -126,29 +139,229 @@ export interface DischargeSummaryVO {
 /** 提交表单结构(与 VO 一致,medications 为子表) */
 export type DischargeSummaryForm = DischargeSummaryVO
 
+// ==================== 字段转换(后端 dischargeAbstract <-> 前端表单) ====================
+// 后端接口与前端表单字段命名不一致,统一在此做对称转换,Form / Print / columns 无需感知后端字段名。
+// 后端字段一览:
+//   elderId/elderName/elderSex/elderSexName/elderAge/region/checkInDate/dischargeDate/reasonDischarge
+//   /doctorSign/anamnesis/recentHealthIssues/physicalActivityAbility/cognitionMentalState
+//   /diseaseTreatmentControl/recentMedicalTreatmentSituation/vitalSigns/physicalFunctions
+//   /keyIssues/safetyProtection/dietMedicalTreatment/signDate/medicines(medicineName/dose/usage/frequency)
+
+/** 生命体征:前端四段(P/R/收缩压/舒张压) -> 后端单个字符串 vitalSigns */
+const vitalSignsToStr = (form: any): string => {
+  const parts: string[] = []
+  if (form.pulse) parts.push(`P:${form.pulse}`)
+  if (form.respiration) parts.push(`R:${form.respiration}`)
+  const bp = [form.systolicPressure, form.diastolicPressure].filter((v) => v !== '' && v != null)
+  if (bp.length) parts.push(`HP:${bp.join('/')}`)
+  return parts.join(';')
+}
+
+/** 生命体征:后端字符串 vitalSigns -> 前端四段 */
+const vitalSignsFromStr = (vitalSigns?: string) => {
+  const res = { pulse: '', respiration: '', systolicPressure: '', diastolicPressure: '' }
+  if (!vitalSigns) return res
+  String(vitalSigns)
+    .split(/[;;]/)
+    .forEach((s) => {
+      s = s.trim()
+      if (s.startsWith('P')) {
+        res.pulse = s.replace(/^P[::]?\s*/, '')
+      } else if (s.startsWith('R')) {
+        res.respiration = s.replace(/^R[::]?\s*/, '')
+      } else if (s.startsWith('HP')) {
+        const [sys = '', dia = ''] = s.replace(/^HP[::]?\s*/, '').split('/')
+        res.systolicPressure = sys.trim()
+        res.diastolicPressure = dia.trim()
+      }
+    })
+  return res
+}
+
+/** 出院原因:前端枚举(+其他补充) -> 后端字符串 reasonDischarge("其他"时拼补充说明) */
+const reasonToStr = (dischargeReason: any, dischargeReasonOther?: string): string => {
+  const label = getDischargeReasonText(dischargeReason)
+  if (!label) return dischargeReasonOther || ''
+  if (Number(dischargeReason) === DISCHARGE_REASON.OTHER) {
+    return dischargeReasonOther ? `${label}:${dischargeReasonOther}` : label
+  }
+  return label
+}
+
+/** 出院原因:后端字符串 reasonDischarge -> 前端枚举(+其他补充) */
+const reasonFromStr = (text?: string) => {
+  if (!text) return { dischargeReason: undefined as number | undefined, dischargeReasonOther: '' }
+  const otherLabel = getDischargeReasonText(DISCHARGE_REASON.OTHER)
+  if (String(text).startsWith(otherLabel)) {
+    return {
+      dischargeReason: DISCHARGE_REASON.OTHER,
+      dischargeReasonOther: String(text).slice(otherLabel.length).replace(/^[::]/, '')
+    }
+  }
+  const matched = DISCHARGE_REASON_OPTIONS.find((o) => o.label === String(text))
+  return { dischargeReason: matched ? (matched.value as number) : undefined, dischargeReasonOther: '' }
+}
+
+/** 用药子表:前端 drugName/dosage/usageMethod/frequency -> 后端 medicineName/dose/usage/frequency */
+const toBackendMedicine = (m: DischargeMedication): any => ({
+  id: m.id,
+  medicineName: m.drugName,
+  dose: m.dosage,
+  usage: m.usageMethod,
+  frequency: m.frequency
+})
+
+/** 用药子表:后端 -> 前端 */
+const fromBackendMedicine = (m: any): DischargeMedication => ({
+  id: m?.id,
+  drugName: m?.medicineName,
+  dosage: m?.dose,
+  usageMethod: m?.usage,
+  frequency: m?.frequency
+})
+
+/**
+ * 枚举字段存库:前端 value(数字) -> 后端字符串 label。
+ * 后端 physicalActivityAbility / cognitionMentalState 均为 string 字段,
+ * 与 reasonDischarge 一致,实际存的是中文 label,故提交时传 label 文本。
+ */
+const optionValueToStored = (options: { value: number; label: string }[], value: any): string => {
+  const found = options.find((o) => Number(o.value) === Number(value))
+  return found ? found.label : value ?? ''
+}
+
+/** 枚举字段回显:后端 string(label 或 value) -> 前端 value(数字) */
+const optionStoredToValue = (
+  options: { value: number; label: string }[],
+  stored: any
+): number | undefined => {
+  if (stored === '' || stored === null || stored === undefined) return undefined
+  // 后端若直接存数字值,原样返回
+  if (options.some((o) => Number(o.value) === Number(stored))) return Number(stored)
+  // 否则按 label 反查 value
+  const found = options.find((o) => o.label === String(stored))
+  return found ? found.value : undefined
+}
+
+/** 提交:前端表单 -> 后端 SaveReqVO */
+const buildSavePayload = (form: DischargeSummaryForm): any => ({
+  id: form.id,
+  elderId: form.elderId,
+  dischargeDate: form.dischargeDate,
+  anamnesis: form.pastMedicalHistory,
+  recentHealthIssues: form.recentHealthProblems,
+  physicalActivityAbility: optionValueToStored(PHYSICAL_ABILITY_OPTIONS, form.physicalAbility),
+  cognitionMentalState: optionValueToStored(COGNITIVE_STATUS_OPTIONS, form.cognitiveStatus),
+  diseaseTreatmentControl: form.diseaseTreatment,
+  recentMedicalTreatmentSituation: form.recentMedicalVisit,
+  reasonDischarge: reasonToStr(form.dischargeReason, form.dischargeReasonOther),
+  vitalSigns: vitalSignsToStr(form),
+  physicalFunctions: form.bodyFunction,
+  keyIssues: form.focusProblems,
+  safetyProtection: form.safetyProtection,
+  dietMedicalTreatment: form.dietAndMedical,
+  doctorSign: form.doctorSign,
+  signDate: form.signDate,
+  tenantId: form.tenantId,
+  medicines: (form.medications || [])
+    .filter((m) => m.drugName)
+    .map((m) => toBackendMedicine(m))
+})
+
+/** 详情/列表项:后端 VO -> 前端表单结构 */
+const fromBackendVO = (data: any): any => {
+  const vitals = vitalSignsFromStr(data.vitalSigns)
+  const reason = reasonFromStr(data.reasonDischarge)
+  return {
+    id: data.id,
+    elderId: data.elderId,
+    elderName: data.elderName,
+    gender: data.elderSex,
+    genderName: data.elderSexName,
+    elderAge: data.elderAge,
+    bedName: data.region, // 居住区域:后端返回 region
+    checkInDate: data.checkInDate,
+    dischargeDate: data.dischargeDate,
+    pastMedicalHistory: data.anamnesis,
+    recentHealthProblems: data.recentHealthIssues,
+    physicalAbility: optionStoredToValue(PHYSICAL_ABILITY_OPTIONS, data.physicalActivityAbility),
+    cognitiveStatus: optionStoredToValue(COGNITIVE_STATUS_OPTIONS, data.cognitionMentalState),
+    diseaseTreatment: data.diseaseTreatmentControl,
+    recentMedicalVisit: data.recentMedicalTreatmentSituation,
+    dischargeReason: reason.dischargeReason,
+    dischargeReasonOther: reason.dischargeReasonOther,
+    dischargeReasonText: data.reasonDischarge || '', // 列表/打印直接展示后端文本
+    pulse: vitals.pulse,
+    respiration: vitals.respiration,
+    systolicPressure: vitals.systolicPressure,
+    diastolicPressure: vitals.diastolicPressure,
+    bodyFunction: data.physicalFunctions,
+    medications: (data.medicines || []).map((m: any) => fromBackendMedicine(m)),
+    focusProblems: data.keyIssues,
+    safetyProtection: data.safetyProtection,
+    dietAndMedical: data.dietMedicalTreatment,
+    doctorSign: data.doctorSign,
+    signDate: data.signDate,
+    tenantId: data.tenantId,
+    creator: data.creator,
+    createTime: data.createTime
+  }
+}
+
+/** 列表项:仅保留列表展示字段,并预计算展示辅助字段 */
+const fromBackendListItem = (item: any): any => {
+  const vo = fromBackendVO(item)
+  return {
+    id: vo.id,
+    elderId: vo.elderId,
+    elderName: vo.elderName,
+    gender: vo.gender,
+    genderName: vo.genderName,
+    elderAge: vo.elderAge,
+    elderAgeText:
+      vo.elderAge === undefined || vo.elderAge === null || vo.elderAge === ''
+        ? ''
+        : `${vo.elderAge} 岁`,
+    bedName: vo.bedName,
+    checkInDate: vo.checkInDate,
+    dischargeDate: vo.dischargeDate,
+    dischargeReason: vo.dischargeReason,
+    dischargeReasonOther: vo.dischargeReasonOther,
+    dischargeReasonText: vo.dischargeReasonText,
+    doctorSign: vo.doctorSign,
+    tenantId: vo.tenantId,
+    createTime: vo.createTime
+  }
+}
+
 // ==================== 接口 ====================
 
 /** 分页查询 */
-export const getDischargeSummaryPage = (params: any) => {
-  return request.get({ url: 'elderly/dischargeSummary/page', params })
+export const getDischargeSummaryPage = async (params: any) => {
+  const data = await request.get({ url: 'elderly/dischargeAbstract/page', params })
+  return {
+    list: (data?.list || []).map((item: any) => fromBackendListItem(item)),
+    total: data?.total || 0
+  }
 }
 
 /** 详情 */
-export const getDischargeSummary = (id: number) => {
-  return request.get({ url: `elderly/dischargeSummary/get?id=${id}` })
+export const getDischargeSummary = async (id: number) => {
+  const data = await request.get({ url: `elderly/dischargeAbstract/get?id=${id}` })
+  return fromBackendVO(data)
 }
 
 /** 新增 */
 export const createDischargeSummary = (data: DischargeSummaryForm) => {
-  return request.post({ url: 'elderly/dischargeSummary/create', data })
+  return request.post({ url: 'elderly/dischargeAbstract/create', data: buildSavePayload(data) })
 }
 
 /** 更新 */
 export const updateDischargeSummary = (data: DischargeSummaryForm) => {
-  return request.put({ url: 'elderly/dischargeSummary/update', data })
+  return request.put({ url: 'elderly/dischargeAbstract/update', data: buildSavePayload(data) })
 }
 
 /** 删除 */
 export const deleteDischargeSummary = (id: number) => {
-  return request.delete({ url: `elderly/dischargeSummary/delete?id=${id}` })
+  return request.delete({ url: `elderly/dischargeAbstract/delete?id=${id}` })
 }

+ 5 - 5
src/views/elderly/apply/discharge-summary/Form.vue

@@ -155,16 +155,17 @@
 
       <el-form-item label="出院原因" prop="dischargeReason">
         <el-radio-group v-model="form.dischargeReason" :disabled="isDetail">
+          <!-- 与「退住原因」字典 back_cause_arr 合并,共享同一套选项 -->
           <el-radio
-            v-for="o in DISCHARGE_REASON_OPTIONS"
-            :key="o.value"
+            v-for="(o, i) in getIntDictOptions(DICT_TYPE.BACK_CAUSE_ARR)"
+            :key="i"
             :value="o.value"
             class="inline-radio"
           >
             {{ o.label }}
           </el-radio>
           <el-input
-            v-if="form.dischargeReason === DISCHARGE_REASON.OTHER"
+            v-if="Number(form.dischargeReason) === DISCHARGE_REASON.OTHER"
             v-model="form.dischargeReasonOther"
             placeholder="请填写其他出院原因"
             style="width: 240px; margin-left: 8px"
@@ -353,14 +354,13 @@ import {
   createDischargeSummary,
   updateDischargeSummary,
   DISCHARGE_REASON,
-  DISCHARGE_REASON_OPTIONS,
   isDeathDischarge,
   PHYSICAL_ABILITY_OPTIONS,
   COGNITIVE_STATUS_OPTIONS,
   type DischargeSummaryForm,
   type DischargeMedication
 } from '@/api/elderly/elder/discharge-summary'
-import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+import { getDictLabel, getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import dayjs from 'dayjs'
 import { getElderInfoById } from '@/api/elderly/elder/elderly-Info'
 import Print from './Print.vue'

+ 10 - 0
src/views/elderly/apply/discharge-summary/Print.vue

@@ -170,6 +170,7 @@ import {
   type DischargeMedication
 } from '@/api/elderly/elder/discharge-summary'
 import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+import { getElderInfoById } from '@/api/elderly/elder/elderly-Info'
 
 defineOptions({ name: 'DischargeSummaryPrint' })
 
@@ -264,6 +265,15 @@ const open = async (row: any = {}) => {
       const data: any = await getDischargeSummary(row.id)
       Object.assign(form, defaultForm, data)
       form.medications = (data.medications || []) as DischargeMedication[]
+      // 居住区域为空时,根据 elderId 调用长者个人信息接口获取 bedName
+      if (!form.bedName && form.elderId) {
+        try {
+          const elderInfo: any = await getElderInfoById(form.elderId)
+          form.bedName = elderInfo?.bedName || ''
+        } catch (e) {
+          // 长者信息获取失败时不影响打印,保持为空即可
+        }
+      }
     }
   } finally {
     loading.value = false

+ 18 - 11
src/views/elderly/apply/discharge-summary/index.vue

@@ -178,18 +178,11 @@ const getList = async () => {
     }
     // 出院日期范围
     if (Array.isArray(queryParams.dischargeDate) && queryParams.dischargeDate.length === 2) {
-      params.beginTime = queryParams.dischargeDate[0]
-      params.endTime = queryParams.dischargeDate[1]
+      params.dischargeDate = [...queryParams.dischargeDate]
     }
+    // 返回的 list 已由 API 层转换并预计算 dischargeReasonText / elderAgeText
     const data = await getDischargeSummaryPage(params)
-    list.value = (data.list || []).map((item: any) => ({
-      ...item,
-      dischargeReasonText: getDischargeReasonText(item.dischargeReason),
-      elderAgeText:
-        item.elderAge === undefined || item.elderAge === null || item.elderAge === ''
-          ? ''
-          : `${item.elderAge} 岁`
-    }))
+    list.value = data.list || []
     total.value = data.total || 0
   } catch (e) {
     // 列表请求失败时填充测试假数据,便于前端联调(控制台有提示,不影响页面渲染)
@@ -262,7 +255,21 @@ const openCheckOut = (row: any = {}) => {
     return
   }
   // elderId 带入当前长者,open 内部会调用 setElderById 默认选中该长者
-  checkOutRef.value.open(tId, undefined, false, undefined, row.elderId)
+  // dischargeDate 带入退住日期,预填退住申请表单里的「退住日期」
+  // reason 带入出院原因作为退住原因;出院原因=其他时,退住描述取「其他」输入值
+  const reason = row.dischargeReason
+  const description =
+    Number(reason) === DISCHARGE_REASON.OTHER ? row.dischargeReasonOther || '' : ''
+  checkOutRef.value.open(
+    tId,
+    undefined,
+    false,
+    undefined,
+    row.elderId,
+    row.dischargeDate,
+    reason,
+    description
+  )
 }
 
 /** 删除 */

+ 15 - 1
src/views/elderly/bak/check-out/Form.vue

@@ -34,8 +34,11 @@ const titleItem = ref('新增')
  * @param detail    是否详情
  * @param status    审批状态
  * @param elderId   外部带入的长者 id(如从出院小结发起退住申请时预选该长者,新增场景)
+ * @param retreatDate 外部带入的退住日期(如从出院小结带入的出院日期)
+ * @param reason     外部带入的退住原因(字典键值,如从出院小结带入的出院原因)
+ * @param description 外部带入的退住描述(如出院原因=其他时的补充说明)
  */
-const open = async (tId, id, detail, status, elderId) => {
+const open = async (tId, id, detail, status, elderId, retreatDate, reason, description) => {
   dialogVisible.value = true
   isDetail.value = detail
   await nextTick()
@@ -47,6 +50,17 @@ const open = async (tId, id, detail, status, elderId) => {
     // 新增且带入长者 id:打开后默认选中该长者(下拉框显示且其证件号/入住日期/床位号自动回填)
     processFormRef.value.setElderById(elderId)
   }
+  // 带入退住日期(外部传入时预填)
+  if (retreatDate) {
+    processFormRef.value.setRetreatDate(retreatDate)
+  }
+  // 带入退住原因 / 退住描述(出院原因=其他时,退住描述取「其他」输入值)
+  if (reason !== '' && reason !== null && reason !== undefined) {
+    processFormRef.value.setReason(reason)
+  }
+  if (description) {
+    processFormRef.value.setDescription(description)
+  }
 }
 
 /** 提交表单 */

+ 32 - 1
src/views/elderly/bak/check-out/ProcessForm.vue

@@ -245,7 +245,38 @@ const setElderById = (id) => {
   }
 }
 
-defineExpose({ init, resetForm, submitForm, getProcess, setTenantId, setElderById }) // 提供 open 方法,用于打开弹窗
+/** 外部带入退住日期(如从出院小结发起退住申请时带入出院日期) */
+const setRetreatDate = (date?: string) => {
+  if (date) {
+    dataForm.value.retreatDate = date
+  }
+}
+
+/** 外部带入退住原因(字典键值,如从出院小结带入出院原因) */
+const setReason = (reason?: string | number) => {
+  if (reason !== '' && reason !== null && reason !== undefined) {
+    dataForm.value.reason = String(reason)
+  }
+}
+
+/** 外部带入退住描述(如出院原因=其他时的补充说明) */
+const setDescription = (desc?: string) => {
+  if (desc) {
+    dataForm.value.description = desc
+  }
+}
+
+defineExpose({
+  init,
+  resetForm,
+  submitForm,
+  getProcess,
+  setTenantId,
+  setElderById,
+  setRetreatDate,
+  setReason,
+  setDescription
+}) // 提供 open 方法,用于打开弹窗
 </script>
 <style lang="scss" scoped>
 .flex {

+ 23 - 0
src/views/system/improvement/patrol-inspection/columns.ts

@@ -0,0 +1,23 @@
+// ==================巡查巡检====================
+// 说明:当前为写死的展示数据,后续接入接口后可在此处调整字段映射。
+export const PatrolInspectionColumns = reactive([
+  {
+    label: '巡查时间',
+    field: 'patrolTime',
+    width: 170
+  },
+  {
+    label: '巡查点名称',
+    field: 'patrolName'
+  },
+  {
+    label: '巡查员',
+    field: 'inspector',
+    width: 120
+  },
+  {
+    label: '状态',
+    field: 'status',
+    width: 90
+  }
+])

+ 165 - 0
src/views/system/improvement/patrol-inspection/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <el-form-item label="巡查点名称" prop="patrolName" label-width="100">
+        <el-input
+          @keyup.enter="handleQuery"
+          v-model="queryParams.patrolName"
+          placeholder="请输入巡查点名称"
+          class="!w-240px"
+          clearable
+        />
+      </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>
+    <Table2
+      v-loading="loading"
+      :data="list"
+      :columns="PatrolInspectionColumns"
+      :queryParams="queryParams"
+      :op-width="110"
+      :show-edit-condition="() => false"
+      :show-detail-condition="() => false"
+      :show-delete-condition="() => false"
+    >
+      <template #next="{ scope }">
+        <el-button link type="primary" @click="handleDetail(scope)">详情</el-button>
+      </template>
+    </Table2>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { PatrolInspectionColumns } from './columns'
+
+defineOptions({ name: 'SystemPatrolInspection' })
+
+const message = useMessage()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<any[]>([])
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  patrolName: undefined as string | undefined
+})
+const queryFormRef = ref()
+
+/** 写死的巡查巡检数据(暂未对接接口) */
+const allList: any[] = [
+  { patrolTime: '2026-09-17 07:58', patrolName: '1楼展示空间房外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:56', patrolName: '1楼拜神、吸烟区内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:54', patrolName: '-1楼厨房后门(4号电梯处)', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:52', patrolName: '-1楼弱电房内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:50', patrolName: '-1楼发电机房储油间内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:48', patrolName: '-2楼停车场过道', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:46', patrolName: '-2楼1号电梯防火门对面(消防栓旁)', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:44', patrolName: '消防泵房内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:42', patrolName: '生活泵房内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:40', patrolName: '22楼天台左侧墙上', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:38', patrolName: '2号电梯机房外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:36', patrolName: '21楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:34', patrolName: '21楼会议室公共厕所外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:32', patrolName: '3楼茶水间内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:30', patrolName: '2楼烘焙室外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:28', patrolName: '22楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:26', patrolName: '20楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:24', patrolName: '20楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:22', patrolName: '19楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:20', patrolName: '19楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:18', patrolName: '18楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:16', patrolName: '18楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:14', patrolName: '17楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:12', patrolName: '17楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:10', patrolName: '16楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:08', patrolName: '16楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:06', patrolName: '15楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:04', patrolName: '15楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:02', patrolName: '14楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 07:00', patrolName: '14楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:58', patrolName: '13楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:56', patrolName: '13楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:54', patrolName: '12楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:52', patrolName: '12楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:50', patrolName: '11楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:48', patrolName: '11楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:46', patrolName: '10楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:44', patrolName: '10楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:42', patrolName: '9楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:40', patrolName: '9楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:38', patrolName: '8楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:36', patrolName: '8楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:34', patrolName: '7楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:32', patrolName: '7楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:30', patrolName: '6楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:28', patrolName: '6楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:26', patrolName: '5楼01房对面', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:24', patrolName: '5楼茶水间外', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:22', patrolName: '4楼洗衣房内', inspector: '欧阳林春', status: '正常' },
+  { patrolTime: '2026-09-17 06:20', patrolName: '4楼饮水机旁', inspector: '欧阳林春', status: '正常' }
+]
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const kw = (queryParams.patrolName || '').trim()
+    const filtered = kw
+      ? allList.filter((item) => item.patrolName.includes(kw))
+      : [...allList]
+    total.value = filtered.length
+    const start = (queryParams.pageNo - 1) * queryParams.pageSize
+    list.value = filtered.slice(start, start + queryParams.pageSize)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 详情(暂无详情数据,仅占位) */
+const handleDetail = (row: any) => {
+  message.info('详情功能开发中')
+}
+
+getList()
+</script>

+ 34 - 0
src/views/system/safety/complaint-management/columns.ts

@@ -0,0 +1,34 @@
+// ==================投诉管理====================
+// 说明:当前为写死的展示数据,后续接入接口后可在此处调整字段映射。
+export const ComplaintManagementColumns = reactive([
+  {
+    label: '日期',
+    field: 'date',
+    width: 120
+  },
+  {
+    label: '来源',
+    field: 'source',
+    width: 90
+  },
+  {
+    label: '类别',
+    field: 'category',
+    width: 100
+  },
+  {
+    label: '工单内容摘要',
+    field: 'summary',
+    minWidth: 280
+  },
+  {
+    label: '投诉人/关联方',
+    field: 'relatedParty',
+    width: 130
+  },
+  {
+    label: '状态',
+    field: 'status',
+    width: 110
+  }
+])

+ 227 - 0
src/views/system/safety/complaint-management/index.vue

@@ -0,0 +1,227 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <el-form-item label="内容摘要" prop="summary">
+        <el-input
+          @keyup.enter="handleQuery"
+          v-model="queryParams.summary"
+          placeholder="请输入工单内容摘要"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          style="width: 160px"
+        >
+          <el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
+        </el-select>
+      </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>
+    <Table2
+      v-loading="loading"
+      :data="list"
+      :columns="ComplaintManagementColumns"
+      :queryParams="queryParams"
+      :op-width="110"
+      :show-edit-condition="() => false"
+      :show-detail-condition="() => false"
+      :show-delete-condition="() => false"
+    >
+      <template #next="{ scope }">
+        <el-button link type="primary" @click="handleDetail(scope)">详情</el-button>
+      </template>
+    </Table2>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ComplaintManagementColumns } from './columns'
+
+defineOptions({ name: 'SystemComplaintManagement' })
+
+const message = useMessage()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<any[]>([])
+
+const statusOptions = ['已解决', '处理中', '撤销工单']
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  summary: undefined as string | undefined,
+  status: undefined as string | undefined
+})
+const queryFormRef = ref()
+
+/** 写死的投诉管理数据(暂未对接接口) */
+const allList: any[] = [
+  {
+    date: '2026-04-10',
+    source: '12345',
+    category: '服务流程',
+    summary: '家属反映院内蚊虫较多,要求加强灭蚊工作',
+    relatedParty: '院内长者家属',
+    status: '已解决'
+  },
+  {
+    date: '2026-03-31',
+    source: '12345',
+    category: '服务流程',
+    summary: '周边居民反映清晨6-8点有广播体操噪音扰民',
+    relatedParty: '周边居民',
+    status: '已解决'
+  },
+  {
+    date: '2026-03-18',
+    source: '12345',
+    category: '服务流程',
+    summary: '居民投诉厨房排风系统不定时发出巨大噪音',
+    relatedParty: '周边居民',
+    status: '已解决'
+  },
+  {
+    date: '2026-03-17',
+    source: '12345',
+    category: '服务流程',
+    summary: '长者外出就医期间,除床位费外仍收取护理费',
+    relatedParty: '董卫红',
+    status: '撤销工单'
+  },
+  {
+    date: '2026-03-02',
+    source: '12345',
+    category: '服务流程',
+    summary: '再次投诉午休12-14时仍听到洗衣房噪音',
+    relatedParty: '周边居民',
+    status: '已解决'
+  },
+  {
+    date: '2026-02-21',
+    source: '12345',
+    category: '服务流程',
+    summary: '投诉建筑背面排风机转动噪音,每日多个时段频发',
+    relatedParty: '周边居民',
+    status: '已解决'
+  },
+  {
+    date: '2026-02-13',
+    source: '12345',
+    category: '服务流程',
+    summary: '投诉4楼洗衣房洗衣机不定时发出噪音扰民',
+    relatedParty: '周边居民',
+    status: '已解决'
+  },
+  {
+    date: '2026-02-13',
+    source: '12345',
+    category: '服务流程',
+    summary: '家属投诉机构老人探视流程及时间安排问题',
+    relatedParty: '宁迎明',
+    status: '已解决'
+  },
+  {
+    date: '2025-12-28',
+    source: '12345',
+    category: '环境卫生',
+    summary: '家属反映老人房间卫生间渗水,地面长期湿滑存在安全隐患',
+    relatedParty: '院内长者家属',
+    status: '已解决'
+  },
+  {
+    date: '2025-12-15',
+    source: '12345',
+    category: '设施设备',
+    summary: '长者反映楼道照明灯损坏多日未维修,夜间出行不便',
+    relatedParty: '院内长者',
+    status: '已解决'
+  },
+  {
+    date: '2025-12-03',
+    source: '12345',
+    category: '服务态度',
+    summary: '家属投诉护理人员沟通态度生硬,缺乏耐心',
+    relatedParty: '院内长者家属',
+    status: '已解决'
+  },
+  {
+    date: '2025-11-20',
+    source: '12345',
+    category: '餐饮照护',
+    summary: '长者反映午餐菜品温度偏低,老年人肠胃不适应',
+    relatedParty: '院内长者',
+    status: '已解决'
+  }
+]
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const kw = (queryParams.summary || '').trim()
+    const st = queryParams.status
+    const filtered = allList.filter((item) => {
+      if (kw && !item.summary.includes(kw)) return false
+      if (st && item.status !== st) return false
+      return true
+    })
+    total.value = filtered.length
+    const start = (queryParams.pageNo - 1) * queryParams.pageSize
+    list.value = filtered.slice(start, start + queryParams.pageSize)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 详情(暂无详情数据,仅占位) */
+const handleDetail = (row: any) => {
+  message.info('详情功能开发中')
+}
+
+getList()
+</script>