unknown 2 месяцев назад
Родитель
Сommit
d1f441c023

+ 42 - 0
src/api/social-work/index.ts

@@ -50,3 +50,45 @@ export const adaptServiceRecordUpdate = async (data) => {
 export const activityServiceRecordUpdate = async (data) => {
   return await request.put({ url: '/elderly-activity-record/update', data:data })
 }
+
+// ==================== 楼层活动记录表 ====================
+export const floorActivityRecordGetInfo = async (id) => {
+  return await request.get({ url: '/floor-activity-record/get?id=' + id })
+}
+
+export const floorActivityRecordDelete = async (id) => {
+  return await request.delete({ url: '/floor-activity-record/delete?id=' + id })
+}
+
+export const floorActivityRecordGetPage = async (params) => {
+  return await request.get({ url: '/floor-activity-record/page', params })
+}
+
+export const floorActivityRecordCreate = async (data) => {
+  return await request.post({ url: '/floor-activity-record/create', data: data })
+}
+
+export const floorActivityRecordUpdate = async (data) => {
+  return await request.put({ url: '/floor-activity-record/update', data: data })
+}
+
+// ==================== 楼层活动照片记录表 ====================
+export const floorActivityPhotoRecordGetInfo = async (id) => {
+  return await request.get({ url: '/floor-activity-photo-record/get?id=' + id })
+}
+
+export const floorActivityPhotoRecordDelete = async (id) => {
+  return await request.delete({ url: '/floor-activity-photo-record/delete?id=' + id })
+}
+
+export const floorActivityPhotoRecordGetPage = async (params) => {
+  return await request.get({ url: '/floor-activity-photo-record/page', params })
+}
+
+export const floorActivityPhotoRecordCreate = async (data) => {
+  return await request.post({ url: '/floor-activity-photo-record/create', data: data })
+}
+
+export const floorActivityPhotoRecordUpdate = async (data) => {
+  return await request.put({ url: '/floor-activity-photo-record/update', data: data })
+}

+ 31 - 33
src/views/elderly/consumption-coupon/index.vue

@@ -88,16 +88,14 @@
     @success="getList"
     :config="{
       title: '导入',
-      downloadUrl: '/elderly/expenseSubsidy/downloadExpenseSubsidyExcel',
-      excelTempName: '长护险导入模板',
-      importUrl: '/elderly/expenseSubsidy/importExpenseSubsidy',
+      downloadUrl: '/elderly/consumer-vouchers/import-template',
+      excelTempName: '政府消费券',
+      importUrl: '/elderly/consumer-vouchers/import',
       failExportUrl: ''
     }"
   />
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import {expenseSubsidyDelete, getExpenseSubsidyPage} from '@/api/elderly/fee/expense-allowance'
 import { expenseAllowanceColumn } from './column'
 import Form from './Form.vue'
 import Details from './Details.vue'
