소스 검색

调整九防自动计算分数

unknown 2 주 전
부모
커밋
a9c849dd2b

+ 8 - 0
src/api/elderly/apply/check-in.ts

@@ -38,6 +38,14 @@ export const getCheckInRecordId = (id, status) => {
     url: `elderlyInfo/getCheckInDetail?checkInRecordId=${id}&status=${status}`,
   })
 }
+// 获取详情
+export const getCheckInDetailByElderId = (id) => {
+  return request.get({
+    url: `/elderlyInfo/getCheckInDetailByElderId?elderId=${id}`,
+  })
+}
+
+
 
 // 获取服务安全风险知情告知书列表
 export const getSafetyRiskNoticePage = (params) => {

+ 239 - 28
src/views/elderly/contracts/ContractManageForm.vue

@@ -1,8 +1,10 @@
 <script setup lang="ts">
-import { reactive, ref } from 'vue'
+import { reactive, ref, computed, nextTick } from 'vue'
+import html2pdf from 'html2pdf.js'
 import ContractBody from './components/ContractBody.vue'
 import AttachmentPage from './components/AttachmentPage.vue'
 import AttachmentSelector from './components/AttachmentSelector.vue'
+import RiskDisclosureBody from './components/RiskDisclosureBody.vue'
 
 const message = useMessage()
 
@@ -41,6 +43,48 @@ const attachments = reactive<Record<string, any>>(
   }, {} as Record<string, any>)
 )
 
