Prechádzať zdrojové kódy

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

chenjun 2 týždňov pred
rodič
commit
a50c5f408d

+ 46 - 18
src/api/elderly/elder/notice-customer/index.ts

@@ -1,37 +1,65 @@
 import request from '@/config/axios'
 
 /**
- * 探访公告(notice-customer)
+ * 须知(notice-customer)
  *
- * 说明:每个院区(租户)可自定义公告内容,小程序端通过接口获取并展示。
- * 接口前缀统一在下方 API_PREFIX 维护,如后端实际路径不同,改这一处即可。
+ * 后端接口:
+ * - 创建:POST /jiashu/notice/create
+ * - 更新:PUT /jiashu/notice/update
+ * - 列表:GET /jiashu/notice/listByType
  */
-const API_PREFIX = 'elderly/notice-customer'
+const API_PREFIX = '/jiashu/notice'
 
-/** 探访公告 VO */
+/**
+ * 须知类型:APPOINTMENT 预约须知 / VISIT 探访须知
+ * 后台只维护 APPOINTMENT(VISIT 为后端固定文案)
+ */
+export type NoticeCustomerType = 'APPOINTMENT' | 'VISIT'
+
+/** 须知 VO */
 export interface NoticeCustomerVO {
+  /** 须知编号,修改时必传 */
   id?: number
+  /** 须知类型:APPOINTMENT 或者 VISIT */
+  type?: NoticeCustomerType
+  /** 须知内容 */
+  content?: string
+  /** 机构(租户)id */
+  tenantId?: number
+}
+
+/**
+ * 根据须知类型 + 机构 id 查询须知列表
+ */
+export const getNoticeCustomerList = async (params: {
+  type: NoticeCustomerType
+  /** 机构 id,不传时获取初始化(默认)须知数据 */
   tenantId?: number
-  /** 公告内容,各条以 \n 换行 */
-  notice?: string
-  createTime?: string
-  updateTime?: string
-  creator?: string
-  updater?: string
+}) => {
+  const data = await request.get<NoticeCustomerVO[]>({
+    url: `${API_PREFIX}/listByType`,
+    params
+  })
+  return (data || []) as NoticeCustomerVO[]
+}
+
+/**
+ * 创建须知
+ */
+export const createNoticeCustomer = (data: NoticeCustomerVO) => {
+  return request.post({ url: `${API_PREFIX}/create`, data })
 }
 
 /**
- * 查询指定院区的探访公告
- * @param tenantId 院区(租户)id
+ * 更新须知(id 必传)
  */
