index.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. <template>
  2. <div
  3. class="qhse-page grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3 gap-x-4 h-[calc(100vh-10px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
  4. <DeptTreeSelect
  5. class="row-span-4"
  6. :top-id="rootDeptId"
  7. :deptId="deptId"
  8. v-model="queryParams.deptId"
  9. :init-select="false"
  10. :show-title="false"
  11. request-api="getSimpleDeptList"
  12. @node-click="handleDeptNodeClick" />
  13. <div class="min-w-0 space-y-3">
  14. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-3 min-w-0">
  15. <el-alert title="证书已过期红色预警" type="error" show-icon :closable="false">
  16. <template #icon>
  17. <Bell />
  18. </template>
  19. </el-alert>
  20. <el-alert
  21. title="证书60天橙色预警"
  22. type="warning"
  23. show-icon
  24. :closable="false"
  25. style="margin-top: 5px">
  26. <template #icon>
  27. <Bell />
  28. </template>
  29. </el-alert>
  30. </div>
  31. <div class="stats-cards">
  32. <div
  33. class="stats-card stats-card--expired stats-card--clickable"
  34. @click="handleStatCardClick('expired')">
  35. <div class="flex items-center gap-2">
  36. <Icon icon="ep:info-filled" color="#de3b3b" />
  37. <div class="stats-card__label">已过期</div>
  38. </div>
  39. <div class="stats-card__value">
  40. <CountTo
  41. :duration="2600"
  42. :end-val="expired"
  43. :start-val="0"
  44. class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
  45. </div>
  46. </div>
  47. <div
  48. class="stats-card stats-card--warn stats-card--clickable"
  49. @click="handleStatCardClick('warn')">
  50. <div class="flex items-center gap-2">
  51. <Icon icon="ep:bell-filled" color="#d97706" />
  52. <div class="stats-card__label">60天预警</div>
  53. </div>
  54. <div class="stats-card__value">
  55. <CountTo
  56. :duration="2600"
  57. :end-val="warn"
  58. :start-val="0"
  59. class="stats-card__value text-[40px]! pt-10 text-center! text-[#d97706]!" />
  60. </div>
  61. </div>
  62. <div
  63. class="stats-card stats-card--total stats-card--clickable"
  64. @click="handleStatCardClick('total')">
  65. <div class="flex items-center gap-2">
  66. <Icon icon="eos-icons:counting" color="#2563eb" />
  67. <div class="stats-card__label">证书总数</div>
  68. </div>
  69. <div class="stats-card__value">
  70. <CountTo
  71. :duration="2600"
  72. :end-val="totalCert"
  73. :start-val="0"
  74. class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. <!-- 搜索工作栏 -->
  80. <el-form
  81. class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-4 flex items-center flex-wrap min-w-0"
  82. :model="queryParams"
  83. ref="queryFormRef"
  84. :inline="true">
  85. <el-form-item label="证书类别" prop="classify">
  86. <el-select v-model="queryParams.classify" placeholder="证书类别" clearable class="!w-150px">
  87. <el-option
  88. v-for="dict in getStrDictOptions(DICT_TYPE.ORG_CERT)"
  89. :key="dict.value"
  90. :label="dict.label"
  91. :value="dict.value" />
  92. </el-select>
  93. </el-form-item>
  94. <el-form-item label="是否过期" prop="expired">
  95. <el-select
  96. v-model="queryParams.expired"
  97. placeholder="请选择是否过期"
  98. clearable
  99. style="width: 150px">
  100. <el-option
  101. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  102. :key="dict.value"
  103. :label="dict.label"
  104. :value="dict.value" />
  105. </el-select>
  106. </el-form-item>
  107. <el-form-item label="是否预警" prop="alertWarn">
  108. <el-select
  109. v-model="queryParams.alertWarn"
  110. placeholder="请选择是否预警"
  111. clearable
  112. style="width: 150px">
  113. <el-option
  114. v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  115. :key="dict.value"
  116. :label="dict.label"
  117. :value="dict.value" />
  118. </el-select>
  119. </el-form-item>
  120. <el-form-item>
  121. <el-button @click="handleAdd" type="primary"
  122. ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
  123. >
  124. <el-button @click="handleQuery"
  125. ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
  126. >
  127. <el-button @click="resetQuery"
  128. ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
  129. >
  130. <el-button @click="handleExport" type="success" plain :loading="exportLoading"
  131. ><Icon icon="ep:download" class="mr-5px" /> 导出</el-button
  132. >
  133. </el-form-item>
  134. </el-form>
  135. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-3 min-w-0">
  136. <div class="flex-1 relative min-h-0">
  137. <el-auto-resizer class="absolute">
  138. <template #default="{ width, height }">
  139. <zm-table
  140. :loading="loading"
  141. :data="list"
  142. :width="width"
  143. :height="height"
  144. :show-overflow-tooltip="true"
  145. :row-style="tableRowStyle"
  146. :row-class-name="tableRowClassName">
  147. <zm-table-column :label="t('monitor.serial')" width="70" align="center">
  148. <template #default="scope">
  149. {{ scope.$index + 1 }}
  150. </template>
  151. </zm-table-column>
  152. <!-- <zm-table-column label="证书类型" align="center" prop="type">
  153. <template #default="scope">
  154. {{ getCertificateTypeText(scope.row.type) }}
  155. </template>
  156. </zm-table-column> -->
  157. <zm-table-column label="证书类别" align="center" width="150" prop="classify">
  158. <template #default="scope">
  159. <dict-tag
  160. v-if="scope.row.type === 'organization'"
  161. :type="DICT_TYPE.ORG_CERT"
  162. :value="scope.row.classify" />
  163. <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
  164. </template>
  165. </zm-table-column>
  166. <zm-table-column
  167. label="证书名称"
  168. align="center"
  169. prop="certName"
  170. show-overflow-tooltip />
  171. <zm-table-column label="所在部门" align="center" prop="deptName" />
  172. <zm-table-column label="颁发机构" align="center" prop="certOrg" />
  173. <zm-table-column label="证书标准" align="center" prop="certStandard" />
  174. <zm-table-column label="颁发时间" align="center" prop="certIssue">
  175. <template #default="scope">
  176. {{ formatDateCorrectly(scope.row.certIssue) }}
  177. </template>
  178. </zm-table-column>
  179. <zm-table-column label="有效期" align="center">
  180. <template #default="scope">
  181. {{ formatDateCorrectly(scope.row.certExpire) }}
  182. </template>
  183. </zm-table-column>
  184. <zm-table-column label="到期提醒" align="center">
  185. <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
  186. </zm-table-column>
  187. <zm-table-column label="备注" align="center" prop="remark" />
  188. <zm-table-column
  189. :label="t('devicePerson.operation')"
  190. align="center"
  191. fixed="right"
  192. min-width="180px"
  193. action>
  194. <template #default="scope">
  195. <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
  196. <el-button link type="danger" @click="handleDelete(scope.row.id)">
  197. 删除
  198. </el-button>
  199. <el-button
  200. link
  201. type="success"
  202. v-if="scope.row.certPic"
  203. @click="viewFile(scope.row.certPic)">
  204. 查看证书
  205. </el-button>
  206. </template>
  207. </zm-table-column>
  208. </zm-table>
  209. </template>
  210. </el-auto-resizer>
  211. </div>
  212. <div class="h-8 mt-2 flex items-center justify-end">
  213. <Pagination
  214. :total="total"
  215. v-model:page="queryParams.pageNo"
  216. v-model:limit="queryParams.pageSize"
  217. @pagination="getList" />
  218. </div>
  219. </div>
  220. </div>
  221. <!-- 新增/编辑证书对话框 -->
  222. <Dialog
  223. :title="dialogTitle"
  224. v-model="dialogVisible"
  225. width="600px"
  226. destroy-on-close
  227. @close="closeDialog">
  228. <el-form
  229. ref="formRef"
  230. :model="formData"
  231. :rules="formRules"
  232. label-width="auto"
  233. v-loading="formLoading">
  234. <el-form-item label="证书类别" prop="classify">
  235. <el-select v-model="formData.classify" placeholder="证书类别" clearable>
  236. <el-option
  237. v-for="dict in getStrDictOptions(DICT_TYPE.ORG_CERT)"
  238. :key="dict.value"
  239. :label="dict.label"
  240. :value="dict.value" />
  241. </el-select>
  242. </el-form-item>
  243. <el-form-item label="证书名称" prop="certName">
  244. <el-input v-model="formData.certName" placeholder="请输入证书名称" />
  245. </el-form-item>
  246. <el-form-item label="所在部门" prop="deptId">
  247. <el-tree-select
  248. clearable
  249. v-model="formData.deptId"
  250. :data="deptList2"
  251. :props="defaultProps"
  252. check-strictly
  253. node-key="id"
  254. filterable
  255. placeholder="请选择所在部门"
  256. @change="handleDeptChange" />
  257. </el-form-item>
  258. <el-form-item label="颁发机构" prop="certOrg">
  259. <el-input v-model="formData.certOrg" placeholder="请输入颁发机构" />
  260. </el-form-item>
  261. <el-form-item label="证书标准" prop="certStandard">
  262. <el-input v-model="formData.certStandard" placeholder="如国标、API等" />
  263. </el-form-item>
  264. <el-form-item label="颁发时间" prop="certIssue">
  265. <el-date-picker
  266. v-model="formData.certIssue"
  267. type="date"
  268. value-format="x"
  269. placeholder="请选择颁发时间"
  270. style="width: 100%" />
  271. </el-form-item>
  272. <el-form-item label="有效期" prop="certExpire">
  273. <el-date-picker
  274. v-model="formData.certExpire"
  275. type="date"
  276. value-format="x"
  277. placeholder="请选择有效期"
  278. style="width: 100%" />
  279. </el-form-item>
  280. <el-form-item label="到期前提醒" prop="noticeBefore">
  281. <el-input-number
  282. v-model="formData.noticeBefore"
  283. :min="0"
  284. :max="365"
  285. placeholder="请输入提前多少天提醒"
  286. style="width: 100%" />
  287. </el-form-item>
  288. <el-form-item label="备注" prop="remark">
  289. <el-input
  290. type="textarea"
  291. v-model="formData.remark"
  292. :rows="2"
  293. placeholder="请输入备注"
  294. style="width: 100%" />
  295. </el-form-item>
  296. <el-form-item label="证书附件" prop="certPic">
  297. <!-- <UploadImage v-model="formData.certPic" /> -->
  298. <UploadFile
  299. v-model="formData.certPic"
  300. :file-type="['pdf', 'jpg', 'png', 'jpeg', 'bmp']"
  301. :file-size="100"
  302. class="min-w-80px" />
  303. </el-form-item>
  304. </el-form>
  305. <template #footer>
  306. <el-button @click="closeDialog">取 消</el-button>
  307. <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
  308. </template>
  309. </Dialog>
  310. <Dialog v-model="dialogFileView" title="证书附件">
  311. <div
  312. v-for="(file, index) in fileList"
  313. :key="index"
  314. class="flex items-center justify-between mt-5">
  315. <span class="file-name-text text-ellipsis!">{{ extractFileName(file) }}</span>
  316. <div>
  317. <el-button link type="primary" @click="viewFileInfo(file)">
  318. <Icon icon="ep:view" class="mr-2px" />查看</el-button
  319. >
  320. <el-button link type="primary" @click="handleDownload(file)">
  321. <Icon icon="ep:download" class="mr-2px" />下载</el-button
  322. >
  323. </div>
  324. </div>
  325. <template #footer>
  326. <div class="dialog-footer mt-10">
  327. <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
  328. </div>
  329. </template>
  330. </Dialog>
  331. </template>
  332. <script setup lang="ts">
  333. import { IotCertificateApi } from '@/api/pms/qhse/index'
  334. import { handleTree } from '@/utils/tree'
  335. import * as DeptApi from '@/api/system/dept'
  336. import { ElMessageBox, ElMessage } from 'element-plus'
  337. const deptList = ref<Tree[]>([]) // 树形结构
  338. const deptList2 = ref<Tree[]>([]) // 树形结构
  339. import { formatDate } from '@/utils/formatTime'
  340. import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
  341. import { defaultProps } from '@/utils/tree'
  342. import { selectedDeptsEmployee } from '@/api/system/user'
  343. import { useUserStore } from '@/store/modules/user'
  344. const userStore = useUserStore()
  345. defineOptions({ name: 'IotQHSECredential' })
  346. const rootDeptId = 156
  347. const deptId = useUserStore().getUser.deptId || rootDeptId
  348. const loading = ref(true) // 列表的加载中
  349. const formLoading = ref(false) // 表单加载中
  350. const submitLoading = ref(false) // 提交按钮加载中
  351. const exportLoading = ref(false) // 导出按钮加载中
  352. const { t } = useI18n()
  353. const list = ref([]) // 列表的数据
  354. const total = ref(0) // 列表的总页数
  355. const queryParams = reactive({
  356. pageNo: 1,
  357. pageSize: 10,
  358. classify: undefined,
  359. deptId: '',
  360. expired: undefined,
  361. alertWarn: undefined
  362. })
  363. const queryFormRef = ref(null) // 搜索的表单
  364. // 对话框相关
  365. const dialogVisible = ref(false)
  366. const dialogTitle = ref('')
  367. const isEdit = ref(false)
  368. // 图片查看对话框
  369. const dialogFileView = ref(false)
  370. // 表单相关
  371. const formRef = ref()
  372. const formData = ref({
  373. classify: '', // 证书类别
  374. userName: '',
  375. certName: '',
  376. certOrg: '', // 证书颁发机构
  377. certStandard: '', // 证书标准
  378. certIssue: '', // 证书颁发时间
  379. certExpire: '', // 证书有效期
  380. noticeBefore: '', // 到期前提醒
  381. certPic: '', // 证书图片上传
  382. remark: '', // 备注
  383. deptId: '' // 部门id
  384. })
  385. // 正确格式化日期的函数
  386. const formatDateCorrectly = (timestamp) => {
  387. if (!timestamp) return ''
  388. // 如果是秒级时间戳,转换为毫秒级
  389. let time = Number(timestamp)
  390. if (time < 10000000000) {
  391. // 小于这个数通常表示秒级时间戳
  392. time = time * 1000
  393. }
  394. return formatDate(time).substring(0, 10)
  395. }
  396. // 表单验证规则
  397. const formRules = {
  398. classify: [{ required: true, message: '证书类别不能为空', trigger: 'blur' }],
  399. deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }],
  400. certOrg: [{ required: true, message: '颁发机构不能为空', trigger: 'blur' }],
  401. certIssue: [{ required: true, message: '颁发时间不能为空', trigger: 'blur' }],
  402. certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
  403. certPic: [{ required: true, message: '证书图片不能为空', trigger: 'blur' }]
  404. }
  405. /** 查询列表 */
  406. const getList = async () => {
  407. loading.value = true
  408. try {
  409. const data = await IotCertificateApi.getCertificateList(queryParams)
  410. list.value = data.list
  411. total.value = data.total
  412. } finally {
  413. loading.value = false
  414. }
  415. }
  416. const handleExport = async () => {
  417. try {
  418. exportLoading.value = true
  419. const response = await IotCertificateApi.exportCertificate(queryParams)
  420. downloadFile(response)
  421. exportLoading.value = false
  422. } catch (error) {
  423. ElMessage.error('导出失败,请重试')
  424. console.error('导出错误:', error)
  425. } finally {
  426. exportLoading.value = false
  427. }
  428. }
  429. /** 首页处理部门被点击 */
  430. const handleDeptNodeClick = async (row) => {
  431. queryParams.deptId = row.id
  432. await getList()
  433. await getStatic()
  434. }
  435. /** 搜索按钮操作 */
  436. const handleQuery = () => {
  437. queryParams.pageNo = 1
  438. getList()
  439. }
  440. const handleStatCardClick = (type: 'expired' | 'warn' | 'total' | 'personal') => {
  441. queryParams.pageNo = 1
  442. if (type === 'expired') {
  443. queryParams.expired = true
  444. queryParams.alertWarn = undefined
  445. queryParams.type = undefined
  446. getList()
  447. return
  448. }
  449. if (type === 'warn') {
  450. queryParams.alertWarn = true
  451. queryParams.expired = undefined
  452. queryParams.type = undefined
  453. getList()
  454. return
  455. }
  456. if (type === 'personal') {
  457. queryParams.type = 'personal'
  458. queryParams.expired = undefined
  459. queryParams.alertWarn = undefined
  460. getList()
  461. return
  462. }
  463. queryParams.type = undefined
  464. queryParams.expired = undefined
  465. queryParams.alertWarn = undefined
  466. getList()
  467. }
  468. /** 重置按钮操作 */
  469. const resetQuery = () => {
  470. queryParams.deptId = ''
  471. queryFormRef.value?.resetFields()
  472. handleQuery()
  473. }
  474. // 显示新增对话框
  475. const handleAdd = () => {
  476. isEdit.value = false
  477. dialogTitle.value = '新增证书'
  478. resetForm()
  479. dialogVisible.value = true
  480. }
  481. // 显示编辑对话框
  482. const handleEdit = (row) => {
  483. isEdit.value = true
  484. dialogTitle.value = '编辑证书'
  485. formData.value = {
  486. ...row,
  487. // 确保日期字段正确处理
  488. issueDate: row.issueDate ? ensureMillisecondTimestamp(row.issueDate) : null,
  489. validityPeriod: row.validityPeriod ? ensureMillisecondTimestamp(row.validityPeriod) : null,
  490. certPic: row.certPic.split(',')
  491. }
  492. dialogVisible.value = true
  493. }
  494. let fileList = ref([])
  495. const viewFile = (file) => {
  496. fileList.value = file.split(',')
  497. dialogFileView.value = true
  498. }
  499. const viewFileInfo = (file) => {
  500. window.open(
  501. 'http://doc.deepoil.cc:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(file))
  502. )
  503. }
  504. const extractFileName = (url: string): string => {
  505. try {
  506. // 移除查询参数和哈希
  507. const cleanUrl = url.split('?')[0].split('#')[0]
  508. // 获取最后一个斜杠后的内容
  509. const parts = cleanUrl.split('/')
  510. const fileName = parts[parts.length - 1]
  511. // URL 解码
  512. return decodeURIComponent(fileName) || url
  513. } catch {
  514. // 如果解析失败,返回原始 URL
  515. return url
  516. }
  517. }
  518. // 确保时间戳是毫秒级的
  519. const ensureMillisecondTimestamp = (timestamp) => {
  520. let time = Number(timestamp)
  521. if (time < 10000000000) {
  522. // 秒级时间戳转为毫秒级
  523. return time * 1000
  524. }
  525. return time
  526. }
  527. //删除证书
  528. const handleDelete = async (id: number) => {
  529. ElMessageBox.confirm('确定要删除该证书吗?', '提示', {
  530. confirmButtonText: '确定',
  531. cancelButtonText: '取消',
  532. type: 'warning'
  533. })
  534. .then(async () => {
  535. try {
  536. await IotCertificateApi.deleteCertificate(id)
  537. ElMessage.success('删除成功')
  538. getList()
  539. } catch (error) {
  540. console.error(error)
  541. }
  542. })
  543. .catch(() => {
  544. // 取消操作
  545. })
  546. }
  547. // 重置表单
  548. const resetForm = () => {
  549. formData.value = {
  550. type: '', // 证书类型
  551. classify: '',
  552. certOrg: '', // 证书颁发机构
  553. certStandard: '', // 证书标准
  554. certIssue: '', // 证书颁发时间
  555. certExpire: '', // 证书有效期
  556. noticeBefore: '', // 到期前提醒
  557. certPic: '', // 证书图片上传
  558. remark: '', // 备注
  559. deptId: '', // 部门id
  560. userId: ''
  561. }
  562. formRef.value?.clearValidate()
  563. }
  564. // 关闭对话框
  565. const closeDialog = () => {
  566. dialogVisible.value = false
  567. resetForm()
  568. }
  569. const tableRowStyle = ({ row }) => {
  570. if (row.expired) {
  571. return { backgroundColor: '#ffe6e6' }
  572. }
  573. if (row.alertWarn) {
  574. return { backgroundColor: '#e19f1a' }
  575. }
  576. return {}
  577. }
  578. const tableRowClassName = ({ row }) => {
  579. if (row.expired) {
  580. return 'expired-row'
  581. }
  582. if (row.alertWarn) {
  583. return 'alert-warn-row'
  584. }
  585. return ''
  586. }
  587. // 提交表单
  588. const submitForm = async () => {
  589. if (!formRef.value) return
  590. try {
  591. await formRef.value.validate()
  592. submitLoading.value = true
  593. console.log('提交数据:', formData.value.certPic)
  594. let certPic: any = null
  595. if (isEdit.value) {
  596. certPic = formData.value.certPic ? formData.value.certPic.join(',') : ''
  597. } else {
  598. certPic = formData.value.certPic
  599. }
  600. // 准备提交数据
  601. const submitData = {
  602. ...formData.value,
  603. // 确保日期字段以正确的格式提交
  604. certIssue: formData.value.certIssue,
  605. certExpire: formData.value.certExpire,
  606. certPic // 将图片数组转换为逗号分隔的字符串
  607. }
  608. if (isEdit.value) {
  609. // 编辑
  610. await IotCertificateApi.updateCertificate(submitData)
  611. ElMessage.success('编辑成功')
  612. } else {
  613. // 新增
  614. await IotCertificateApi.createCertificate(submitData)
  615. ElMessage.success('新增成功')
  616. }
  617. dialogVisible.value = false
  618. getList()
  619. } catch (error) {
  620. console.error(error)
  621. } finally {
  622. submitLoading.value = false
  623. }
  624. }
  625. // 下载文件函数
  626. const downloadFile = (response: any) => {
  627. const blob = new Blob([response], {
  628. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
  629. })
  630. let fileName = '证书台账.xlsx'
  631. const disposition = response.headers ? response.headers['content-disposition'] : ''
  632. if (disposition) {
  633. const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
  634. const matches = filenameRegex.exec(disposition)
  635. if (matches != null && matches[1]) {
  636. fileName = matches[1].replace(/['"]/g, '')
  637. }
  638. }
  639. const url = window.URL.createObjectURL(blob)
  640. const link = document.createElement('a')
  641. link.href = url
  642. link.setAttribute('download', fileName)
  643. document.body.appendChild(link)
  644. link.click()
  645. document.body.removeChild(link)
  646. window.URL.revokeObjectURL(url)
  647. }
  648. const handleDownload = async (url) => {
  649. try {
  650. const response = await fetch(url)
  651. const blob = await response.blob()
  652. const downloadUrl = window.URL.createObjectURL(blob)
  653. const link = document.createElement('a')
  654. link.href = downloadUrl
  655. link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
  656. link.click()
  657. URL.revokeObjectURL(downloadUrl)
  658. } catch (error) {
  659. console.error('下载失败:', error)
  660. }
  661. }
  662. let userList = ref([])
  663. const handleDeptChange = async (value) => {
  664. const res = await selectedDeptsEmployee({
  665. deptIds: value
  666. })
  667. userList.value = res
  668. console.log('value>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', userList.value)
  669. }
  670. let totalCert = ref(0)
  671. let expired = ref(0)
  672. let warn = ref(0)
  673. let personal = ref(0)
  674. let organization = ref(0)
  675. async function getStatic() {
  676. if (queryParams.deptId) {
  677. const res = await IotCertificateApi.getCertificateStatistics(queryParams.deptId)
  678. totalCert.value = res.total
  679. expired.value = res.expired
  680. warn.value = res.warn
  681. personal.value = res.personal
  682. organization.value = res.organization
  683. } else {
  684. const res = await IotCertificateApi.getCertificateStatistics(userStore.user.deptId)
  685. totalCert.value = res.total
  686. expired.value = res.expired
  687. warn.value = res.warn
  688. personal.value = res.personal
  689. organization.value = res.organization
  690. }
  691. }
  692. onMounted(async () => {
  693. getList()
  694. getStatic()
  695. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  696. deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
  697. })
  698. </script>
  699. <style scoped>
  700. ::deep(.el-tree--highlight-current) {
  701. height: 200px !important;
  702. }
  703. ::deep(.el-transfer-panel__body) {
  704. height: 700px !important;
  705. }
  706. .stats-cards {
  707. display: grid;
  708. grid-template-columns: repeat(3, minmax(0, 1fr));
  709. gap: 12px;
  710. margin-bottom: 5px;
  711. }
  712. .stats-card {
  713. padding: 14px 16px;
  714. background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
  715. border: 1px solid #e4ecf7;
  716. border-radius: 10px;
  717. box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
  718. }
  719. .stats-card--clickable {
  720. cursor: pointer;
  721. transition:
  722. transform 0.18s ease,
  723. box-shadow 0.18s ease,
  724. border-color 0.18s ease;
  725. }
  726. .stats-card--clickable:hover {
  727. transform: translateY(-2px);
  728. box-shadow: 0 10px 24px rgb(31 91 184 / 14%);
  729. }
  730. .stats-card__label {
  731. font-size: 14px;
  732. font-weight: 600;
  733. color: #6b7280;
  734. line-height: 1.4;
  735. }
  736. .stats-card__value {
  737. margin-top: 8px;
  738. font-size: 28px;
  739. font-weight: 700;
  740. line-height: 1;
  741. color: #1f5bb8;
  742. }
  743. .stats-card--expired {
  744. background: linear-gradient(180deg, #fff4f4 0%, #ffe8e8 100%);
  745. border-color: #ffcfcf;
  746. }
  747. .stats-card--expired .stats-card__value {
  748. color: #de3b3b;
  749. }
  750. .stats-card--warn {
  751. background: linear-gradient(180deg, #fff8ef 0%, #ffeed9 100%);
  752. border-color: #ffd7a1;
  753. }
  754. .stats-card--warn .stats-card__value {
  755. color: #d97706;
  756. }
  757. .stats-card--total .stats-card__value {
  758. color: #2563eb;
  759. }
  760. .stats-card--personal .stats-card__value {
  761. color: #16a34a;
  762. }
  763. .stats-card--organization .stats-card__value {
  764. color: #7c3aed;
  765. }
  766. /* 过期行的红色背景 - 基础状态 */
  767. :deep(.el-table__body tr.expired-row > td.el-table__cell) {
  768. background-color: #ffe6e6 !important;
  769. }
  770. /* 过期行 - 鼠标悬浮状态 */
  771. :deep(.el-table__body tr.expired-row:hover > td.el-table__cell) {
  772. background-color: #ffcccc !important;
  773. }
  774. /* 确保斑马纹不影响过期行 */
  775. :deep(.el-table__body tr.expired-row.el-table__row--striped > td.el-table__cell) {
  776. background-color: #ffe6e6 !important;
  777. }
  778. :deep(.el-table__body tr.expired-row.el-table__row--striped:hover > td.el-table__cell) {
  779. background-color: #ffcccc !important;
  780. }
  781. /* 预警行的橙色背景 - 基础状态 */
  782. :deep(.el-table__body tr.alert-warn-row > td.el-table__cell) {
  783. background-color: #fff1df !important;
  784. }
  785. /* 预警行 - 鼠标悬浮状态 */
  786. :deep(.el-table__body tr.alert-warn-row:hover > td.el-table__cell) {
  787. background-color: #ffe2bf !important;
  788. }
  789. /* 确保斑马纹不影响预警行 */
  790. :deep(.el-table__body tr.alert-warn-row.el-table__row--striped > td.el-table__cell) {
  791. background-color: #fff1df !important;
  792. }
  793. :deep(.el-table__body tr.alert-warn-row.el-table__row--striped:hover > td.el-table__cell) {
  794. background-color: #ffe2bf !important;
  795. }
  796. </style>