educationTrainingPlanManagementDept.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <template>
  2. <div class="safety-platform-container">
  3. <header class="safety-platform-container__header">
  4. <div class="breadcrumb-title"> 教育培训计划管理(部门)</div>
  5. </header>
  6. <main class="safety-platform-container__main">
  7. <div class="search-table-container">
  8. <header>
  9. <div style="position: relative">
  10. <!-- <el-button type="primary" class="search-table-container--button" @click="handleCreate">
  11. 添加
  12. </el-button> -->
  13. <!-- <el-button plain class="search-table-container--button" @click="handleImport">
  14. 导入
  15. </el-button>
  16. -->
  17. </div>
  18. <div class="act-search">
  19. <section class="select-box">
  20. <div class="select-box--item">
  21. <span>培训内容/计划名称:</span>
  22. <el-input
  23. v-model="tableQuery.queryParam.keyword"
  24. placeholder="搜索培训内容或计划名称"
  25. class="act-search-input"
  26. />
  27. </div>
  28. <div class="select-box--item">
  29. <span>状态:</span>
  30. <el-select v-model="tableQuery.queryParam.status" placeholder="请选择状态" clearable>
  31. <el-option v-for="item in STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
  32. </el-select>
  33. </div>
  34. <div class="select-box--item">
  35. <span>分类名称:</span>
  36. <el-select
  37. v-model="tableQuery.queryParam.classifyName"
  38. placeholder="请选择分类名称"
  39. filterable
  40. clearable
  41. >
  42. <el-option
  43. v-for="item in classifyNameOptions"
  44. :key="item.value"
  45. :label="item.label"
  46. :value="item.value"
  47. />
  48. </el-select>
  49. </div>
  50. <div>
  51. <span>计划日期范围:</span>
  52. <el-date-picker
  53. v-model="dateRange"
  54. type="daterange"
  55. range-separator="至"
  56. start-placeholder="开始日期"
  57. end-placeholder="结束日期"
  58. value-format="YYYY-MM-DD"
  59. format="YYYY-MM-DD"
  60. />
  61. </div>
  62. </section>
  63. <section class="search-btn">
  64. <el-button type="primary" @click="handleSearch">查询</el-button>
  65. <el-button @click="handleReset">重置</el-button>
  66. <el-button plain class="search-table-container--button" @click="handleDownload">
  67. 导出
  68. </el-button>
  69. </section>
  70. </div>
  71. </header>
  72. <div class="batch-table">
  73. <BasicTable
  74. ref="basicTableRef"
  75. :tableData="tableData"
  76. :tableConfig="tableConfig"
  77. @update:pageSize="handleSizeChange"
  78. @update:pageNumber="handleCurrentChange"
  79. >
  80. <template #action="scope">
  81. <div class="action-container--div" style="justify-content: left">
  82. <ActionButton text="查看" @click="handleView(scope.row.id)" />
  83. <el-button link v-if="scope.row.status===2" @click="handleSummary(scope.row)"> 小结 </el-button>
  84. </div>
  85. </template>
  86. </BasicTable>
  87. </div>
  88. </div>
  89. </main>
  90. <BatchImport
  91. v-if="batchImportVisible"
  92. :visible="batchImportVisible"
  93. :import-api-url="importApiUrl"
  94. :template-url="templateUrl"
  95. template-name="下载模板"
  96. :show-template="false"
  97. @close="batchImportVisible = false"
  98. @update="handleUpdate"
  99. />
  100. <el-dialog v-model="dialogVisible" title="填写培训小结">
  101. <el-form>
  102. <el-form-item label="材料上传" required>
  103. <el-upload
  104. action=""
  105. ref="courseContentUpload"
  106. :auto-upload="false"
  107. :on-change="handleFileChange"
  108. accept=".rar, .zip, .doc, .docx, .pdf, .mp4"
  109. :file-list="fileList"
  110. >
  111. <el-button type="default">
  112. <el-icon style="margin-right: 6px">
  113. <UploadFilled />
  114. </el-icon>
  115. 选择附件
  116. </el-button>
  117. <template #tip>
  118. <div class="el-upload__tip"> 支持格式:.rar .zip .doc .docx .pdf .mp4,单个文件不能超过20MB </div>
  119. </template>
  120. </el-upload>
  121. </el-form-item>
  122. <el-form-item label="培训小结" required>
  123. <el-input type="textarea" v-model="form.trainingSummary"></el-input>
  124. </el-form-item>
  125. </el-form>
  126. <template #footer>
  127. <div class="dialog-footer">
  128. <el-button @click="handleCancel">取消</el-button>
  129. <el-button type="primary" @click="saveSummary" >
  130. 确认
  131. </el-button>
  132. </div>
  133. </template>
  134. </el-dialog>
  135. </div>
  136. </template>
  137. <script setup lang="ts">
  138. import { onMounted, reactive, ref } from 'vue';
  139. import { ElMessage } from 'element-plus';
  140. import BasicTable from '@/components/BasicTable.vue';
  141. import useTableConfig from '@/hooks/useTableConfigHook';
  142. import ActionButton from '@/components/ActionButton.vue';
  143. import { TABLE_OPTIONS, TABLE_COLUMNS, STATUS_OPTIONS } from './configs/tables';
  144. import { useRouter } from 'vue-router';
  145. import type { QueryPageRequest } from '@/types/basic-query';
  146. import { getEducationAndTrainingProgramList, updateEducationTrainingPlanCourseSummary,exportTableData } from '@/api/production-education-training-plan-dept';
  147. import { downloadByData } from '@/utils/file/download';
  148. import BatchImport from '@/components/batch-import/BatchImport.vue';
  149. import { useGlobSetting } from '@/hooks/setting';
  150. import urlJoin from 'url-join';
  151. import { UploadFilled, Plus, Delete, Download, ZoomIn } from '@element-plus/icons-vue';
  152. import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
  153. const router = useRouter();
  154. // 表格
  155. const basicTableRef = ref<InstanceType<typeof BasicTable>>();
  156. const { tableConfig, pagination } = useTableConfig(TABLE_COLUMNS, TABLE_OPTIONS);
  157. const tableData = ref<any[]>([]);
  158. // 日期范围
  159. const dateRange = ref<[string, string] | null>(null);
  160. // 分类名称选项
  161. const classifyNameOptions = ref<Array<{ label: string; value: string }>>([
  162. { label: '全部', value: '全部' },
  163. { label: '全员安全培训', value: '全员安全培训' },
  164. { label: '新员工培训', value: '新员工培训' },
  165. { label: '岗位资质培训', value: '岗位资质培训' },
  166. { label: '生产作业安全培训', value: '生产作业安全培训' },
  167. { label: '安全管理人员培训', value: '安全管理人员培训' },
  168. ]);
  169. // 验证文件类型
  170. const allowedTypes = [
  171. 'application/rar',
  172. 'application/zip',
  173. 'application/msword',
  174. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  175. 'application/pdf',
  176. 'video/mp4',
  177. ];
  178. const tableQuery = reactive<QueryPageRequest<any>>({
  179. pageNumber: pagination.pageNumber,
  180. pageSize: pagination.pageSize,
  181. queryParam: {
  182. keyword: '',
  183. status: undefined,
  184. categoryName: '',
  185. startDate: '',
  186. endDate: '',
  187. },
  188. });
  189. const fileList = ref([])
  190. const dialogVisible = ref(false)
  191. const handleCancel = ()=>{
  192. dialogVisible.value = false
  193. }
  194. const form = reactive({
  195. id:'',
  196. uploadAttach: '',
  197. trainingSummary: ''
  198. })
  199. const beforeUpload = (file) => {
  200. const isAllowedType = allowedTypes.includes(file.type);
  201. const isLt20M = file.size / 1024 / 1024 < 20;
  202. if (!isAllowedType) {
  203. ElMessage.error('上传文件格式不正确!');
  204. }
  205. if (!isLt20M) {
  206. ElMessage.error('上传文件大小不能超过20MB!');
  207. }
  208. return isAllowedType && isLt20M;
  209. };
  210. // 上传文件
  211. const formatAttachment = async (data: any) => {
  212. if (!data) return data;
  213. const uuid = Math.random().toString(36).substring(2, 9);
  214. const timestamp = Date.now().toString();
  215. const random = Math.random().toString(36).substring(2, 4);
  216. const fileName = data.name;
  217. const res = await uploadFileApi({
  218. bizType: UPLOAD_BIZ_TYPE.ATTACHMENT,
  219. fileName: `${uuid}-${timestamp}-${random}`,
  220. file: data,
  221. });
  222. return res
  223. };
  224. // 文件选择更新
  225. const courseContentUpload = ref();
  226. const handleFileExceed = (files) => {
  227. courseContentUpload.value!.clearFiles(); // 清空文件列表
  228. const file = files[0];
  229. if (!beforeUpload(file)) {
  230. return;
  231. }
  232. courseContentUpload.value!.handleStart(file); // 手动触发上传
  233. };
  234. // 课程内容文件上传
  235. const handleFileChange = async (file, fileLists) => {
  236. if (!allowedTypes.includes(file.raw.type)) {
  237. ElMessage.error('不支持的文件格式');
  238. return;
  239. }
  240. if (file.raw.size > 20 * 1024 * 1024) {
  241. ElMessage.error('文件大小不能超过20MB');
  242. return;
  243. }
  244. if(file.raw){
  245. try {
  246. const res = await formatAttachment(file.raw);
  247. const targetFile = fileLists.find(f => f.uid === file.uid);
  248. if (targetFile) {
  249. targetFile.url = res.url;
  250. targetFile.contentType = res.contentType
  251. }
  252. fileList.value = fileLists;
  253. form.uploadAttach = JSON.stringify(fileList.value);
  254. ElMessage.success('上传成功');
  255. } catch (error) {
  256. ElMessage.error('上传失败,请重试');
  257. // 上传失败时,可以从 fileLists 中移除该文件
  258. fileList.value = fileLists.filter(f => f.uid !== file.uid);
  259. }
  260. }
  261. };
  262. const saveSummary = async()=>{
  263. if(!fileList.value.length){
  264. ElMessage.error('请先上传材料');
  265. return;
  266. }
  267. if(!form.trainingSummary){
  268. ElMessage.error('请输入小结内容');
  269. return;
  270. }
  271. try {
  272. await updateEducationTrainingPlanCourseSummary(form);
  273. ElMessage.success('更新小结成功');
  274. getTableData();
  275. } catch (e) {
  276. ElMessage.error('更新小结失败');
  277. }
  278. dialogVisible.value = false
  279. }
  280. const handleSummary = async (row) => {
  281. dialogVisible.value = true
  282. form.id = row.id
  283. form.trainingSummary = ''
  284. form.uploadAttach = ''
  285. };
  286. const handleSizeChange = (value: number) => {
  287. pagination.pageSize = value;
  288. tableQuery.pageSize = value;
  289. getTableData();
  290. };
  291. const handleCurrentChange = (value: number) => {
  292. pagination.pageNumber = value;
  293. tableQuery.pageNumber = value;
  294. getTableData();
  295. };
  296. async function getTableData() {
  297. tableConfig.loading = true;
  298. try {
  299. tableQuery.queryParam.startDate = dateRange.value ? dateRange.value[0] : '';
  300. tableQuery.queryParam.endDate = dateRange.value ? dateRange.value[1] : '';
  301. const res = await getEducationAndTrainingProgramList(tableQuery);
  302. if (res) {
  303. // 映射返回数据字段到表格字段
  304. tableData.value = res.records;
  305. pagination.total = res.totalRow;
  306. }
  307. } catch (e) {
  308. tableData.value = [];
  309. pagination.total = 0;
  310. } finally {
  311. tableConfig.loading = false;
  312. }
  313. }
  314. const handleSearch = () => {
  315. pagination.pageNumber = 1;
  316. tableQuery.pageNumber = 1;
  317. getTableData();
  318. };
  319. const handleReset = () => {
  320. pagination.pageNumber = 1;
  321. Object.assign(tableQuery, {
  322. pageNumber: 1,
  323. pageSize: pagination.pageSize,
  324. queryParam: {
  325. keyword: '',
  326. status: '',
  327. categoryName: '',
  328. startDate: '',
  329. endDate: '',
  330. },
  331. });
  332. dateRange.value = null;
  333. handleSearch();
  334. };
  335. // 批量导入
  336. const batchImportVisible = ref(false);
  337. const { urlPrefix } = useGlobSetting();
  338. const importApiUrl = ref(urlJoin(urlPrefix, '/inventory/importInventory'));
  339. const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-inventory-template.xlsx');
  340. const handleImport = () => {
  341. batchImportVisible.value = true;
  342. };
  343. const handleUpdate = () => {
  344. batchImportVisible.value = false;
  345. getTableData();
  346. };
  347. const handleDownload = async () => {
  348. try {
  349. const response = await exportTableData(tableQuery.queryParam);
  350. if (response) {
  351. const fileName = `教育培训计划管理(部门)_${new Date().toISOString().split('T')[0]}.xlsx`;
  352. downloadByData(response, fileName);
  353. ElMessage.success('导出成功');
  354. }
  355. } catch (e) {
  356. console.error('导出教育培训计划管理(部门)失败:', e);
  357. ElMessage.error('导出教育培训计划管理(部门)失败,请重试');
  358. }
  359. };
  360. const handleView = (id: number) => {
  361. router.push({
  362. name: 'educationTrainingPlanManagementDeptItem',
  363. query: {
  364. id,
  365. operate: 'education-training-plan-management-dept-view',
  366. },
  367. });
  368. };
  369. onMounted(() => {
  370. getTableData();
  371. });
  372. </script>
  373. <style scoped lang="scss">
  374. @use '@/styles/page-details-layout.scss' as *;
  375. @use '@/styles/page-main-layout.scss' as *;
  376. @use '@/styles/basic-table-action.scss' as *;
  377. @use '@/views/traffic/violation/style/act-search-table.scss' as *;
  378. </style>