-export const getNoticeCustomer = async (tenantId: number) => {
-  const data = await request.get({ url: `${API_PREFIX}/get`, params: { tenantId } })
-  return (data || {}) as NoticeCustomerVO
+export const updateNoticeCustomer = (data: NoticeCustomerVO) => {
+  return request.put({ url: `${API_PREFIX}/update`, data })
 }
 
 /**
- * 保存指定院区的探访公告(存在 id 走更新,否则新增)
+ * 保存须知:有 id 走更新,无 id 走新增
  */
 export const saveNoticeCustomer = (data: NoticeCustomerVO) => {
-  return request.post({ url: `${API_PREFIX}/save`, data })
+  return data.id ? updateNoticeCustomer(data) : createNoticeCustomer(data)
 }

+ 71 - 18
src/views/elderly/elder/notice-customer/index.vue

@@ -28,7 +28,7 @@
   <ContentWrap>
     <div class="mb-15px flex items-center justify-between">
       <div class="flex items-center">
-        <span class="text-16px font-bold">探访公告内容</span>
+        <span class="text-16px font-bold">预约须知内容</span>
         <span class="ml-10px text-12px text-gray-500">保存后小程序端将展示该内容</span>
       </div>
       <div>
@@ -43,33 +43,37 @@
 
     <el-input
       v-loading="loading"
-      v-model="formData.notice"
+      v-model="formData.content"
       type="textarea"
       :rows="18"
       maxlength="2000"
       show-word-limit
-      placeholder="请输入探访公告内容,每条建议单独一行(换行即视为一条)"
+      placeholder="请输入须知内容,每条建议单独一行(换行即视为一条)"
     />
   </ContentWrap>
 
   <ContentWrap>
     <div class="mb-15px text-16px font-bold">小程序预览</div>
     <div class="preview-wrap">
-      <div class="preview-title">探访公告</div>
+      <div class="preview-title">预约须知</div>
       <div class="preview-content">
         <template v-if="previewLines.length">
           <p v-for="(line, index) in previewLines" :key="index" class="preview-line">
             {{ line }}
           </p>
         </template>
-        <div v-else class="text-gray-400">暂无公告内容</div>
+        <div v-else class="text-gray-400">暂无须知内容</div>
       </div>
     </div>
   </ContentWrap>
 </template>
 
 <script lang="ts" setup>
-import { getNoticeCustomer, saveNoticeCustomer } from '@/api/elderly/elder/notice-customer'
+import {
+  createNoticeCustomer,
+  getNoticeCustomerList,
+  saveNoticeCustomer
+} from '@/api/elderly/elder/notice-customer'
 import { useUserStore } from '@/store/modules/user'
 
 defineOptions({ name: 'NoticeCustomer' })
@@ -77,6 +81,9 @@ defineOptions({ name: 'NoticeCustomer' })
 const userStore = useUserStore()
 const message = useMessage()
 
+/** 须知类型固定为预约须知(APPOINTMENT,VISIT 为后端固定文案) */
+const NOTICE_TYPE = 'APPOINTMENT' as const
+
 const loading = ref(false)
 const saveLoading = ref(false)
 
@@ -85,12 +92,12 @@ const queryParams = reactive({
 })
 const formData = reactive({
   id: undefined as number | undefined,
-  notice: ''
+  content: ''
 })
 
 // 预览:按换行拆分为多条
 const previewLines = computed(() =>
-  (formData.notice || '')
+  (formData.content || '')
     .split('\n')
     .map((item) => item.trim())
     .filter((item) => item)
@@ -102,23 +109,68 @@ const currentTenantId = computed(() => queryParams.tenantIds?.[0])
 /** 是否集团账号:集团账号可切换院区,普通院区账号固定为当前院区 */
 const canSwitchTenant = computed(() => !!userStore.getOrgTenant?.multiple)
 
-/** 查询院区公告 */
+/** 按机构查询须知(返回第一条) */
+const fetchDetail = async () => {
+  const list = await getNoticeCustomerList({
+    type: NOTICE_TYPE,
+    tenantId: currentTenantId.value
+  })
+  return list?.[0]
+}
+
+/**
+ * 用初始化数据为当前机构创建须知
+ * 后端未做初始化:机构下无数据时,不传 tenantId 再查一次拿到初始须知,
+ * 自动创建到当前机构下
+ */
+const initNotice = async () => {
+  try {
+    const defaultList = await getNoticeCustomerList({ type: NOTICE_TYPE })
+    const initContent = defaultList?.[0]?.content
+    if (!initContent) {
+      return false
+    }
+    await createNoticeCustomer({
+      type: NOTICE_TYPE,
+      tenantId: currentTenantId.value,
+      content: initContent
+    })
+    return true
+  } catch {
+    return false
+  }
+}
+
+/** 查询院区须知:无数据时先自动初始化,再查询回填 */
 const getDetail = async () => {
   if (!currentTenantId.value) {
-    message.warning('请先选择院区')
+    message.warning('请先选择机构')
     return
   }
   loading.value = true
   try {
-    const data = await getNoticeCustomer(currentTenantId.value)
-    formData.id = data?.id
-    formData.notice = data?.notice || ''
+    const detail = await fetchDetail()
+    if (detail) {
+      formData.id = detail.id
+      formData.content = detail.content || ''
+      return
+    }
+    // 机构下无数据 -> 自动初始化后再查一次回填
+    const created = await initNotice()
+    if (created) {
+      const newDetail = await fetchDetail()
+      formData.id = newDetail?.id
+      formData.content = newDetail?.content || ''
+      return
+    }
+    formData.id = undefined
+    formData.content = ''
   } finally {
     loading.value = false
   }
 }
 
-/** 切换院区 / 查询 */
+/** 切换机构 / 查询 */
 const handleQuery = () => {
   getDetail()
 }
@@ -126,19 +178,20 @@ const handleQuery = () => {
 /** 保存 */
 const handleSave = async () => {
   if (!currentTenantId.value) {
-    message.warning('请先选择院区')
+    message.warning('请先选择机构')
     return
   }
-  if (!formData.notice || !formData.notice.trim()) {
-    message.warning('公告内容不能为空')
+  if (!formData.content || !formData.content.trim()) {
+    message.warning('须知内容不能为空')
     return
   }
   saveLoading.value = true
   try {
     await saveNoticeCustomer({
       id: formData.id,
+      type: NOTICE_TYPE,
       tenantId: currentTenantId.value,
-      notice: formData.notice
+      content: formData.content
     })
     message.success('保存成功')
     await getDetail()

+ 51 - 1
src/views/elderly/fee/deposit/Print.vue

@@ -64,6 +64,10 @@
         <span>出纳:{{ dataForm.createdBy }}</span>
         <span>操作人:{{ user.nickname }}</span>
       </p>
+      <!-- 机构签章:叠在表格右下角,旋转 -45° -->
+      <div v-if="sealUrl" class="seal-overlay">
+        <img :src="sealUrl" class="seal-img" alt="机构公章" />
+      </div>
     </div>
     <template #footer>
       <el-button @click="handleClosed">取消</el-button>
@@ -80,6 +84,8 @@ import { getDictLabel, DICT_TYPE } from '@/utils/dict'
 import { getDepositDetail } from '@/api/elderly/fee/deposit'
 import { fmtDict } from '@/utils'
 import { getOverheadListByType } from '@/api/elderly/fee/overheadCharge'
+import { getTenant } from '@/api/system/tenant'
+import { getTenantId } from '@/utils/auth'
 defineOptions({ name: 'DepositPrint' })
 
 const tenantName = useUserStore().getTenantName // 当前登录的编号
@@ -119,10 +125,20 @@ const pageTitle = ref('')
 const pageType = ref(0)
 const payText = ref('')
 const singleType = ref(true)
+// 当前机构公章(来自租户接口 expenseCachetUrl);为空字符串时签章区不渲染
+const sealUrl = ref('')
 
 /** 打开弹窗 */
 const open = async (elderId, id, type = 1, single = true) => {
   dialogVisible.value = true
+  // 并行拉取机构公章(容错,租户接口失败不影响打印主流程)
+  getTenant(getTenantId() as number)
+    .then((tenant) => {
+      sealUrl.value = tenant?.expenseCachetUrl || ''
+    })
+    .catch(() => {
+      sealUrl.value = ''
+    })
   pageType.value = type
   singleType.value = single
   /** 押金 */
@@ -209,6 +225,7 @@ const handleClosed = () => {
   dialogVisible.value = false
   total.value = 0
   payText.value = ''
+  sealUrl.value = ''
   dataForm.value = {
     orderNumber: '',
     createdTime: '',
@@ -225,12 +242,21 @@ const getList = async () => {
   list.value = res
 }
 
+// 打印样式:与预览保持一致
+// - #printArea 加 padding-bottom 给签章留出空间,否则签章会贴到纸张边缘或被裁掉
+// - 同步印章定位(打印 iframe 不会继承 scoped 样式,需在此重新声明)
+const printExtraCss = `
+  html, body { height: auto !important; overflow: visible !important; }
+  #printArea { height: auto !important; overflow: visible !important; padding-bottom: 12mm !important; }
+  .seal-overlay { right: 5mm !important; bottom: 0mm !important; }
+`
+
 // 打印
 const print = {
   id: 'printArea',
   extraCss: '',
   popTitle: '', // 打印配置页上方的标题
-  extraHead: '', // 最上方的头部文字,附加在head标签上的额外标签,使用逗号分割
+  extraHead: `<style>${printExtraCss}</style>`, // 最上方的头部文字,附加在head标签上的额外标签,使用逗号分割
   preview: false, // 是否启动预览模式,默认是false
   previewTitle: '对账单预览', // 打印预览的标题
   previewPrintBtnLabel: '预览结束,开始打印', // 打印预览的标题下方的按钮文本,点击可进入打印
@@ -258,6 +284,8 @@ const print = {
 </script>
 <style lang="scss" scoped>
 .deposit-print {
+  position: relative; // 让 .seal-overlay 相对本容器定位
+  padding-bottom: 12mm; // 给印章留出空间,避免被 dialog 滚动容器或打印纸张裁切
   .wrap {
     position: relative;
     .title {
@@ -288,5 +316,27 @@ const print = {
   .fr {
     float: right;
   }
+  // 机构签章:绝对定位叠在表格右下角,旋转 -45°
+  // - right: 5mm / bottom: 0mm,让印章落在容器右下角的留白区域内完整可见
+  // - z-index: 20 保证印章叠在表格之上;pointer-events: none 不挡表格交互
+  // - opacity: 0.85 略透,不完全遮住底部文字
+  .seal-overlay {
+    position: absolute;
+    right: 15mm;
+    bottom: 16mm;
+    width: 30mm;
+    height: 30mm;
+    z-index: 20;
+    pointer-events: none;
+
+    .seal-img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+      // transform: rotate(-45deg);
+      transform-origin: 100% 100%;
+      opacity: 0.85;
+    }
+  }
 }
 </style>