+const riskDisclosureForm = reactive<Record<string, any>>({
+  elderName: '',
+  companyName: '颐年医养',
+  assessor: '',
+  assessDate: '',
+  partyBSign: '',
+  partyBDate: '',
+  partyCSign: '',
+  partyCDate: ''
+})
+
+const contractSignatureData = ref<Record<string, any>>({
+  partyASign: '',
+  partyALegalSign: '',
+  partyADate: '',
+  partyBSign: '',
+  partyBDate: '',
+  partyCSign: '',
+  partyCDate: ''
+})
+
+const handleContractDataChange = (data: any) => {
+  if (!data) return
+  contractSignatureData.value.partyASign = data.partyASign || ''
+  contractSignatureData.value.partyALegalSign = data.partyALegalSign || ''
+  contractSignatureData.value.partyADate = data.partyADate || ''
+  contractSignatureData.value.partyBSign = data.partyBSign || ''
+  contractSignatureData.value.partyBDate = data.partyBDate || ''
+  contractSignatureData.value.partyCSign = data.partyCSign || ''
+  contractSignatureData.value.partyCDate = data.partyCDate || ''
+}
+
+const selectedAttachments = computed(() => {
+  return attachmentList.filter((a) => {
+    const data = attachments[`attach_${a.no}`]
+    if (!data) return false
+    if (Array.isArray(data)) return data.length > 0
+    if (typeof data === 'object') return Object.keys(data).length > 0
+    return !!data
+  })
+})
+
 const selectorVisible = ref<{
   show: boolean
   current: string
@@ -61,6 +105,20 @@ const open = (row?: any) => {
   attachmentList.forEach((it) => {
     attachments[`attach_${it.no}`] = null
   })
+  riskDisclosureForm.elderName = ''
+  riskDisclosureForm.assessor = ''
+  riskDisclosureForm.assessDate = ''
+  riskDisclosureForm.partyBSign = ''
+  riskDisclosureForm.partyBDate = ''
+  riskDisclosureForm.partyCSign = ''
+  riskDisclosureForm.partyCDate = ''
+  contractSignatureData.value.partyASign = ''
+  contractSignatureData.value.partyALegalSign = ''
+  contractSignatureData.value.partyADate = ''
+  contractSignatureData.value.partyBSign = ''
+  contractSignatureData.value.partyBDate = ''
+  contractSignatureData.value.partyCSign = ''
+  contractSignatureData.value.partyCDate = ''
   visible.value = true
   setTimeout(() => {
     formLoading.value = false
@@ -95,7 +153,8 @@ const handleClose = () => {
 const handlePrint = async () => {
   isPrint.value = true
   isTextMode.value = true
-  await new Promise((resolve) => setTimeout(resolve, 200))
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
   window.print()
   setTimeout(() => {
     isPrint.value = false
@@ -103,8 +162,74 @@ const handlePrint = async () => {
   }, 500)
 }
 
-const handleExport = () => {
-  message.success('导出功能已触发(待接入实际接口)')
+const handleExport = async () => {
+  isPrint.value = true
+  isTextMode.value = true
+
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 2500))
+
+  const container = document.querySelector('.print-container') as HTMLElement | null
+  if (!container) {
+    message.error('未找到打印内容,请重试')
+    isPrint.value = false
+    isTextMode.value = false
+    return
+  }
+
+  // html2canvas 无法渲染 display:none 的元素,因此临时显示(移出可视区域,避免闪烁)
+  const originalDisplay = container.style.display
+  const originalPosition = container.style.position
+  const originalLeft = container.style.left
+  const originalTop = container.style.top
+  const originalWidth = container.style.width
+  container.style.display = 'block'
+  container.style.position = 'absolute'
+  container.style.left = '-10000px'
+  container.style.top = '0px'
+  container.style.width = '210mm'
+
+  const filename = recordRow.value?.elderName
+    ? `${recordRow.value.elderName}-养老服务合同.pdf`
+    : '养老服务合同.pdf'
+
+  try {
+    await html2pdf()
+      .set({
+        margin: 0,
+        filename,
+        image: { type: 'jpeg', quality: 0.98 },
+        html2canvas: {
+          scale: 2,
+          useCORS: true,
+          backgroundColor: '#ffffff',
+          logging: false,
+          foreignObjectRendering: false
+        },
+        jsPDF: {
+          unit: 'mm',
+          format: 'a4',
+          orientation: 'portrait'
+        },
+        pagebreak: { mode: ['css'] }
+      })
+      .from(container)
+      .save()
+    message.success('导出成功')
+  } catch (e) {
+    console.error('导出PDF失败', e)
+    message.error('导出失败,请尝试使用打印功能另存为PDF')
+  } finally {
+    container.style.display = originalDisplay
+    container.style.position = originalPosition
+    container.style.left = originalLeft
+    container.style.top = originalTop
+    container.style.width = originalWidth
+    setTimeout(() => {
+      isPrint.value = false
+      isTextMode.value = false
+    }, 500)
+  }
 }
 
 const toggleTextMode = () => {
@@ -169,7 +294,7 @@ const renderAttachmentContent = (key: string) => {
         <div class="section-divider">
           <span>合同主体</span>
         </div>
-        <ContractBody ref="contractBodyRef" :record-row="recordRow" :is-text-mode="isTextMode" />
+        <ContractBody ref="contractBodyRef" :record-row="recordRow" :is-text-mode="isTextMode" v-model="contractSignatureData" @update:data="handleContractDataChange" />
 
         <!-- ============ 16个附件 ============ -->
         <div class="section-divider" style="margin-top: 30px;">
@@ -186,7 +311,10 @@ const renderAttachmentContent = (key: string) => {
           @select="openAttachmentSelector(attach)"
         >
           <template #default="{ data }">
-            <div class="attachment-body">
+            <div v-if="attach.no === 1" class="attachment-content">
+              <RiskDisclosureBody v-model="riskDisclosureForm" :is-text-mode="isTextMode" />
+            </div>
+            <div v-else class="attachment-body">
               <p><strong>已选择:</strong>{{ renderAttachmentContent(`attach_${attach.no}`) }}</p>
               <p><strong>版本:</strong>{{ data?.version || '-' }}</p>
               <p><strong>日期:</strong>{{ data?.createTime || '-' }}</p>
@@ -198,12 +326,12 @@ const renderAttachmentContent = (key: string) => {
         </AttachmentPage>
       </div>
 
-      <div class="content-footer" v-if="!isPrint">
+      <!-- <div class="content-footer" v-if="!isPrint">
         <el-button type="primary" :loading="formLoading" @click="handleSave">保存</el-button>
         <el-button @click="handleClose">关闭</el-button>
         <el-button type="success" @click="handlePrint">打印</el-button>
         <el-button type="warning" @click="handleExport">导出</el-button>
-      </div>
+      </div> -->
     </div>
   </el-drawer>
 
@@ -214,6 +342,37 @@ const renderAttachmentContent = (key: string) => {
     @select="handleAttachmentSelect"
     @update:visible="handleAttachmentSelectorClose"
   />
+
+  <!-- ========== 打印容器:Teleport 到 body,确保是 body 直接子元素 ========== -->
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <!-- 合同主体 -->
+      <ContractBody :record-row="recordRow" :is-text-mode="true" v-model="contractSignatureData" />
+
+      <!-- 已选的附件 -->
+      <template v-if="selectedAttachments.length > 0">
+        <div
+          v-for="attach in selectedAttachments"
+          :key="attach.no"
+          class="print-attachment"
+        >
+          <!-- 附件1:知情同意书,使用 RiskDisclosureBody -->
+          <RiskDisclosureBody v-if="attach.no === 1" v-model="riskDisclosureForm" :is-text-mode="true" />
+          <!-- 其他附件:展示标题和信息 -->
+          <div v-else class="a4-page">
+            <div class="print-attachment-header">
+              <div class="print-attachment-title">附件{{ attach.no }}:{{ attach.title }}</div>
+            </div>
+            <div class="print-attachment-info">
+              <p><strong>名称:</strong>{{ renderAttachmentContent(`attach_${attach.no}`) }}</p>
+              <p><strong>版本:</strong>{{ attachments[`attach_${attach.no}`]?.version || '-' }}</p>
+              <p><strong>日期:</strong>{{ attachments[`attach_${attach.no}`]?.createTime || '-' }}</p>
+            </div>
+          </div>
+        </div>
+      </template>
+    </div>
+  </Teleport>
 </template>
 
 <style lang="scss">
@@ -232,41 +391,66 @@ const renderAttachmentContent = (key: string) => {
   }
 }
 
-@media print {
-  body * {
-    visibility: hidden;
-  }
+/* ========== 打印容器(屏幕上隐藏,仅在 html2pdf 导出时临时显示,或在 @media print 时显示) ========== */
+.print-container {
+  display: none;
+}
 
-  .contract-manage-drawer,
-  .contract-manage-drawer .el-drawer__body {
-    visibility: visible;
-    position: static !important;
+/* ========== 打印样式核心逻辑:只显示 Teleport 到 body 的 print-container ========== */
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
     overflow: visible !important;
   }
 
-  .contract-manage-drawer .content-toolbar,
-  .contract-manage-drawer .content-footer,
-  .contract-manage-drawer .el-drawer__header,
-  .contract-manage-drawer .content-body .section-divider {
+  /* 1. body 的所有直接子元素都隐藏,除了 print-container */
+  body > *:not(.print-container) {
     display: none !important;
   }
 
-  .contract-manage-drawer .content-body {
-    visibility: visible;
-    overflow: visible !important;
+  /* 2. 打印容器本身显示出来 */
+  body > .print-container {
+    display: block !important;
+  }
+
+  /* 3. A4 页:去除容器 padding,改用 @page margin 控制每页四周间距 */
+  .print-container .a4-page {
+    width: 100% !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    background: #fff !important;
+    box-shadow: none !important;
+    box-sizing: border-box !important;
+  }
+
+  /* 封面页保留完整 A4 高度 */
+  .print-container .a4-page.cover-a4 {
+    min-height: 297mm !important;
   }
 
-  .contract-manage-drawer .content-body > * {
-    visibility: visible;
+  /* 其他页自适应高度 */
+  .print-container .a4-page:not(.cover-a4) {
+    min-height: auto !important;
   }
 
-  .contract-manage-drawer .drawer-content {
-    display: block;
+  /* 4. 防止标题、列表项、签名区被分页切断 */
+  .print-container .section-title,
+  .print-container .sub-title {
+    break-after: avoid-page;
+    page-break-after: avoid;
+  }
+  .print-container .num-list li,
+  .print-container .signature-group {
+    break-inside: avoid-page;
+    page-break-inside: avoid;
   }
 
   @page {
     size: A4;
-    margin: 0;
+    margin: 18mm;
   }
 }
 </style>
@@ -339,4 +523,31 @@ const renderAttachmentContent = (key: string) => {
   margin: 4px 0;
   line-height: 1.8;
 }
+
+.attachment-content {
+  padding: 10px 0;
+}
+
+/* 打印模式下其他附件标题样式 */
+.print-attachment-header {
+  padding-bottom: 10px;
+  border-bottom: 1.5px solid #333;
+  margin-bottom: 16px;
+}
+
+.print-attachment-title {
+  font-size: 18px;
+  font-weight: bold;
+  color: #222;
+}
+
+.print-attachment-info {
+  font-size: 14px;
+  line-height: 1.9;
+  color: #333;
+}
+
+.print-attachment-info p {
+  margin: 6px 0;
+}
 </style>

+ 1181 - 0
src/views/elderly/contracts/check-in-tab/AddForm.vue

@@ -0,0 +1,1181 @@
+<template>
+  <el-drawer
+    v-model="dialogVisible"
+    :title="title"
+    resizable
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :destroy-on-close="true"
+    size="70%"
+    :before-close="handleClosed"
+  >
+
+    <div class="attack-risk-form">
+      <h1 class="form-title">风险知情书(附件)</h1>
+      <!-- 基本信息 -->
+      <el-row :gutter="40">
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>长者姓名</text>
+          <search-the-elderly ref="selectElderRef" :disabled="isDetail" @update_elder="elderUp" v-model="dataForm.elderName" :tId="dataForm.tenantId"/>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>档案号</text>
+          <el-input v-if="!isDetail" v-model="dataForm.contractNumber" disabled />
+          <el-text v-else disabled="">{{dataForm.contractNumber}}</el-text>
+        </el-col>
+      </el-row>
+      <el-row :gutter="40">
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>入院日期</text>
+          <el-input v-if="!isDetail" :model-value="dayjs(dataForm.checkInTime).format('YYYY-MM-DD')=='Invalid Date'?'':dayjs(dataForm.checkInTime).format('YYYY-MM-DD')" disabled />
+          <el-text v-else disabled="">{{dayjs(dataForm.checkInTime).format('YYYY-MM-DD')}}</el-text>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>床位号</text>
+          <el-input v-if="!isDetail" v-model="dataForm.bedName" disabled />
+          <el-text v-else disabled="">{{dataForm.bedName}}</el-text>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="40">
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>记录人</text>
+          <el-input :disabled="isDetail" v-model="form.assessor"  />
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12" class="row">
+          <text>记录日期</text>
+          <el-date-picker :disabled="isDetail" v-model="form.assessDate" type="date" style="width: 100%;"/>
+
+        </el-col>
+      </el-row>
+
+      <!-- 内容区域 -->
+      <div class="form-body">
+        <RiskDisclosureBody
+          v-model="riskDisclosureForm"
+          :is-text-mode="isDetail"
+        />
+      </div>
+
+    </div>
+
+
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button  v-if="isDetail" type="success" @click="handleExport">打印</el-button>
+      <el-button style="margin-left: 22px;margin-right: 30px" v-loading="formLoading" type="primary" v-show="!isDetail" @click="submitForm">确定</el-button>
+    </template>
+
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch, reactive } from 'vue'
+
+import dayjs from 'dayjs'
+import RiskDisclosureBody from '../components/RiskDisclosureBody.vue'
+
+import { attackRiskCreate, attackRiskGetById, attackRiskUpdate, attackRiskGetByElderId } from "@/api/social-work";
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const title = ref('')
+const dialogVisible = ref(false) // 弹窗
+const formRef = ref() // 表单 Ref
+const selectElderRef = ref() // 表单 Ref
+const isDetail = ref(false) // 是否详情打开
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+let dataForm = ref({
+  // 表单字段
+  id: undefined,
+  idCard: '',
+  contractNumber: '', //档案号
+  elderName: '',//长者姓名
+  bedName: '', //床位号
+  elderAge: '', //年龄
+  elderSex: '', //性别
+  checkInTime: '', //入院日期
+  elderId: '',
+  tenantId: undefined
+})
+
+
+
+
+const elderUp = (e) => {
+  dataForm.value.elderName = e.elderName
+  dataForm.value.elderId = e.id
+  dataForm.value.elderSex = e.elderSex === 1 ? '男' : '女'
+  dataForm.value.bedName = e.bedName || ''
+  dataForm.value.checkInTime = e.checkInTime
+  dataForm.value.contractNumber = e.contractNumber||e.fileNumber
+  dataForm.value.elderAge = e.elderAge
+}
+
+// ========== 攻击风险因素评估量表 表单序列化方法 ==========
+
+/** 风险程度文本 */
+const riskLevelText = computed(() => {
+  const level = form.attackLevel
+  if (!level || level === 0) return '无风险'
+  if (level === 1) return 'I级'
+  if (level === 2) return 'II级'
+  if (level === 3) return 'III级'
+  return 'IV级'
+})
+
+/** 风险程度样式类 */
+const riskLevelClass = computed(() => {
+  const level = form.attackLevel
+  if (!level || level === 0) return 'risk-none'
+  if (level === 1) return 'risk-low'
+  if (level === 2) return 'risk-medium'
+  return 'risk-high'
+})
+
+/** 自动判断风险等级 */
+const autoJudgeRiskLevel = () => {
+  const level = form.attackLevel
+  if (!level || level === 0) form.riskLevel = 'none'
+  else if (level === 1) form.riskLevel = 'low'
+  else if (level === 2) form.riskLevel = 'medium'
+  else form.riskLevel = 'high'
+}
+
+/** 处理级别选择变化 - 清空其他级别的选中项 */
+const handleLevelChange = (val: number) => {
+  // 根据选中的级别,清空其他级别的多选框
+  if (val === 1) {
+    form.attackLevel2Items = []
+    form.attackLevel3Items = []
+    form.attackLevel4Items = []
+  } else if (val === 2) {
+    form.attackLevel1Items = []
+    form.attackLevel3Items = []
+    form.attackLevel4Items = []
+  } else if (val === 3) {
+    form.attackLevel1Items = []
+    form.attackLevel2Items = []
+    form.attackLevel4Items = []
+  } else if (val === 4) {
+    form.attackLevel1Items = []
+    form.attackLevel2Items = []
+    form.attackLevel3Items = []
+  }
+}
+
+
+
+/** 将表单数据序列化为 JSON 对象 */
+const serializeFormData = () => {
+  return {
+    // 基本信息
+    assessor: form.assessor || '',
+    assessDate: form.assessDate ? dayjs(form.assessDate).format('YYYY-MM-DD') : '',
+
+    // 攻击风险等级(1=I级,2=II级,3=III级,4=IV级)
+    attackLevel: form.attackLevel || 0,
+
+    // 各级别选择的具体项
+    attackLevel1Items: form.attackLevel1Items || [],
+    attackLevel2Items: form.attackLevel2Items || [],
+    attackLevel3Items: form.attackLevel3Items || [],
+    attackLevel4Items: form.attackLevel4Items || [],
+
+    // 风险程度
+    riskLevel: form.riskLevel || '',
+
+    // 预防措施
+    preventiveMeasures: form.preventiveMeasures || [],
+    preventiveMeasuresOther: form.preventiveMeasuresOther || '',
+
+    // 签名
+    familySignature: form.familySignature || '',
+    familySignDate: form.familySignDate ? dayjs(form.familySignDate).format('YYYY-MM-DD') : '',
+
+    // 风险知情书内容
+    riskDisclosure: {
+      companyName: riskDisclosureForm.companyName || '',
+      partyBSign: riskDisclosureForm.partyBSign || '',
+      partyBDate: riskDisclosureForm.partyBDate || '',
+      partyCSign: riskDisclosureForm.partyCSign || '',
+      partyCDate: riskDisclosureForm.partyCDate || ''
+    }
+  }
+}
+
+/** 将 JSON 对象反序列化为表单数据 */
+const deserializeFormData = (formData: Record<string, any>) => {
+  if (!formData) return
+
+  // 基本信息
+  form.assessor = formData.assessor || ''
+  form.assessDate = formData.assessDate ? dayjs(formData.assessDate).toDate() : ''
+
+  // 攻击风险等级
+  form.attackLevel = formData.attackLevel || 0
+
+  // 各级别选择的具体项
+  form.attackLevel1Items = formData.attackLevel1Items || []
+  form.attackLevel2Items = formData.attackLevel2Items || []
+  form.attackLevel3Items = formData.attackLevel3Items || []
+  form.attackLevel4Items = formData.attackLevel4Items || []
+
+  // 风险程度
+  form.riskLevel = formData.riskLevel || ''
+
+  // 预防措施
+  form.preventiveMeasures = formData.preventiveMeasures || []
+  form.preventiveMeasuresOther = formData.preventiveMeasuresOther || ''
+
+  // 签名
+  form.familySignature = formData.familySignature || ''
+  form.familySignDate = formData.familySignDate ? dayjs(formData.familySignDate).toDate() : ''
+
+  // 风险知情书内容
+  if (formData.riskDisclosure) {
+    riskDisclosureForm.companyName = formData.riskDisclosure.companyName || ''
+    riskDisclosureForm.partyBSign = formData.riskDisclosure.partyBSign || ''
+    riskDisclosureForm.partyBDate = formData.riskDisclosure.partyBDate || ''
+    riskDisclosureForm.partyCSign = formData.riskDisclosure.partyCSign || ''
+    riskDisclosureForm.partyCDate = formData.riskDisclosure.partyCDate || ''
+  }
+}
+
+/** 重置攻击风险因素评估表表单数据 */
+const resetAttackRiskForm = () => {
+  form.assessor = ''
+  form.assessDate = ''
+
+  // 攻击风险等级
+  form.attackLevel = 0
+
+  // 各级别选择的具体项
+  form.attackLevel1Items = []
+  form.attackLevel2Items = []
+  form.attackLevel3Items = []
+  form.attackLevel4Items = []
+
+  // 风险程度
+  form.riskLevel = ''
+
+  // 预防措施
+  form.preventiveMeasures = []
+  form.preventiveMeasuresOther = ''
+
+  // 签名
+  form.familySignature = ''
+  form.familySignDate = ''
+
+  // 风险知情书
+  riskDisclosureForm.companyName = '颐年医养'
+  riskDisclosureForm.partyBSign = ''
+  riskDisclosureForm.partyBDate = ''
+  riskDisclosureForm.partyCSign = ''
+  riskDisclosureForm.partyCDate = ''
+}
+
+/** 打开弹窗 */
+const open = async (tenantId, id?: any, detail: boolean = false) => {
+  resetForm()
+  dialogVisible.value = true
+  dataForm.value.id = id || undefined
+  dataForm.value.tenantId = tenantId
+  isDetail.value = detail
+  if (id) {
+    title.value = "编辑-攻击风险因素评估"
+    // 加载评估数据
+    await loadAttackRiskData(id)
+  } else {
+    title.value = "新增-攻击风险因素评估"
+    
+  }
+}
+
+/** 加载评估数据 */
+const loadAttackRiskData = async (id: number) => {
+  try {
+    const res = await attackRiskGetById(id)
+    if (res) {
+      // 填充长者基本信息
+      dataForm.value.elderName = res.elderName || ''
+      dataForm.value.elderId = res.elderId || ''
+      dataForm.value.elderSex = res.elderSex || ''
+      dataForm.value.bedName = res.bedName || ''
+      dataForm.value.checkInTime = res.checkInTime || ''
+      dataForm.value.contractNumber = res.fileNumber || ''
+      dataForm.value.elderAge = res.elderAge || ''
+      await selectElderRef.value.upData(res.elderName, res.elderId)
+      // 解析 assessData
+      if (res.assessData) {
+        const formData = JSON.parse(res.assessData)
+        deserializeFormData(formData)
+      }
+    }
+  } catch (error) {
+    message.error('加载评估数据失败')
+  }
+}
+
+/** 根据长者ID加载评估数据 */
+const loadAttackRiskByElderId = async (elderId: number) => {
+  try {
+    const res = await attackRiskGetByElderId(elderId)
+    if (res && res.assessData) {
+      const formData = JSON.parse(res.assessData)
+      deserializeFormData(formData)
+    }
+  } catch (error) {
+    // 无历史数据,不处理
+  }
+}
+
+const form = reactive({
+  // 基本信息
+  assessor: '',
+  assessDate: '',
+
+  // 攻击风险等级(0=无,1=I级,2=II级,3=III级,4=IV级)
+  attackLevel: 0,
+
+  // 各级别选择的具体项
+  attackLevel1Items: [], // I级选择的项(1-7)
+  attackLevel2Items: [], // II级选择的项(1-3)
+  attackLevel3Items: [], // III级选择的项(1-4)
+  attackLevel4Items: [], // IV级选择的项(1-2)
+
+  // 风险程度
+  riskLevel: '',
+
+  // 预防措施
+  preventiveMeasures: [],
+  preventiveMeasuresOther: '',
+
+  // 签名
+  familySignature: '',
+  familySignDate: ''
+})
+
+// 监听攻击等级变化,自动判断风险等级
+watch(() => form.attackLevel, () => {
+  autoJudgeRiskLevel()
+})
+
+// 风险知情书表单数据
+const riskDisclosureForm = reactive({
+  elderName: '',
+  companyName: '颐年医养',
+  assessor: '',
+  assessDate: '',
+  partyBSign: '',
+  partyBDate: '',
+  partyCSign: '',
+  partyCDate: ''
+})
+
+// 长者信息变化时同步到风险告知书
+watch(
+  () => dataForm.value.elderName,
+  (name) => {
+    riskDisclosureForm.elderName = name
+  }
+)
+
+// 记录人变化时同步
+watch(
+  () => form.assessor,
+  (val) => {
+    riskDisclosureForm.assessor = val
+  }
+)
+
+watch(
+  () => form.assessDate,
+  (val) => {
+    riskDisclosureForm.assessDate = val ? dayjs(val).format('YYYY-MM-DD') : ''
+  }
+)
+
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if (formLoading.value) {
+    return
+  }
+  formLoading.value = true
+  // 提交请求
+  try {
+    const assessData = serializeFormData()
+    const tempParams = {
+      ...dataForm.value,
+      assessData: JSON.stringify(assessData),
+      attackLevel: form.attackLevel,
+      riskLevel: form.riskLevel,
+      assessor: form.assessor,
+      assessScore: form.attackLevel,
+      assessDate: form.assessDate ? dayjs(form.assessDate).format('YYYY-MM-DD') : ''
+    }
+
+    if (dataForm.value.id) {
+      const res = await attackRiskUpdate(tempParams)
+      if (res) {
+        message.success(t('common.updateSuccess'))
+        dialogVisible.value = false
+        // 发送操作成功的事件
+        emit('success')
+      }
+    } else {
+      const res = await attackRiskCreate(tempParams)
+      if (res) {
+        message.success(t('common.createSuccess'))
+        dialogVisible.value = false
+        // 发送操作成功的事件
+        emit('success')
+      }
+    }
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 500)
+  }
+}
+
+
+
+/** 重置表单 */
+const resetForm = () => {
+  dataForm.value = {
+    id: undefined,
+    idCard: '',
+    contractNumber: '', //档案号
+    elderName: '',//长者姓名
+    bedName: '', //床位号
+    elderAge: '', //年龄
+    elderSex: '', //性别
+    checkInTime: '', //入院日期
+    elderId: '',
+    tenantId: undefined
+  }
+  formRef.value?.resetFields()
+
+  // 重置攻击风险因素评估表表单
+  resetAttackRiskForm()
+}
+
+// 关闭表单
+const handleClosed = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+
+/** 导出打印 */
+const handleExport = () => {
+  // 创建打印窗口
+  const printWindow = window.open('', '_blank')
+  if (!printWindow) {
+    message.error('请允许弹出窗口')
+    return
+  }
+
+  // 构建打印内容
+  const printContent = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+      <meta charset="UTF-8">
+      <title>攻击风险因素评估量表 - ${dataForm.value.elderName || ''}</title>
+      <style>
+        @media print {
+          @page { size: A4 portrait; margin: 15mm; }
+        }
+        body {
+          font-family: 'SimSun', 'Microsoft YaHei', serif;
+          font-size: 10pt;
+          line-height: 1.4;
+          color: #333;
+        }
+        .header {
+          text-align: center;
+          margin-bottom: 15px;
+          border-bottom: 2px solid #333;
+          padding-bottom: 10px;
+        }
+        .header h1 {
+          font-size: 16pt;
+          margin: 0;
+          letter-spacing: 2px;
+        }
+        .info-section {
+          margin-bottom: 15px;
+          padding: 10px;
+          border: 1px solid #999;
+          background: #fafafa;
+        }
+        .info-row {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 20px;
+        }
+        .info-item {
+          display: flex;
+          align-items: center;
+        }
+        .info-item .label {
+          font-weight: bold;
+          margin-right: 8px;
+          color: #555;
+        }
+        .info-item .value {
+          border-bottom: 1px solid #333;
+          min-width: 80px;
+          padding: 0 5px;
+          text-align: center;
+        }
+        .score-summary {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          padding: 10px 15px;
+          background: #f0f0f0;
+          border: 2px solid #333;
+          margin-bottom: 15px;
+        }
+        .total-score {
+          font-size: 14pt;
+          font-weight: bold;
+        }
+        .total-score .score-value {
+          color: #d9534f;
+          font-size: 18pt;
+        }
+
+        .assessment-table {
+          width: 100%;
+          border-collapse: collapse;
+          margin-bottom: 15px;
+        }
+        .assessment-table th, .assessment-table td {
+          border: 1px solid #333;
+          padding: 8px;
+          text-align: left;
+          vertical-align: top;
+        }
+        .assessment-table th {
+          background: #e8e8e8;
+          font-weight: bold;
+          text-align: center;
+        }
+        .assessment-table .content-col {
+          width: 65%;
+        }
+        .assessment-table .level-col {
+          width: 10%;
+          text-align: center;
+        }
+        .assessment-table .select-col {
+          width: 25%;
+          text-align: center;
+        }
+        .assessment-table .content-title {
+          font-weight: bold;
+          margin-bottom: 5px;
+        }
+        .assessment-table .content-item {
+          margin-left: 15px;
+          margin-bottom: 3px;
+        }
+        .assessment-table .select {
+          text-align: center;
+          vertical-align: middle;
+        }
+        .assessment-table .print-multi-select {
+          margin-top: 8px;
+          padding-top: 8px;
+          border-top: 1px dashed #ccc;
+          font-size: 9pt;
+        }
+
+        .risk-judgment-section {
+          margin: 15px 0;
+          padding: 10px;
+          border: 1px solid #999;
+        }
+        .risk-judgment-title {
+          font-weight: bold;
+          margin-bottom: 10px;
+        }
+        .risk-options {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 15px;
+        }
+
+        .measures-section {
+          margin: 15px 0;
+          border: 1px solid #999;
+        }
+        .measures-title {
+          font-weight: bold;
+          background: #e8e8e8;
+          padding: 8px 12px;
+          border-bottom: 1px solid #999;
+        }
+        .measures-content {
+          padding: 10px 12px;
+        }
+        .measure-item {
+          margin-bottom: 8px;
+          font-size: 10pt;
+        }
+        .measure-other {
+          margin-left: 10px;
+          border-bottom: 1px solid #333;
+          min-width: 200px;
+          display: inline-block;
+        }
+
+        .signature-section {
+          margin-top: 20px;
+          padding: 15px;
+          border: 1px solid #999;
+        }
+        .signature-row {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+        }
+        .signature-item {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+        }
+        .signature-label {
+          font-weight: bold;
+        }
+        .signature-value {
+          border-bottom: 1px solid #333;
+          min-width: 150px;
+          height: 25px;
+        }
+
+        .note-section {
+          margin-top: 15px;
+          padding: 10px;
+          border: 1px solid #999;
+          background: #fafafa;
+        }
+        .note-title {
+          font-weight: bold;
+          margin-bottom: 8px;
+        }
+        .note-content {
+          font-size: 9pt;
+          line-height: 1.6;
+        }
+        .note-content p {
+          margin: 3px 0;
+        }
+        .note-content .indent {
+          margin-left: 20px;
+        }
+      </style>
+    </head>
+    <body>
+      <div class="header">
+        <h1>攻击风险因素评估量表</h1>
+      </div>
+
+      <div class="info-section">
+        <div class="info-row">
+          <div class="info-item">
+            <span class="label">长者姓名:</span>
+            <span class="value">${dataForm.value.elderName || ''}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">档案号:</span>
+            <span class="value">${dataForm.value.contractNumber || ''}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">床位号:</span>
+            <span class="value">${dataForm.value.bedName || ''}</span>
+          </div>
+        </div>
+        <div class="info-row" style="margin-top: 10px;">
+          <div class="info-item">
+            <span class="label">评估日期:</span>
+            <span class="value">${form.assessDate ? dayjs(form.assessDate).format('YYYY-MM-DD') : ''}</span>
+          </div>
+          <div class="info-item">
+            <span class="label">评估人:</span>
+            <span class="value">${form.assessor || ''}</span>
+          </div>
+        </div>
+      </div>
+
+
+
+      <!-- 评估表格 -->
+      <table class="assessment-table">
+        <thead>
+          <tr>
+            <th class="content-col">评估内容</th>
+            <th class="level-col">级别</th>
+            <th class="select-col">评估</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td class="content">
+              <div class="content-title">存在右之一者,若为男性则有两项:</div>
+              <div class="content-item">(1) 男性</div>
+              <div class="content-item">(2) 精神分裂症,伴有幻听或被害妄想</div>
+              <div class="content-item">(3) 躁狂</div>
+              <div class="content-item">(4) 酒药依赖的脱瘾期</div>
+              <div class="content-item">(5) 意识障碍伴行为紊乱</div>
+              <div class="content-item">(6) 痴呆伴行为紊乱</div>
+              <div class="content-item">(7) 既往人格不良者(有冲动、边缘型人格障碍)</div>
+            </td>
+            <td class="level">I级</td>
+            <td class="select">
+              <div>${form.attackLevel === 1 ? '☑' : '☐'} 符合</div>
+              <div class="print-multi-select">
+                ${[1,2,3,4,5,6,7].map(n => `${form.attackLevel1Items?.includes(n) ? '☑' : '☐'} ${n}`).join('&nbsp;&nbsp;')}
+              </div>
+            </td>
+          </tr>
+          <tr>
+            <td class="content">
+              <div class="content-title">存在右侧情形之一者</div>
+              <div class="content-item">(1) 被动的言语攻击行为,表现为激惹性增高,如无对象的抱怨、发牢骚、说怪话</div>
+              <div class="content-item">(2) 交谈时态度不好、抵触、有敌意或不信任</div>
+              <div class="content-item">(3) 或精神分裂症有命令性幻听者</div>
+            </td>
+            <td class="level">II级</td>
+            <td class="select">
+              <div>${form.attackLevel === 2 ? '☑' : '☐'} 符合</div>
+              <div class="print-multi-select">
+                ${[1,2,3].map(n => `${form.attackLevel2Items?.includes(n) ? '☑' : '☐'} ${n}`).join('&nbsp;&nbsp;')}
+              </div>
+            </td>
+          </tr>
+          <tr>
+            <td class="content">
+              <div class="content-title">存在右侧情形之一者</div>
+              <div class="content-item">(1) 主动的言语攻击行为,如有对象的辱骂</div>
+              <div class="content-item">(2) 被动的躯体攻击行为如毁物</div>
+              <div class="content-item">(3) 在交往时出现社交粗暴(交谈时突然离去、躲避、推挡他人善意的躯体接触)</div>
+              <div class="content-item">(4) 既往曾有过主动的躯体攻击行为</div>
+            </td>
+            <td class="level">III级</td>
+            <td class="select">
+              <div>${form.attackLevel === 3 ? '☑' : '☐'} 符合</div>
+              <div class="print-multi-select">
+                ${[1,2,3,4].map(n => `${form.attackLevel3Items?.includes(n) ? '☑' : '☐'} ${n}`).join('&nbsp;&nbsp;')}
+              </div>
+            </td>
+          </tr>
+          <tr>
+            <td class="content">
+              <div class="content-title">存在右侧情形之一者</div>
+              <div class="content-item">(1) 有主动的躯体攻击行为,如踢、打、咬或使用物</div>
+              <div class="content-item">(2) 攻击行为在一天内至少出现两次以上或攻击行为造成了他人肉体上的伤害</div>
+            </td>
+            <td class="level">IV级</td>
+            <td class="select">
+              <div>${form.attackLevel === 4 ? '☑' : '☐'} 符合</div>
+              <div class="print-multi-select">
+                ${[1,2].map(n => `${form.attackLevel4Items?.includes(n) ? '☑' : '☐'} ${n}`).join('&nbsp;&nbsp;')}
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- 风险程度判断 -->
+      <div class="risk-judgment-section">
+        <div class="risk-judgment-title">风险程度判断:</div>
+        <div class="risk-options">
+          <span class="risk-option">${form.riskLevel === 'none' ? '☑' : '☐'} 无风险:无级别</span>
+          <span class="risk-option">${form.riskLevel === 'low' ? '☑' : '☐'} 低风险:I级</span>
+          <span class="risk-option">${form.riskLevel === 'medium' ? '☑' : '☐'} 中风险:II级</span>
+          <span class="risk-option">${form.riskLevel === 'high' ? '☑' : '☐'} 高风险:III级、IV级</span>
+        </div>
+      </div>
+
+      <!-- 预防措施 -->
+      <div class="measures-section">
+        <div class="measures-title">预防措施</div>
+        <div class="measures-content">
+          <div class="measure-item">${form.preventiveMeasures?.includes('patrol') ? '☑' : '☐'} 加强巡视</div>
+          <div class="measure-item">${form.preventiveMeasures?.includes('handover') ? '☑' : '☐'} 严格交接班</div>
+          <div class="measure-item">${form.preventiveMeasures?.includes('checkItems') ? '☑' : '☐'} 检查有无危险物品</div>
+          <div class="measure-item">${form.preventiveMeasures?.includes('emotion') ? '☑' : '☐'} 注重长者情绪行为</div>
+          <div class="measure-item">
+            ${form.preventiveMeasures?.includes('other') ? '☑' : '☐'} 其他
+            ${form.preventiveMeasures?.includes('other') ? '<span class="measure-other">' + (form.preventiveMeasuresOther || '') + '</span>' : ''}
+          </div>
+        </div>
+      </div>
+
+
+
+      <!-- 说明 -->
+      <div class="note-section">
+        <div class="note-title">说明:</div>
+        <div class="note-content">
+          <p>1. 新入住长者应在长者入住24小时内完成首次评估;</p>
+          <p>2. 定期评估:</p>
+          <p class="indent">(1)认知照护专区:高风险1个月评估一次,中风险、低风险3个月评估一次;</p>
+          <p class="indent">(2)非认知照护专区:高风险1个月评估一次,中风险3个月评估一次,低风险6个月评估一次;</p>
+          <p>3. 当长者身体发生变化者,应及时进行动态评估。</p>
+        </div>
+      </div>
+    </body>
+    </html>
+  `
+
+  // 写入内容并打印
+  printWindow.document.write(printContent)
+  printWindow.document.close()
+
+  // 延迟打印,确保样式加载完成
+  setTimeout(() => {
+    printWindow.print()
+  }, 500)
+}
+
+</script>
+
+<style scoped lang="scss">
+
+.form-title {
+  text-align: center;
+  font-size: 20px;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.attack-risk-form {
+  max-width: 1200px;
+  margin: 0 auto;
+  background: #fff;
+
+
+
+
+
+  .info-row {
+    display: flex;
+    gap: 40px;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .info-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .label {
+        width: 82px;
+        text-align: right;
+        margin-right: 4px;
+        white-space: nowrap;
+      }
+    }
+  }
+
+
+  .form-body {
+    border: 1px solid #333;
+    padding: 15px;
+  }
+
+  // 调整 Element Plus 组件样式
+  :deep(.el-input__inner) {
+    height: 28px;
+    line-height: 28px;
+    border-top: none;
+    border-left: none;
+    border-right: none;
+    border-radius: 0;
+    padding: 0 4px;
+
+    &:focus {
+      border-color: #409eff;
+    }
+  }
+
+  :deep(.el-checkbox__label),
+  :deep(.el-radio__label) {
+    padding-left: 4px;
+  }
+}
+.row{
+  margin-bottom: 12px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  text{
+    text-align: right;
+    margin-right: 4px;
+    width: 82px;
+  }
+}
+
+// NGASR自杀风险评估量表 特有样式
+.total-score-section {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-bottom: 20px;
+
+  .total-score {
+    display: flex;
+    align-items: baseline;
+    gap: 4px;
+
+    .score-label {
+      font-size: 16px;
+      font-weight: bold;
+    }
+
+    .score-value {
+      font-size: 28px;
+      font-weight: bold;
+      color: #409eff;
+    }
+
+    .score-max {
+      font-size: 14px;
+      color: #909399;
+    }
+  }
+
+  .risk-level {
+    font-size: 16px;
+    font-weight: bold;
+    padding: 8px 16px;
+    border-radius: 4px;
+
+    &.risk-none {
+      color: #67c23a;
+      background: #f0f9eb;
+    }
+
+    &.risk-low {
+      color: #e6a23c;
+      background: #fdf6ec;
+    }
+
+    &.risk-medium {
+      color: #f56c6c;
+      background: #fef0f0;
+    }
+
+    &.risk-high {
+      color: #f56c6c;
+      background: #fef0f0;
+      border: 1px solid #f56c6c;
+    }
+  }
+}
+
+.assessment-table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-bottom: 20px;
+
+  table {
+    width: 100%;
+    border: 1px solid #333;
+
+    th, td {
+      border: 1px solid #333;
+      padding: 12px;
+      text-align: left;
+      vertical-align: top;
+    }
+
+    th {
+      background: #f5f7fa;
+      font-weight: bold;
+      text-align: center;
+    }
+
+    .content-col {
+      width: 65%;
+    }
+
+    .level-col {
+      width: 10%;
+      text-align: center;
+    }
+
+    .select-col {
+      width: 25%;
+      text-align: center;
+    }
+
+    .content {
+      text-align: left;
+
+      .content-title {
+        font-weight: bold;
+        margin-bottom: 8px;
+      }
+
+      .content-item {
+        margin-left: 15px;
+        margin-bottom: 4px;
+        font-size: 13px;
+      }
+    }
+
+    .level {
+      text-align: center;
+      font-weight: bold;
+    }
+
+    .select {
+      text-align: center;
+
+      :deep(.el-radio) {
+        margin-right: 0;
+      }
+    }
+  }
+}
+
+.risk-judgment-section {
+  margin: 20px 0;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+
+  .risk-judgment-title {
+    font-weight: bold;
+    font-size: 16px;
+    margin-bottom: 12px;
+    color: #303133;
+  }
+
+  .risk-options {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+
+    :deep(.el-radio) {
+      margin-right: 0;
+    }
+  }
+}
+
+.preventive-section {
+  margin: 20px 0;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+
+  .preventive-title {
+    font-weight: bold;
+    font-size: 16px;
+    margin-bottom: 12px;
+    color: #303133;
+  }
+
+  .preventive-options {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 15px;
+
+    :deep(.el-checkbox) {
+      margin-right: 20px;
+      margin-bottom: 8px;
+    }
+
+    .other-input {
+      width: 200px;
+      margin-left: 10px;
+    }
+  }
+}
+
+.signature-section {
+  margin: 20px 0;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+
+  .signature-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 20px;
+  }
+
+  .signature-item {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    &.date-item {
+      :deep(.el-date-picker) {
+        width: 150px;
+      }
+    }
+  }
+
+  .signature-label {
+    font-weight: bold;
+    white-space: nowrap;
+  }
+
+  .signature-input {
+    width: 150px;
+  }
+
+  .date-picker {
+    width: 150px;
+  }
+}
+
+.note-section {
+  margin-top: 20px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+
+  .note-title {
+    font-weight: bold;
+    font-size: 16px;
+    margin-bottom: 12px;
+    color: #303133;
+  }
+
+  .note-content {
+    font-size: 13px;
+    line-height: 1.8;
+    color: #606266;
+
+    p {
+      margin: 5px 0;
+    }
+
+    .indent {
+      margin-left: 20px;
+    }
+  }
+}
+</style>

+ 344 - 0
src/views/elderly/contracts/check-in-tab/index.vue

@@ -0,0 +1,344 @@
+<template>
+  <ContentWrap>
+    <!-- 保护性约束观察记录表 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="110px"
+    >
+      <el-form-item label="长者姓名">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="长者姓名"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="评估人">
+        <el-input
+          v-model="queryParams.assessor"
+          placeholder="长者姓名"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="记录日期">
+        <el-date-picker
+          size="default"
+          ref="selectRef"
+          class="!w-240px"
+          v-model="queryParams.assessDate"
+          type="daterange"
+          :clearable="true"
+          :editable="false"
+          placeholder="选择记录日期"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          date-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button @click="handleQuery" style="margin-left: 2vw"><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(undefined)"  />
+      <!--      <el-button @click="inputH" type="success"><Icon icon="fa:cloud-upload" :size="16" class="mr-1"/>导入</el-button>-->
+      <!--      <ButtonImport @click="handleImportCard"  />-->
+
+
+    </div>
+    <el-table v-loading="loading" :data="list" :header-cell-style="tableHeaderColor">
+      <el-table-column header-align="center" align="center" label="序号" width="60">
+        <template #default="scope">
+          {{
+            scope.$index + (queryParams.pageNo * queryParams.pageSize - queryParams.pageSize) + 1
+          }}
+        </template>
+      </el-table-column>
+
+
+      <el-table-column prop="elderName" header-align="center" align="center" label="长者姓名" min-width="150" show-overflow-tooltip/>
+      <el-table-column prop="assessor" header-align="center" align="center" label="评估人" min-width="200" show-overflow-tooltip/>
+      <el-table-column header-align="center" align="center" label="风险等级" min-width="200" show-overflow-tooltip>
+        <template #default="scope">
+          <el-tag v-if="getRiskLevelInfo(scope.row.assessData).text" :type="getRiskLevelInfo(scope.row.assessData).type">
+            {{ getRiskLevelInfo(scope.row.assessData).text }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="assessDate" header-align="center" align="center" label="记录日期" min-width="200" show-overflow-tooltip/>
+      <el-table-column prop="creator" header-align="center" align="center" label="记录人" min-width="200" show-overflow-tooltip/>
+
+
+      <el-table-column label="操作" align="center" width="200" >
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openFormEdit(scope.row, scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="warning"
+            @click="openFormDetail(scope.row,scope.row.id)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="openClose(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" />
+
+  <!-- 通用批量导出弹窗 -->
+  <BatchExportDialog
+    :show="showBatchExport"
+    :title="exportConfig.title"
+    :loading="exportLoading"
+    :batch-min="exportConfig.batchMin"
+    :batch-max="exportConfig.batchMax"
+    :count-min="exportConfig.countMin"
+    :count-max="exportConfig.countMax"
+    :default-batch="exportConfig.defaultBatch"
+    :default-count="exportConfig.defaultCount"
+    :description="exportConfig.description"
+    @update:show="showBatchExport = $event"
+    @confirm="handleBatchExport"
+    @cancel="showBatchExport = false"
+  />
+
+
+  <Import
+    ref="importRef"
+    @success="getList"
+    :config="{
+      title: '导入',
+      downloadUrl: 'elderly-service-order/get-import-template',
+      excelTempName: '居家工单-【导入】模板',
+      importUrl: '/elderly-service-order/import',
+      failExportUrl: '',
+    }"
+  />
+
+</template>
+
+<script setup lang="ts">
+import AddForm from "./AddForm.vue";
+import ButtonAdd from "@/components/ButtonAdd/src/ButtonAdd.vue";
+import ButtonImport from "@/components/ButtonImport/src/ButtonImport.vue";
+import { useUserStore } from '@/store/modules/user'
+import {formatTimestamp, getCurrentMonthRange} from "@/utils/dateUtil";
+import Import from "@/components/ImportFile/index.vue";
+import {restraintCreatePage, restraintDelete} from "@/api/elderly/nursing/keep-fit";
+import {
+  attackRiskDelete, attackRiskPage,
+} from "@/api/social-work";
+import { getRiskLevelInfo } from "@/utils/risk-assessment";
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const userStore = useUserStore()
+const loading = ref(true) // 列表的加载中
+const detailRef = ref()
+const importRef = ref()
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+
+// 状态管理
+const showBatchExport = ref(false)
+const exportConfig = ref({
+  title: '',
+  batchMin: 1,
+  batchMax: 999,
+  countMin: 1,
+  countMax: 200,
+  defaultBatch: 1,
+  defaultCount: 200,
+  description: [] as string[]
+})
+const exportLoading = ref(false)
+
+let queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  assessor: '',
+  // assessDate: getCurrentMonthRange(),
+  assessDate: [],
+  tenantIds: userStore.orgTenantId
+})
+const queryFormRef = ref() // 搜索的表单
+
+
+// 打开导出弹窗
+const handleImportCard = async () => {
+
+  // showBatchExport.value = true
+}
+
+// 处理批量导出
+const handleBatchExport = async (batch: number, count: number) => {
+  exportLoading.value = true
+  console.log(batch,count)
+  // try {
+  //   let queryParams = {
+  //     pageNo: batch,
+  //     pageSize: count,
+  //     tenantId: userStore.orgTenantId[0]
+  //   }
+  //   const list = await careRecordsPage(queryParams)
+  //   if (list.length <= 0) {
+  //     message.error('暂无数据可以导出!')
+  //     return
+  //   }
+  //   const headers = [
+  //     { key: 'organizationName', title: '机构名称' },
+  //     { key: 'visitName', title: '姓名' },
+  //     { key: 'visitPhone', title: '手机号' },
+  //     { key: 'visitDate', title: '预约时间' },
+  //     { key: 'accompanyCount', title: '陪同人数' },
+  //     { key: 'reason', title: '理由' }
+  //   ]
+  //   exportWithExpandedObjectArrays(list, headers, `医疗护理记录${formatToDateTime()}.xlsx`, '医疗护理记录')
+  // } catch (_) {}
+
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    //let queryP = {...queryParams,discoveryTime:queryParams.discoveryTime?[queryParams.discoveryTime[0]+" 00:00:00",queryParams.discoveryTime[1]+" 23:59:59"]:null}
+    const data = await attackRiskPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  if (!queryFormRef.value) return
+  const valid = await queryFormRef.value.validate()
+  if (!valid) return
+  queryParams.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.elderName = ''
+  queryParams.assessor = ''
+  queryParams.tenantIds = userStore.orgTenantId
+  // queryParams.assessDate= getCurrentMonthRange()
+  queryParams.assessDate = []
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (id?: number) => {
+  if(queryParams.tenantIds.length == 0 || queryParams.tenantIds.length > 1){
+    message.error('新增只能选择一个机构')
+    return
+  }
+  formRef.value.open(queryParams.tenantIds[0], id,false)
+}
+
+const editRef = ref()
+const openFormEdit = (row: any = {}, id?: number) => {
+  formRef.value.open(row.tenantId, id,false)
+}
+
+
+const openFormDetail = (row: any = {},id?: number) => {
+  formRef.value.open(row.tenantId,id,true)
+}
+
+
+
+
+
+const openClose = async (item) => {
+  try {
+    console.log("任务ID",item)
+    const res = await message.confirm('确定要删除吗?', '提示')
+    if (res == 'confirm') {
+      // 发起
+      try {
+        const res = await attackRiskDelete(item.id)
+        if (res){
+          message.success(t('common.updateSuccess'))
+        }
+      }catch(err) {}
+    }
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 取消按钮操作 */
+const cancelDelete = async (id: number) => {
+  try {
+    // 取消的二次确认
+    await message.cancelConfirm()
+    // 发起取消
+    // await elderlyBakDel(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+const route = useRoute()
+/** 初始化 **/
+onMounted(() => {
+  if(route.query && route.query.elderName){
+    queryParams.elderName = route.query.elderName as string
+  }
+  getList()
+})
+
+// 表头格式
+const tableHeaderColor = ({ rowIndex }: any) => {
+  if (rowIndex === 0) {
+    return {
+      backgroundColor: '#f8f8f9',
+      color: '#666666',
+      fontWeight: 'bold'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 12 - 1
src/views/elderly/contracts/components/AttachmentPage.vue

@@ -21,7 +21,7 @@ const hasData = computed(() => {
 </script>
 
 <template>
-  <div class="attachment-page">
+  <div class="attachment-page" :class="{ 'is-filled': hasData }">
     <div class="attachment-header">
       <div class="attachment-title">
         附件{{ attachmentNo }}:{{ title }}
@@ -58,6 +58,13 @@ const hasData = computed(() => {
   background: #fff;
   box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
   box-sizing: border-box;
+
+  &.is-filled {
+    padding: 0;
+    background: transparent;
+    box-shadow: none;
+    min-height: auto;
+  }
 }
 
 .attachment-header {
@@ -101,6 +108,10 @@ const hasData = computed(() => {
     break-after: page;
   }
 
+  .attachment-page.is-filled {
+    padding: 0 !important;
+  }
+
   .attachment-page:last-child {
     page-break-after: auto;
     break-after: auto;

+ 94 - 7
src/views/elderly/contracts/components/ContractBody.vue

@@ -2,17 +2,20 @@
 import { reactive, computed, ref, watch } from 'vue'
 import { formatBackendDateTime } from '@/utils/formatTime'
 import { DICT_TYPE, getDictLabel } from '@/utils/dict'
-import { getCheckInRecordId } from '@/api/elderly/apply/check-in'
+import {getCheckInDetailByElderId, getCheckInRecordId} from '@/api/elderly/apply/check-in'
 import FieldItem from './FieldItem.vue'
 import SignatureItem from './SignatureItem.vue'
 
 const props = defineProps<{
   recordRow?: any
   isTextMode?: boolean
+  signatureData?: any
+  modelValue?: any
 }>()
 
 const emit = defineEmits<{
   (e: 'update:data', val: any): void
+  (e: 'update:modelValue', val: any): void
 }>()
 
 const loading = ref(false)
@@ -288,6 +291,19 @@ const fillContractForm = (data: any) => {
   contractForm.partyCHomeAddr = relative.nowAddress || ''
   contractForm.partyCWorkUnit = relative.workUnit || ''
 
+  // 如果外部传入了签名数据(如打印时),覆盖合同表单中的签名字段
+  // 优先使用 v-model 传入的 modelValue,其次是 signatureData
+  const externalSignData = props.modelValue || props.signatureData
+  if (externalSignData) {
+    if (externalSignData.partyASign != null && externalSignData.partyASign !== '') contractForm.partyASign = externalSignData.partyASign
+    if (externalSignData.partyALegalSign != null && externalSignData.partyALegalSign !== '') contractForm.partyALegalSign = externalSignData.partyALegalSign
+    if (externalSignData.partyADate != null && externalSignData.partyADate !== '') contractForm.partyADate = externalSignData.partyADate
+    if (externalSignData.partyBSign != null && externalSignData.partyBSign !== '') contractForm.partyBSign = externalSignData.partyBSign
+    if (externalSignData.partyBDate != null && externalSignData.partyBDate !== '') contractForm.partyBDate = externalSignData.partyBDate
+    if (externalSignData.partyCSign != null && externalSignData.partyCSign !== '') contractForm.partyCSign = externalSignData.partyCSign
+    if (externalSignData.partyCDate != null && externalSignData.partyCDate !== '') contractForm.partyCDate = externalSignData.partyCDate
+  }
+
   emit('update:data', { ...contractForm })
 }
 
@@ -307,7 +323,7 @@ const fetchContractData = async (row: any) => {
   loading.value = true
   try {
     const status = row.status ?? ''
-    const res = await getCheckInRecordId(recordId, status)
+    const res = await getCheckInDetailByElderId(row.elderId)
     if (res && res.data) {
       fillContractForm(res.data)
     }
@@ -328,6 +344,21 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => props.signatureData,
+  (val) => {
+    if (!val) return
+    if (val.partyASign) contractForm.partyASign = val.partyASign
+    if (val.partyALegalSign) contractForm.partyALegalSign = val.partyALegalSign
+    if (val.partyADate) contractForm.partyADate = val.partyADate
+    if (val.partyBSign) contractForm.partyBSign = val.partyBSign
+    if (val.partyBDate) contractForm.partyBDate = val.partyBDate
+    if (val.partyCSign) contractForm.partyCSign = val.partyCSign
+    if (val.partyCDate) contractForm.partyCDate = val.partyCDate
+  },
+  { deep: true }
+)
+
 watch(
   () => contractForm.depositAmt1,
   (val) => {
@@ -348,6 +379,47 @@ watch(
   }
 )
 
+// 外部 modelValue 变化时,同步签名和日期字段到 contractForm
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (!val) return
+    if (val.partyASign != null && val.partyASign !== '') contractForm.partyASign = val.partyASign
+    if (val.partyALegalSign != null && val.partyALegalSign !== '') contractForm.partyALegalSign = val.partyALegalSign
+    if (val.partyADate != null && val.partyADate !== '') contractForm.partyADate = val.partyADate
+    if (val.partyBSign != null && val.partyBSign !== '') contractForm.partyBSign = val.partyBSign
+    if (val.partyBDate != null && val.partyBDate !== '') contractForm.partyBDate = val.partyBDate
+    if (val.partyCSign != null && val.partyCSign !== '') contractForm.partyCSign = val.partyCSign
+    if (val.partyCDate != null && val.partyCDate !== '') contractForm.partyCDate = val.partyCDate
+  },
+  { immediate: true, deep: true }
+)
+
+// 签名或日期变化时,同步给父组件(用于打印时共享签名数据)
+watch(
+  () => [
+    contractForm.partyASign,
+    contractForm.partyALegalSign,
+    contractForm.partyADate,
+    contractForm.partyBSign,
+    contractForm.partyBDate,
+    contractForm.partyCSign,
+    contractForm.partyCDate
+  ],
+  () => {
+    emit('update:data', { ...contractForm })
+    emit('update:modelValue', {
+      partyASign: contractForm.partyASign,
+      partyALegalSign: contractForm.partyALegalSign,
+      partyADate: contractForm.partyADate,
+      partyBSign: contractForm.partyBSign,
+      partyBDate: contractForm.partyBDate,
+      partyCSign: contractForm.partyCSign,
+      partyCDate: contractForm.partyCDate
+    })
+  }
+)
+
 const displayDate = computed(() => {
   const today = new Date()
   return `${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日`
@@ -357,7 +429,7 @@ const displayDate = computed(() => {
 <template>
   <div class="contract-body">
     <!-- ========== 第1页:封面 ========== -->
-    <div class="a4-page">
+    <div class="a4-page cover-a4">
       <div class="page-header">
         <div class="logo-line">
           <span class="logo-circle">颐</span>
@@ -1697,13 +1769,28 @@ const displayDate = computed(() => {
     width: 100%;
   }
 
-  .a4-page {
+  /* 封面页:保留完整 A4 高度 */
+  .a4-page.cover-a4 {
+    width: 100% !important;
+    min-height: 297mm !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    background: #fff !important;
+    box-shadow: none !important;
+    box-sizing: border-box !important;
+    page-break-after: always;
+    break-after: page;
+  }
+
+  /* 其他页:自适应高度,由浏览器自动分页 */
+  .a4-page:not(.cover-a4) {
     width: 100% !important;
     min-height: auto !important;
-    height: auto !important;
     margin: 0 !important;
-    padding: 10mm 14mm !important;
+    padding: 0 !important;
+    background: #fff !important;
     box-shadow: none !important;
+    box-sizing: border-box !important;
     page-break-after: always;
     break-after: page;
   }
@@ -1715,7 +1802,7 @@ const displayDate = computed(() => {
 
   @page {
     size: A4;
-    margin: 0;
+    margin: 18mm;
   }
 }
 </style>

+ 58 - 80
src/views/elderly/contracts/components/RiskDisclosureBody.vue

@@ -55,11 +55,9 @@ const updateForm = (key: keyof FormData, val: any) => {
 
 <template>
   <div class="risk-disclosure">
-    <!-- Page 1 -->
     <div class="a4-page">
-      <div class="doc-header">
-        <div class="doc-logo">颐年医养</div>
-        <div class="doc-title">风险知情告知书</div>
+      <div class="doc-logo" style="margin-bottom: 20px;">
+        附件1.知情同意书
       </div>
 
       <div class="doc-body">
@@ -123,16 +121,7 @@ const updateForm = (key: keyof FormData, val: any) => {
           <li>对于乙方请假外出,需自行解决往返交通。</li>
           <li>其他相关请假外出规定。</li>
         </ol>
-      </div>
-    </div>
 
-    <!-- Page 2 -->
-    <div class="a4-page">
-      <div class="doc-header">
-        <div class="doc-logo">颐年医养</div>
-      </div>
-
-      <div class="doc-body">
         <h2 class="section-title">二、难免性压疮风险</h2>
         <p>
           鉴于乙方体质特殊,身体素质欠佳且营养吸收能力较弱,加之患病、皮肤抵抗力低下、大小便失禁以及长期使用石膏绷带和夹板等因素,在治疗与护理过程中极易引发压疮。因此,乙方在入住期间,将严格遵照甲方护理级别,采取定期翻身、清洁身体、避免潮湿、摩擦及排泄物刺激、促进局部血液循环、增加营养摄入等防范措施。乙方与丙方应配合甲方积极开展治疗。即便甲方积极采取相应措施,压疮仍有可能发生,乙方、丙方及其他亲属需对压疮风险予以知悉并理解。
@@ -159,16 +148,7 @@ const updateForm = (key: keyof FormData, val: any) => {
           <li>老年人应全面、客观地评估自身身体状况,及时向工作人员寻求协助,并认真听取工作人员提出的合理化建议。若从事超出自身能力范围的事务,以避免发生跌倒情况。若不慎发生跌倒或坠床,切勿急于起身;若身体受伤,应留在原地并大声呼救。</li>
           <li>为防止此风险发生,乙方与丙方应遵循甲方工作人员的建议,将该情况告知其他家属、监护人及陪护人员。</li>
         </ol>
-      </div>
-    </div>
-
-    <!-- Page 3 -->
-    <div class="a4-page">
-      <div class="doc-header">
-        <div class="doc-logo">颐年医养</div>
-      </div>
 
-      <div class="doc-body">
         <h2 class="section-title">四、误吸噎食风险</h2>
         <p>
           鉴于老年人因年老体弱或罹患疾病等缘故,常出现吞咽反射迟缓或吞咽功能障碍的情况,易引发误吸、噎食现象。为降低此类情况的发生几率,现将相关危险因素及防范措施告知如下:
@@ -209,26 +189,37 @@ const updateForm = (key: keyof FormData, val: any) => {
         </p>
         <ol class="num-list">
           <li>丙方不得携带不适宜乙方食用或服用的食品和药品。</li>
-          <li>若乙方存在记忆力差、认知功能减退、视力异常等情形,则不适合自行准备和服用药物。关于乙方服药事宜,应与甲方工作人员进行沟通协商。丙方不得擅自让乙方自行保管药物及服药,乙方和丙方应当遵守甲方的自备药管理规定。为避免此类风险的发生,乙方和丙方应听取甲方工作人员的建议,并将此类情况告知其他家</li>
+          <li>若乙方存在记忆力差、认知功能减退、视力异常等情形,则不适合自行准备和服用药物。关于乙方服药事宜,应与甲方工作人员进行沟通协商。丙方不得擅自让乙方自行保管药物及服药,乙方和丙方应当遵守甲方的自备药管理规定。为避免此类风险的发生,乙方和丙方应听取甲方工作人员的建议,并将此类情况告知其他家属、监护人及陪护人员。</li>
         </ol>
-      </div>
-    </div>
 
-    <!-- Page 4 - Signatures -->
-    <div class="a4-page">
-      <div class="doc-body">
-        <div style="margin-top: 60px; font-size: 14px; line-height: 2.4;">
-          <p style="margin-bottom: 30px;">
-            本人(乙方及丙方)已仔细阅读并充分理解以上告知事项的全部内容,对上述风险已有清楚的认知。本人自愿选择入住甲方,并承诺在入住期间严格遵守甲方的相关规章制度。乙方及丙方确认:在甲方工作人员已就上述风险向本人进行了充分的告知和解释,本人已完全知悉并理解上述风险的内容及可能产生的后果。</p>
-          <p style="margin-bottom: 30px;">
-            乙方及丙方同意:如因乙方或丙方的原因(包括但不限于违反告知书的相关规定、未遵守甲方管理规定、未听从甲方工作人员的合理建议等)导致发生意外情况,乙方及丙方自愿承担相应风险及后果,与甲方无关。</p>
-          <p style="margin-bottom: 40px;">
-            本告知书自各方签字之日起生效。</p>
+        <p style="text-indent: 2em;">
+          针对此情况,丙方以及乙方的亲属、朋友应当定期与乙方进行交流沟通,前往探望乙方,或者接乙方外出实现团聚。同时,丙方需将此建议传达给乙方的家属或监护人。
+        </p>
+
+        <h2 class="section-title">七、他伤和自伤风险</h2>
+        <p>
+          乙方由于精神、心理、情绪、性格、生活习惯等方面的因素,无法完全适应养老院的生活。倘若亲属未能时常向老人表达关爱之情,乙方的精神慰藉需求将无法得到满足。当乙方处于精神和情绪不稳定的状态时,可能会产生悲观轻生的念头,或者容易被激怒,进而出现自伤或伤人的行为。
+        </p>
+
+        <h2 class="section-title">八、走失风险</h2>
+        <p>
+          鉴于乙方存在意识或定向障碍、记忆力减退等情况,其面临走失风险。为降低乙方走失事件的发生几率,丙方及乙方家属不得擅自带领乙方离开居住区或院区。
+        </p>
+
+        <h2 class="section-title">九、文娱活动意外风险</h2>
+        <p>
+          乙方因年事已高、身体孱弱、行动不便或患有疾病等缘由,在参与文娱或康乐活动期间,较易出现意外状况,诸如摔倒、突发疾病等。针对此情况,甲方工作人员会采取相应的防护举措,然而文娱活动中的意外状况仍有可能发生。
+        </p>
+
+        <div style="margin-top: 40px; font-size: 14px; line-height: 1.9;">
+          <p style="text-indent: 2em;">
+            本附件内容甲方已详尽告知,我方已审慎阅读并予以认同,同时将全力配合甲方做好乙方的安全保障工作。对于本附件所提及风险的发生,我方表示理解并承诺不会追究甲方的责任。
+          </p>
         </div>
 
-        <div class="signatures" style="margin-top: 80px; font-size: 14px;">
+        <div class="signatures" style="margin-top: 60px; font-size: 14px;">
           <div class="signature-group">
-            <p>乙方(签字):
+            <p>乙方签阅(签字按手印):
               <SignatureItem
                 :model-value="form.partyBSign"
                 :is-text-mode="isTextMode"
@@ -239,53 +230,24 @@ const updateForm = (key: keyof FormData, val: any) => {
                 @update:model-value="(v: any) => updateForm('partyBSign', v)"
               />
             </p>
-            <p>日期:
-              <FieldItem
-                :model-value="form.partyBDate"
-                :is-text-mode="isTextMode"
-                type="date"
-                width="200px"
-                placeholder="请选择日期"
-                @update:model-value="(v: any) => updateForm('partyBDate', v)"
-              />
-            </p>
           </div>
 
           <div class="signature-group">
-            <p>丙方(签字/盖章):
+            <p>丙方签阅(签字按手印):
               <SignatureItem
                 :model-value="form.partyCSign"
                 :is-text-mode="isTextMode"
                 width="200"
                 height="80"
-                placeholder="(签字/盖章处)"
+                placeholder="(签字处)"
                 title="丙方签字"
                 @update:model-value="(v: any) => updateForm('partyCSign', v)"
               />
             </p>
-            <p>日期:
-              <FieldItem
-                :model-value="form.partyCDate"
-                :is-text-mode="isTextMode"
-                type="date"
-                width="200px"
-                placeholder="请选择日期"
-                @update:model-value="(v: any) => updateForm('partyCDate', v)"
-              />
-            </p>
           </div>
 
           <div class="signature-group">
-            <p>记录人(签字):
-              <FieldItem
-                :model-value="form.assessor"
-                :is-text-mode="isTextMode"
-                width="200px"
-                placeholder="请填写记录人"
-                @update:model-value="(v: any) => updateForm('assessor', v)"
-              />
-            </p>
-            <p>记录日期:
+            <p>日期:
               <FieldItem
                 :model-value="form.assessDate"
                 :is-text-mode="isTextMode"
@@ -309,9 +271,8 @@ const updateForm = (key: keyof FormData, val: any) => {
 
 .a4-page {
   width: 210mm;
-  min-height: 297mm;
   margin: 0 auto 20px;
-  padding: 20mm 18mm;
+  padding: 10mm 18mm;
   background: #fff;
   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
   box-sizing: border-box;
@@ -333,7 +294,7 @@ const updateForm = (key: keyof FormData, val: any) => {
 }
 
 .doc-title {
-  font-size: 22px;
+  font-size: 14px;
   font-weight: 700;
   color: #000;
   letter-spacing: 4px;
@@ -359,6 +320,8 @@ const updateForm = (key: keyof FormData, val: any) => {
   color: #000;
   margin: 24px 0 10px;
   text-align: center;
+  break-after: avoid-page;
+  page-break-after: avoid;
 }
 
 .sub-title {
@@ -367,6 +330,8 @@ const updateForm = (key: keyof FormData, val: any) => {
   color: #000;
   margin: 14px 0 6px;
   padding-left: 0;
+  break-after: avoid-page;
+  page-break-after: avoid;
 }
 
 .num-list {
@@ -379,6 +344,8 @@ const updateForm = (key: keyof FormData, val: any) => {
   margin-bottom: 5px;
   line-height: 1.9;
   text-align: justify;
+  break-inside: avoid-page;
+  page-break-inside: avoid;
 }
 
 .signatures {
@@ -398,20 +365,31 @@ const updateForm = (key: keyof FormData, val: any) => {
 @media print {
   .a4-page {
     width: 100% !important;
-    min-height: auto !important;
     margin: 0 !important;
-    padding: 15mm 15mm !important;
+    padding: 0 !important;
+    background: #fff !important;
     box-shadow: none !important;
-    page-break-after: always;
-    break-after: page;
+    box-sizing: border-box !important;
+  }
+  .section-title {
+    break-after: avoid-page;
+    page-break-after: avoid;
   }
-  .a4-page:last-child {
-    page-break-after: auto;
-    break-after: auto;
+  .sub-title {
+    break-after: avoid-page;
+    page-break-after: avoid;
+  }
+  .num-list li {
+    break-inside: avoid-page;
+    page-break-inside: avoid;
+  }
+  .signature-group {
+    break-inside: avoid-page;
+    page-break-inside: avoid;
   }
   @page {
     size: A4;
-    margin: 0;
+    margin: 18mm;
   }
   body {
     margin: 0;

+ 39 - 1
src/views/elderly/contracts/components/SignatureItem.vue

@@ -182,7 +182,22 @@ onUpdated(async () => {
   <span class="signature-item" :class="{ 'is-text': isTextMode }" :style="{ width: width + 'px' }">
     <template v-if="isTextMode">
       <template v-if="hasSignature">
-        <canvas ref="textCanvasRef" :width="width" :height="height" class="signature-canvas"></canvas>
+        <!-- 图片格式签名:直接用 img 标签,浏览器和 html2canvas 都能稳定渲染 -->
+        <img
+          v-if="signFormat === 'image'"
+          :src="resolvedImageSrc"
+          class="signature-image"
+          :style="{ width: width + 'px', height: height + 'px' }"
+          alt="签名"
+        />
+        <!-- 矢量格式签名:仍然使用 canvas 绘制 -->
+        <canvas
+          v-else
+          ref="textCanvasRef"
+          :width="width"
+          :height="height"
+          class="signature-canvas"
+        ></canvas>
       </template>
       <span v-else class="fill-blank" :style="{ width: width + 'px' }"></span>
     </template>
@@ -238,6 +253,14 @@ onUpdated(async () => {
   height: 100%;
 }
 
+.signature-image {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  background: transparent;
+}
+
 .signature-empty {
   display: flex;
   align-items: center;
@@ -275,5 +298,20 @@ onUpdated(async () => {
     vertical-align: middle;
     background: transparent;
   }
+
+  .signature-image {
+    display: inline-block;
+    vertical-align: middle;
+    background: transparent;
+  }
+}
+
+@media print {
+  .signature-image,
+  .signature-canvas {
+    display: inline-block !important;
+    visibility: visible !important;
+    opacity: 1 !important;
+  }
 }
 </style>

+ 2 - 2
src/views/elderly/elder/contract/index.vue

@@ -154,8 +154,8 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (row: any = {}, type: number = 1) => {
-  //contractManageFormRef.value.open(row, type)
-  formRef.value.open(row, type)
+  contractManageFormRef.value.open(row, type)
+  //formRef.value.open(row, type)
 }
 
 // 变更/续签