index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <script setup lang="ts">
  2. import { onMounted, reactive, ref } from 'vue'
  3. import { clientDownloadExcel, clientGet, clientPost } from '@/utils/request.ts'
  4. import type { FormInstance, UploadProps } from 'element-plus'
  5. import { ElForm, ElLoading, ElMessage, ElMessageBox } from 'element-plus'
  6. import { Delete, Download, Edit, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
  7. // 假设 BaseResponse 和 PageType 在全局或 request.ts 中已定义
  8. interface BaseResponse {
  9. code: number
  10. msg: string
  11. }
  12. interface PageType<T> {
  13. records: T[]
  14. total: number
  15. size: number
  16. current: number
  17. pages: number
  18. }
  19. interface Property {
  20. id: string // 主键
  21. entName?: string // 企业(机构)名称
  22. uniCode?: string // 统一社会信用代码
  23. certificateIssuanceDate?: string // 发证日期
  24. certificateType?: string // 证书类型
  25. productName?: string // 产品名称及单元(主)
  26. certificateNo?: string // 证书编号
  27. deadlineDate?: string // 截止日期
  28. createTime?: string // 记录创建时间
  29. updateTime?: string // 记录最后更新时间
  30. updateBy?: string // 修改人
  31. createBy?: string // 创建人
  32. }
  33. interface PropertyOneResponse extends BaseResponse {
  34. data: Property
  35. }
  36. interface PropertyListResponse extends BaseResponse {
  37. data: PageType<Property>
  38. }
  39. interface AddProperty {
  40. entName?: string // 企业(机构)名称
  41. uniCode?: string // 统一社会信用代码
  42. certificateIssuanceDate?: string // 发证日期
  43. certificateType?: string // 证书类型
  44. productName?: string // 产品名称及单元(主)
  45. certificateNo?: string // 证书编号
  46. deadlineDate?: string // 截止日期
  47. }
  48. interface UpdateProperty {
  49. id?: string
  50. entName?: string // 企业(机构)名称
  51. uniCode?: string // 统一社会信用代码
  52. certificateIssuanceDate?: string // 发证日期
  53. certificateType?: string // 证书类型
  54. productName?: string // 产品名称及单元(主)
  55. certificateNo?: string // 证书编号
  56. deadlineDate?: string // 截止日期
  57. }
  58. // 响应式状态变量
  59. const tableData = ref<Property[]>([])
  60. const total = ref(0)
  61. const pageSize = ref(10)
  62. const pageNum = ref(1)
  63. const searchForm = reactive({
  64. entName: '',
  65. certificateType: '',
  66. certificateNo: '',
  67. })
  68. const dialogVisible = ref(false)
  69. const dialogTitle = ref('')
  70. const isEdit = ref(false)
  71. const formData = reactive<Property>({})
  72. const formRef = ref<FormInstance>()
  73. const selectedIds = ref<string[]>([])
  74. // 证书类型选项
  75. const certificateTypeOptions = [
  76. { label: '营业执照', value: '营业执照' },
  77. { label: '资质证书', value: '资质证书' },
  78. { label: '经营许可证', value: '经营许可证' },
  79. { label: '其他', value: '其他' },
  80. ]
  81. // 新增
  82. const add = async (data: AddProperty) => {
  83. const res = await clientPost<AddProperty, BaseResponse>('/equalificationCertificate/save', {
  84. ...data,
  85. })
  86. if (res.code !== 200) {
  87. ElMessage.error(res.msg)
  88. return false
  89. }
  90. ElMessage.success(res.msg)
  91. return true
  92. }
  93. // 根据id获取数据
  94. const getById = async (id: string) => {
  95. const res = await clientGet<null, PropertyOneResponse>('/equalificationCertificate/getById/' + id)
  96. if (res.code !== 200) {
  97. ElMessage.error(res.msg)
  98. return null
  99. }
  100. return res.data
  101. }
  102. // 分页获取数据
  103. const getList = async () => {
  104. // 构建参数对象,只包含有值的搜索条件
  105. const params = {
  106. pageNum: pageNum.value,
  107. pageSize: pageSize.value,
  108. ...(searchForm.entName ? { entName: searchForm.entName } : {}),
  109. ...(searchForm.certificateType ? { certificateType: searchForm.certificateType } : {}),
  110. ...(searchForm.certificateNo ? { certificateNo: searchForm.certificateNo } : {}),
  111. }
  112. const res = await clientGet<
  113. {
  114. pageNum: number
  115. pageSize: number
  116. entName?: string
  117. certificateType?: string
  118. certificateNo?: string
  119. },
  120. PropertyListResponse
  121. >('/equalificationCertificate/findByPage', {
  122. params,
  123. })
  124. if (res.code !== 200) {
  125. ElMessage.error(res.msg)
  126. return
  127. }
  128. tableData.value = res.data.records
  129. total.value = res.data.total
  130. }
  131. // 批量删除
  132. const delBatch = async (ids: string[]) => {
  133. const res = await clientPost<string[], BaseResponse>(
  134. '/equalificationCertificate/deleteBatch',
  135. ids,
  136. )
  137. if (res.code !== 200) {
  138. ElMessage.error(res.msg)
  139. return false
  140. }
  141. ElMessage.success(res.msg)
  142. return true
  143. }
  144. // 修改
  145. const update = async (data: UpdateProperty) => {
  146. const res = await clientPost<UpdateProperty, BaseResponse>('/equalificationCertificate/update', {
  147. ...data,
  148. })
  149. if (res.code !== 200) {
  150. ElMessage.error(res.msg)
  151. return false
  152. }
  153. ElMessage.success(res.msg)
  154. return true
  155. }
  156. // 导出为excel
  157. const exportExcel = async () => {
  158. const loading = ElLoading.service({
  159. lock: true,
  160. text: '导出中,请稍候...',
  161. background: 'rgba(0, 0, 0, 0.1)',
  162. fullscreen: true,
  163. })
  164. try {
  165. // 构建参数对象,只包含有值的搜索条件
  166. const params = {
  167. ...(searchForm.entName ? { entName: searchForm.entName } : {}),
  168. ...(searchForm.certificateType ? { certificateType: searchForm.certificateType } : {}),
  169. ...(searchForm.certificateNo ? { certificateNo: searchForm.certificateNo } : {}),
  170. }
  171. await clientDownloadExcel('/equalificationCertificate/exportData', {
  172. params,
  173. })
  174. ElMessage.success('导出成功')
  175. } catch (error) {
  176. console.error('导出失败:', error)
  177. ElMessage.error('导出失败')
  178. } finally {
  179. loading.close()
  180. }
  181. }
  182. // excel导入
  183. const importExcel = async (file: File) => {
  184. const formData = new FormData()
  185. formData.append('file', file)
  186. const res = await clientPost<FormData, BaseResponse>(
  187. '/equalificationCertificate/importData',
  188. formData,
  189. {
  190. headers: {
  191. 'Content-Type': 'multipart/form-data',
  192. },
  193. },
  194. )
  195. if (res.code !== 200) {
  196. ElMessage.error(res.msg)
  197. return false
  198. }
  199. ElMessage.success(res.msg)
  200. return true
  201. }
  202. // --- CRUD 操作和处理函数 ---
  203. const handleAdd = () => {
  204. isEdit.value = false
  205. dialogTitle.value = '新增资质证书'
  206. // 重置表单数据
  207. Object.keys(formData).forEach((key) => delete formData[key as keyof Property])
  208. dialogVisible.value = true
  209. }
  210. const handleEdit = async (row: Property) => {
  211. isEdit.value = true
  212. dialogTitle.value = '编辑资质证书'
  213. const data = await getById(row.id)
  214. if (data) {
  215. // 直接将后端返回的字符串赋值给 formData
  216. // el-date-picker 配合 value-format 可以直接处理字符串
  217. Object.assign(formData, data)
  218. dialogVisible.value = true
  219. }
  220. }
  221. const handleDelete = async (id: string) => {
  222. ElMessageBox.confirm('确定删除此条记录吗?', '提示', {
  223. confirmButtonText: '确定',
  224. cancelButtonText: '取消',
  225. type: 'warning',
  226. })
  227. .then(async () => {
  228. const success = await delBatch([id])
  229. if (success) {
  230. getList()
  231. }
  232. })
  233. .catch(() => {
  234. ElMessage.info('已取消删除')
  235. })
  236. }
  237. const handleBatchDelete = () => {
  238. if (selectedIds.value.length === 0) {
  239. ElMessage.warning('请选择要删除的记录')
  240. return
  241. }
  242. ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.length} 条记录吗?`, '提示', {
  243. confirmButtonText: '确定',
  244. cancelButtonText: '取消',
  245. type: 'warning',
  246. })
  247. .then(async () => {
  248. const success = await delBatch(selectedIds.value)
  249. if (success) {
  250. getList()
  251. }
  252. })
  253. .catch(() => {
  254. ElMessage.info('已取消批量删除')
  255. })
  256. }
  257. const handleSearch = () => {
  258. pageNum.value = 1 // 搜索时重置到第一页
  259. getList()
  260. }
  261. const handleResetSearch = () => {
  262. searchForm.entName = ''
  263. searchForm.certificateType = ''
  264. searchForm.certificateNo = ''
  265. pageNum.value = 1
  266. getList()
  267. }
  268. const handleConfirm = async () => {
  269. if (!formRef.value) return
  270. await formRef.value.validate(async (valid) => {
  271. if (valid) {
  272. let success = false
  273. const dataToSend = { ...formData }
  274. if (isEdit.value) {
  275. success = await update(dataToSend)
  276. } else {
  277. success = await add(dataToSend)
  278. }
  279. if (success) {
  280. dialogVisible.value = false
  281. getList()
  282. }
  283. } else {
  284. ElMessage.error('请检查表单填写')
  285. }
  286. })
  287. }
  288. const handleCurrentChange = (val: number) => {
  289. pageNum.value = val
  290. getList()
  291. }
  292. const handleSizeChange = (val: number) => {
  293. pageSize.value = val
  294. pageNum.value = 1 // 页大小改变时重置到第一页
  295. getList()
  296. }
  297. const handleSelectionChange = (selection: Property[]) => {
  298. selectedIds.value = selection.map((item) => item.id)
  299. }
  300. const handleUploadSuccess: UploadProps['onSuccess'] = async (response, uploadFile) => {
  301. if (response && response.code === 200) {
  302. ElMessage.success('文件导入成功')
  303. getList()
  304. } else {
  305. ElMessage.error(`文件导入失败: ${response?.msg || '未知错误'}`)
  306. }
  307. }
  308. const handleUploadError: UploadProps['onError'] = (error) => {
  309. ElMessage.error(`文件导入失败: ${error.message}`)
  310. }
  311. const handleBeforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
  312. const isExcel =
  313. rawFile.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
  314. rawFile.type === 'application/vnd.ms-excel'
  315. if (!isExcel) {
  316. ElMessage.error('导入文件只能是 Excel 格式!')
  317. return false
  318. }
  319. return true
  320. }
  321. const init = () => {
  322. getList()
  323. }
  324. onMounted(() => {
  325. init()
  326. })
  327. </script>
  328. <template>
  329. <div class="p-4">
  330. <h1 class="text-2xl font-bold mb-6">资质证书管理信息</h1>
  331. <!-- Search and Action Bar -->
  332. <div class="mb-6 p-4 bg-white rounded-lg shadow-sm flex flex-wrap items-center gap-4">
  333. <el-form :inline="true" :model="searchForm" class="flex-grow flex flex-wrap gap-x-4">
  334. <el-form-item label="企业名称">
  335. <el-input v-model="searchForm.entName" placeholder="输入企业名称" clearable />
  336. </el-form-item>
  337. <el-form-item label="证书类型">
  338. <el-select
  339. v-model="searchForm.certificateType"
  340. placeholder="请选择证书类型"
  341. style="width: 150px"
  342. >
  343. <el-option
  344. v-for="item in certificateTypeOptions"
  345. :key="item.value"
  346. :label="item.label"
  347. :value="item.value"
  348. />
  349. </el-select>
  350. </el-form-item>
  351. <el-form-item label="证书编号">
  352. <el-input v-model="searchForm.certificateNo" placeholder="请输入证书编号" clearable />
  353. </el-form-item>
  354. <el-form-item>
  355. <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
  356. <el-button :icon="Refresh" @click="handleResetSearch">重置</el-button>
  357. </el-form-item>
  358. </el-form>
  359. <div class="flex gap-2">
  360. <el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
  361. <el-button
  362. type="danger"
  363. :icon="Delete"
  364. @click="handleBatchDelete"
  365. :disabled="selectedIds.length === 0"
  366. >批量删除</el-button
  367. >
  368. <el-button type="success" :icon="Download" @click="exportExcel">导出</el-button>
  369. <el-upload
  370. class="inline-block ml-2"
  371. action="/api/equalificationCertificate/importData"
  372. :show-file-list="false"
  373. :on-success="handleUploadSuccess"
  374. :on-error="handleUploadError"
  375. :before-upload="handleBeforeUpload"
  376. :http-request="(options) => importExcel(options.file)"
  377. >
  378. <el-button type="info" :icon="Upload">导入</el-button>
  379. </el-upload>
  380. </div>
  381. </div>
  382. <!-- Table -->
  383. <div class="bg-white rounded-lg shadow-sm p-4 overflow-x-auto">
  384. <el-table
  385. :data="tableData"
  386. style="width: 100%"
  387. border
  388. @selection-change="handleSelectionChange"
  389. >
  390. <el-table-column type="selection" width="55" fixed="left" />
  391. <el-table-column prop="entName" label="企业(机构)名称" width="150" />
  392. <el-table-column prop="uniCode" label="统一社会信用代码" width="180" />
  393. <el-table-column prop="certificateType" label="证书类型" width="120">
  394. <template #default="{ row }">
  395. {{ row.certificateType || '-' }}
  396. </template>
  397. </el-table-column>
  398. <el-table-column prop="certificateNo" label="证书编号" width="150" />
  399. <el-table-column prop="certificateIssuanceDate" label="发证日期" width="150">
  400. <template #default="{ row }">
  401. {{
  402. row.certificateIssuanceDate
  403. ? new Date(row.certificateIssuanceDate).toLocaleDateString('zh-CN')
  404. : '-'
  405. }}
  406. </template>
  407. </el-table-column>
  408. <el-table-column prop="deadlineDate" label="截止日期" width="150">
  409. <template #default="{ row }">
  410. {{ row.deadlineDate ? new Date(row.deadlineDate).toLocaleDateString('zh-CN') : '-' }}
  411. </template>
  412. </el-table-column>
  413. <el-table-column prop="productName" label="产品名称及单元" width="200" />
  414. <el-table-column label="操作" width="185" fixed="right">
  415. <template #default="{ row }">
  416. <el-button :icon="Edit" size="small" @click="handleEdit(row)">编辑</el-button>
  417. <el-button type="danger" :icon="Delete" size="small" @click="handleDelete(row.id)"
  418. >删除</el-button
  419. >
  420. </template>
  421. </el-table-column>
  422. </el-table>
  423. <!-- Pagination -->
  424. <div class="mt-4 flex justify-end">
  425. <el-pagination
  426. @size-change="handleSizeChange"
  427. @current-change="handleCurrentChange"
  428. :current-page="pageNum"
  429. :page-sizes="[10, 20, 50, 100]"
  430. :page-size="pageSize"
  431. layout="total, sizes, prev, pager, next, jumper"
  432. :total="total"
  433. />
  434. </div>
  435. </div>
  436. <!-- Add/Edit Dialog -->
  437. <el-dialog
  438. v-model="dialogVisible"
  439. :title="dialogTitle"
  440. width="800px"
  441. :close-on-click-modal="false"
  442. :close-on-press-escape="false"
  443. >
  444. <el-form
  445. :model="formData"
  446. ref="formRef"
  447. label-width="120px"
  448. class="grid grid-cols-1 md:grid-cols-2 gap-4"
  449. >
  450. <el-form-item
  451. label="企业(机构)名称"
  452. prop="entName"
  453. :rules="[{ required: true, message: '请输入企业(机构)名称', trigger: 'blur' }]"
  454. >
  455. <el-input v-model="formData.entName" placeholder="请输入企业(机构)名称" />
  456. </el-form-item>
  457. <el-form-item label="统一社会信用代码" prop="uniCode">
  458. <el-input v-model="formData.uniCode" placeholder="请输入统一社会信用代码" />
  459. </el-form-item>
  460. <el-form-item
  461. label="证书类型"
  462. prop="certificateType"
  463. :rules="[{ required: true, message: '请选择证书类型', trigger: 'change' }]"
  464. >
  465. <el-select
  466. v-model="formData.certificateType"
  467. placeholder="请选择证书类型"
  468. style="width: 100%"
  469. >
  470. <el-option
  471. v-for="item in certificateTypeOptions"
  472. :key="item.value"
  473. :label="item.label"
  474. :value="item.value"
  475. />
  476. </el-select>
  477. </el-form-item>
  478. <el-form-item
  479. label="证书编号"
  480. prop="certificateNo"
  481. :rules="[{ required: true, message: '请输入证书编号', trigger: 'blur' }]"
  482. >
  483. <el-input v-model="formData.certificateNo" placeholder="请输入证书编号" />
  484. </el-form-item>
  485. <el-form-item label="发证日期" prop="certificateIssuanceDate">
  486. <el-date-picker
  487. v-model="formData.certificateIssuanceDate"
  488. type="date"
  489. placeholder="选择发证日期"
  490. value-format="YYYY-MM-DD"
  491. style="width: 100%"
  492. />
  493. </el-form-item>
  494. <el-form-item label="截止日期" prop="deadlineDate">
  495. <el-date-picker
  496. v-model="formData.deadlineDate"
  497. type="date"
  498. placeholder="选择截止日期"
  499. value-format="YYYY-MM-DD"
  500. style="width: 100%"
  501. />
  502. </el-form-item>
  503. <el-form-item label="产品名称及单元" prop="productName" class="md:col-span-2">
  504. <el-input
  505. v-model="formData.productName"
  506. placeholder="请输入产品名称及单元"
  507. type="textarea"
  508. :rows="2"
  509. />
  510. </el-form-item>
  511. </el-form>
  512. <template #footer>
  513. <span class="dialog-footer">
  514. <el-button @click="dialogVisible = false">取消</el-button>
  515. <el-button type="primary" @click="handleConfirm">确定</el-button>
  516. </span>
  517. </template>
  518. </el-dialog>
  519. </div>
  520. </template>
  521. <style scoped>
  522. /* Add any specific styles here if needed, otherwise UnoCSS handles most */
  523. </style>