Преглед изворни кода

公章管理重构,修改账单和押金展示公章接口

xiongxing пре 1 недеља
родитељ
комит
7262a86e58

+ 70 - 0
src/api/system/commonSeal/index.ts

@@ -0,0 +1,70 @@
+import request from '@/config/axios'
+
+/** 公章信息 */
+export interface CommonSealVO {
+  /** id,修改 / 删除时必传 */
+  id?: number
+  /** 类型,取值见字典 common_seal(deposit_seal 押金公章 / expense_order_seal 账单公章) */
+  type?: string
+  /** 公章 url */
+  url?: string
+  /** 机构 id */
+  tenantId?: number
+  createTime?: string
+  updateTime?: string
+  creator?: string
+  updater?: string
+}
+
+/** 公章列表查询参数 */
+export interface CommonSealListReqVO {
+  type?: string
+  /** 机构 id,不传则默认当前登录租户 */
+  tenantId?: number
+}
+
+/** 公章类型取值(对应字典 common_seal) */
+export const SEAL_TYPE = {
+  /** 押金公章 */
+  DEPOSIT: 'deposit_seal',
+  /** 账单公章 */
+  EXPENSE_ORDER: 'expense_order_seal'
+}
+
+/** 获得公章列表 */
+export const getCommonSealList = (params?: CommonSealListReqVO) => {
+  return request.get({ url: '/system/common-seal/list', params })
+}
+
+/** 获得公章 */
+export const getCommonSeal = (id: number) => {
+  return request.get({ url: '/system/common-seal/get?id=' + id })
+}
+
+/** 创建公章 */
+export const createCommonSeal = (data: CommonSealVO) => {
+  return request.post({ url: '/system/common-seal/create', data })
+}
+
+/** 更新公章 */
+export const updateCommonSeal = (data: CommonSealVO) => {
+  return request.put({ url: '/system/common-seal/update', data })
+}
+
+/** 删除公章 */
+export const deleteCommonSeal = (id: number) => {
+  return request.delete({ url: '/system/common-seal/delete?id=' + id })
+}
+
+/**
+ * 按类型取当前机构的公章 url(取第一条)
+ * 容错处理:接口异常时返回空字符串,避免影响打印 / 预览主流程
+ */
+export const getSealUrlByType = async (type: string, tenantId?: number): Promise<string> => {
+  try {
+    const list: CommonSealVO[] = (await getCommonSealList({ type, tenantId })) || []
+    return list[0]?.url || ''
+  } catch {
+    return ''
+  }
+}

+ 1 - 0
src/utils/dict.ts

