certificate.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <!-- src/views/pms/qhse/certificate.vue -->
  2. <template>
  3. <el-row :gutter="20">
  4. <!-- 左侧部门树 -->
  5. <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
  6. <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
  7. <ContentWrap>
  8. <!-- 搜索工作栏 -->
  9. <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
  10. <el-form-item label="证书类型" prop="type">
  11. <el-select v-model="queryParams.type" placeholder="请选择证书类型" style="width: 120px">
  12. <el-option label="个人证书" value="personal" />
  13. <el-option label="组织证书" value="organization" />
  14. <el-option label="其他" value="other" />
  15. </el-select>
  16. </el-form-item>
  17. <el-form-item label="证书类别" prop="classify">
  18. <el-select
  19. v-model="queryParams.classify"
  20. placeholder="证书类别"
  21. clearable
  22. class="!w-150px"
  23. >
  24. <el-option
  25. v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT).concat(
  26. getStrDictOptions(DICT_TYPE.ORG_CERT)
  27. )"
  28. :key="dict.value"
  29. :label="dict.label"
  30. :value="dict.value"
  31. />
  32. </el-select>
  33. </el-form-item>
  34. <el-form-item label="所属人" prop="userName">
  35. <el-input placeholder="输入所属人" v-model="queryParams.userName" />
  36. </el-form-item>
  37. <el-form-item label="是否过期" prop="expired">
  38. <el-select
  39. v-model="queryParams.expired"
  40. placeholder="请选择是否过期"
  41. clearable
  42. style="width: 150px"
  43. >
  44. <el-option
  45. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  46. :key="dict.value"
  47. :label="dict.label"
  48. :value="dict.value"
  49. />
  50. </el-select>
  51. </el-form-item>
  52. <el-form-item>
  53. <el-button @click="handleAdd" type="primary"
  54. ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
  55. >
  56. <el-button @click="handleQuery"
  57. ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
  58. >
  59. <el-button @click="resetQuery"
  60. ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
  61. >
  62. <el-button @click="handleExport" type="success" plain :loading="exportLoading"
  63. ><Icon icon="ep:download" class="mr-5px" /> 导出</el-button
  64. >
  65. </el-form-item>
  66. </el-form>
  67. </ContentWrap>
  68. <!-- 列表 -->
  69. <ContentWrap class="flex-1 overflow-hidden mt-15px">
  70. <el-table
  71. v-loading="loading"
  72. :data="list"
  73. height="calc(80vh - 203px)"
  74. :show-overflow-tooltip="true"
  75. :row-style="tableRowStyle"
  76. >
  77. <el-table-column :label="t('monitor.serial')" width="70" align="center">
  78. <template #default="scope">
  79. {{ scope.$index + 1 }}
  80. </template>
  81. </el-table-column>
  82. <el-table-column label="证书类型" align="center" prop="type">
  83. <template #default="scope">
  84. {{ getCertificateTypeText(scope.row.type) }}
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="证书类别" align="center" width="150" prop="classify">
  88. <template #default="scope">
  89. <dict-tag
  90. v-if="scope.row.type === 'organization'"
  91. :type="DICT_TYPE.ORG_CERT"
  92. :value="scope.row.classify"
  93. />
  94. <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
  95. </template>
  96. </el-table-column>
  97. <el-table-column
  98. label="证书名称"
  99. width="150"
  100. align="center"
  101. prop="certName"
  102. show-overflow-tooltip
  103. />
  104. <el-table-column label="所属人" align="center" prop="userName" />
  105. <el-table-column label="所在部门" align="center" prop="deptName" />
  106. <el-table-column label="颁发机构" align="center" prop="certOrg" width="120" />
  107. <el-table-column label="证书标准" align="center" prop="certStandard" width="120" />
  108. <el-table-column label="颁发时间" align="center" prop="certIssue">
  109. <template #default="scope">
  110. {{ formatDateCorrectly(scope.row.certIssue) }}
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="有效期" align="center">
  114. <template #default="scope">
  115. {{ formatDateCorrectly(scope.row.certExpire) }}
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="到期提醒" align="center">
  119. <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
  120. </el-table-column>
  121. <el-table-column label="备注" align="center" prop="remark" />
  122. <el-table-column
  123. :label="t('devicePerson.operation')"
  124. align="center"
  125. fixed="right"
  126. min-width="180px"
  127. >
  128. <template #default="scope">
  129. <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
  130. <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
  131. <el-button
  132. link
  133. type="success"
  134. v-if="scope.row.certPic"
  135. @click="handleViewImage(scope.row.certPic)"
  136. >
  137. 查看证书
  138. </el-button>
  139. </template>
  140. </el-table-column>
  141. </el-table>
  142. <!-- 分页 -->
  143. <Pagination
  144. :total="total"
  145. v-model:page="queryParams.pageNo"
  146. v-model:limit="queryParams.pageSize"
  147. @pagination="getList"
  148. />
  149. </ContentWrap>
  150. <ContentWrap style="margin-top: -5px">
  151. <el-alert title="证书已过期红色预警" type="error" show-icon :closable="false">
  152. <template #icon>
  153. <Bell />
  154. </template>
  155. </el-alert>
  156. </ContentWrap>
  157. </el-col>
  158. </el-row>
  159. <!-- 新增/编辑证书对话框 -->
  160. <el-dialog
  161. :title="dialogTitle"
  162. v-model="dialogVisible"
  163. width="600px"
  164. destroy-on-close
  165. @close="closeDialog"
  166. >
  167. <el-form
  168. ref="formRef"
  169. :model="formData"
  170. :rules="formRules"
  171. label-width="120px"
  172. v-loading="formLoading"
  173. >
  174. <el-form-item label="证书类型" prop="type">
  175. <el-select
  176. v-model="formData.type"
  177. placeholder="请选择证书类型"
  178. @change="formData.classify = ''"
  179. >
  180. <el-option label="个人证书" value="personal" />
  181. <el-option label="组织证书" value="organization" />
  182. <el-option label="其他" value="other" />
  183. </el-select>
  184. </el-form-item>
  185. <span class="absolute left-16 text-red" v-if="formData.type !== 'other'">*</span>
  186. <el-form-item label="证书类别" prop="classify" v-show="formData.type !== 'other'">
  187. <el-select
  188. v-if="formData.type === 'personal'"
  189. v-model="formData.classify"
  190. placeholder="证书类别"
  191. clearable
  192. >
  193. <el-option
  194. v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT)"
  195. :key="dict.value"
  196. :label="dict.label"
  197. :value="dict.value"
  198. />
  199. </el-select>
  200. <el-select v-else v-model="formData.classify" placeholder="证书类别" clearable>
  201. <el-option
  202. v-for="dict in getStrDictOptions(DICT_TYPE.ORG_CERT)"
  203. :key="dict.value"
  204. :label="dict.label"
  205. :value="dict.value"
  206. />
  207. </el-select>
  208. </el-form-item>
  209. <el-form-item label="证书名称" prop="certName">
  210. <el-input v-model="formData.certName" placeholder="请输入证书名称" />
  211. </el-form-item>
  212. <el-form-item label="所在部门" prop="deptId">
  213. <el-tree-select
  214. clearable
  215. v-model="formData.deptId"
  216. :data="deptList2"
  217. :props="defaultProps"
  218. check-strictly
  219. node-key="id"
  220. filterable
  221. placeholder="请选择所在部门"
  222. @change="handleDeptChange"
  223. />
  224. </el-form-item>
  225. <el-form-item label="所属人" prop="userName">
  226. <el-input v-model="formData.userName" placeholder="请输入所属人" />
  227. </el-form-item>
  228. <el-form-item label="颁发机构" prop="certOrg">
  229. <el-input v-model="formData.certOrg" placeholder="请输入颁发机构" />
  230. </el-form-item>
  231. <el-form-item label="证书标准" prop="certStandard">
  232. <el-input v-model="formData.certStandard" placeholder="如国标、API等" />
  233. </el-form-item>
  234. <el-form-item label="颁发时间" prop="certIssue">
  235. <el-date-picker
  236. v-model="formData.certIssue"
  237. type="date"
  238. value-format="x"
  239. placeholder="请选择颁发时间"
  240. style="width: 100%"
  241. />
  242. </el-form-item>
  243. <el-form-item label="有效期" prop="certExpire">
  244. <el-date-picker
  245. v-model="formData.certExpire"
  246. type="date"
  247. value-format="x"
  248. placeholder="请选择有效期"
  249. style="width: 100%"
  250. />
  251. </el-form-item>
  252. <el-form-item label="到期前提醒" prop="noticeBefore">
  253. <el-input-number
  254. v-model="formData.noticeBefore"
  255. :min="0"
  256. :max="365"
  257. placeholder="请输入提前多少天提醒"
  258. style="width: 100%"
  259. />
  260. </el-form-item>
  261. <el-form-item label="备注" prop="remark">
  262. <el-input
  263. type="textarea"
  264. v-model="formData.remark"
  265. :rows="2"
  266. placeholder="请输入备注"
  267. style="width: 100%"
  268. />
  269. </el-form-item>
  270. <el-form-item label="证书图片" prop="certPic">
  271. <UploadImage v-model="formData.certPic" />
  272. </el-form-item>
  273. </el-form>
  274. <template #footer>
  275. <el-button @click="closeDialog">取 消</el-button>
  276. <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
  277. </template>
  278. </el-dialog>
  279. <!-- 查看证书图片对话框 -->
  280. <el-dialog :title="imageDialogTitle" v-model="imageDialogVisible" width="800px" center>
  281. <!-- <img :src="imagePreviewUrl" alt="证书图片" style="max-width: 100%; max-height: 80vh" /> -->
  282. <div
  283. style="display: flex; justify-content: center; align-items: center; flex-direction: column"
  284. >
  285. <img
  286. v-for="url in imagePreviewUrl"
  287. :src="url"
  288. :key="url"
  289. alt="证书图片"
  290. style="max-width: 100%"
  291. />
  292. </div>
  293. </el-dialog>
  294. </template>
  295. <script setup lang="ts">
  296. import { IotMeasureCertApi } from '@/api/pms/qhse/index'
  297. import DeptTree from '@/views/system/user/DeptTree2.vue'
  298. import { handleTree } from '@/utils/tree'
  299. import * as DeptApi from '@/api/system/dept'
  300. import { ElMessageBox, ElMessage } from 'element-plus'
  301. const deptList = ref<Tree[]>([]) // 树形结构
  302. const deptList2 = ref<Tree[]>([]) // 树形结构
  303. import { formatDate } from '@/utils/formatTime'
  304. import UploadImage from '@/components/UploadFile/src/QHSEUploadImgs.vue'
  305. import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
  306. import { defaultProps } from '@/utils/tree'
  307. import { selectedDeptsEmployee } from '@/api/system/user'
  308. defineOptions({ name: 'IotQHSECertificate' })
  309. const loading = ref(true) // 列表的加载中
  310. const formLoading = ref(false) // 表单加载中
  311. const submitLoading = ref(false) // 提交按钮加载中
  312. const exportLoading = ref(false) // 导出按钮加载中
  313. const isLeftContentCollapsed = ref(false)
  314. const { t } = useI18n()
  315. const list = ref([]) // 列表的数据
  316. const total = ref(0) // 列表的总页数
  317. const queryParams = reactive({
  318. pageNo: 1,
  319. pageSize: 10,
  320. type: undefined,
  321. classify: undefined,
  322. deptId: '',
  323. expired: undefined
  324. })
  325. const queryFormRef = ref(null) // 搜索的表单
  326. // 对话框相关
  327. const dialogVisible = ref(false)
  328. const dialogTitle = ref('')
  329. const isEdit = ref(false)
  330. // 图片查看对话框
  331. const imageDialogVisible = ref(false)
  332. const imageDialogTitle = ref('证书图片')
  333. const imagePreviewUrl = ref('')
  334. // 表单相关
  335. const formRef = ref()
  336. const formData = ref({
  337. type: '', // 证书类型
  338. classify: '', // 证书类别
  339. userName: '',
  340. certName: '',
  341. certOrg: '', // 证书颁发机构
  342. certStandard: '', // 证书标准
  343. certIssue: '', // 证书颁发时间
  344. certExpire: '', // 证书有效期
  345. noticeBefore: '', // 到期前提醒
  346. certPic: '', // 证书图片上传
  347. remark: '', // 备注
  348. deptId: '' // 部门id
  349. })
  350. // 获取证书类型文本
  351. const getCertificateTypeText = (type: string) => {
  352. const map: Record<string, string> = {
  353. personal: '个人证书',
  354. organization: '组织证书',
  355. other: '其他'
  356. }
  357. return map[type] || type
  358. }
  359. // 正确格式化日期的函数
  360. const formatDateCorrectly = (timestamp) => {
  361. if (!timestamp) return ''
  362. // 如果是秒级时间戳,转换为毫秒级
  363. let time = Number(timestamp)
  364. if (time < 10000000000) {
  365. // 小于这个数通常表示秒级时间戳
  366. time = time * 1000
  367. }
  368. return formatDate(time).substring(0, 10)
  369. }
  370. // 表单验证规则
  371. const formRules = {
  372. type: [{ required: true, message: '证书类型不能为空', trigger: 'blur' }],
  373. classify: [
  374. {
  375. required: false, // 默认设为非必填
  376. validator: (rule, value, callback) => {
  377. // 只有当证书类型不是 "other" 时才验证
  378. if (formData.value.type !== 'other') {
  379. if (!value) {
  380. callback(new Error('证书类别不能为空'))
  381. } else {
  382. callback()
  383. }
  384. } else {
  385. callback() // other 类型时不需要验证
  386. }
  387. },
  388. trigger: ['blur', 'change']
  389. }
  390. ],
  391. deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }],
  392. certOrg: [{ required: true, message: '颁发机构不能为空', trigger: 'blur' }],
  393. certIssue: [{ required: true, message: '颁发时间不能为空', trigger: 'blur' }],
  394. certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }]
  395. }
  396. /** 查询列表 */
  397. const getList = async () => {
  398. loading.value = true
  399. try {
  400. const data = await IotMeasureCertApi.getIotMeasureCertPage(queryParams)
  401. list.value = data.list
  402. total.value = data.total
  403. } finally {
  404. loading.value = false
  405. }
  406. }
  407. const handleExport = async () => {
  408. try {
  409. exportLoading.value = true
  410. const response = await IotMeasureCertApi.exportIotMeasureCert(queryParams)
  411. downloadFile(response)
  412. exportLoading.value = false
  413. } catch (error) {
  414. ElMessage.error('导出失败,请重试')
  415. console.error('导出错误:', error)
  416. } finally {
  417. exportLoading.value = false
  418. }
  419. }
  420. /** 首页处理部门被点击 */
  421. const handleDeptNodeClick = async (row) => {
  422. queryParams.deptId = row.id
  423. await getList()
  424. }
  425. /** 搜索按钮操作 */
  426. const handleQuery = () => {
  427. queryParams.pageNo = 1
  428. getList()
  429. }
  430. /** 重置按钮操作 */
  431. const resetQuery = () => {
  432. queryParams.deptId = ''
  433. queryFormRef.value?.resetFields()
  434. handleQuery()
  435. }
  436. // 显示新增对话框
  437. const handleAdd = () => {
  438. isEdit.value = false
  439. dialogTitle.value = '新增证书'
  440. resetForm()
  441. dialogVisible.value = true
  442. }
  443. // 显示编辑对话框
  444. const handleEdit = (row) => {
  445. isEdit.value = true
  446. dialogTitle.value = '编辑证书'
  447. formData.value = {
  448. ...row,
  449. // 确保日期字段正确处理
  450. issueDate: row.issueDate ? ensureMillisecondTimestamp(row.issueDate) : null,
  451. validityPeriod: row.validityPeriod ? ensureMillisecondTimestamp(row.validityPeriod) : null,
  452. certPic: row.certPic.split(',')
  453. }
  454. dialogVisible.value = true
  455. }
  456. // 查看证书图片
  457. const handleViewImage = (imageUrl: any) => {
  458. imagePreviewUrl.value = imageUrl.split(',')
  459. imageDialogTitle.value = '证书图片'
  460. imageDialogVisible.value = true
  461. }
  462. // 确保时间戳是毫秒级的
  463. const ensureMillisecondTimestamp = (timestamp) => {
  464. let time = Number(timestamp)
  465. if (time < 10000000000) {
  466. // 秒级时间戳转为毫秒级
  467. return time * 1000
  468. }
  469. return time
  470. }
  471. //删除证书
  472. const handleDelete = async (id: number) => {
  473. ElMessageBox.confirm('确定要删除该证书吗?', '提示', {
  474. confirmButtonText: '确定',
  475. cancelButtonText: '取消',
  476. type: 'warning'
  477. })
  478. .then(async () => {
  479. try {
  480. await IotMeasureCertApi.deleteIotMeasureCert(id)
  481. ElMessage.success('删除成功')
  482. getList()
  483. } catch (error) {
  484. console.error(error)
  485. }
  486. })
  487. .catch(() => {
  488. // 取消操作
  489. })
  490. }
  491. // 重置表单
  492. const resetForm = () => {
  493. formData.value = {
  494. type: '', // 证书类型
  495. classify: '',
  496. certOrg: '', // 证书颁发机构
  497. certStandard: '', // 证书标准
  498. certIssue: '', // 证书颁发时间
  499. certExpire: '', // 证书有效期
  500. noticeBefore: '', // 到期前提醒
  501. certPic: '', // 证书图片上传
  502. remark: '', // 备注
  503. deptId: '', // 部门id
  504. userId: ''
  505. }
  506. formRef.value?.clearValidate()
  507. }
  508. // 关闭对话框
  509. const closeDialog = () => {
  510. dialogVisible.value = false
  511. resetForm()
  512. }
  513. const tableRowStyle = ({ row }) => {
  514. if (row.expired) {
  515. return { backgroundColor: '#ffe6e6' }
  516. }
  517. return {}
  518. }
  519. // 提交表单
  520. const submitForm = async () => {
  521. if (!formRef.value) return
  522. try {
  523. await formRef.value.validate()
  524. submitLoading.value = true
  525. // 准备提交数据
  526. const submitData = {
  527. ...formData.value,
  528. // 确保日期字段以正确的格式提交
  529. certIssue: formData.value.certIssue,
  530. certExpire: formData.value.certExpire,
  531. certPic: formData.value.certPic ? formData.value.certPic.join(',') : '' // 将图片数组转换为逗号分隔的字符串
  532. }
  533. if (isEdit.value) {
  534. // 编辑
  535. await IotMeasureCertApi.updateIotMeasureCert(submitData)
  536. ElMessage.success('编辑成功')
  537. } else {
  538. // 新增
  539. await IotMeasureCertApi.createIotMeasureCert(submitData)
  540. ElMessage.success('新增成功')
  541. }
  542. dialogVisible.value = false
  543. getList()
  544. } catch (error) {
  545. console.error(error)
  546. } finally {
  547. submitLoading.value = false
  548. }
  549. }
  550. // 下载文件函数
  551. const downloadFile = (response: any) => {
  552. const blob = new Blob([response], {
  553. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
  554. })
  555. let fileName = '证书台账.xlsx'
  556. const disposition = response.headers ? response.headers['content-disposition'] : ''
  557. if (disposition) {
  558. const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
  559. const matches = filenameRegex.exec(disposition)
  560. if (matches != null && matches[1]) {
  561. fileName = matches[1].replace(/['"]/g, '')
  562. }
  563. }
  564. const url = window.URL.createObjectURL(blob)
  565. const link = document.createElement('a')
  566. link.href = url
  567. link.setAttribute('download', fileName)
  568. document.body.appendChild(link)
  569. link.click()
  570. document.body.removeChild(link)
  571. window.URL.revokeObjectURL(url)
  572. }
  573. let userList = ref([])
  574. const handleDeptChange = async (value) => {
  575. const res = await selectedDeptsEmployee({
  576. deptIds: value
  577. })
  578. userList.value = res
  579. console.log('value>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', userList.value)
  580. }
  581. onMounted(async () => {
  582. getList()
  583. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  584. deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
  585. })
  586. </script>
  587. <style scoped>
  588. ::deep(.el-tree--highlight-current) {
  589. height: 200px !important;
  590. }
  591. ::deep(.el-transfer-panel__body) {
  592. height: 700px !important;
  593. }
  594. .image-preview {
  595. margin-top: 10px;
  596. display: flex;
  597. justify-content: center;
  598. }
  599. ::deep(.el-table__body tr.expired-row) {
  600. background-color: #ffe6e6 !important;
  601. }
  602. ::deep(.el-table__body tr.expired-row:hover td) {
  603. background-color: #ffcccc !important;
  604. }
  605. </style>