Quellcode durchsuchen

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

unknown vor 3 Wochen
Ursprung
Commit
842724c754

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

@@ -312,8 +312,8 @@ export const getNursingLogPage = (data) => {
   })
 }
 
-// 护理日志详情
-export const getNursingLogDetail = (params) => {
+// 护理日志详情:业务 data 为 { header, records: { list, total } }(axios 已解包外层 data)
+export const getNursingLogDetail = (params: Recordable) => {
   return request.get({
     url: `elderly-nursing-log/detail`,
     params

+ 39 - 0
src/api/elderly/nursing/special-nursing-log/index.ts

@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+/** 特殊照护完成日志(单行) */
+export interface ElderlySpecialNursingLogRespVO {
+  id?: number
+  specialPlanItemId?: number
+  elderId?: number
+  elderName?: string
+  bedName?: string
+  nurseLevelName?: string
+  finishTime?: string
+  remark?: string
+  /** 多图时逗号分隔等,与列表展示约定一致 */
+  imageUrl?: string
+  tenantId?: number
+}
+
+/** 分页列表 */
+export const getSpecialNursingLogPage = (params: Recordable) => {
+  return request.get({
+    url: 'nursing/special-nursing-log/page',
+    params
+  })
+}
+
+/**
+ * 某长者在完成日期区间内的详情日志(服务端返回列表,前端可做分页切片)
+ * finishTime:完成日期范围,如 ['2024-01-01','2024-01-31']
+ */
+export const getSpecialNursingLogListByElderAndFinishTime = (params: {
+  elderId: number | string
+  finishTime: [string, string]
+  tenantId?: number
+}) => {
+  return request.get<ElderlySpecialNursingLogRespVO[]>({
+    url: 'nursing/special-nursing-log/list-by-elder-and-finish-time',
+    params
+  })
+}

+ 104 - 0
src/api/elderly/nursing/special-plan/index.ts

@@ -0,0 +1,104 @@
+import request from '@/config/axios'
+
+/** 明细(详情接口返回) */
+export interface SpecialNursingPlanItemRespVO {
+  id?: number
+  specialNursingPlanId?: number
+  nurseItemId?: number
+  nurseItemName?: string
+  /** 0:未完成,1:已完成(任务侧) */
+  status?: number
+  tenantId?: number
+  createTime?: string
+  updateTime?: string
+  creator?: string
+  updater?: string
+}
+
+/** 详情 / 分页列表单行:特殊护理计划 */
+export interface SpecialNursingPlanRespVO {
+  id?: number
+  elderId?: number
+  elderName?: string
+  tenantId?: number
+  /** 以下为扩展展示字段(部分环境详情接口会与长者快照一并返回) */
+  bedName?: string
+  nurseLevelName?: string
+  createTime?: string
+  updateTime?: string
+  creator?: string
+  updater?: string
+  items?: SpecialNursingPlanItemRespVO[]
+}
+
+/** 保存项:与普通护理计划区分开,后端字段见 SpecialNursingPlanItemSaveReqVO */
+export interface SpecialNursingPlanItemSaveReqVO {
+  /** 明细主键,修改明细时传递 */
+  id?: number
+  /** 所属特殊护理计划 ID,更新必填 */
+  specialNursingPlanId?: number
+  nurseItemId: number
+  nurseItemName?: string
+  /** 0:未完成,1:已完成(任务侧维护,本模块表单不传) */
+  status?: number
+  tenantId: number
+}
+
+export interface SpecialNursingPlanSaveReqVO {
+  /** 护理计划主键,修改时必传 */
+  id?: number
+  elderId: number
+  elderName?: string
+  tenantId: number
+  items?: SpecialNursingPlanItemSaveReqVO[]
+}
+
+/** 分页查询 */
+export const getSpecialNursingPlanPage = (params: Recordable) => {
+  return request.get({
+    url: 'nursing/special-plan/page',
+    params
+  })
+}
+
+export const createSpecialNursingPlan = (data: SpecialNursingPlanSaveReqVO) => {
+  return request.post({
+    url: 'nursing/special-plan/create',
+    data
+  })
+}
+
+export const updateSpecialNursingPlan = (data: SpecialNursingPlanSaveReqVO) => {
+  return request.put({
+    url: 'nursing/special-plan/update',
+    data
+  })
+}
+
+export const deleteSpecialNursingPlan = (id: number | string) => {
+  return request.delete({
+    url: `nursing/special-plan/delete`,
+    params: { id }
+  })
+}
+
+/**
+ * 查看详情 / 编辑前拉取完整数据
+ * GET `/nursing/special-plan/get`
+ * @param id 护理计划主键 ID(列表行 id,即接口文档中的「任务 id」所指业务主键)
+ * 通用响应:`{ code, data: SpecialNursingPlanRespVO, msg }`,request 封装后返回 `data` 即为 `SpecialNursingPlanRespVO`
+ */
+export const getSpecialNursingPlan = (id: number | string) => {
+  return request.get<SpecialNursingPlanRespVO>({
+    url: 'nursing/special-plan/get',
+    params: { id }
+  })
+}
+
+/** 更新特殊护理计划明细(如撤销完成状态:status 传 0) */
+export const updateSpecialPlanItem = (data: { id: number; specialItemId: number; status?: number }) => {
+  return request.put({
+    url: '/nursing/special-nursing-log/cancel',
+    data
+  })
+}

+ 26 - 3
src/components/UploadFile/src/UploadFile.vue

@@ -68,7 +68,13 @@
 </template>
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
-import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
+import {
+  ElMessage,
+  type UploadInstance,
+  type UploadProps,
+  type UploadRawFile,
+  type UploadUserFile
+} from 'element-plus'
 import { isString } from '@/utils/is'
 import { useUpload } from '@/components/UploadFile/src/useUpload'
 import { UploadFile } from 'element-plus/es/components/upload/src/upload'
@@ -99,6 +105,21 @@ const uploadNumber = ref<number>(0)
 
 const { uploadUrl, httpRequest } = useUpload()
 
+/** 上传中常驻提示(duration:0),成功或失败后手动关闭 */
+let uploadingMessageClose: { close(): void } | null = null
+const closeUploadingMessage = () => {
+  uploadingMessageClose?.close()
+  uploadingMessageClose = null
+}
+const openUploadingMessageOnce = () => {
+  if (uploadingMessageClose) return
+  uploadingMessageClose = ElMessage.warning({
+    message: '正在上传文件,请稍候...',
+    duration: 0,
+    showClose: false
+  })
+}
+
 // 文件上传之前判断
 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
   if (fileList.value.length >= props.limit) {
@@ -122,7 +143,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
     message.error(`上传文件大小不能超过${props.fileSize}MB!`)
     return false
   }
-  message.success('正在上传文件,请稍候...')
+  openUploadingMessageOnce()
   uploadNumber.value++
 }
 // 处理上传的文件发生变化
@@ -131,7 +152,6 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
 // }
 // 文件上传成功
 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
-  message.success('上传成功')
   // 删除自身
   const index = fileList.value.findIndex((item) => item.response?.data === res.data)
   fileList.value.splice(index, 1)
@@ -141,6 +161,8 @@ const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
     name: res.data
   })
   if (uploadList.value.length == uploadNumber.value) {
+    closeUploadingMessage()
+    message.success('上传成功')
     fileList.value.push(...uploadList.value)
     uploadList.value = []
     uploadNumber.value = 0
@@ -153,6 +175,7 @@ const handleExceed: UploadProps['onExceed'] = (): void => {
 }
 // 上传错误提示
 const excelUploadError: UploadProps['onError'] = (): void => {
+  closeUploadingMessage()
   message.error('导入数据失败,请您重新上传!')
 }
 // 删除上传文件

+ 15 - 7
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -30,6 +30,7 @@
               <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
             </template>
             <form-create
+              :key="'approve-task-' + (runningTask?.id ?? '') + '-' + (runningTask?.formId ?? '')"
               v-model="approveForm.value"
               v-model:api="approveFormFApi"
               :option="approveForm.option"
@@ -694,7 +695,7 @@ const closePropover = (type: string, formRef: FormInstance | undefined) => {
 
 /** 处理审批通过和不通过的操作 */
 const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => {
-  if(formLoading.value){
+  if (formLoading.value) {
     return
   }
   formLoading.value = true
@@ -715,10 +716,17 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
       if (runningTask.value.signEnable) {
         data.signPicUrl = approveReasonForm.signPicUrl
       }
-      // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
-      // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
-      const formCreateApi = approveFormFApi.value
-      if (Object.keys(formCreateApi)?.length > 0) {
+      // 多表单处理:任务级自定义表单(form-create);须校验并将填写值写入 variables
+      // 注意:不可用 Object.keys(fApi) 判断 —— loadTodoTask 会先把 approveFormFApi 置为 {},
+      // 若在 API 尚未重新注入前判断会得到 0 键,从而跳过校验且 variables 未覆盖为审批表单的值。
+      const hasTaskApproveForm = runningTask.value?.formId > 0
+      if (hasTaskApproveForm) {
+        await nextTick()
+        const formCreateApi = approveFormFApi.value
+        if (typeof formCreateApi?.validate !== 'function') {
+          message.warning('审批自定义表单未完成初始化,请稍后再试')
+          return
+        }
         await formCreateApi.validate()
         // @ts-ignore
         data.variables = approveForm.value.value
@@ -741,9 +749,9 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
     // 加载最新数据
     reload()
   } finally {
-    setTimeout(()=>{
+    setTimeout(() => {
       formLoading.value = false
-    },400)
+    }, 400)
   }
 }
 

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

@@ -118,6 +118,50 @@ export const LifeCarePlanColumns = reactive([
   // }
 ])
 
+// ===============特殊护理计划=====================
+export const SpecialNursingPlanColumns = reactive([
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '创建时间',
+    field: 'createTime',
+    type: '9',
+    width: 170
+  },
+  {
+    label: '更新时间',
+    field: 'updateTime',
+    type: '9',
+    width: 170
+  },
+  {
+    label: '创建人',
+    field: 'creator'
+  },
+  {
+    label: '更新人',
+    field: 'updater'
+  }
+])
+
+// =============特殊照护完成日志=====================
+export const SpecialNursingLogColumns = reactive([
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '床位',
+    field: 'bedName'
+  },
+  {
+    label: '护理等级',
+    field: 'nurseLevelName'
+  }
+])
+
 // =============护理日志=====================
 export const NurseLogColumns = reactive([
   {

+ 121 - 31
src/views/elderly/nursing/life-care-plan/Form.vue

@@ -32,16 +32,16 @@
           </el-col>
           <el-col :span="12" :xs="24">
             <el-form-item label="床位号" prop="bedName">
-             <el-input v-if="!isDetail" v-model="dataForm.bedName" disabled/>
-              <el-text v-else>{{dataForm.bedName}}</el-text>
+              <el-input v-if="!isDetail" v-model="dataForm.bedName" disabled />
+              <el-text v-else>{{ dataForm.bedName }}</el-text>
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="20">
           <el-col :span="12" :xs="24">
             <el-form-item label="护理等级" prop="nurseLevelName">
-              <el-input v-if="!isDetail" v-model="dataForm. nurseLevelName" disabled/>
-              <el-text v-else>{{dataForm.nurseLevelName}}</el-text>
+              <el-input v-if="!isDetail" v-model="dataForm.nurseLevelName" disabled />
+              <el-text v-else>{{ dataForm.nurseLevelName }}</el-text>
             </el-form-item>
           </el-col>
           <el-col :span="12" :xs="24">
@@ -70,15 +70,40 @@
             v-for="(p, i) in dataForm.items"
             :key="i"
           >
-            <div class="border">
-              <el-checkbox
-                :label="p.nurseItemName"
-                v-model="p.checked"
-                @change="(arg) => handleChangeCheckBox(arg, p)"
-                v-if="!isDetail"
-              />
-              <div v-else class="itemName">{{ p.nurseItemName }}</div>
-            </div>
+            <el-row :gutter="20">
+              <el-col :span="16">
+                <div class="item-left">
+                  <el-checkbox
+                    :label="`${p.nurseItemName}(${p.frequencyCategory})`"
+                    v-model="p.checked"
+                    @change="(arg) => handleChangeCheckBox(arg, p)"
+                    v-if="!isDetail"
+                  />
+                  <div v-else class="itemName"
+                    >{{ p.nurseItemName }}({{ p.frequencyCategory }})</div
+                  >
+                </div>
+              </el-col>
+              <el-col :span="8">
+                <div class="item-time" v-if="!isDetail">
+                  <span class="time-label required" v-if="p.frequencyCategoryType == 1"
+                    >护理开始时间</span
+                  >
+                  <el-time-picker
+                    v-if="p.frequencyCategoryType == 1"
+                    v-model="p.beginTime"
+                    value-format="HH:mm:ss"
+                    placeholder="请选择开始时间"
+                  />
+                </div>
+                <div v-else class="item-time detail-time">
+                  <span class="time-label" v-if="p.frequencyCategoryType == 1">护理开始时间</span>
+                  <span v-if="p.frequencyCategoryType == 1">{{
+                    formatBeginTimeDetail(p.beginTime)
+                  }}</span>
+                </div>
+              </el-col>
+            </el-row>
           </el-col>
         </el-row>
       </div>
@@ -117,15 +142,15 @@ const state = reactive<planType>({
     nurseLevelName: '',
     effectiveDate: '',
     items: [],
-    extraItems: [],
+    extraItems: []
   },
   dataRule: {
     elderId: [{ required: true, message: '长者姓名不能为空', trigger: 'blur' }],
     effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'blur' }]
-  },
+  }
 })
 const { dataForm, dataRule } = toRefs(state)
-const resetFormField =  reactive({ ...dataForm.value })
+const resetFormField = reactive({ ...dataForm.value })
 const route = useRoute()
 const formLoading = ref(false)
 const isDetail = ref(false)
@@ -135,9 +160,40 @@ const itemTitle = computed(() => {
   return isDetail.value ? '详情' : !dataForm.value.id ? '新增' : '修改'
 })
 
+/** 详情接口可能返回时间戳或整段日期时间,转为时间选择器用的 HH:mm:ss */
+const toTimePickerValue = (beginTime: unknown): string => {
+  if (beginTime == null || beginTime === '') return ''
+  if (typeof beginTime === 'number') {
+    const d = new Date(beginTime)
+    if (Number.isNaN(d.getTime())) return ''
+    const pad = (n: number) => String(n).padStart(2, '0')
+    return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+  }
+  if (typeof beginTime === 'string') {
+    if (/^\d+$/.test(beginTime)) return toTimePickerValue(Number(beginTime))
+    if (beginTime.includes(' ')) return beginTime.split(' ')[1] || ''
+  }
+  return String(beginTime)
+}
+
+/** 详情展示:时间戳或日期时间转为可读时间 */
+const formatBeginTimeDetail = (beginTime: unknown): string => {
+  if (beginTime == null || beginTime === '') return '-'
+  if (typeof beginTime === 'number' || (typeof beginTime === 'string' && /^\d+$/.test(beginTime))) {
+    const d = new Date(Number(beginTime))
+    if (Number.isNaN(d.getTime())) return '-'
+    const pad = (n: number) => String(n).padStart(2, '0')
+    return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+  }
+  if (typeof beginTime === 'string' && beginTime.includes(' ')) {
+    return beginTime.split(' ')[1] || '-'
+  }
+  return String(beginTime)
+}
+
 /** 打开弹窗 */
 const open = async (id, detail) => {
-  dataForm.value.id=''
+  dataForm.value.id = ''
   dialogVisible.value = true
   isDetail.value = detail
   if (id) {
@@ -153,7 +209,11 @@ const open = async (id, detail) => {
     dataForm.value = res
     await formatExtraItem()
     dataForm.value.items = res.items?.length > 0 ? res.items : res.extraItems
-    console.log('dataForm.value', dataForm.value)
+    dataForm.value.items?.forEach((item) => {
+      if (item.frequencyCategoryType === 1 && item.beginTime != null && item.beginTime !== '') {
+        item.beginTime = toTimePickerValue(item.beginTime)
+      }
+    })
   }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -175,7 +235,7 @@ const formatExtraItem = async () => {
 }
 
 const handleClosed = () => {
-  dataForm.value = {...resetFormField}
+  dataForm.value = { ...resetFormField }
   dialogVisible.value = false
 }
 
@@ -185,17 +245,20 @@ const handleAdd = () => {
 }
 
 const getItemList = (val) => {
+  console.log('val', val)
   // 添加的内容放到护理项目中
   val.map((item) => {
     dataForm.value.items.push({
       nurseItemId: item.id,
       nurseItemName: item.itemName,
-      frequency: 1,
-      frequencyUnit: item.frequency,
-      operatingMode: item.operatingMode,
+      frequencyType: item.frequencyType,
+      frequency: item.frequency,
+      frequencyCategory: item.frequencyCategory,
+      frequencyCategoryType: item.frequencyCategoryType,
       checked: true,
       price: item.price,
-      isExtra: item.type == 1 ? 1 : 0
+      isExtra: item.type == 1 ? 1 : 0,
+      beginTime: ''
     })
   })
 }
@@ -216,20 +279,31 @@ const handleChangeCheckBox = (val, item) => {
 
 const formRef = ref()
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
 const submitForm = async () => {
   // 校验表单
   if (!formRef.value) return
   const valid = await formRef.value.validate()
   if (!valid) return
+
+  const missingTime = dataForm.value.items.find(
+    (item) => item.checked && !item.beginTime && item.frequencyCategoryType == 1
+  )
+  if (missingTime) {
+    message.warning(`请先选择【${missingTime.nurseItemName}】的护理开始时间`)
+    return
+  }
+
   try {
     formLoading.value = true
-    const params = {
-      type: route.path.indexOf('medical-care-plan') > -1 ? 2 : 1, // 2:医疗护理 1 生活护理,
-      ...dataForm.value,
-    }
-    const res = params.id
-      ? await updateNursingPlan(params)
-      : await createNursingPlan(params)
+    let params = JSON.parse(JSON.stringify(dataForm.value))
+    ;(params.type = route.path.indexOf('medical-care-plan') > -1 ? 2 : 1), // 2:医疗护理 1 生活护理,
+      params.items.forEach((item) => {
+        if (item.frequencyCategoryType == 1 && item.beginTime) {
+          item.beginTime = new Date(`${params.effectiveDate} ${item.beginTime}`).getTime()
+        }
+      })
+    const res = params.id ? await updateNursingPlan(params) : await createNursingPlan(params)
     if (res) {
       message.success(t('common.updateSuccess'))
       handleClosed()
@@ -252,11 +326,27 @@ const submitForm = async () => {
   .itemName {
     line-height: 30px;
   }
-  .border {
+  .item-left {
+    display: flex;
+    align-items: center;
+    min-height: 32px;
     padding: 1px 11px;
+    flex: 1;
     box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
     border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
   }
+  .item-time {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+  .time-label {
+    white-space: nowrap;
+    color: var(--el-text-color-secondary);
+  }
+  .detail-time {
+    color: var(--el-text-color-regular);
+  }
   .border-warning {
     border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
     box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-color-warning)) inset;

+ 4 - 7
src/views/elderly/nursing/life-care-plan/life-item-dialog.vue

@@ -14,11 +14,8 @@
         <el-table-column prop="itemName" label="项目名称" />
         <el-table-column prop="cName" label="护理分类" align="center" />
         <el-table-column prop="categoryName" label="项目分类" align="center" />
-        <!-- <el-table-column prop="fee" label="是否额外收费" align="center">
-          <template #default="scope">
-            <TgSwitch v-model="scope.row.fee"/>
-          </template>
-        </el-table-column> -->
+        <el-table-column prop="frequency" label="频次" align="center" />
+        <el-table-column prop="frequencyCategory" label="频次类型" align="center" />
         <el-table-column prop="type" label="收费项目" align="center">
           <template #default="scope">
             {{ getDictLabel(DICT_TYPE.COMMON_STATUS6, scope.row.type) }}
@@ -29,9 +26,9 @@
             {{ formatNum(scope.row.price) }}
           </template>
         </el-table-column>
-        <el-table-column prop="frequency" label="收费单位" align="center">
+        <el-table-column prop="frequencyType" label="频次计量单位" align="center">
           <template #default="scope">
-            {{ getDictLabel(DICT_TYPE.NURSING_FREQUENCY_TYPE, scope.row.frequency) }}
+            {{ getDictLabel(DICT_TYPE.NURSING_FREQUENCY_TYPE, scope.row.frequencyType) }}
           </template>
         </el-table-column>
       </el-table>

+ 32 - 8
src/views/elderly/nursing/nurse-item/Form.vue

@@ -29,8 +29,23 @@
           >
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="计量方式" prop="frequency">
-        <el-select v-model="dataForm.frequency" placeholder="请选择计量方式">
+      <el-form-item label="频次类型" prop="frequencyCategoryType" :rules="[{ required: true, message: '频次类型不能为空', trigger: 'blur' }]">
+        <el-select v-model="dataForm.frequencyCategoryType" placeholder="请选择频次类型(高/中低)">
+          <el-option
+            label="高"
+            :value="1"
+          />
+          <el-option
+            label="中/低"
+            :value="2"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="频次" prop="frequency" v-if="dataForm.frequencyCategoryType == 1" :rules="dataForm.frequencyCategoryType == 1 ? [{ required: true, message: '频次不能为空', trigger: 'blur' }] : []">
+        <el-input v-model="dataForm.frequency" type="number" placeholder="请输入频次(例:2h/次)" />
+      </el-form-item>
+      <el-form-item label="计量方式" prop="frequencyType">
+        <el-select v-model="dataForm.frequencyType" placeholder="请选择计量方式">
           <el-option
             v-for="(dict, index) in getIntDictOptions(DICT_TYPE.NURSING_FREQUENCY_TYPE)"
             :key="index"
@@ -39,7 +54,7 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="超时提醒" prop="timeout">
+      <el-form-item label="超时提醒" prop="timeout" v-if="dataForm.frequencyCategoryType == 1" :rules="dataForm.frequencyCategoryType == 1 ? [{ required: true, message: '频次不能为空', trigger: 'blur' }] : []">
         <el-input v-model.num="dataForm.timeout" placeholder="距上次护理时间超过设定值后提醒">
           <template #append>小时</template>
         </el-input>
@@ -95,11 +110,14 @@ const dataForm = ref({
   image: '',
   type: '',
   price: undefined,
-  frequency: '',
+  frequencyType: '',
   timeout: '',
   status: 1,
   remark: '',
-  tenantId: ''
+  tenantId: '',
+  frequency:'',
+  frequencyCategory:'',
+  frequencyCategoryType:0 as number
 })
 const superiorsName = ref('') // 费用分类名称
 
@@ -132,6 +150,7 @@ const open = async (tenantId, category, id, detail) => {
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
+
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
@@ -142,9 +161,11 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
+    let frequencyCategory = dataForm.value.frequencyCategoryType == 1 ? '高' : '中/低'
     let params = {
       ...dataForm.value,
-      timeout: Number(dataForm.value.timeout)
+      timeout: Number(dataForm.value.timeout),
+      frequencyCategory:frequencyCategory
     }
     const res = dataForm.value.id ? await editNurseItem(params) : await addNurseItem(params)
     if (res) {
@@ -167,11 +188,14 @@ const resetForm = () => {
     image: '',
     type: '',
     price: undefined,
-    frequency: '',
+    frequencyType: '',
     timeout: '',
     status: 1,
     remark: '',
-    tenantId: ''
+    tenantId: '',
+    frequency:'',
+    frequencyCategory:'',
+    frequencyCategoryType:0 as number
   }
   formRef.value?.resetFields()
 }

Datei-Diff unterdrückt, da er zu groß ist
+ 794 - 87
src/views/elderly/nursing/nursing-log-list/Detail.vue


+ 3 - 22
src/views/elderly/nursing/nursing-log-list/index.vue

@@ -15,16 +15,6 @@
       <el-form-item label="长者名称" prop="elderName">
         <TgInput @keyup.enter="handleQuery" v-model="queryParams.elderName" class="!w-240px" />
       </el-form-item>
-      <el-form-item label="护理日期" prop="nurseDate">
-        <el-date-picker
-          end-placeholder="结束日期"
-          start-placeholder="开始日期"
-          type="daterange"
-          value-format="YYYY-MM-DD"
-          range-separator="至"
-          v-model="queryParams.nurseDate"
-        />
-      </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>
@@ -61,19 +51,11 @@ import { useUserStore } from '@/store/modules/user'
 const userStore = useUserStore()
 
 defineOptions({ name: 'NursingLog' })
-const getDefaultMonthRange = () => {
-  const now = new Date()
-  const start = new Date(now.getFullYear(), now.getMonth(), 1)
-  const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
-  const format = (date: Date) => date.toISOString().slice(0, 10)
-  return [format(start), format(end)]
-}
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   elderName: '',
-  tenantIds: userStore.orgTenantId,
-  nurseDate: getDefaultMonthRange()
+  tenantIds: userStore.orgTenantId
 })
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
@@ -89,7 +71,6 @@ const handleQuery = () => {
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value.resetFields()
-  queryParams.nurseDate = getDefaultMonthRange()
   handleQuery()
 }
 
@@ -106,8 +87,8 @@ const getList = async () => {
 }
 
 const detailRef = ref()
-const openDetail = (row:{}) => {
-  detailRef.value.open(row,queryParams.nurseDate)
+const openDetail = (row: Record<string, unknown>) => {
+  detailRef.value.open(row)
 }
 
 onMounted(()=>{

+ 295 - 0
src/views/elderly/nursing/special-nurse-log/Detail.vue

@@ -0,0 +1,295 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    width="90%"
+    @close="handleClosed"
+    title="特殊照护任务完成日志"
+    class="form-tag-dialog"
+    scroll
+  >
+    <div class="special-nursing-log-detail">
+      <div class="info-title">长者信息</div>
+      <div class="info-wrap">
+        <el-row :gutter="20">
+          <el-col :span="8" :xs="24" class="header-item"
+            >长者名称:{{ header.elderName || '-' }}</el-col
+          >
+          <el-col :span="8" :xs="24" class="header-item"
+            >护理等级:{{ header.nurseLevelName || '-' }}</el-col
+          >
+          <el-col :span="8" :xs="24" class="header-item"
+            >床位号:{{ header.bedName || '-' }}</el-col
+          >
+        </el-row>
+      </div>
+      <el-form :model="queryForm" class="detail-query-form mb-15px" label-width="100px">
+        <el-row :gutter="12" align="middle">
+          <el-col :xs="24" :sm="16" :md="10" :lg="8">
+            <el-form-item label="完成时间范围" prop="finishTime">
+              <el-date-picker
+                v-model="queryForm.finishTime"
+                type="daterange"
+                range-separator="至"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                value-format="YYYY-MM-DD"
+                class="finish-time-range-picker"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="8" :md="6" :lg="4">
+            <el-form-item label-width="0">
+              <el-button type="primary" :loading="detailLoading" @click="reloadDetail"
+                >查询</el-button
+              >
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <el-table
+        v-loading="detailLoading"
+        :data="pagedList"
+        height="55vh"
+        :header-cell-style="(d) => tableHeaderColor(d) || {}"
+      >
+        <el-table-column
+          label="项目名称"
+          prop="specialPlanItemName"
+          min-width="180"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          label="完成时间"
+          prop="finishTime"
+          min-width="180"
+          :formatter="dateFormatter"
+          show-overflow-tooltip
+        />
+        <el-table-column label="项目状态" prop="status" min-width="180"
+          ><template #default="scope">
+            <el-tag v-if="scope.row.status == 0" type="success">已完成</el-tag>
+            <el-tag v-if="scope.row.status == 1" type="danger">已撤销</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" prop="remark" min-width="300" show-overflow-tooltip />
+        <el-table-column label="图片" min-width="300">
+          <template #default="scope">
+            <div v-if="splitImages(scope.row.imageUrl).length" class="image-list">
+              <el-image
+                v-for="(img, index) in splitImages(scope.row.imageUrl)"
+                :key="`${img}-${index}`"
+                :src="img"
+                :preview-src-list="splitImages(scope.row.imageUrl)"
+                :initial-index="Number(index)"
+                preview-teleported
+                fit="cover"
+                class="image-item"
+              />
+            </div>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="90" align="center" fixed="right">
+          <template #default="scope">
+            <el-button
+              v-hasPermi="['special-nurse-log:revocation']"
+              v-if="scope.row.status == 0"
+              link
+              type="danger"
+              :loading="revokeLoadingId == scope.row.id"
+              @click="handleRevoke(scope.row)"
+            >
+              撤销
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="flex justify-end mt-12px">
+        <el-pagination
+          v-if="records.length > 0"
+          layout="total, sizes, prev, pager, next"
+          :total="records.length"
+          v-model:page-size="detailPageSize"
+          v-model:current-page="detailPageNo"
+          :page-sizes="[10, 20, 50, 100]"
+          background
+        />
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getSpecialNursingLogListByElderAndFinishTime } from '@/api/elderly/nursing/special-nursing-log'
+import type { ElderlySpecialNursingLogRespVO } from '@/api/elderly/nursing/special-nursing-log'
+import { updateSpecialPlanItem } from '@/api/elderly/nursing/special-plan'
+import { getTenantId } from '@/utils/auth'
+import { tableHeaderColor } from '@/utils/table'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'SpecialNursingLogDetail' })
+
+const message = useMessage()
+const { t } = useI18n()
+const emit = defineEmits(['refresh'])
+
+const dialogVisible = ref(false)
+const detailLoading = ref(false)
+const revokeLoadingId = ref<number | string | null>(null)
+
+const header = reactive({
+  elderName: '',
+  bedName: '',
+  nurseLevelName: ''
+})
+
+const records = ref<ElderlySpecialNursingLogRespVO[]>([])
+
+const queryForm = reactive({
+  /** 完成时间范围,与接口 finishTime 一致,YYYY-MM-DD */
+  finishTime: [] as string[]
+})
+
+/** 详情内分页(接口为整列表,前端分页) */
+const detailPageNo = ref(1)
+const detailPageSize = ref(10)
+
+const defaultMonthRange = () => {
+  const now = new Date()
+  const start = new Date(now.getFullYear(), now.getMonth(), 1)
+  const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
+  const fmt = (d: Date) => d.toISOString().slice(0, 10)
+  return [fmt(start), fmt(end)]
+}
+
+let currentElderId: number | string | undefined
+
+const splitImages = (raw?: string) => {
+  if (!raw) return []
+  return String(raw)
+    .split(',')
+    .map((s) => s.trim())
+    .filter(Boolean)
+}
+
+const pagedList = computed(() => {
+  const start = (detailPageNo.value - 1) * detailPageSize.value
+  return records.value.slice(start, start + detailPageSize.value)
+})
+
+watch(
+  () => records.value.length,
+  () => {
+    detailPageNo.value = 1
+  }
+)
+
+const resolveElderId = (row: Recordable) => {
+  const v = row.elderId ?? row.elder_id ?? row.id
+  if (v === '' || v === undefined || v === null) return undefined
+  return v
+}
+
+const reloadDetail = async () => {
+  if (currentElderId == null || currentElderId === '') return
+  if (!queryForm.finishTime?.length || queryForm.finishTime.length < 2) {
+    queryForm.finishTime = defaultMonthRange()
+  }
+  detailLoading.value = true
+  try {
+    const range = queryForm.finishTime
+    const data = await getSpecialNursingLogListByElderAndFinishTime({
+      elderId: currentElderId,
+      finishTime: [range[0], range[1]],
+      tenantId: Number(getTenantId())
+    })
+    records.value = Array.isArray(data) ? data : []
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+const handleRevoke = async (row: ElderlySpecialNursingLogRespVO) => {
+  try {
+    await message.delConfirm('确认撤销该条完成记录?将恢复为未完成状态。')
+    revokeLoadingId.value = row.id ?? 0
+    await updateSpecialPlanItem({
+      id: row.id ?? 0,
+      specialItemId: row.specialPlanItemId ?? 0,
+      status: 0
+    })
+    message.success(t('common.updateSuccess'))
+    emit('refresh')
+    await reloadDetail()
+  } catch {
+    //
+  } finally {
+    revokeLoadingId.value = null
+  }
+}
+
+const open = async (row: Recordable) => {
+  const eid = resolveElderId(row)
+  if (eid == null) {
+    message.warning('当前行缺少长者信息,无法加载详情')
+    return
+  }
+  currentElderId = eid
+  header.elderName = row.elderName ?? ''
+  header.bedName = row.bedName ?? ''
+  header.nurseLevelName = row.nurseLevelName ?? ''
+  queryForm.finishTime = defaultMonthRange()
+  dialogVisible.value = true
+  await reloadDetail()
+}
+
+defineExpose({ open })
+
+const handleClosed = () => {
+  records.value = []
+  header.elderName = ''
+  header.bedName = ''
+  header.nurseLevelName = ''
+  currentElderId = undefined
+  dialogVisible.value = false
+}
+</script>
+<style lang="scss" scoped>
+.special-nursing-log-detail {
+  .info-title {
+    font-weight: 600;
+    color: #303133;
+    margin: 10px 0 6px;
+  }
+  .info-wrap {
+    padding: 0 20px;
+    margin-bottom: 10px;
+  }
+  .header-item {
+    color: #606266;
+    padding-bottom: 10px;
+  }
+  .detail-query-form {
+    :deep(.el-form-item) {
+      margin-bottom: 0;
+    }
+  }
+  .finish-time-range-picker {
+    width: 100%;
+    max-width: 240px;
+  }
+  .image-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 6px;
+  }
+  .image-item {
+    width: 48px;
+    height: 48px;
+    border-radius: 4px;
+  }
+}
+</style>

+ 103 - 0
src/views/elderly/nursing/special-nurse-log/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <el-form-item prop="tenantIds">
+        <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
+      </el-form-item>
+      <el-form-item label="长者姓名" prop="elderName">
+        <TgInput
+          @keyup.enter="handleQuery"
+          v-model="queryParams.elderName"
+          placeholder="模糊搜索"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <Table2 v-loading="loading" :list="list" :columns="SpecialNursingLogColumns" :queryParams="queryParams">
+      <template #pre="{ scope }">
+        <el-button
+          v-hasPermi="['nursing-log-list:details']"
+          link
+          type="primary"
+          @click="openDetail(scope)"
+        >
+          查看
+        </el-button>
+      </template>
+    </Table2>
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+    <Detail ref="detailRef" @refresh="getList" />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getSpecialNursingLogPage } from '@/api/elderly/nursing/special-nursing-log'
+import Detail from './Detail.vue'
+import { SpecialNursingLogColumns } from '../column'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
+
+defineOptions({ name: 'SpecialNursingLog' })
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  tenantIds: userStore.orgTenantId
+})
+const loading = ref(true)
+const total = ref(0)
+const list = ref([])
+const queryFormRef = ref()
+
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields?.()
+  handleQuery()
+}
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await getSpecialNursingLogPage(queryParams)
+    list.value = data.list ?? []
+    total.value = data.total ?? 0
+  } finally {
+    loading.value = false
+  }
+}
+
+const detailRef = ref()
+
+/** Table2 slot #pre:传入当前行 */
+const openDetail = (row: Recordable) => {
+  detailRef.value?.open(row)
+}
+
+onMounted(() => {
+  getList()
+})
+</script>

+ 306 - 0
src/views/elderly/nursing/special-nurse/Form.vue

@@ -0,0 +1,306 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    :title="itemTitle"
+    width="70%"
+    class="form-tag-dialog life-care-plan-form special-nursing-plan-form"
+    @close="handleClosed"
+    scroll
+  >
+    <el-form
+      ref="formRef"
+      :model="dataForm"
+      :rules="dataRule"
+      label-width="80px"
+      :toggleType="isDetail"
+    >
+      <div class="info-title">基本信息</div>
+      <div class="info-wrap">
+        <el-row :gutter="20">
+          <el-col :span="12" :xs="24">
+            <el-form-item label="长者姓名" prop="elderId">
+              <SelectElder
+                v-model="dataForm.elderId"
+                @elder="handleElder"
+                :style="[{ width: dataForm.id ? '30%' : '100%' }]"
+                :toggleType="!!dataForm.id"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" :xs="24">
+            <el-form-item label="床位号" prop="bedName">
+              <el-input v-if="!isDetail" v-model="dataForm.bedName" disabled />
+              <el-text v-else>{{ dataForm.bedName }}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12" :xs="24">
+            <el-form-item label="护理等级" prop="nurseLevelName">
+              <el-input v-if="!isDetail" v-model="dataForm.nurseLevelName" disabled />
+              <el-text v-else>{{ dataForm.nurseLevelName }}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <div class="info">
+        <div class="info-title">护理项目</div>
+        <span v-show="dataForm.elderId">
+          <el-button type="primary" class="left" @click="handleAdd" v-if="!isDetail">
+            <Icon icon="ep:zoom-in" class="mr-5px" />添加项目
+          </el-button>
+        </span>
+      </div>
+      <div class="info-wrap">
+        <el-row>
+          <el-col
+            :xs="24"
+            :sm="24"
+            :md="24"
+            :lg="24"
+            class="mb5"
+            v-for="(p, i) in dataForm.items"
+            :key="i"
+          >
+            <div class="border item-row">
+              <div class="item-left">
+                <el-checkbox
+                  :label="p.nurseItemName"
+                  v-model="p.checked"
+                  @change="(v) => handleChangeCheckBox(!!v, p)"
+                  v-if="!isDetail"
+                />
+                <div v-else class="itemName">{{ p.nurseItemName }}</div>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleClosed">取消</el-button>
+      <el-button v-loading="formLoading" type="primary" @click="submitForm" v-if="!isDetail"
+        >确定</el-button
+      >
+    </template>
+  </Dialog>
+  <lifeItem ref="itemRef" category-type-name="生活护理" @success="getItemList" />
+</template>
+
+<script setup lang="ts">
+import {
+  createSpecialNursingPlan,
+  updateSpecialNursingPlan,
+  getSpecialNursingPlan
+} from '@/api/elderly/nursing/special-plan'
+import type {
+  SpecialNursingPlanItemRespVO,
+  SpecialNursingPlanItemSaveReqVO,
+  SpecialNursingPlanSaveReqVO
+} from '@/api/elderly/nursing/special-plan'
+import { getTenantId } from '@/utils/auth'
+import { getElderInfoById } from '@/api/elderly/elder/elderly-Info'
+import lifeItem from './life-item-dialog.vue'
+
+defineOptions({ name: 'SpecialNursingPlanForm' })
+
+interface PlanItemRow {
+  /** 明细表主键,编辑时回填 */
+  id?: number
+  nurseItemId: number | string
+  nurseItemName: string
+  checked?: boolean
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const dialogVisible = ref(false)
+const dataForm = ref({
+  id: '' as number | string | '',
+  elderId: '' as number | string | '',
+  elderName: '',
+  bedName: '',
+  nurseLevelName: '',
+  items: [] as PlanItemRow[],
+  tenantId: undefined as number | undefined
+})
+const resetSnapshot = reactive({
+  id: '',
+  elderId: '',
+  elderName: '',
+  bedName: '',
+  nurseLevelName: '',
+  items: [] as PlanItemRow[],
+  tenantId: undefined as number | undefined
+})
+const dataRule = ref({
+  elderId: [{ required: true, message: '长者姓名不能为空', trigger: 'blur' }]
+})
+const formLoading = ref(false)
+const isDetail = ref(false)
+
+const itemTitle = computed(() => {
+  return isDetail.value ? '详情' : !dataForm.value.id ? '新增' : '修改'
+})
+
+const normalizeItemsFromDetail = (list?: SpecialNursingPlanItemRespVO[]): PlanItemRow[] =>
+  (list || []).map((row) => ({
+    id: row.id != null ? Number(row.id) : undefined,
+    nurseItemId: row.nurseItemId as number | string,
+    nurseItemName: row.nurseItemName || '',
+    checked: true
+  }))
+
+/** 编辑/详情时计划接口不包含床位与护理等级,根据长者详情补全 */
+const fillElderBedAndNurseLevel = async (elderId?: number | string) => {
+  if (elderId == null || elderId === '') return
+  try {
+    const elder = (await getElderInfoById(elderId)) as Recordable
+    if (!elder) return
+    const bed = elder.bedName ?? elder.bed
+    const level = elder.nurseLevelName ?? elder.nursingLevelName ?? elder.nursingLevel ?? ''
+    if (bed !== undefined && bed !== null && bed !== '') {
+      dataForm.value.bedName = String(bed)
+    }
+    if (level !== undefined && level !== null && String(level).length) {
+      dataForm.value.nurseLevelName = String(level)
+    }
+  } catch {
+    // 静默:仍展示计划中已有或空白
+  }
+}
+
+const open = async (id?: number | string, detail?: boolean, prefetch?: Recordable) => {
+  dataForm.value = JSON.parse(JSON.stringify(resetSnapshot))
+  dataForm.value.id = ''
+  isDetail.value = !!detail
+  dialogVisible.value = true
+  if (!id) return
+  try {
+    const res = await getSpecialNursingPlan(id)
+    dataForm.value.id = res.id ?? id
+    dataForm.value.elderId = res.elderId ?? ''
+    dataForm.value.elderName = res.elderName ?? ''
+    dataForm.value.tenantId = res.tenantId
+    dataForm.value.bedName = res.bedName ?? prefetch?.bedName ?? ''
+    dataForm.value.nurseLevelName = res.nurseLevelName ?? prefetch?.nurseLevelName ?? ''
+    await fillElderBedAndNurseLevel(res.elderId)
+    dataForm.value.items = normalizeItemsFromDetail(res.items || [])
+    if (!dataForm.value.items.length && prefetch?.elderId) {
+      message.warning('该计划暂未返回明细,请补充护理项目后保存')
+    }
+  } catch {
+    dialogVisible.value = false
+    message.error('获取特殊护理计划详情失败')
+  }
+}
+defineExpose({ open })
+
+const handleClosed = () => {
+  dataForm.value = JSON.parse(JSON.stringify(resetSnapshot))
+  dialogVisible.value = false
+}
+
+const itemRef = ref()
+const handleAdd = () => {
+  itemRef.value.open(dataForm.value.items)
+}
+
+const getItemList = (val: Recordable[]) => {
+  val.forEach((item) => {
+    dataForm.value.items.push({
+      nurseItemId: item.id,
+      nurseItemName: item.itemName,
+      checked: true
+    })
+  })
+}
+
+const handleElder = (item: Recordable) => {
+  dataForm.value.elderName = item.elderName
+  dataForm.value.bedName = item.bedName
+  dataForm.value.nurseLevelName = item.nurseLevelName
+}
+
+const handleChangeCheckBox = (val: boolean, item: PlanItemRow) => {
+  if (!val) {
+    const index = dataForm.value.items.findIndex((p) => p.nurseItemId == item.nurseItemId)
+    if (index > -1) dataForm.value.items.splice(index, 1)
+  }
+}
+
+const formRef = ref()
+const emit = defineEmits(['success'])
+
+const buildTenantId = () => Number(getTenantId())
+
+const submitForm = async () => {
+  if (!formRef.value) return
+  try {
+    await formRef.value.validate()
+  } catch {
+    return
+  }
+  const tenantId = buildTenantId()
+  const planId = dataForm.value.id ? Number(dataForm.value.id) : undefined
+  const itemsPayload: SpecialNursingPlanItemSaveReqVO[] = []
+  ;(dataForm.value.items || []).forEach((row) => {
+    if (!row.checked && row.checked !== undefined) return
+    const one: SpecialNursingPlanItemSaveReqVO = {
+      nurseItemId: Number(row.nurseItemId),
+      nurseItemName: row.nurseItemName,
+      tenantId
+    }
+    if (planId) {
+      one.specialNursingPlanId = planId
+      if (row.id != null) one.id = row.id
+    }
+    itemsPayload.push(one)
+  })
+  const payload: SpecialNursingPlanSaveReqVO = {
+    elderId: Number(dataForm.value.elderId),
+    elderName: dataForm.value.elderName || undefined,
+    tenantId,
+    items: itemsPayload
+  }
+  if (planId) payload.id = planId
+
+  try {
+    formLoading.value = true
+    await (planId ? updateSpecialNursingPlan(payload) : createSpecialNursingPlan(payload))
+    message.success(planId ? t('common.updateSuccess') : t('common.createSuccess'))
+    handleClosed()
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+</script>
+<style lang="scss" scoped>
+.special-nursing-plan-form {
+  .item-row {
+    display: flex;
+    align-items: center;
+  }
+  .item-left {
+    display: flex;
+    align-items: center;
+    min-height: 32px;
+    padding: 1px 11px;
+    flex: 1;
+    box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
+    border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
+  }
+  .itemName {
+    line-height: 30px;
+  }
+  .info {
+    position: relative;
+    .left {
+      position: absolute;
+      left: 150px;
+      top: 8px;
+    }
+  }
+}
+</style>

+ 122 - 0
src/views/elderly/nursing/special-nurse/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <el-form-item label="长者姓名" prop="elderName">
+        <el-input @keyup.enter="handleQuery" v-model="queryParams.elderName" placeholder="请输入长者姓名" class="!w-240px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <TabBarBtn @add="openForm()" />
+    <Table2
+      v-loading="loading"
+      :data="list"
+      :columns="SpecialNursingPlanColumns"
+      :queryParams="queryParams"
+      @row-click="handleRowClick"
+      @edit="openForm"
+      @detail="(arg) => openForm(arg, true)"
+      @del="handleDelete"
+    />
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <Form ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { getSpecialNursingPlanPage, deleteSpecialNursingPlan } from '@/api/elderly/nursing/special-plan'
+import { SpecialNursingPlanColumns } from '../column'
+import { useUserStore } from '@/store/modules/user'
+import Form from './Form.vue'
+
+defineOptions({ name: 'SpecialNursingPlan' })
+
+const message = useMessage()
+const { t } = useI18n()
+const userStore = useUserStore()
+const route = useRoute()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  tenantIds: userStore.orgTenantId
+})
+const queryFormRef = ref()
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await getSpecialNursingPlanPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields?.()
+  handleQuery()
+}
+
+/** 添加/修改操作 — Table2 @edit/@detail 传入整行 */
+const formRef = ref()
+const openForm = (row: Recordable = {}, isDetail: boolean = false) => {
+  const r = row && typeof row === 'object' && 'id' in row ? row : {}
+  formRef.value?.open(r.id, isDetail, r)
+}
+
+/** 删除 */
+const handleDelete = async (id: string | number) => {
+  try {
+    await message.delConfirm()
+    await deleteSpecialNursingPlan(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+const currItem = ref({})
+const handleRowClick = (val) => {
+  currItem.value = val
+}
+
+onMounted(() => {
+  if (route.query?.elderName) {
+    queryParams.elderName = route.query.elderName as string
+  }
+  getList()
+})
+</script>

+ 130 - 0
src/views/elderly/nursing/special-nurse/life-item-dialog.vue

@@ -0,0 +1,130 @@
+<template>
+  <Dialog v-model="dialogVisible" title="选择护理项目" @close="handleClosed" width="60%">
+    <el-form ref="formRef" :model="dataForm" label-width="80px" inline>
+      <el-form-item label="项目名称" prop="nurseItemName">
+        <TgInput v-model="dataForm.nurseItemName" />
+      </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-table :data="list" height="400" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" />
+        <el-table-column prop="itemName" label="项目名称" />
+        <el-table-column prop="cName" label="护理分类" align="center" />
+        <el-table-column prop="categoryName" label="项目分类" align="center" />
+        <el-table-column prop="type" label="收费项目" align="center">
+          <template #default="scope">
+            {{ getDictLabel(DICT_TYPE.COMMON_STATUS6, scope.row.type) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="price" label="收费金额(元)" align="center">
+          <template #default="scope">
+            {{ formatNum(scope.row.price) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="frequencyType" label="频次计量单位" align="center">
+          <template #default="scope">
+            {{ getDictLabel(DICT_TYPE.NURSING_FREQUENCY_TYPE, scope.row.frequencyType) }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleClosed">取消</el-button>
+      <el-button v-loading="formLoading" type="primary" @click="submitForm">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { findNurseItemListByName } from '@/api/elderly/nursing'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { formatNum } from '@/utils/formatter'
+import { itemsType } from '../types'
+defineOptions({ name: 'SpecialCarePlanNurseItemForm' })
+
+const props = defineProps({
+  categoryTypeName: {
+    type: String,
+    default: ''
+  }
+})
+
+const dialogVisible = ref(false)
+const list = ref<any[]>([])
+const formLoading = ref(false)
+const route = useRoute()
+const resolvedCategoryTypeName = computed(
+  () =>
+    props.categoryTypeName ||
+    (route.path.indexOf('medical-care-plan') > -1 ? '医疗护理' : '生活护理')
+)
+const dataForm = reactive({
+  categoryTypeName: '生活护理',
+  nurseItemName: ''
+})
+
+const exitArr = ref<itemsType[]>([])
+/** 打开弹窗 */
+const open = async (arr: itemsType[]) => {
+  dataForm.categoryTypeName = resolvedCategoryTypeName.value
+  dialogVisible.value = true
+  exitArr.value = arr || []
+  handleQuery()
+}
+defineExpose({ open })
+
+const handleClosed = () => {
+  list.value = []
+  dataForm.nurseItemName = ''
+  dialogVisible.value = false
+}
+
+const loading = ref(false)
+
+const formRef = ref()
+const resetQuery = () => {
+  formRef.value?.resetFields?.()
+  handleQuery()
+}
+
+const handleQuery = async () => {
+  loading.value = true
+  try {
+    dataForm.categoryTypeName = resolvedCategoryTypeName.value
+    const res = await findNurseItemListByName(dataForm)
+    exitArr.value?.forEach((item) => {
+      res?.forEach((r) => {
+        if (r.id == item.nurseItemId) {
+          r.same = true
+        }
+      })
+    })
+    const arr: any[] = []
+    res?.forEach((item) => {
+      if (!item.same) {
+        arr.push(item)
+      }
+    })
+    arr.forEach((item) => {
+      item.cName = dataForm.categoryTypeName
+      item.flag = false
+    })
+    list.value = arr
+  } finally {
+    loading.value = false
+  }
+}
+
+const multipleSelection = ref([])
+const handleSelectionChange = (val) => {
+  multipleSelection.value = val
+}
+
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  emit('success', multipleSelection.value)
+  handleClosed()
+}
+</script>

+ 4 - 11
src/views/elderly/nursing/types.ts

@@ -86,29 +86,22 @@ export interface planType {
     bedName: string
     nurseLevelName: string
     effectiveDate: string
-    // diet: string
-    // dietaryType: string
-    // riskPrevention: string
-    // nursingCharacteristics: string
-    // dietaryTaboo: string[]
     items: itemsType[]
     extraItems: itemsType[]
   }
   dataRule: FormRules
-  // tempCopyForm: {
-  //   items: itemsType[]
-  //   extraItems: itemsType[]
-  // }
 }
 
 export interface itemsType {
   isExtra: number
   nurseItemName: string
-  frequencyUnit: string
   frequency: number
-  operatingMode: string
   price?: number
   checked?: boolean
   nurseItemId: string
+  beginTime?: string | number
   nursingPlanId?: string
+  frequencyType: number
+  frequencyCategory: string
+  frequencyCategoryType: number
 }

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.