@@ -201,34 +199,34 @@ const handleImport = () => {
 }
 //导出
 const handleExport = async ()=>{
-  if(loading.value){
-    return
-  }
-  try {
-    loading.value = true
-    queryParams.pageSize = 10000
-    const list = await getList()
-    if (list.length <= 0) {
-      message.error('暂无数据可以导出!')
-      return
-    }
-    const headers = [
-      { key: 'elderName', title: '长者姓名' },
-      { key: 'month', title: '所属月份' },
-      { key: 'subsidyDestination', title: '补贴去向(1线下返还,2账单抵扣)' },
-      { key: 'status', title: '使用状态(1已抵扣,0未抵扣)' },
-      { key: 'amount', title: '补贴金额' },
-      { key: 'deductionBillMonth', title: '抵扣账单月' },
-      { key: 'remarks', title: '备注' }
-    ]
-    exportWithCustomHeaders(list, headers, `长护险-${formatToDateTime()}.xlsx`, '长护险补贴')
-  } catch (e) {
-    message.error('暂无数据可以导出!')
-    console.log(e)
-  }finally {
-    queryParams.pageSize = 10
-    loading.value = false
-  }
+  // if(loading.value){
+  //   return
+  // }
+  // try {
+  //   loading.value = true
+  //   queryParams.pageSize = 10000
+  //   const list = await getList()
+  //   if (list.length <= 0) {
+  //     message.error('暂无数据可以导出!')
+  //     return
+  //   }
+  //   const headers = [
+  //     { key: 'elderName', title: '长者姓名' },
+  //     { key: 'month', title: '所属月份' },
+  //     { key: 'subsidyDestination', title: '补贴去向(1线下返还,2账单抵扣)' },
+  //     { key: 'status', title: '使用状态(1已抵扣,0未抵扣)' },
+  //     { key: 'amount', title: '补贴金额' },
+  //     { key: 'deductionBillMonth', title: '抵扣账单月' },
+  //     { key: 'remarks', title: '备注' }
+  //   ]
+  //   exportWithCustomHeaders(list, headers, `长护险-${formatToDateTime()}.xlsx`, '长护险补贴')
+  // } catch (e) {
+  //   message.error('暂无数据可以导出!')
+  //   console.log(e)
+  // }finally {
+  //   queryParams.pageSize = 10
+  //   loading.value = false
+  // }
 }
 /** 初始化 **/
 onMounted(() => {

+ 10 - 7
src/views/elderly/kanban/room-diagram/index.vue

@@ -171,10 +171,10 @@
                   <!-- 房间 -->
                   <el-scrollbar v-if="item.roomUsage=='1'">
                     <div class="item-wrap">
-                      <div  :style="{border: (i.select?systemTheme:'#00000000')+' solid '+ (i.select?'2px':'0')}" class="item" v-for="(i) in item.bedList" :key="i">
+                      <div  @click="bedClick(i,item.roomName,1)" :style="{border: (i.select?systemTheme:'#00000000')+' solid '+ (i.select?'2px':'0')}" class="item" v-for="(i) in item.bedList" :key="i">
                         <div class="tag">{{ i.bedName }}</div>
 
-                        <el-dropdown size="large" >
+                        <el-dropdown size="large"  >
                          <div style="display: flex;flex-direction: column;align-items: center;">
                            <img class="bed" src="@/assets/imgs/bed13.png" v-if="(i.status == 1 && i.elderName)"/>
                            <img class="bed" src="@/assets/imgs/bed14.png" v-else/>
@@ -184,9 +184,9 @@
                            <span class="name">{{ (i.status == 1 && i.elderName) ?i.elderName: '空床'   }}</span>
                            <span class="desc">{{ (i.status == 1 && i.nurseLevelName) ? i.nurseLevelName:'无护理'  }}</span>
                          </div>
-                          <template #dropdown>
-                            <el-dropdown-menu>
-                              <el-dropdown-item @click="bedClick(i,item.roomName)" :disabled="!(i.status == 1 && i.elderName)">长者信息</el-dropdown-item>
+                          <template #dropdown >
+                            <el-dropdown-menu v-if="props.type!=2">
+                              <el-dropdown-item @click="bedClick(i,item.roomName,2)" :disabled="!(i.status == 1 && i.elderName)">长者信息</el-dropdown-item>
                               <el-dropdown-item @click="bedChangeClick(i)" :disabled="!(i.status == 1 && i.elderName)">床位变更</el-dropdown-item>
                             </el-dropdown-menu>
                           </template>
@@ -346,8 +346,11 @@ const handleClickBuild = async (item) => {
 }
 
 let isA = false;
-const bedClick = (i,roomName) => {
-  console.log(i)
+const bedClick = (i,roomName,e) => {
+  if(props.type!=2 && e==1){
+    return;
+  }
+  //console.log(i)
   if(props.type!=2){//跳转到长者档案
     if(i.elderId){
       router.push({

+ 660 - 0
src/views/social-worker/floor-activity-record/photo/AddForm.vue

@@ -0,0 +1,660 @@
+<template>
+  <Dialog
+    style="max-width: 100vw; min-width: 90vw"
+    v-model="dialogVisible"
+    :title="title"
+    scroll
+    class="PhotoActivityRecord"
+    noPaddingEL="PhotoActivityRecord"
+  >
+    <el-form v-loading="loading" ref="formRef" :model="dataForm" :rules="isDetail?[]:dataRule" :label-width="labelWidth">
+      <!-- 基础信息 -->
+      <div class="info-wrap">
+        <el-row>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="记录年月" prop="recordYear">
+              <el-date-picker
+                v-if="!isDetail"
+                v-model="dataForm.recordYear"
+                type="month"
+                placeholder="选择年月"
+                value-format="YYYY-MM"
+                style="width: 100%"
+                @change="handleYearChange"
+              />
+              <el-text v-else>{{dataForm.recordYear}}</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="第几周" prop="weekNumber">
+              <el-select v-if="!isDetail" v-model="dataForm.weekNumber" placeholder="选择周次" style="width: 100%">
+                <el-option v-for="w in 5" :key="w" :label="'第' + ['一', '二', '三', '四', '五'][w-1] + '周'" :value="w" />
+              </el-select>
+              <el-text v-else>第{{['一', '二', '三', '四', '五'][dataForm.weekNumber-1]}}周</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="楼栋" prop="buildName">
+              <el-select v-if="!isDetail" v-model="dataForm.buildName" placeholder="选择楼栋" style="width: 100%">
+                <el-option @click="handleBuildChange(item)" v-for="(item,index) in buildList" :key="index" :label="item.buildName" :value="item.buildName" />
+              </el-select>
+              <el-text v-else>{{dataForm.buildName}}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="楼层" prop="floorName">
+              <el-select v-if="!isDetail" v-model="dataForm.floorName" placeholder="选择楼层" style="width: 100%">
+                <el-option v-for="(item,index) in floorList" :key="index" :label="item.floorName" :value="item.floorName" />
+              </el-select>
+              <el-text v-else>{{dataForm.floorName}}</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="记录时间" prop="recordTime">
+              <el-date-picker
+                v-if="!isDetail"
+                v-model="dataForm.recordTime"
+                type="datetime"
+                placeholder="选择记录时间"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                style="width: 100%"
+              />
+              <el-text v-else>{{dataForm.recordTime}}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+            <el-form-item label="参与长者">
+              <el-input type="textarea" placeholder="需要注意,这里的长者会用于列表页的长者名称模糊搜索" maxlength="500" show-word-limit :rows="4">{{ dateRangeText }}</el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+
+      <el-divider style="margin-top: 1px" />
+
+      <!-- 照片记录表格 -->
+      <div class="photo-table-wrap">
+        <table class="photo-table">
+          <thead>
+            <tr>
+              <th class="header-cell" colspan="2">
+                <div class="table-title">长者日常活动照片记录</div>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <!-- 所属楼层和活动日期 -->
+            <tr>
+              <td class="label-cell">所属楼层</td>
+              <td class="value-cell">{{ dataForm.floorName || 'X' }}楼</td>
+            </tr>
+            <tr>
+              <td class="label-cell">活动日期</td>
+              <td class="value-cell">{{ dateRangeText }}</td>
+            </tr>
+            <!-- 活动内容 -->
+            <tr>
+              <td class="label-cell">活动内容</td>
+              <td class="value-cell content-cell">
+                <el-input
+                  v-if="!isDetail"
+                  v-model="dataForm.activityContent"
+                  type="textarea"
+                  :rows="3"
+                  placeholder="请输入活动内容描述,例如:开心写真,图中的长者坐在轮椅上,开心地做出互动手势,这样的状态有助于其保持积极情绪,提升社交参与感,同时也能在互动中锻炼手部动作灵活性。"
+                />
+                <el-text v-else>{{ dataForm.activityContent || '-' }}</el-text>
+              </td>
+            </tr>
+            <!-- 活动照片 -->
+            <tr>
+              <td class="label-cell photo-label-cell">
+                <div class="photo-label">
+                  <div>活动照片</div>
+                  <div class="photo-tip">(每周提交4-5张照片,照片清晰,长者衣着整洁;照片体现不同的参与者、不同的服务内容。需要按照要求,注描述长者在做什么,对长者而言有什么功能)</div>
+                </div>
+              </td>
+              <td class="value-cell photo-upload-cell">
+                <div class="photo-list">
+                  <!-- 添加照片按钮 -->
+                  <el-button
+                    v-if="!isDetail && photoList.length < 5"
+                    class="add-photo-btn"
+                    type="warning"
+                    plain
+                    @click="addPhoto"
+                  >
+                    <el-icon><Plus /></el-icon>
+                    添加照片
+                  </el-button>
+
+
+                  <div v-for="(photo, index) in photoList" :key="index" class="photo-item">
+                    <div class="photo-title">照片{{ index + 1 }}:</div>
+                    <div class="photo-upload-wrapper">
+                      <div style="width: 160px;" v-if="!isDetail">
+                        <SelectUpload
+                          v-if="!isDetail" :limit="1" v-model="dataForm.changeFilesList" fun-name="附件" :is-detail="isDetail"
+                        />
+                      </div>
+                      <div v-else class="photo-preview-wrapper">
+                        <img v-if="photo.url" :src="photo.url" class="photo-preview"  alt=""/>
+                        <div v-else class="no-photo">暂无照片</div>
+                      </div>
+                      <!-- 照片描述 -->
+                      <div class="photo-desc-wrapper">
+                        <el-input
+                          v-if="!isDetail"
+                          v-model="photo.description"
+                          type="textarea"
+                          :rows="3"
+                          :maxlength="600"
+                          placeholder="请描述照片内容:长者在做什么,对长者而言有什么功能"
+                        />
+                        <el-text v-else class="photo-desc-text">{{ photo.description || '-' }}</el-text>
+                      </div>
+                      <!-- 删除按钮 -->
+                      <el-button
+                        v-if="!isDetail && photoList.length > 1"
+                        class="delete-btn"
+                        type="danger"
+                        link
+                        @click="removePhoto(index)"
+                      >
+                        <el-icon size="20"><Delete /></el-icon>
+                      </el-button>
+                    </div>
+                  </div>
+
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-loading="formLoading" type="primary" v-show="!isDetail" @click="submitForm">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue'
+import { FormRules } from 'element-plus'
+import { useMediaQuery } from '@vueuse/core'
+import { Plus, Delete } from '@element-plus/icons-vue'
+import dayjs from 'dayjs'
+import {
+  floorActivityPhotoRecordCreate,
+  floorActivityPhotoRecordUpdate,
+  floorActivityPhotoRecordGetInfo
+} from "@/api/social-work";
+import { getBuildList } from "@/api/system/badManage";
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const title = ref('')
+const dialogVisible = ref(false) // 弹窗
+const loading = ref(false) // 弹窗
+const formRef = ref() // 表单 Ref
+const isDetail = ref(false) // 是否详情打开
+const formLoading = ref(false) // 表单的加载中
+
+// 上传配置
+const uploadAction = import.meta.env.VITE_UPLOAD_URL
+const uploadHeaders = {
+  Authorization: 'Bearer ' + getAccessToken(),
+  'tenant-id': getTenantId()
+}
+
+// 照片列表
+const photoList = ref<Array<{ url: string; description: string }>>([
+  { url: '', description: '' }
+])
+
+// 表单数据
+let dataForm = ref({
+  id: undefined,
+  recordYear: dayjs().format('YYYY-MM'), // 记录年份
+  recordMonth: dayjs().month() + 1, // 记录月份
+  weekNumber: 1, // 第几周
+  buildName: '', // 楼栋
+  floorName: '', // 楼层
+  recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), // 记录时间
+  startDate: '', // 开始日期
+  endDate: '', // 结束日期
+  activityContent: '', // 活动内容
+  photos: '', // 照片JSON
+  tenantId: undefined
+})
+
+// 表单规则
+const dataRule = reactive<FormRules>({
+  recordYear: [{ required: true, message: '记录年份不能为空', trigger: 'change' }],
+  weekNumber: [{ required: true, message: '周次不能为空', trigger: 'change' }],
+  buildName: [{ required: true, message: '楼栋不能为空', trigger: 'change' }],
+  floorName: [{ required: true, message: '楼层不能为空', trigger: 'change' }],
+  recordTime: [{ required: true, message: '记录时间不能为空', trigger: 'blur' }],
+  activityContent: [{ required: true, message: '活动内容不能为空', trigger: 'blur' }]
+})
+
+// 计算日期范围文本
+const dateRangeText = computed(() => {
+  if (!dataForm.value.startDate || !dataForm.value.endDate) {
+    return 'X月X日-X月X日'
+  }
+  return `${dataForm.value.startDate}-${dataForm.value.endDate}`
+})
+
+// 获取指定年月第几周的起止日期
+const getWeekDateRange = (year: number, month: number, weekNum: number) => {
+  // 获取该月第一天
+  const firstDayOfMonth = dayjs(`${year}-${month}-01`)
+  // 获取该月第一天是星期几 (0=周日, 1=周一...)
+  const firstDayWeek = firstDayOfMonth.day()
+  // 计算第一周的起始日期(周日开始)
+  let weekStart = firstDayOfMonth.subtract(firstDayWeek, 'day')
+
+  // 调整到指定的周
+  weekStart = weekStart.add((weekNum - 1) * 7, 'day')
+  const weekEnd = weekStart.add(6, 'day')
+
+  return {
+    start: weekStart.format('M月D日'),
+    end: weekEnd.format('M月D日')
+  }
+}
+
+// 更新周日期显示
+const updateWeekDays = () => {
+  const yearMonth = dataForm.value.recordYear
+  const weekNum = dataForm.value.weekNumber
+
+  if (yearMonth && weekNum) {
+    const [year, month] = yearMonth.split('-').map(Number)
+    const range = getWeekDateRange(year, month, weekNum)
+    dataForm.value.startDate = range.start
+    dataForm.value.endDate = range.end
+  }
+}
+
+// 年份变化
+const handleYearChange = () => {
+  updateWeekDays()
+}
+
+// 监听周次变化
+watch(() => dataForm.value.weekNumber, () => {
+  updateWeekDays()
+})
+
+// 计算窗口大小
+const currentWidth = useMediaQuery('(max-width: 800px)')
+// 计算文字大小
+const labelWidth = computed(() => {
+  return currentWidth.value ? '100px' : '100px'
+})
+
+const buildList = ref([])
+const floorList = ref([])
+
+const handleBuildChange = (e) => {
+  floorList.value = e.floorList
+}
+
+// 照片上传成功
+const handlePhotoSuccess = (res: any, index: number) => {
+  if (res.code === 0) {
+    photoList.value[index].url = res.data
+    message.success('上传成功')
+  } else {
+    message.error(res.msg || '上传失败')
+  }
+}
+
+// 照片上传前校验
+const beforePhotoUpload = (file: File) => {
+  const isJPG = file.type === 'image/jpeg'
+  const isPNG = file.type === 'image/png'
+  const isLt10M = file.size / 1024 / 1024 < 10
+
+  if (!isJPG && !isPNG) {
+    message.error('上传照片只能是 JPG 或 PNG 格式!')
+    return false
+  }
+  if (!isLt10M) {
+    message.error('上传照片大小不能超过 10MB!')
+    return false
+  }
+  return true
+}
+
+// 添加照片
+const addPhoto = () => {
+  photoList.value.push({ url: '', description: '' })
+}
+
+// 删除照片
+const removePhoto = (index: number) => {
+  photoList.value.splice(index, 1)
+}
+
+/** 打开弹窗 */
+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
+
+  try {
+    buildList.value = await getBuildList({ tenantIds: tenantId })
+  } catch (e) { }
+
+  if (id) {
+    title.value = detail ? "详情-楼层活动照片记录" : "编辑-楼层活动照片记录"
+  } else {
+    title.value = "新增-楼层活动照片记录"
+  }
+
+  // 初始化周日期
+  updateWeekDays()
+
+  if (id) {
+    try {
+      loading.value = true
+      const res = await floorActivityPhotoRecordGetInfo(id)
+      // 解析后端返回的数据
+      dataForm.value = {
+        ...res,
+        recordYear: res.recordYear ? `${res.recordYear}-${String(res.recordMonth).padStart(2, '0')}` : dayjs().format('YYYY-MM'),
+      }
+      // 解析照片数据
+      if (res.photos) {
+        try {
+          photoList.value = JSON.parse(res.photos)
+        } catch (e) {
+          photoList.value = [{ url: '', description: '' }]
+        }
+      }
+      updateWeekDays()
+      loading.value = false
+    } catch (err) {
+      loading.value = false
+      message.error('获取详情失败')
+    }
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if (formLoading.value) {
+    return
+  }
+  formLoading.value = true
+  // 提交请求
+  try {
+    // 校验表单
+    if (!formRef.value) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+
+    // 过滤掉空照片
+    const validPhotos = photoList.value.filter(p => p.url || p.description)
+    if (validPhotos.length === 0) {
+      message.error('请至少上传一张照片')
+      return
+    }
+
+    // 构建提交参数
+    const [year, month] = dataForm.value.recordYear.split('-').map(Number)
+    const tempParams = {
+      ...dataForm.value,
+      recordYear: year,
+      recordMonth: month,
+      photos: JSON.stringify(photoList.value)
+    }
+
+    if (dataForm.value.id) {
+      const res = await floorActivityPhotoRecordUpdate(tempParams)
+      if (res) {
+        message.success(t('common.updateSuccess'))
+        dialogVisible.value = false
+        // 发送操作成功的事件
+        emit('success')
+      }
+    } else {
+      const res = await floorActivityPhotoRecordCreate(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,
+    recordYear: dayjs().format('YYYY-MM'),
+    recordMonth: dayjs().month() + 1,
+    weekNumber: 1,
+    buildName: '',
+    floorName: '',
+    recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    startDate: '',
+    endDate: '',
+    activityContent: '',
+    photos: '',
+    tenantId: undefined
+  }
+  photoList.value = [{ url: '', description: '' }]
+  formRef.value?.resetFields()
+  floorList.value = []
+}
+
+// 关闭表单
+const handleClosed = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+</script>
+
+<style lang="scss" scoped>
+.PhotoActivityRecord {
+  .el-form {
+    padding: 15px !important;
+  }
+
+  .info-wrap {
+    margin: 0 15px 15px 15px;
+  }
+
+  .photo-table-wrap {
+    margin: 15px;
+    overflow-x: auto;
+  }
+
+  .photo-table {
+    width: 100%;
+    border-collapse: collapse;
+    border: 1px solid #dcdfe6;
+    font-size: 14px;
+
+    th,
+    td {
+      border: 1px solid #dcdfe6;
+      padding: 12px;
+    }
+
+    .header-cell {
+      background-color: #f5f7fa;
+      text-align: center;
+
+      .table-title {
+        font-size: 18px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+
+    .label-cell {
+      width: 120px;
+      background-color: #f5f7fa;
+      font-weight: 600;
+      text-align: center;
+      vertical-align: middle;
+    }
+
+    .value-cell {
+      text-align: left;
+      vertical-align: middle;
+    }
+
+    .content-cell {
+      :deep(.el-textarea__inner) {
+        min-height: 80px !important;
+      }
+    }
+
+    .photo-label-cell {
+      vertical-align: top;
+
+      .photo-label {
+        .photo-tip {
+          font-size: 12px;
+          color: #909399;
+          font-weight: normal;
+          margin-top: 8px;
+          line-height: 1.5;
+        }
+      }
+    }
+
+    .photo-upload-cell {
+      padding: 16px;
+    }
+  }
+
+  .photo-list {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+
+    .photo-item {
+      .photo-title {
+        font-weight: 600;
+        margin-bottom: 8px;
+        color: #303133;
+      }
+
+      .photo-upload-wrapper {
+        display: flex;
+        align-items: flex-start;
+        gap: 16px;
+
+        .photo-uploader {
+          :deep(.el-upload) {
+            border: 1px dashed #d9d9d9;
+            border-radius: 6px;
+            cursor: pointer;
+            position: relative;
+            overflow: hidden;
+            transition: border-color 0.3s;
+
+            &:hover {
+              border-color: #409eff;
+            }
+          }
+        }
+
+        .upload-placeholder {
+          width: 200px;
+          height: 150px;
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          color: #8c939d;
+
+          .el-icon {
+            font-size: 28px;
+            margin-bottom: 8px;
+          }
+
+          .upload-text {
+            font-size: 12px;
+          }
+        }
+
+        .photo-preview {
+          width: 200px;
+          height: 150px;
+          object-fit: cover;
+          display: block;
+        }
+
+        .photo-preview-wrapper {
+          .no-photo {
+            width: 200px;
+            height: 150px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-color: #f5f7fa;
+            color: #909399;
+            border-radius: 6px;
+          }
+        }
+
+        .photo-desc-wrapper {
+          flex: 1;
+          min-width: 300px;
+
+          :deep(.el-textarea__inner) {
+            min-height: 150px !important;
+          }
+
+          .photo-desc-text {
+            color: #606266;
+            line-height: 1.6;
+          }
+        }
+
+        .delete-btn {
+          margin-top: 0;
+        }
+      }
+    }
+
+    .add-photo-btn {
+      align-self: flex-start;
+      margin-top: 10px;
+    }
+  }
+
+  @media (max-width: 1200px) {
+    :deep(.el-form-item--default .el-form-item__label) {
+      line-height: 0 !important;
+    }
+  }
+}
+</style>

+ 369 - 0
src/views/social-worker/floor-activity-record/photo/index.vue

@@ -0,0 +1,369 @@
+<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-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="楼栋">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="输入楼栋"
+          class="!w-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="楼层">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="输入楼层"
+          class="!w-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="记录年月">
+        <el-date-picker
+          size="default"
+          ref="selectRef"
+          class="!w-240px"
+          v-model="queryParams.recordTime"
+          type="monthrange"
+          :clearable="true"
+          :editable="false"
+          placeholder="选择记录年月"
+          value-format="YYYY-MM"
+          format="YYYY-MM"
+          date-format="YYYY-MM"
+        />
+      </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)" v-hasPermi="['accommodation-adaptation-services:add']" />
+      <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="bedInfo" header-align="center" align="center" label="床位号" min-width="200" show-overflow-tooltip/>-->
+      <el-table-column prop="recordTime" header-align="center" align="center" label="记录时间" min-width="150" show-overflow-tooltip>
+        <template #default="scope">
+          {{(scope.row.recordTime)}}
+        </template>
+      </el-table-column>
+      <el-table-column prop="templateName" header-align="center" align="center" label="表名" min-width="220" show-overflow-tooltip/>
+      <el-table-column prop="creator" header-align="center" align="center" label="操作人" min-width="150" show-overflow-tooltip/>
+
+      <el-table-column label="操作" align="center" min-width="200" >
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openFormEdit(scope.row, scope.row.id)"
+            v-hasPermi="['accommodation-adaptation-services:edit']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="warning"
+            @click="openFormDetail(scope.row.id)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="openClose(scope.row)"
+            v-hasPermi="['accommodation-adaptation-services:delete']"
+          >
+            删除
+          </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" />
+  <DetailForm ref="detailRef" @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"
+  />
+
+  <div style="position: fixed; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden;">
+    <form-create
+      id="element-to-print"
+      v-model:api="fApi"
+      v-model="taskForm.value"
+      :option="taskForm.option"
+      :rule="taskForm.rule"
+    />
+  </div>
+
+</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 DetailForm from "@/views/preSalesManage/Appointment/DetailForm.vue";
+import { useUserStore } from '@/store/modules/user'
+import {getCurrentMonthRange} from "@/utils/dateUtil";
+import {
+  activityServiceRecordDelete,
+  activityServiceRecordGetPage,
+  adaptServiceRecordDelete,
+  adaptServiceRecordGetPage
+} from "@/api/social-work";
+import {ref} from "vue";
+import type {ApiAttrs} from "@form-create/element-ui/types/config";
+import {batchGeneratePDFsAndZipWithProcess} from "@/utils/outBedCard";
+import {setConfAndFields2} from "@/utils/formCreate";
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const userStore = useUserStore()
+const loading = ref(true) // 列表的加载中
+const detailRef = ref()
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+let queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  recordTime: getCurrentMonthRange(),
+  tenantId: userStore.orgTenantId[0]
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    let queryP = {...queryParams,recordTime:queryParams.recordTime?[queryParams.recordTime[0]+" 00:00:00",queryParams.recordTime[1]+" 23:59:59"]:null}
+    const data = await activityServiceRecordGetPage(queryP)
+    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.tenantId = userStore.orgTenantId[0]
+  queryParams.recordTime= getCurrentMonthRange()
+  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.tenantId, id,false)
+}
+
+const editRef = ref()
+const openFormEdit = (row: any = {}, id?: number) => {
+  formRef.value.open(row.tenantId, id,false)
+}
+
+
+
+const openFormDetail = (id?: number) => {
+  formRef.value.open(undefined,id,true)
+}
+
+
+
+
+
+const openClose = async (item) => {
+  try {
+    const res = await message.confirm('确定要删除吗?', '提示')
+    if (res == 'confirm') {
+      // 发起
+      try {
+        const res = await activityServiceRecordDelete(item.id)
+        if (res){
+          message.success(t('common.updateSuccess'))
+        }
+      }catch(err) {}
+    }
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+
+
+const route = useRoute()
+/** 初始化 **/
+onMounted(() => {
+
+  getList()
+
+})
+
+
+
+const taskForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程任务的表单详情
+const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
+const showBatchExport = ref(false)
+const exportConfig = ref({
+  title: '',
+  batchMin: 1,
+  batchMax: 999,
+  countMin: 1,
+  countMax: 500,
+  defaultBatch: 1,
+  defaultCount: 100,
+  description: [] as string[]
+})
+const exportLoading = ref(false)
+
+// 打开导出弹窗
+const handleImportCard = () => {
+  exportConfig.value = {
+    title: "批量导出",
+    batchMin: 1,
+    batchMax: 999,
+    countMin: 1,
+    countMax: 500,
+    defaultBatch: 1,
+    defaultCount: 100,
+    description: [
+      '1. 请输入需要导出的数量',
+      '2. 一次导不完的话,输入批次 2 再次导出,以此类推。',
+      '3. 建议单次导出数量不超过100,以免影响系统性能',
+      '4. 导出需要较长时间,请耐心等待。'
+    ]
+  }
+  showBatchExport.value = true
+}
+
+// 处理批量导出
+const handleBatchExport = async (batch: number, count: number) => {
+  exportLoading.value = true
+  console.log(batch,count)
+
+  try {
+    let queryP = {pageNo:batch,pageSize:count,recordTime:null,elderName:'',tenantId: userStore.orgTenantId[0]}
+    const data = await activityServiceRecordGetPage(queryP)
+
+    await batchGeneratePDFsAndZipWithProcess({
+      dataList: data.list,
+      elementId: 'element-to-print',
+      processItem: async (item: any, index: number) => {
+        setConfAndFields2(taskForm, item.jsonOption, item.activityPlan, '2')
+      },
+      getFilename: (item: any, index: number) => {
+        return `${item.elderName || '记录表'}_${item.id}`
+      },
+      zipFileName: `楼层活动记录_${batch}.zip`,
+      pdfOptions: {
+        margin: [10, 10, 10, 10],
+        image: { type: 'jpeg', quality: 1 },
+        html2canvas: { scale: 1, useCORS: true },
+        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
+      },
+      onProgress: (current: number, total: number) => {
+        console.log(`正在处理第 ${current}/${total} 个文件`)
+      }
+    })
+
+    message.success(`成功生成 ${data.list.length} 个 PDF 文件并打包下载`)
+
+  } catch (error) {
+    console.error('批量导出失败:', error)
+    message.error('批量导出失败,请重试')
+  } finally {
+    exportLoading.value = false
+  }
+
+}
+
+
+
+// 表头格式
+const tableHeaderColor = ({ rowIndex }: any) => {
+  if (rowIndex === 0) {
+    return {
+      backgroundColor: '#f8f8f9',
+      color: '#666666',
+      fontWeight: 'bold'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 611 - 0
src/views/social-worker/floor-activity-record/text/AddForm.vue

@@ -0,0 +1,611 @@
+<template>
+  <Dialog
+    style="max-width: 100vw; min-width: 90vw"
+    v-model="dialogVisible"
+    :title="title"
+    scroll
+    class="FloorActivityRecord"
+    noPaddingEL="FloorActivityRecord"
+  >
+
+    <el-form v-loading="loading" ref="formRef" :model="dataForm" :rules="isDetail?[]:dataRule" :label-width="labelWidth">
+      <!-- 基础信息 -->
+      <div class="info-wrap">
+        <el-row>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="记录年月" prop="recordYear">
+              <el-date-picker
+                v-if="!isDetail"
+                v-model="dataForm.recordYear"
+                type="month"
+                placeholder="选择年月"
+                value-format="YYYY-MM"
+                style="width: 100%"
+                @change="handleYearChange"
+              />
+              <el-text v-else>{{dataForm.recordYear}}</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="第几周" prop="weekNumber">
+              <el-select v-if="!isDetail" v-model="dataForm.weekNumber" placeholder="选择周次" style="width: 100%">
+                <el-option v-for="w in 5" :key="w" :label="'第' + ['一', '二', '三', '四', '五'][w-1] + '周'" :value="w" />
+              </el-select>
+              <el-text v-else>第{{['一', '二', '三', '四', '五'][dataForm.weekNumber-1]}}周</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="楼栋" prop="floorName">
+              <el-select  v-if="!isDetail" v-model="dataForm.buildName" placeholder="选择楼栋" style="width: 100%">
+                <el-option @click="handleBuildChange(item)" v-for="(item,index) in buildList" :key="index" :label="item.buildName" :value="item.buildName" />
+              </el-select>
+              <el-text v-else>{{dataForm.buildName}}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="楼层" prop="weekNumber">
+              <el-select v-if="!isDetail" v-model="dataForm.floorName" placeholder="选择楼层" style="width: 100%">
+                <el-option v-for="(item,index) in floorList" :key="index" :label="item.floorName" :value="item.floorName" />
+              </el-select>
+              <el-text v-else>{{dataForm.floorName}}</el-text>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+            <el-form-item label="记录时间" prop="recordTime">
+              <el-date-picker
+                v-if="!isDetail"
+                v-model="dataForm.recordTime"
+                type="datetime"
+                placeholder="选择记录时间"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                style="width: 100%"
+              />
+              <el-text v-else>{{dataForm.recordTime}}</el-text>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+            <el-form-item label="参与长者">
+              <el-input type="textarea" placeholder="需要注意,这里的长者会用于列表页的长者名称模糊搜索" maxlength="500" show-word-limit :rows="4">{{ dateRangeText }}</el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+
+      <el-divider style="margin-top: 1px" />
+
+      <!-- 活动记录表格 -->
+      <div class="activity-table-wrap">
+        <table class="activity-table">
+          <thead>
+            <tr>
+              <th class="time-col">时间/内容</th>
+              <th v-for="(day, index) in weekDays" :key="index" class="day-col">
+                <div class="day-header">
+                  <div class="day-name">{{ day.name }}</div>
+                  <div class="day-date">{{ day.date }}</div>
+                </div>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <!-- 8:00-9:00 早报/早操 -->
+            <tr>
+              <td class="time-slot">8:00-9:00</td>
+              <td v-for="(day, index) in weekDays" :key="index" class="activity-cell">
+                <el-input
+                  v-if="!isDetail"
+                  v-model="dataForm.morningExercise[index]"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="早报/早操内容"
+                />
+                <el-text v-else>{{ dataForm.morningExercise[index] || '-' }}</el-text>
+              </td>
+            </tr>
+            <!-- 9:00-10:30 上午活动 -->
+            <tr>
+              <td class="time-slot">9:00-10:30</td>
+              <td v-for="(day, index) in weekDays" :key="index" class="activity-cell">
+                <el-input
+                  v-if="!isDetail"
+                  v-model="dataForm.morningActivity[index]"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="上午活动内容"
+                />
+                <el-text v-else>{{ dataForm.morningActivity[index] || '-' }}</el-text>
+              </td>
+            </tr>
+            <!-- 11:00-12:00 长者午餐 -->
+            <tr>
+              <td class="time-slot">11:00-12:00</td>
+              <td colspan="7" class="merge-cell">
+                <span class="merge-text">长者午餐</span>
+              </td>
+            </tr>
+            <!-- 12:00-14:30 长者午休、起床 -->
+            <tr>
+              <td class="time-slot">12:00-14:30</td>
+              <td colspan="7" class="merge-cell">
+                <span class="merge-text">长者午休、起床</span>
+              </td>
+            </tr>
+            <!-- 15:00-16:30 下午活动 -->
+            <tr>
+              <td class="time-slot">15:00-16:30</td>
+              <td v-for="(day, index) in weekDays" :key="index" class="activity-cell">
+                <el-input
+                  v-if="!isDetail"
+                  v-model="dataForm.afternoonActivity[index]"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="下午活动内容"
+                />
+                <el-text v-else>{{ dataForm.afternoonActivity[index] || '-' }}</el-text>
+              </td>
+            </tr>
+            <!-- 执行人姓名 -->
+            <tr>
+              <td class="time-slot">执行人姓名</td>
+              <td v-for="(day, index) in weekDays" :key="index" class="activity-cell">
+                <el-input
+                  v-if="!isDetail"
+                  v-model="dataForm.executorName[index]"
+                  placeholder="执行人"
+                />
+                <el-text v-else>{{ dataForm.executorName[index] || '-' }}</el-text>
+              </td>
+            </tr>
+            <!-- 执行情况记录 -->
+            <tr>
+              <td class="time-slot execution-col">
+                <div class="execution-title">执行情况记录</div>
+              </td>
+              <td v-for="(day, index) in weekDays" :key="index" class="execution-cell">
+                <div class="checkbox-group">
+                  <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].suitablePhysiology">适宜长者生理、心理特征</el-checkbox>
+                  <el-text v-else v-show="dataForm.executionRecord[index].suitablePhysiology">适宜长者生理、心理特征</el-text>
+
+                  <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].improveSecurity">有助于提高长者安全感、归属感</el-checkbox>
+                  <el-text v-else v-show="dataForm.executionRecord[index].improveSecurity">有助于提高长者安全感、归属感</el-text>
+
+                  <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].maintainMemory">有助于维系长者认知和记忆</el-checkbox>
+                  <el-text v-else v-show="dataForm.executionRecord[index].maintainMemory">有助于维系长者认知和记忆</el-text>
+
+                  <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].improveCoordination">有助于提高长者手眼协调能力</el-checkbox>
+                  <el-text v-else v-show="dataForm.executionRecord[index].improveCoordination">有助于提高长者手眼协调能力</el-text>
+
+                  <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].improveCommunication">有助于提高长者沟通与表达能力</el-checkbox>
+                  <el-text v-else v-show="dataForm.executionRecord[index].improveCommunication">有助于提高长者沟通与表达能力</el-text>
+
+                  <div class="other-remark">
+                    <el-checkbox v-if="!isDetail" v-model="dataForm.executionRecord[index].hasOther">其他请注明</el-checkbox>
+                    <el-text v-else v-show="dataForm.executionRecord[index].hasOther">其他请注明</el-text>
+                    <el-input
+                      v-if="!isDetail"
+                      v-model="dataForm.executionRecord[index].otherRemark"
+                      placeholder="请输入"
+                      style="width: 120px; margin-left: 8px;"
+                    />
+                    <el-text v-else v-show="dataForm.executionRecord[index].hasOther" style="margin-left: 8px;">{{ dataForm.executionRecord[index].otherRemark }}</el-text>
+                  </div>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-loading="formLoading" type="primary" v-show="!isDetail" @click="submitForm">确定</el-button>
+    </template>
+  </Dialog>
+
+
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue'
+import { FormRules } from 'element-plus'
+import { useMediaQuery } from '@vueuse/core'
+import dayjs from 'dayjs'
+import {
+  floorActivityRecordCreate,
+  floorActivityRecordUpdate,
+  floorActivityRecordGetInfo
+} from "@/api/social-work";
+import {getBuildList} from "@/api/system/badManage";
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const selectBedRef = ref()
+const title = ref('')
+const dialogVisible = ref(false) // 弹窗
+const loading = ref(false) // 弹窗
+const formRef = ref() // 表单 Ref
+const isDetail = ref(false) // 是否详情打开
+const formLoading = ref(false) // 表单的加载中
+
+// 星期配置
+const weekDayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+
+// 计算日期范围
+const weekDays = ref<{ name: string; date: string }[]>([])
+
+// 初始化执行记录
+const initExecutionRecord = () => {
+  return {
+    suitablePhysiology: false,
+    improveSecurity: false,
+    maintainMemory: false,
+    improveCoordination: false,
+    improveCommunication: false,
+    hasOther: false,
+    otherRemark: ''
+  }
+}
+
+// 表单数据
+let dataForm = ref({
+  id: undefined,
+  recordYear: dayjs().format('YYYY-MM'), // 记录年份
+  recordMonth: dayjs().month() + 1, // 记录月份
+  weekNumber: 1, // 第几周
+  floorName: '', // 养护楼层
+  recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), // 记录时间
+  startDate: '', // 开始日期
+  endDate: '', // 结束日期
+  // 7天的活动内容
+  morningExercise: ['', '', '', '', '', '', ''], // 8:00-9:00 早报/早操
+  morningActivity: ['', '', '', '', '', '', ''], // 9:00-10:30 上午活动
+  afternoonActivity: ['', '', '', '', '', '', ''], // 15:00-16:30 下午活动
+  executorName: ['', '', '', '', '', '', ''], // 执行人姓名
+  executionRecord: Array(7).fill(null).map(() => initExecutionRecord()), // 执行情况记录
+  tenantId: undefined
+})
+
+// 表单规则
+const dataRule = reactive<FormRules>({
+  recordYear: [{ required: true, message: '记录年份不能为空', trigger: 'change' }],
+  recordMonth: [{ required: true, message: '记录月份不能为空', trigger: 'change' }],
+  weekNumber: [{ required: true, message: '周次不能为空', trigger: 'change' }],
+  floorName: [{ required: true, message: '养护楼层不能为空', trigger: 'blur' }],
+  recordTime: [{ required: true, message: '记录时间不能为空', trigger: 'blur' }],
+})
+
+// 计算日期范围文本
+const dateRangeText = computed(() => {
+  if (!dataForm.value.startDate || !dataForm.value.endDate) {
+    return '-'
+  }
+  return `${dataForm.value.startDate} 至 ${dataForm.value.endDate}`
+})
+
+// 获取指定年月第几周的起止日期
+const getWeekDateRange = (year: number, month: number, weekNum: number) => {
+  // 获取该月第一天
+  const firstDayOfMonth = dayjs(`${year}-${month}-01`)
+  // 获取该月第一天是星期几 (0=周日, 1=周一...)
+  const firstDayWeek = firstDayOfMonth.day()
+  // 计算第一周的起始日期(周日开始)
+  let weekStart = firstDayOfMonth.subtract(firstDayWeek, 'day')
+  
+  // 调整到指定的周
+  weekStart = weekStart.add((weekNum - 1) * 7, 'day')
+  const weekEnd = weekStart.add(6, 'day')
+  
+  return {
+    start: weekStart.format('MM月DD日'),
+    end: weekEnd.format('MM月DD日'),
+    dates: Array.from({ length: 7 }, (_, i) => weekStart.add(i, 'day').format('MM-DD'))
+  }
+}
+
+// 更新周日期显示
+const updateWeekDays = () => {
+  const yearMonth = dataForm.value.recordYear // 格式: YYYY-MM
+  const weekNum = dataForm.value.weekNumber
+  
+  if (yearMonth && weekNum) {
+    const [year, month] = yearMonth.split('-').map(Number)
+    const range = getWeekDateRange(year, month, weekNum)
+    dataForm.value.startDate = range.start
+    dataForm.value.endDate = range.end
+    
+    weekDays.value = weekDayNames.map((name, index) => ({
+      name,
+      date: range.dates[index]
+    }))
+  }
+}
+
+// 年份变化
+const handleYearChange = () => {
+  updateWeekDays()
+}
+
+// 月份变化
+const handleMonthChange = () => {
+  updateWeekDays()
+}
+
+// 监听周次变化
+watch(() => dataForm.value.weekNumber, () => {
+  updateWeekDays()
+})
+
+
+
+// 计算窗口大小
+const currentWidth = useMediaQuery('(max-width: 800px)')
+// 计算文字大小
+const labelWidth = computed(() => {
+  return currentWidth.value ? '100px' : '100px'
+})
+
+
+const buildList = ref([])
+const floorList = ref([])
+
+const handleBuildChange = (e) => {
+  floorList.value= e.floorList
+}
+
+
+/** 打开弹窗 */
+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
+
+  try {
+    buildList.value = await getBuildList({tenantIds: tenantId})
+  }catch (e) {}
+
+  if (id) {
+    title.value = detail ? "详情-楼层活动记录" : "编辑-楼层活动记录"
+  } else {
+    title.value = "新增-楼层活动记录"
+  }
+
+  // 初始化周日期
+  updateWeekDays()
+
+  if (id) {
+    try {
+      loading.value = true
+      const res = await floorActivityRecordGetInfo(id)
+      // 解析后端返回的数据
+      dataForm.value = {
+        ...res,
+        morningExercise: res.morningExercise ? JSON.parse(res.morningExercise) : ['', '', '', '', '', '', ''],
+        morningActivity: res.morningActivity ? JSON.parse(res.morningActivity) : ['', '', '', '', '', '', ''],
+        afternoonActivity: res.afternoonActivity ? JSON.parse(res.afternoonActivity) : ['', '', '', '', '', '', ''],
+        executorName: res.executorName ? JSON.parse(res.executorName) : ['', '', '', '', '', '', ''],
+        executionRecord: res.executionRecord ? JSON.parse(res.executionRecord) : Array(7).fill(null).map(() => initExecutionRecord()),
+      }
+      updateWeekDays()
+      loading.value = false
+    } catch (err) {
+      loading.value = false
+      message.error('获取详情失败')
+    }
+  }
+
+
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if (formLoading.value) {
+    return
+  }
+  formLoading.value = true
+  // 提交请求
+  try {
+    // 校验表单
+    if (!formRef.value) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+
+    // 构建提交参数
+    const tempParams = {
+      ...dataForm.value,
+      morningExercise: JSON.stringify(dataForm.value.morningExercise),
+      morningActivity: JSON.stringify(dataForm.value.morningActivity),
+      afternoonActivity: JSON.stringify(dataForm.value.afternoonActivity),
+      executorName: JSON.stringify(dataForm.value.executorName),
+      executionRecord: JSON.stringify(dataForm.value.executionRecord),
+    }
+
+    if (dataForm.value.id) {
+      const res = await floorActivityRecordUpdate(tempParams)
+      if (res) {
+        message.success(t('common.updateSuccess'))
+        dialogVisible.value = false
+        // 发送操作成功的事件
+        emit('success')
+      }
+    } else {
+      const res = await floorActivityRecordCreate(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,
+    recordYear: dayjs().format('YYYY-MM'),
+    recordMonth: dayjs().month() + 1,
+    weekNumber: 1,
+    floorName: '',
+    recordTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    startDate: '',
+    endDate: '',
+    morningExercise: ['', '', '', '', '', '', ''],
+    morningActivity: ['', '', '', '', '', '', ''],
+    afternoonActivity: ['', '', '', '', '', '', ''],
+    executorName: ['', '', '', '', '', '', ''],
+    executionRecord: Array(7).fill(null).map(() => initExecutionRecord()),
+    tenantId: undefined
+  }
+  formRef.value?.resetFields()
+  weekDays.value = []
+}
+
+// 关闭表单
+const handleClosed = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+</script>
+
+<style lang="scss" scoped>
+.FloorActivityRecord {
+  .el-form {
+    padding: 15px !important;
+  }
+
+  .info-wrap {
+    margin: 0 15px 15px 15px;
+  }
+
+  .activity-table-wrap {
+    margin: 15px;
+    overflow-x: auto;
+  }
+
+  .activity-table {
+    width: 100%;
+    border-collapse: collapse;
+    border: 1px solid #dcdfe6;
+    font-size: 14px;
+
+    th,
+    td {
+      border: 1px solid #dcdfe6;
+      padding: 8px;
+      text-align: center;
+    }
+
+    th {
+      background-color: #f5f7fa;
+      font-weight: 600;
+    }
+
+    .time-col {
+      width: 118px;
+      background-color: #f5f7fa;
+      font-weight: 600;
+    }
+
+    .day-col {
+      min-width: 140px;
+
+      .day-header {
+        .day-name {
+          color: #555555;
+          font-weight: 600;
+          font-size: 13px;
+          margin-bottom: 4px;
+        }
+
+        .day-date {
+          color: #222222;
+          font-size: 13px;
+        }
+      }
+    }
+
+    .time-slot {
+      background-color: #f5f7fa;
+      font-weight: 600;
+      vertical-align: middle;
+    }
+
+    .activity-cell {
+      min-width: 140px;
+      vertical-align: top;
+
+      :deep(.el-textarea__inner) {
+        min-height: 60px !important;
+      }
+    }
+
+    .merge-cell {
+      background-color: #f5f7fa;
+      text-align: center;
+
+      .merge-text {
+        font-weight: 600;
+        color: #606266;
+      }
+    }
+
+    .execution-col {
+      vertical-align: middle;
+
+      .execution-title {
+        writing-mode: vertical-rl;
+        text-orientation: upright;
+        letter-spacing: 4px;
+        margin: 0 auto;
+      }
+    }
+
+    .execution-cell {
+      min-width: 160px;
+      text-align: left;
+      vertical-align: top;
+
+      .checkbox-group {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+
+        :deep(.el-checkbox) {
+          margin-right: 0;
+          height: auto;
+          white-space: normal;
+          line-height: 1.4;
+        }
+
+        .other-remark {
+          display: flex;
+          align-items: center;
+          flex-wrap: wrap;
+        }
+      }
+    }
+  }
+
+  @media (max-width: 1200px) {
+    :deep(.el-form-item--default .el-form-item__label) {
+      line-height: 0 !important;
+    }
+  }
+}
+</style>

+ 342 - 0
src/views/social-worker/floor-activity-record/text/index.vue

@@ -0,0 +1,342 @@
+<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-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="楼栋">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="输入楼栋"
+          class="!w-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="楼层">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="输入楼层"
+          class="!w-200px"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="记录年月">
+        <el-date-picker
+          size="default"
+          ref="selectRef"
+          class="!w-240px"
+          v-model="queryParams.recordTime"
+          type="monthrange"
+          :clearable="true"
+          :editable="false"
+          placeholder="选择记录年月"
+          value-format="YYYY-MM"
+          format="YYYY-MM"
+          date-format="YYYY-MM"
+        />
+      </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)" v-hasPermi="['accommodation-adaptation-services:add']" />
+      <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="bedInfo" header-align="center" align="center" label="床位号" min-width="200" show-overflow-tooltip/>-->
+      <el-table-column prop="recordTime" header-align="center" align="center" label="记录时间" min-width="150" show-overflow-tooltip>
+        <template #default="scope">
+          {{(scope.row.recordTime)}}
+        </template>
+      </el-table-column>
+      <el-table-column prop="templateName" header-align="center" align="center" label="表名" min-width="220" show-overflow-tooltip/>
+      <el-table-column prop="creator" header-align="center" align="center" label="操作人" min-width="150" show-overflow-tooltip/>
+
+      <el-table-column label="操作" align="center" min-width="200" >
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openFormEdit(scope.row, scope.row.id)"
+            v-hasPermi="['accommodation-adaptation-services:edit']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="warning"
+            @click="openFormDetail(scope.row.id)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="openClose(scope.row)"
+            v-hasPermi="['accommodation-adaptation-services:delete']"
+          >
+            删除
+          </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" />
+
+
+
+
+</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 DetailForm from "@/views/preSalesManage/Appointment/DetailForm.vue";
+import { useUserStore } from '@/store/modules/user'
+import {getCurrentMonthRange} from "@/utils/dateUtil";
+import {
+  activityServiceRecordDelete,
+  activityServiceRecordGetPage,
+} from "@/api/social-work";
+import {ref} from "vue";
+import type {ApiAttrs} from "@form-create/element-ui/types/config";
+import {batchGeneratePDFsAndZipWithProcess} from "@/utils/outBedCard";
+import {setConfAndFields2} from "@/utils/formCreate";
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const userStore = useUserStore()
+const loading = ref(true) // 列表的加载中
+const detailRef = ref()
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+let queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  recordTime: getCurrentMonthRange(),
+  tenantId: userStore.orgTenantId[0]
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    let queryP = {...queryParams,recordTime:queryParams.recordTime?[queryParams.recordTime[0]+" 00:00:00",queryParams.recordTime[1]+" 23:59:59"]:null}
+    const data = await activityServiceRecordGetPage(queryP)
+    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.tenantId = userStore.orgTenantId[0]
+  queryParams.recordTime= getCurrentMonthRange()
+  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.tenantId, id,false)
+}
+
+const editRef = ref()
+const openFormEdit = (row: any = {}, id?: number) => {
+  formRef.value.open(row.tenantId, id,false)
+}
+
+
+
+const openFormDetail = (id?: number) => {
+  formRef.value.open(undefined,id,true)
+}
+
+
+
+
+
+const openClose = async (item) => {
+  try {
+    const res = await message.confirm('确定要删除吗?', '提示')
+    if (res == 'confirm') {
+      // 发起
+      try {
+        const res = await activityServiceRecordDelete(item.id)
+        if (res){
+          message.success(t('common.updateSuccess'))
+        }
+      }catch(err) {}
+    }
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+
+
+const route = useRoute()
+/** 初始化 **/
+onMounted(() => {
+
+  getList()
+
+})
+
+
+
+const taskForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程任务的表单详情
+const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
+const showBatchExport = ref(false)
+const exportConfig = ref({
+  title: '',
+  batchMin: 1,
+  batchMax: 999,
+  countMin: 1,
+  countMax: 500,
+  defaultBatch: 1,
+  defaultCount: 100,
+  description: [] as string[]
+})
+const exportLoading = ref(false)
+
+// 打开导出弹窗
+const handleImportCard = () => {
+  exportConfig.value = {
+    title: "批量导出",
+    batchMin: 1,
+    batchMax: 999,
+    countMin: 1,
+    countMax: 500,
+    defaultBatch: 1,
+    defaultCount: 100,
+    description: [
+      '1. 请输入需要导出的数量',
+      '2. 一次导不完的话,输入批次 2 再次导出,以此类推。',
+      '3. 建议单次导出数量不超过100,以免影响系统性能',
+      '4. 导出需要较长时间,请耐心等待。'
+    ]
+  }
+  showBatchExport.value = true
+}
+
+// 处理批量导出
+const handleBatchExport = async (batch: number, count: number) => {
+  exportLoading.value = true
+  console.log(batch,count)
+
+  try {
+    let queryP = {pageNo:batch,pageSize:count,recordTime:null,elderName:'',tenantId: userStore.orgTenantId[0]}
+    const data = await activityServiceRecordGetPage(queryP)
+
+    await batchGeneratePDFsAndZipWithProcess({
+      dataList: data.list,
+      elementId: 'element-to-print',
+      processItem: async (item: any, index: number) => {
+        setConfAndFields2(taskForm, item.jsonOption, item.activityPlan, '2')
+      },
+      getFilename: (item: any, index: number) => {
+        return `${item.elderName || '记录表'}_${item.id}`
+      },
+      zipFileName: `楼层活动记录_${batch}.zip`,
+      pdfOptions: {
+        margin: [10, 10, 10, 10],
+        image: { type: 'jpeg', quality: 1 },
+        html2canvas: { scale: 1, useCORS: true },
+        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
+      },
+      onProgress: (current: number, total: number) => {
+        console.log(`正在处理第 ${current}/${total} 个文件`)
+      }
+    })
+
+    message.success(`成功生成 ${data.list.length} 个 PDF 文件并打包下载`)
+
+  } catch (error) {
+    console.error('批量导出失败:', error)
+    message.error('批量导出失败,请重试')
+  } finally {
+    exportLoading.value = false
+  }
+
+}
+
+
+
+// 表头格式
+const tableHeaderColor = ({ rowIndex }: any) => {
+  if (rowIndex === 0) {
+    return {
+      backgroundColor: '#f8f8f9',
+      color: '#666666',
+      fontWeight: 'bold'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss"></style>