Bladeren bron

餐厅管理增加ai建议试点,首页增加院区运营数据统计

xiongxing 2 maanden geleden
bovenliggende
commit
d798b6f6d5

+ 7 - 0
src/api/elderly/elder/elderly-Info/index.ts

@@ -188,3 +188,10 @@ export const delPhotoById = (id) => {
     url: `elderly/photo/delete?id=${id}`
   })
 }
+
+// 获取每周运营数据汇总
+export const getWeeklyOperationSummary = () => {
+  return request.get({
+    url: '/elderlyInfo/getWeeklyOperationSummary'
+  })
+}

+ 5 - 0
src/api/system/foods/index.ts

@@ -173,4 +173,9 @@ export const createReportTimeslot = (data) => {
 // 修改核销时间段 
 export const updateReportTimeslot = (data) => {
   return request.post({ url: 'restaurant/dishesOrderStatistics/updateReportTimeslot', data})
+}
+
+// AI 建议(新增餐厅超时辅助)
+export const getAiSuggest = (tenantId) => {
+  return request.get({ url: `/system/restaurantManagement/getAiSuggest?tenantId=${tenantId}` })
 }

+ 67 - 0
src/views/Home/Index.vue

@@ -4,6 +4,10 @@
       <div class="title">我的常用</div>
       <el-scrollbar>
         <div class="icon-wrap">
+          <div class="icon-item" @click="handleGridDataStatistics">
+            <Icon :icon="'ep:copy-document'" :size="56" />
+            <div class="text">院区数据统计</div>
+          </div>
           <div class="icon-item" v-for="(c, index) in commonList" :key="index" @click="handleTo(c)">
             <img
               v-if="c.meta.iconImg"
@@ -22,6 +26,7 @@
             />
             <div class="text">添加</div>
           </div>
+
         </div>
       </el-scrollbar>
     </div>
@@ -102,6 +107,21 @@
   </el-drawer>
 
   <MessageDrawer v-model="messageVisible"/>
+
+  <el-dialog
+    v-model="statsDialogVisible"
+    title="院区运营数据统计"
+    width="520px"
+    destroy-on-close
+  >
+    <div v-loading="statsLoading" style="min-height: 220px; line-height: 2; white-space: pre-line">
+      {{ statsText }}
+    </div>
+    <template #footer>
+      <el-button @click="handleCopyStatistics">复制</el-button>
+      <el-button type="primary" @click="statsDialogVisible = false">关闭</el-button>
+    </template>
+  </el-dialog>
   </div>
 </template>
 <script lang="ts" setup>
@@ -116,6 +136,8 @@ import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { useAppStore } from '@/store/modules/app'
 import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
 import { getBpmListCount } from '@/api/bpm/processInstance'
+import * as ElderlyInfoApi from '@/api/elderly/elder/elderly-Info'
+import { ElMessage } from 'element-plus'
 import MessageDrawer from './components/message-drawer.vue'
 import ProcessDetail from '@/views/bpm/task/process-list/detail.vue'
 import MyCommon from './MyCommon.vue'
@@ -139,8 +161,53 @@ const xl = breakpoints.between('lg', 'xl')
 const xxl = breakpoints.between('xl', '2xl')
 const xxxl = breakpoints['2xl']
 const messageVisible = ref(false)
+const statsDialogVisible = ref(false)
+const statsLoading = ref(false)
+const statsText = ref('')
 const router = useRouter()
 
+const buildStatsText = (data: Record<string, any>) => {
+  const lines = [
+    `1、院名:${data?.institutionName ?? '-'}`,
+    `2、本周期初数:${data?.periodBeginCount ?? 0}`,
+    `3、本周入住数:${data?.weeklyCheckInCount ?? 0}`,
+    `4、本周退住数:${data?.weeklyCheckOutCount ?? 0}`,
+    `5、本周期末数:${data?.periodEndCount ?? 0}`,
+    `6、本周净增长长者数:${data?.weeklyNetGrowthCount ?? 0}`,
+    `7、本月净增长长者数合计:${data?.monthlyNetGrowthCount ?? 0}`,
+    `8、本周期末院内空余床位数:${data?.periodEndEmptyBedCount ?? 0}`
+  ]
+  return lines.join('\n')
+}
+
+const handleGridDataStatistics = async () => {
+  statsDialogVisible.value = true
+  statsLoading.value = true
+  try {
+    const res = await ElderlyInfoApi.getWeeklyOperationSummary()
+    const data = res?.data ?? res
+    statsText.value = buildStatsText(data || {})
+  } catch (error) {
+    statsText.value = ''
+    ElMessage.error('获取运营数据失败')
+  } finally {
+    statsLoading.value = false
+  }
+}
+
+const handleCopyStatistics = async () => {
+  if (!statsText.value) {
+    ElMessage.warning('暂无可复制内容')
+    return
+  }
+  try {
+    await navigator.clipboard.writeText(statsText.value)
+    ElMessage.success('复制成功')
+  } catch (error) {
+    ElMessage.error('复制失败,请手动复制')
+  }
+}
+
 // 页面宽度
 const pageStyle = computed(() => {
   // if(appStore.newScene){

+ 7 - 1
src/views/elderly/fee/bill-pay/Form.vue

@@ -345,7 +345,13 @@
             </template>
           </el-table-column>
           <el-table-column prop="count" label="数量" width="100" align="center" />
-          <el-table-column prop="totalAmount" label="应收金额(元)" align="center">
+          <!-- <el-table-column prop="count" label="数量" width="180" align="center">
+            <template #default="scope">
+              <el-input-number v-if="isEdit && scope.row.expenseSource == 'daily_expenses'" @blur="handlePrice(scope.row, 5)" v-model="scope.row.count" controls-position="right"/>
+              <span v-else>{{ scope.row.count }}</span>
+            </template>
+          </el-table-column> -->
+          <el-table-column prop="totalAmount" width="180" label="应收金额(元)" align="center">
             <template #default="scope">
               <el-input-number  v-if="isEdit" @blur="handlePrice(scope.row, 5)" v-model="scope.row.totalAmount" controls-position="right"/>
               <span v-else>{{ tableRowPay(scope.row) }}</span>

+ 43 - 83
src/views/system/food/canteenManage/AddForm.vue

@@ -1,6 +1,5 @@
 <template>
   <Dialog
-
     style="max-width: 100vw; min-width: 40vw;max-height: 70vh;min-height: 45vh"
     v-model="dialogVisible"
     :title="title"
@@ -17,6 +16,7 @@
                 placeholder="餐厅名称"
                 show-word-limit
                 :maxlength="25"
+                @input="handleFormActivity"
               />
             </el-form-item>
           </el-col>
@@ -31,6 +31,7 @@
                 show-word-limit
                 :maxlength="200"
                 :rows="4"
+                @input="handleFormActivity"
               />
             </el-form-item>
           </el-col>
@@ -39,18 +40,6 @@
         <el-row align="bottom" justify="start" style="margin-top: 8px">
           <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
             <el-form-item label="餐厅图片" prop="elderName">
-              <!-- <el-upload
-                :file-list="dataForm.pictures"
-                :action="uploadUrl"
-                list-type="picture-card"
-                :http-request="httpRequest"
-                :before-upload="beforeAvatarUpload"
-                :on-change="roomImageListChange"
-                :on-remove="handleRemove"
-                class="my-custom-upload"
-              >
-                <el-icon><Plus /></el-icon>
-              </el-upload> -->
               <SelectUpload v-model="dataForm.pictures" fun-name="餐厅图片" />
             </el-form-item>
           </el-col>
@@ -67,75 +56,35 @@
 
 <script lang="ts" setup>
 import { computed, ref } from 'vue'
-import { FormRules, UploadProps } from 'element-plus'
+import { FormRules } from 'element-plus'
 import { useMediaQuery } from '@vueuse/core'
-const { isValidNumberDis } = useValidator()
 
-import { useValidator } from '@/hooks/web/useValidator'
 import { useUpload } from '@/components/UploadFile/src/useUpload'
-import {
-  getChargeCategoryTree,
-  getTreeById
-} from '@/api/elderly/fee/chargeCategory'
-import { filteredTreeData } from '@/utils/tree'
-import {restaurantManagementAdd, restaurantManagementEdit} from "@/api/system/foods";
-
-const { uploadUrl, httpRequest } = useUpload()
+import { restaurantManagementAdd, restaurantManagementEdit } from '@/api/system/foods'
 
-//defineOptions({ name: 'bakCheckOutForm' })
+// const { uploadUrl, httpRequest } = useUpload()
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
-const canteenValue = ref('') //
-const canteenList = ref([{ value: '1', label: '你好餐厅' }]) //餐厅
-const selectTreeData = ref() // 树形结构数据
 const dialogVisible = ref(false) // 弹窗
 const title = ref('') // 表单 Ref
 const formRef = ref() // 表单 Ref
-const switchValue = ref(false) //
-const checkList = ref([]) //
-const isDetail = ref(false) // 是否详情打开
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 
-const defaultProps = ref({
-  // 树形属性
-  children: 'childrenList',
-  label: 'name',
-  value: 'id'
-})
-
 let dataForm = ref({
   // 表单字段
   id: undefined,
-  restaurantName: undefined,
-  restaurantDescription: undefined,
-  pictures: [],
-
+  restaurantName: '',
+  restaurantDescription: '',
+  pictures: [] as any[]
 })
 // 表单规则
 const dataRule = reactive<FormRules>({
   restaurantName: [{ required: true, message: '餐厅名称不能为空', trigger: 'blur' }],
-  restaurantDescription: [{ required: true, message: '餐厅描述不能为空', trigger: 'blur' }],
-
+  restaurantDescription: [{ required: true, message: '餐厅描述不能为空', trigger: 'blur' }]
 })
 
-const changesCheck = (e) => {
-  console.log(e)
-  console.log(checkList.value)
-}
-
-// 获取树形
-const getTreeDetail = async () => {
-  const res = await getTreeById(dataForm.value.id)
-  dataList.value = filteredTreeData([res], 'childrenList')
-}
-// 获取上级节点数据
-const getTreeData = async () => {
-  const res = await getChargeCategoryTree()
-  selectTreeData.value = res
-}
-
 // 计算窗口大小
 const currentWidth = useMediaQuery('(max-width: 800px)')
 // 计算文字大小
@@ -143,45 +92,55 @@ const labelWidth = computed(() => {
   return currentWidth.value ? '110px' : '120px'
 })
 
-
-
-
-
+/** 提交表单 */
+const emit = defineEmits(['success', 'visible-change', 'form-activity']) // 定义 success 事件,用于操作成功后的回调
 
 /** 打开弹窗 */
 const open = async (id?: any) => {
   resetForm()
   dialogVisible.value = true
+  emit('visible-change', true)
   if (id !== undefined) {
     try {
-      console.log("餐厅",id)
       dataForm.value = JSON.parse(JSON.stringify(id))
       if (dataForm.value.pictures && dataForm.value.pictures.length > 0) {
         for (const idElement of dataForm.value.pictures) {
-          console.log(idElement.url)
-          idElement.fileUrl = (idElement.url)?idElement.url:idElement.fileUrl
+          idElement.fileUrl = idElement.url ? idElement.url : idElement.fileUrl
         }
       }
-      console.log("餐厅",dataForm.value)
-    }catch (error) {}
+    } catch (error) {}
 
     title.value = '编辑餐厅'
   } else {
     title.value = '新增餐厅'
   }
+}
 
+const handleFormActivity = () => {
+  emit('form-activity')
+}
 
-  if (id) {
-    try {
-    } catch (err) {}
+const applyAiSuggest = (suggest: { restaurantName?: string; restaurantDescription?: string }) => {
+  if (!dataForm.value.restaurantName && suggest?.restaurantName) {
+    dataForm.value.restaurantName = suggest.restaurantName as string
   }
+  if (!dataForm.value.restaurantDescription && suggest?.restaurantDescription) {
+    dataForm.value.restaurantDescription = suggest.restaurantDescription as string
+  }
+  emit('form-activity')
 }
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const getFormData = () => {
+  return {
+    restaurantName: dataForm.value.restaurantName,
+    restaurantDescription: dataForm.value.restaurantDescription
+  }
+}
+
+defineExpose({ open, applyAiSuggest, getFormData }) // 提供 open 方法,用于打开弹窗
+
 const submitForm = async () => {
-  if(formLoading.value){
+  if (formLoading.value) {
     return
   }
   // 提交请求
@@ -202,7 +161,7 @@ const submitForm = async () => {
       if (res) {
         message.success(t('common.updateSuccess'))
         dialogVisible.value = false
-        // 发送操作成功的事件
+        emit('visible-change', false)
         emit('success')
       }
     } else {
@@ -210,14 +169,14 @@ const submitForm = async () => {
       if (res) {
         message.success(t('common.updateSuccess'))
         dialogVisible.value = false
-        // 发送操作成功的事件
+        emit('visible-change', false)
         emit('success')
       }
     }
   } finally {
-    setTimeout(()=>{
+    setTimeout(() => {
       formLoading.value = false
-    },500)
+    }, 500)
   }
 }
 
@@ -225,9 +184,9 @@ const submitForm = async () => {
 const resetForm = () => {
   dataForm.value = {
     id: undefined,
-    restaurantName: undefined,
-    restaurantDescription: undefined,
-    pictures: [],
+    restaurantName: '',
+    restaurantDescription: '',
+    pictures: []
   }
   formRef.value?.resetFields()
 }
@@ -235,6 +194,7 @@ const resetForm = () => {
 // 关闭表单
 const handleClosed = () => {
   dialogVisible.value = false
+  emit('visible-change', false)
   resetForm()
 }
 </script>

+ 167 - 29
src/views/system/food/canteenManage/index.vue

@@ -72,14 +72,21 @@
         show-overflow-tooltip
         width="450"
       />
-      <el-table-column  label="图片" align="center"  width="120">
+      <el-table-column label="图片" align="center" width="120">
         <template #default="scope">
-          <div class="row-el" @click.stop="aa"  style="height: 40px;justify-content: center;align-items: center;">
+          <div
+            class="row-el"
+            @click.stop="aa"
+            style="height: 40px;justify-content: center;align-items: center;"
+          >
             <image-preview
-                v-show="(scope.row.pictures!=undefined && scope.row.pictures.length>0)" :is-p="true" border-radius="5%"
-                           style="width: 40px;height: 40px;" :src="scope.row.picturesStr" @error="() => true" >
-              <!--                 <img src="@/assets/images/defvip.png" alt=""/>-->
-            </image-preview>
+              v-show="scope.row.pictures != undefined && scope.row.pictures.length > 0"
+              :is-p="true"
+              border-radius="5%"
+              style="width: 40px;height: 40px;"
+              :src="scope.row.picturesStr"
+              @error="() => true"
+            />
           </div>
         </template>
       </el-table-column>
@@ -114,24 +121,30 @@
       </el-table-column>
     </el-table>
     <!-- 分页 -->
-<!--    <Pagination-->
-<!--      :total="total"-->
-<!--      v-model:page="queryParams.pageNo"-->
-<!--      v-model:limit="queryParams.pageSize"-->
-<!--      @pagination="getList"-->
-<!--    />-->
+    <!--    <Pagination-->
+    <!--      :total="total"-->
+    <!--      v-model:page="queryParams.pageNo"-->
+    <!--      v-model:limit="queryParams.pageSize"-->
+    <!--      @pagination="getList"-->
+    <!--    />-->
   </ContentWrap>
 
-  <AddForm ref="addFoodRef" @success="getList" />
+  <AddForm
+    ref="addFoodRef"
+    @success="handleAddSuccess"
+    @visible-change="handleAddFormVisibleChange"
+    @form-activity="handleAddFormActivity"
+  />
   <DetailsForm ref="foodDetailsForm" @success="getList" />
 </template>
 
 <script setup lang="ts">
-
 import AddForm from '@/views/system/food/canteenManage/AddForm.vue'
 import DetailsForm from '@/views/system/food/canteenManage/DetailsForm.vue'
-import {chargeCategoryDel} from '@/api/elderly/fee/chargeCategory'
-import {getListRestaurant, restaurantManagementDelete} from "@/api/system/foods";
+import { chargeCategoryDel } from '@/api/elderly/fee/chargeCategory'
+import { getAiSuggest,getListRestaurant, restaurantManagementDelete } from '@/api/system/foods'
+import { getTenantId } from '@/utils/auth'
+import { ElMessageBox } from 'element-plus'
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -142,23 +155,144 @@ const addFoodRef = ref()
 const foodDetailsForm = ref()
 const total = ref(0) // 列表的总页数
 
+const suggestInterval = ref(5000)
+
 const list = ref([]) // 列表的数据
 let queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   elderName: '',
   approvalStatus: '',
-  categoryId: '',
+  categoryId: ''
 })
 
+const aiSuggestTimer = ref<any>(null)
+const isAddFormVisible = ref(false)
+const isAddMode = ref(false)
+const isAiSuggestRunning = ref(false)
+const isAiSuggestStopped = ref(true)
 
+const clearAiSuggestTimer = () => {
+  if (aiSuggestTimer.value) {
+    clearTimeout(aiSuggestTimer.value)
+    aiSuggestTimer.value = null
+  }
+}
 
-const deleteItems = async (id) => {
+const startAiSuggestTimer = () => {
+  clearAiSuggestTimer()
+  if (isAiSuggestStopped.value) return
+  if (!isAddFormVisible.value || !isAddMode.value) return
+  aiSuggestTimer.value = setTimeout(() => {
+    triggerAiSuggestFlow()
+  }, suggestInterval.value)
+}
+
+const triggerAiSuggestFlow = async () => {
+  if (!isAddFormVisible.value || !isAddMode.value) return
+  if (isAiSuggestRunning.value) return
+
+  const rawTenantId = getTenantId()
+  const tenantId = Array.isArray(rawTenantId) ? rawTenantId[0] : rawTenantId
+  if (!tenantId) {
+    startAiSuggestTimer()
+    return
+  }
+
+  let needRestartTimer = false
+  isAiSuggestRunning.value = true
   try {
-    if(id===undefined){
-      id=multipleSelection.value[0].id
+    const suggest = await getAiSuggest(tenantId)
+    // const suggest = {
+    //   restaurantName: '测试餐厅名称',
+    //   restaurantDescription: '测试餐厅描述'
+    // }
+    if (!suggest || (!suggest.restaurantName && !suggest.restaurantDescription)) {
+      needRestartTimer = true
+      return
+    }
+
+    if (!isAddFormVisible.value || !isAddMode.value) {
+      clearAiSuggestTimer()
+      return
+    }
+
+    const formData = addFoodRef.value?.getFormData?.()
+    const hasRestaurantName = !!formData?.restaurantName?.toString().trim()
+    const hasRestaurantDescription = !!formData?.restaurantDescription?.toString().trim()
+    if (hasRestaurantName && hasRestaurantDescription) {
+      needRestartTimer = true
+      return
+    }
+
+    const suggestContentList = [
+      suggest.restaurantName ? `餐厅名称建议:<br/>${suggest.restaurantName}` : '',
+      suggest.restaurantDescription ? `餐厅描述建议:<br/>${suggest.restaurantDescription}` : ''
+    ].filter(Boolean)
+
+    const confirmMessage = [
+      '检测到您在当前页面停留时间过长,是否遇到了问题,我给出了一些表单内容的建议是否进行采纳填写?',
+      ...suggestContentList
+    ].join('<br/><br/>')
+
+    await ElMessageBox.confirm(confirmMessage, '提示', {
+      confirmButtonText: '是',
+      cancelButtonText: '否',
+      type: 'warning',
+      center: true,
+      dangerouslyUseHTMLString: true,
+      closeOnClickModal: false,
+      closeOnPressEscape: false
+    })
+
+    addFoodRef.value?.applyAiSuggest?.({
+      restaurantName: suggest.restaurantName,
+      restaurantDescription: suggest.restaurantDescription
+    })
+
+    clearAiSuggestTimer()
+  } catch (error) {
+    needRestartTimer = true
+  } finally {
+    isAiSuggestRunning.value = false
+    if (needRestartTimer && isAddFormVisible.value && isAddMode.value) {
+      startAiSuggestTimer()
     }
+  }
+}
 
+const handleAddFormActivity = () => {
+  if (!isAddFormVisible.value || !isAddMode.value) return
+  startAiSuggestTimer()
+}
+
+const handleAddFormVisibleChange = (visible: boolean) => {
+  isAddFormVisible.value = visible
+  if (!visible) {
+    isAiSuggestStopped.value = true
+    isAddMode.value = false
+    isAiSuggestRunning.value = false
+    clearAiSuggestTimer()
+    return
+  }
+  if (isAddMode.value) {
+    isAiSuggestStopped.value = false
+    startAiSuggestTimer()
+  }
+}
+
+const handleAddSuccess = async () => {
+  clearAiSuggestTimer()
+  isAiSuggestRunning.value = false
+  isAddMode.value = false
+  await getList()
+}
+
+const deleteItems = async (id) => {
+  try {
+    if (id === undefined) {
+      id = multipleSelection.value[0].id
+    }
 
     // 取消的二次确认
     await message.delConfirm('确定要删除这条餐厅数据吗?')
@@ -172,11 +306,16 @@ const deleteItems = async (id) => {
 
 //添加菜
 const addCK = () => {
+  isAddMode.value = true
+  isAiSuggestRunning.value = false
   addFoodRef.value.open(undefined)
 }
 
 //编辑加菜
 const editCK = (item) => {
+  isAddMode.value = false
+  isAiSuggestRunning.value = false
+  clearAiSuggestTimer()
   addFoodRef.value.open(item)
 }
 
@@ -231,22 +370,17 @@ const treeRef = ref() // 树的表单
 const getList = async () => {
   loading.value = true
   try {
-   // console.log('AAA', queryParams)
-    //console.log('AAA', data)
     list.value = await getListRestaurant(queryParams)
 
     for (const datum of list.value) {
-      let dd=datum.pictures||''
+      let dd = datum.pictures || ''
       datum.pictures = JSON.parse(dd)
-      let li = datum.pictures||[]
-      datum.picturesStr =''
+      let li = datum.pictures || []
+      datum.picturesStr = ''
       for (const ddElement of li) {
-        datum.picturesStr += `${ddElement.url?ddElement.url:ddElement.fileUrl},`
+        datum.picturesStr += `${ddElement.url ? ddElement.url : ddElement.fileUrl},`
       }
     }
-
-    //console.log('最终值:',list.value)
-   // total.value = data.total
   } finally {
     loading.value = false
   }
@@ -316,6 +450,10 @@ onMounted(() => {
   getList()
 })
 
+onBeforeUnmount(() => {
+  clearAiSuggestTimer()
+})
+
 // 表头格式
 const tableHeaderColor = ({ rowIndex }: any) => {
   if (rowIndex === 0) {