@@ -423,6 +423,7 @@ export enum DICT_TYPE {
   PAY_TYPE = 'pay_type', // 支付状态
   PUSH_STATUS = 'push_status', // 推送状态
   BILL_TYPE = 'bill_type', // 账单类型
+  COMMON_SEAL = 'common_seal', // 公章类型(押金公章 / 账单公章)
 
   // 床位类型
   PRIVATE_ROOM = 'private_room', // 床位

+ 6 - 7
src/views/elderly/fee/bill-pay/preview.vue

@@ -229,8 +229,7 @@
 import { getExpenseOrderDetail } from '@/api/elderly/fee/bill-pay'
 import dayjs from 'dayjs'
 import { useUserStore } from '@/store/modules/user'
-import { getTenant } from '@/api/system/tenant'
-import { getTenantId } from '@/utils/auth'
+import { getSealUrlByType, SEAL_TYPE } from '@/api/system/commonSeal'
 import {
   fixedNum,
   formatCeil,
@@ -270,7 +269,7 @@ const longTermAmount = ref(0)
 const payText = ref('')
 const roundDown = ref(0)
 
-// 当前机构公章(来自租户接口 expenseCachetUrl);为空字符串时签章区不渲染
+// 当前机构账单公章(来自 /system/common-seal/list,type=expense_order_seal);为空字符串时签章区不渲染
 const sealUrl = ref('')
 
 const formatPayCreatedTime = (createdTime) => {
@@ -283,12 +282,12 @@ const formatPayCreatedTime = (createdTime) => {
 /** 打开弹窗 */
 const open = async (id, row) => {
   dialogVisible.value = true
-  // 并行:账单详情 + 当前机构公章(容错,租户接口失败不影响主流程)
-  const [res, tenant] = await Promise.all([
+  // 并行:账单详情 + 当前机构账单公章(容错,公章接口失败不影响主流程)
+  const [res, seal] = await Promise.all([
     getExpenseOrderDetail(id),
-    getTenant(getTenantId() as number).catch(() => null)
+    getSealUrlByType(SEAL_TYPE.EXPENSE_ORDER)
   ])
-  sealUrl.value = tenant?.expenseCachetUrl || ''
+  sealUrl.value = seal || ''
   let flag = false
   dataForm.value = res
   res.items = res.items.filter((item) => item.isShow == 1)

+ 6 - 11
src/views/elderly/fee/deposit/Print.vue

@@ -84,8 +84,7 @@ 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'
+import { getSealUrlByType, SEAL_TYPE } from '@/api/system/commonSeal'
 defineOptions({ name: 'DepositPrint' })
 
 const tenantName = useUserStore().getTenantName // 当前登录的编号
@@ -125,20 +124,16 @@ const pageTitle = ref('')
 const pageType = ref(0)
 const payText = ref('')
 const singleType = ref(true)
-// 当前机构公章(来自租户接口 expenseCachetUrl);为空字符串时签章区不渲染
+// 当前机构押金公章(来自 /system/common-seal/list,type=deposit_seal);为空字符串时签章区不渲染
 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 = ''
-    })
+  // 并行拉取机构押金公章(容错,公章接口失败不影响打印主流程)
+  getSealUrlByType(SEAL_TYPE.DEPOSIT).then((url) => {
+    sealUrl.value = url || ''
+  })
   pageType.value = type
   singleType.value = single
   /** 押金 */

+ 155 - 73
src/views/system/official-seal/index.vue

@@ -1,137 +1,219 @@
 <template>
   <ContentWrap title="机构公章管理">
-    <div v-loading="loading" class="seal-container">
-      <el-alert type="info" :closable="false" show-icon class="mb-20px">
-        当前机构:<strong>{{ tenantName || '—' }}</strong>,公章仅对本机构生效。
-      </el-alert>
-
-      <div class="seal-box">
-        <UploadImg
-          v-model="sealUrl"
-          v-model:fileName="fileName"
-          :width="'220px'"
-          :height="'220px'"
-          :show-delete="false"
-        />
+    <template #header>
+      <div class="seal-header">
+        <el-button :loading="loading" @click="handleRefresh">
+          <Icon icon="ep:refresh" class="mr-5px" />刷新
+        </el-button>
       </div>
+    </template>
+    <el-alert type="info" :closable="false" show-icon class="mb-20px">
+      当前机构:<strong>{{ tenantName || '—' }}</strong
+      >,公章仅对本机构生效,每个类型只能上传一枚公章。
+    </el-alert>
+
+    <div v-loading="loading" class="seal-container">
+      <el-empty
+        v-if="!typeOptions.length"
+        description="未获取到公章类型字典(common_seal),请先配置字典"
+      />
+
+      <div v-for="opt in typeOptions" :key="opt.value" class="seal-card">
+        <div class="seal-card-title">{{ opt.label }}</div>
 
-      <div class="seal-tip">支持 jpg / png / gif,大小不超过 5MB。点击图片即可上传或替换。</div>
+        <div class="seal-box">
+          <UploadImg
+            v-model="formMap[opt.value].url"
+            v-model:fileName="formMap[opt.value].fileName"
+            :width="'220px'"
+            :height="'220px'"
+            :show-delete="false"
+          />
+        </div>
 
-      <div class="seal-actions">
-        <el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
-        <el-button type="danger" plain @click="handleDelete">删除公章</el-button>
+        <div class="seal-tip">支持 jpg / png / gif,大小不超过 5MB。点击图片即可上传或替换。</div>
+
+        <div class="seal-actions">
+          <el-button
+            type="primary"
+            :loading="savingType === opt.value"
+            @click="handleSave(opt.value)"
+          >
+            保存
+          </el-button>
+          <el-button type="danger" plain :disabled="!formMap[opt.value].id" @click="handleDelete(opt.value)">
+            删除公章
+          </el-button>
+        </div>
       </div>
     </div>
   </ContentWrap>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted } from 'vue'
 import { ElMessageBox } from 'element-plus'
 import { UploadImg } from '@/components/UploadFile'
 import { getTenantId } from '@/utils/auth'
 import { useUserStore } from '@/store/modules/user'
+import { useDictStore } from '@/store/modules/dict'
 import { useMessage } from '@/hooks/web/useMessage'
-import { getTenant, updateTenant, type TenantVO } from '@/api/system/tenant'
+import { getStrDictOptions, DICT_TYPE, type StringDictDataType } from '@/utils/dict'
+import {
+  getCommonSealList,
+  createCommonSeal,
+  updateCommonSeal,
+  deleteCommonSeal,
+  type CommonSealVO
+} from '@/api/system/commonSeal'
+
+defineOptions({ name: 'OfficialSeal' })
+
+/** 单个类型公章的编辑态 */
+interface SealForm {
+  id?: number
+  url: string
+  fileName: string
+}
 
 const message = useMessage()
 const userStore = useUserStore()
-const tenantId = getTenantId()
+const dictStore = useDictStore()
+const tenantId = getTenantId() as number
 const tenantName = computed(() => userStore.tenant?.name || '')
 
 const loading = ref(false)
-const saving = ref(false)
-const sealUrl = ref('')
-const fileName = ref('')
-
-// 当前租户全量信息(保存 / 删除时要带回全部字段,避免被后端清空)
-const currentTenant = ref<TenantVO | null>(null)
-
-// 演示用占位公章:用于租户尚未上传时的占位预览
-const DEMO_SEAL =
-  'data:image/svg+xml;utf8,' +
-  encodeURIComponent(
-    '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><circle cx="100" cy="100" r="92" fill="none" stroke="#c0392b" stroke-width="6"/><circle cx="100" cy="100" r="76" fill="none" stroke="#c0392b" stroke-width="2"/><text x="100" y="108" font-size="22" fill="#c0392b" text-anchor="middle" font-family="serif">公章示例</text></svg>'
-  )
-
-/** 从租户接口读取当前机构公章(字段 expenseCachetUrl) */
-const loadSeal = async () => {
+const savingType = ref('')
+
+/** 公章类型(字典 common_seal) */
+const typeOptions = ref<StringDictDataType[]>([])
+/** 各类型公章编辑数据,key 为字典值 */
+const formMap = reactive<Record<string, SealForm>>({})
+
+/** 按字典构建表单结构,保证模板中 formMap[type] 一定存在 */
+const buildForms = () => {
+  typeOptions.value = getStrDictOptions(DICT_TYPE.COMMON_SEAL)
+  typeOptions.value.forEach((opt) => {
+    if (!formMap[opt.value]) {
+      formMap[opt.value] = { id: undefined, url: '', fileName: '' }
+    }
+  })
+}
+
+/** 拉取当前机构公章列表,按类型回填 */
+const loadSealList = async () => {
   loading.value = true
   try {
-    const data: any = await getTenant(tenantId as number)
-    currentTenant.value = data
-    sealUrl.value = data?.expenseCachetUrl || ''
-    fileName.value = ''
+    const list: CommonSealVO[] = (await getCommonSealList({ tenantId })) || []
+    Object.keys(formMap).forEach((key) => {
+      formMap[key].id = undefined
+      formMap[key].url = ''
+      formMap[key].fileName = ''
+    })
+    list.forEach((item) => {
+      const key = item?.type || ''
+      if (!key) return
+      if (!formMap[key]) {
+        formMap[key] = { id: undefined, url: '', fileName: '' }
+      }
+      formMap[key].id = item.id
+      formMap[key].url = item.url || ''
+    })
   } catch {
-    sealUrl.value = DEMO_SEAL
-    message.notifyWarning('租户接口读取失败,当前显示演示数据')
+    message.error('公章列表加载失败,请稍后重试')
   } finally {
     loading.value = false
   }
 }
 
-const handleSave = async () => {
-  if (!sealUrl.value || sealUrl.value === DEMO_SEAL) {
+/** 手动刷新:重新拉取列表,避免新增 / 更新后数据延迟 */
+const handleRefresh = async () => {
+  await loadSealList()
+  message.success('已刷新')
+}
+
+/** 保存:已存在则更新,否则创建 */
+const handleSave = async (type: string) => {
+  const form = formMap[type]
+  if (!form?.url) {
     message.error('请先上传公章图片')
     return
   }
-  if (!currentTenant.value) {
-    message.error('租户信息未加载,请刷新页面后重试')
-    return
-  }
-  saving.value = true
+  savingType.value = type
   try {
-    // 全量带回其他字段,只覆盖 expenseCachetUrl
-    await updateTenant({
-      ...currentTenant.value,
-      expenseCachetUrl: sealUrl.value
-    })
+    if (form.id) {
+      await updateCommonSeal({ id: form.id, type, url: form.url, tenantId })
+    } else {
+      await createCommonSeal({ type, url: form.url, tenantId })
+    }
     message.success('公章已保存')
+    await loadSealList()
   } catch {
-    message.error('保存失败,请稍后重试')
+    // 失败提示由请求拦截器统一处理
   } finally {
-    saving.value = false
+    savingType.value = ''
   }
 }
 
-const handleDelete = async () => {
-  if (!sealUrl.value || sealUrl.value === DEMO_SEAL) {
+/** 删除公章 */
+const handleDelete = async (type: string) => {
+  const form = formMap[type]
+  if (!form?.id) {
     message.warning('当前没有可删除的公章')
     return
   }
-  if (!currentTenant.value) {
-    message.error('租户信息未加载,请刷新页面后重试')
-    return
-  }
   try {
-    await ElMessageBox.confirm('确认删除当前机构公章?删除后需重新上传。', '提示', {
+    await ElMessageBox.confirm('确认删除该类型公章?删除后需重新上传。', '提示', {
       type: 'warning'
     })
   } catch {
     return
   }
   try {
-    // 后端无 delete 接口:"删除"= update 时把字段清空
-    await updateTenant({
-      ...currentTenant.value,
-      expenseCachetUrl: ''
-    })
-    sealUrl.value = ''
-    fileName.value = ''
+    await deleteCommonSeal(form.id)
+    form.id = undefined
+    form.url = ''
+    form.fileName = ''
     message.success('公章已删除')
   } catch {
-    message.error('删除失败,请稍后重试')
+    // 失败提示由请求拦截器统一处理
   }
 }
 
-onMounted(loadSeal)
+onMounted(async () => {
+  // 字典可能尚未加载(或新增了 common_seal),兜底重新拉取一次
+  if (!getStrDictOptions(DICT_TYPE.COMMON_SEAL).length) {
+    await dictStore.resetDict()
+  }
+  buildForms()
+  await loadSealList()
+})
 </script>
 
 <style scoped lang="scss">
+.seal-header {
+  display: flex;
+  justify-content: flex-end;
+  flex: 1;
+}
 .seal-container {
-  max-width: 480px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 24px;
   padding: 10px 0;
 }
+.seal-card {
+  width: 280px;
+  padding: 16px;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+.seal-card-title {
+  margin-bottom: 12px;
+  font-size: 15px;
+  font-weight: 600;
+  text-align: center;
+}
 .seal-box {
   display: flex;
   justify-content: center;