index.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <ContentWrap title="机构公章管理">
  3. <template #header>
  4. <div class="seal-header">
  5. <el-button :loading="loading" @click="handleRefresh">
  6. <Icon icon="ep:refresh" class="mr-5px" />刷新
  7. </el-button>
  8. </div>
  9. </template>
  10. <el-alert type="info" :closable="false" show-icon class="mb-20px">
  11. 当前机构:<strong>{{ tenantName || '—' }}</strong
  12. >,公章仅对本机构生效,每个类型只能上传一枚公章。
  13. </el-alert>
  14. <div v-loading="loading" class="seal-container">
  15. <el-empty
  16. v-if="!typeOptions.length"
  17. description="未获取到公章类型字典(common_seal),请先配置字典"
  18. />
  19. <div v-for="opt in typeOptions" :key="opt.value" class="seal-card">
  20. <div class="seal-card-title">{{ opt.label }}</div>
  21. <div class="seal-box">
  22. <UploadImg
  23. v-model="formMap[opt.value].url"
  24. v-model:fileName="formMap[opt.value].fileName"
  25. :width="'220px'"
  26. :height="'220px'"
  27. :show-delete="false"
  28. />
  29. </div>
  30. <div class="seal-tip">支持 jpg / png / gif,大小不超过 5MB。点击图片即可上传或替换。</div>
  31. <div class="seal-actions">
  32. <el-button
  33. type="primary"
  34. :loading="savingType === opt.value"
  35. @click="handleSave(opt.value)"
  36. >
  37. 保存
  38. </el-button>
  39. <el-button type="danger" plain :disabled="!formMap[opt.value].id" @click="handleDelete(opt.value)">
  40. 删除公章
  41. </el-button>
  42. </div>
  43. </div>
  44. </div>
  45. </ContentWrap>
  46. </template>
  47. <script setup lang="ts">
  48. import { ref, reactive, computed, onMounted } from 'vue'
  49. import { ElMessageBox } from 'element-plus'
  50. import { UploadImg } from '@/components/UploadFile'
  51. import { getTenantId } from '@/utils/auth'
  52. import { useUserStore } from '@/store/modules/user'
  53. import { useDictStore } from '@/store/modules/dict'
  54. import { useMessage } from '@/hooks/web/useMessage'
  55. import { getStrDictOptions, DICT_TYPE, type StringDictDataType } from '@/utils/dict'
  56. import {
  57. getCommonSealList,
  58. createCommonSeal,
  59. updateCommonSeal,
  60. deleteCommonSeal,
  61. type CommonSealVO
  62. } from '@/api/system/commonSeal'
  63. defineOptions({ name: 'OfficialSeal' })
  64. /** 单个类型公章的编辑态 */
  65. interface SealForm {
  66. id?: number
  67. url: string
  68. fileName: string
  69. }
  70. const message = useMessage()
  71. const userStore = useUserStore()
  72. const dictStore = useDictStore()
  73. const tenantId = getTenantId() as number
  74. const tenantName = computed(() => userStore.tenant?.name || '')
  75. const loading = ref(false)
  76. const savingType = ref('')
  77. /** 公章类型(字典 common_seal) */
  78. const typeOptions = ref<StringDictDataType[]>([])
  79. /** 各类型公章编辑数据,key 为字典值 */
  80. const formMap = reactive<Record<string, SealForm>>({})
  81. /** 按字典构建表单结构,保证模板中 formMap[type] 一定存在 */
  82. const buildForms = () => {
  83. typeOptions.value = getStrDictOptions(DICT_TYPE.COMMON_SEAL)
  84. typeOptions.value.forEach((opt) => {
  85. if (!formMap[opt.value]) {
  86. formMap[opt.value] = { id: undefined, url: '', fileName: '' }
  87. }
  88. })
  89. }
  90. /** 拉取当前机构公章列表,按类型回填 */
  91. const loadSealList = async () => {
  92. loading.value = true
  93. try {
  94. const list: CommonSealVO[] = (await getCommonSealList({ tenantId })) || []
  95. Object.keys(formMap).forEach((key) => {
  96. formMap[key].id = undefined
  97. formMap[key].url = ''
  98. formMap[key].fileName = ''
  99. })
  100. list.forEach((item) => {
  101. const key = item?.type || ''
  102. if (!key) return
  103. if (!formMap[key]) {
  104. formMap[key] = { id: undefined, url: '', fileName: '' }
  105. }
  106. formMap[key].id = item.id
  107. formMap[key].url = item.url || ''
  108. })
  109. } catch {
  110. message.error('公章列表加载失败,请稍后重试')
  111. } finally {
  112. loading.value = false
  113. }
  114. }
  115. /** 手动刷新:重新拉取列表,避免新增 / 更新后数据延迟 */
  116. const handleRefresh = async () => {
  117. await loadSealList()
  118. message.success('已刷新')
  119. }
  120. /** 保存:已存在则更新,否则创建 */
  121. const handleSave = async (type: string) => {
  122. const form = formMap[type]
  123. if (!form?.url) {
  124. message.error('请先上传公章图片')
  125. return
  126. }
  127. savingType.value = type
  128. try {
  129. if (form.id) {
  130. await updateCommonSeal({ id: form.id, type, url: form.url, tenantId })
  131. } else {
  132. await createCommonSeal({ type, url: form.url, tenantId })
  133. }
  134. message.success('公章已保存')
  135. await loadSealList()
  136. } catch {
  137. // 失败提示由请求拦截器统一处理
  138. } finally {
  139. savingType.value = ''
  140. }
  141. }
  142. /** 删除公章 */
  143. const handleDelete = async (type: string) => {
  144. const form = formMap[type]
  145. if (!form?.id) {
  146. message.warning('当前没有可删除的公章')
  147. return
  148. }
  149. try {
  150. await ElMessageBox.confirm('确认删除该类型公章?删除后需重新上传。', '提示', {
  151. type: 'warning'
  152. })
  153. } catch {
  154. return
  155. }
  156. try {
  157. await deleteCommonSeal(form.id)
  158. form.id = undefined
  159. form.url = ''
  160. form.fileName = ''
  161. message.success('公章已删除')
  162. } catch {
  163. // 失败提示由请求拦截器统一处理
  164. }
  165. }
  166. onMounted(async () => {
  167. // 字典可能尚未加载(或新增了 common_seal),兜底重新拉取一次
  168. if (!getStrDictOptions(DICT_TYPE.COMMON_SEAL).length) {
  169. await dictStore.resetDict()
  170. }
  171. buildForms()
  172. await loadSealList()
  173. })
  174. </script>
  175. <style scoped lang="scss">
  176. .seal-header {
  177. display: flex;
  178. justify-content: flex-end;
  179. flex: 1;
  180. }
  181. .seal-container {
  182. display: flex;
  183. flex-wrap: wrap;
  184. gap: 24px;
  185. padding: 10px 0;
  186. }
  187. .seal-card {
  188. width: 280px;
  189. padding: 16px;
  190. border: 1px solid var(--el-border-color-lighter);
  191. border-radius: 8px;
  192. }
  193. .seal-card-title {
  194. margin-bottom: 12px;
  195. font-size: 15px;
  196. font-weight: 600;
  197. text-align: center;
  198. }
  199. .seal-box {
  200. display: flex;
  201. justify-content: center;
  202. margin: 16px 0;
  203. }
  204. .seal-tip {
  205. text-align: center;
  206. font-size: 13px;
  207. color: var(--el-text-color-secondary);
  208. margin-bottom: 18px;
  209. }
  210. .seal-actions {
  211. display: flex;
  212. justify-content: center;
  213. gap: 12px;
  214. }
  215. </style>