oneByOneManagement.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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="handleDownload">
  14. 导出
  15. </el-button>
  16. </div>
  17. <div class="act-search">
  18. <section class="select-box">
  19. <div class="select-box--item">
  20. <span>隐患问题:</span>
  21. <el-input
  22. v-model="tableQuery.queryParam.problem"
  23. placeholder="请输入隐患问题"
  24. class="act-search-input"
  25. />
  26. </div>
  27. <div class="select-box--item">
  28. <span>状态:</span>
  29. <el-select
  30. v-model="tableQuery.queryParam.statusId"
  31. placeholder="请选择状态"
  32. clearable
  33. >
  34. <el-option label="未下发" :value="2" />
  35. <el-option label="待反馈" :value="3" />
  36. <el-option label="待审核" :value="4" />
  37. <el-option label="已完成" :value="5" />
  38. <el-option label="已作废" :value="6" />
  39. </el-select>
  40. </div>
  41. <div class="select-box--item">
  42. <span>计划日期范围:</span>
  43. <el-date-picker
  44. v-model="planDateRange"
  45. type="daterange"
  46. range-separator="至"
  47. start-placeholder="开始日期"
  48. end-placeholder="结束日期"
  49. value-format="YYYY-MM-DD"
  50. clearable
  51. class="act-search-input"
  52. />
  53. </div>
  54. </section>
  55. <section class="search-btn">
  56. <el-button type="primary" @click="handleSearch">查询</el-button>
  57. <el-button @click="handleReset">重置</el-button>
  58. </section>
  59. </div>
  60. </header>
  61. <div class="batch-table">
  62. <BasicTable
  63. ref="basicTableRef"
  64. :tableData="tableData"
  65. :tableConfig="tableConfig"
  66. @update:pageSize="handleSizeChange"
  67. @update:pageNumber="handleCurrentChange"
  68. >
  69. <template #status="scope">
  70. <span>{{ scope.row.statusName || '-' }}</span>
  71. </template>
  72. <template #feedbackRatio="scope">
  73. <span>{{ scope.row.feedbackRatio ?? (scope.row.issueCount ? `${scope.row.feedbackCount ?? 0}/${scope.row.issueCount}` : '-') }}</span>
  74. </template>
  75. <template #action="scope">
  76. <div class="action-container--div" style="justify-content: left">
  77. <!-- 未下发 statusId=1 或 2 -->
  78. <template v-if="scope.row.statusId === 1 || scope.row.statusId === 2">
  79. <ActionButton text="编辑" @click="handleEdit(scope.row.id)" />
  80. <ActionButton
  81. text="删除"
  82. :popconfirm="{ title: '确定要删除?' }"
  83. @confirm="handleDelete(scope.row.id)"
  84. />
  85. <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
  86. <ActionButton text="发送" @click="handleSend(scope.row)" />
  87. </template>
  88. <!-- 待反馈 statusId=3 -->
  89. <template v-else-if="scope.row.statusId === 3">
  90. <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
  91. <!-- <ActionButton text="发送" @click="handleSend(scope.row)" /> -->
  92. <ActionButton
  93. text="作废"
  94. :popconfirm="{ title: '确定要作废该记录?' }"
  95. @confirm="handleCancel(scope.row.id)"
  96. />
  97. </template>
  98. <!-- 待审核 statusId=4 -->
  99. <template v-else-if="scope.row.statusId === 4">
  100. <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
  101. <ActionButton text="审核" @click="handleAudit(scope.row.id)" />
  102. <ActionButton
  103. text="作废"
  104. :popconfirm="{ title: '确定要作废该记录?' }"
  105. @confirm="handleCancel(scope.row.id)"
  106. />
  107. </template>
  108. <!-- 已完成 statusId=5 -->
  109. <template v-else-if="scope.row.statusId === 5">
  110. <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
  111. </template>
  112. <!-- 已作废 statusId=6 -->
  113. <template v-else-if="scope.row.statusId === 6">
  114. <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
  115. <ActionButton
  116. text="删除"
  117. :popconfirm="{ title: '确定要删除?' }"
  118. @confirm="handleDelete(scope.row.id)"
  119. />
  120. </template>
  121. </div>
  122. </template>
  123. </BasicTable>
  124. </div>
  125. </div>
  126. </main>
  127. <!-- 下发举一反三弹窗:点击「发送」弹出,保存时调用 api/drawLessons/admin/issue -->
  128. <el-dialog
  129. v-model="showIssueDialog"
  130. title="下发举一反三"
  131. width="520px"
  132. destroy-on-close
  133. @close="resetIssueForm"
  134. >
  135. <el-form ref="issueFormRef" :model="issueForm" :rules="issueRules" label-width="120px">
  136. <el-form-item label="下发分组名称" prop="groupDeptId" required>
  137. <el-select
  138. v-model="issueForm.groupDeptId"
  139. placeholder="请选择分组名称"
  140. clearable
  141. filterable
  142. style="width: 100%"
  143. @change="onIssueGroupChange"
  144. >
  145. <el-option
  146. v-for="d in groupOptions"
  147. :key="d.id"
  148. :label="d.deptName"
  149. :value="d.id"
  150. />
  151. </el-select>
  152. </el-form-item>
  153. <el-form-item label="计划开始日期" prop="planStartDate" required>
  154. <el-date-picker
  155. v-model="issueForm.planStartDate"
  156. type="date"
  157. value-format="YYYY-MM-DD"
  158. placeholder="选择计划开始日期"
  159. style="width: 100%"
  160. />
  161. </el-form-item>
  162. <el-form-item label="计划结束时间" prop="planEndTime" required>
  163. <el-date-picker
  164. v-model="issueForm.planEndTime"
  165. type="date"
  166. value-format="YYYY-MM-DD"
  167. placeholder="选择计划结束日期"
  168. style="width: 100%"
  169. />
  170. </el-form-item>
  171. </el-form>
  172. <template #footer>
  173. <el-button @click="showIssueDialog = false">取消</el-button>
  174. <el-button type="primary" @click="handleIssueSubmit">保存</el-button>
  175. </template>
  176. </el-dialog>
  177. <BatchImport
  178. v-if="batchImportVisible"
  179. :visible="batchImportVisible"
  180. :import-api-url="importApiUrl"
  181. :template-url="templateUrl"
  182. template-name="下载模板"
  183. :show-template="false"
  184. @close="batchImportVisible = false"
  185. @update="handleUpdate"
  186. />
  187. </div>
  188. </template>
  189. <script setup lang="ts">
  190. import { onMounted, reactive, ref } from 'vue';
  191. import { ElMessage } from 'element-plus';
  192. import BasicTable from '@/components/BasicTable.vue';
  193. import useTableConfig from '@/hooks/useTableConfigHook';
  194. import ActionButton from '@/components/ActionButton.vue';
  195. import { TABLE_OPTIONS, DRAW_LESSONS_TABLE_COLUMNS } from './configs/tables';
  196. import { useRouter } from 'vue-router';
  197. import type { QueryPageRequest } from '@/types/basic-query';
  198. import type { FormInstance, FormRules } from 'element-plus';
  199. import {
  200. queryDrawLessonsAdminPage,
  201. deleteDrawLessons,
  202. voidDrawLessons,
  203. issueDrawLessons,
  204. type DrawLessonsQueryParam,
  205. } from '@/api/drawLessons';
  206. import { getAllDepartments } from '@/api/auth/dept';
  207. import type { DeptTree } from '@/types/dept/type';
  208. import { downloadByData } from '@/utils/file/download';
  209. import { useGlobSetting } from '@/hooks/setting';
  210. import urlJoin from 'url-join';
  211. const router = useRouter();
  212. // 表格
  213. const basicTableRef = ref<InstanceType<typeof BasicTable>>();
  214. const { tableConfig, pagination } = useTableConfig(DRAW_LESSONS_TABLE_COLUMNS, TABLE_OPTIONS);
  215. const tableData = ref<any[]>([]);
  216. const planDateRange = ref<[string, string] | null>(null);
  217. const tableQuery = reactive<QueryPageRequest<DrawLessonsQueryParam>>({
  218. pageNumber: pagination.pageNumber,
  219. pageSize: pagination.pageSize,
  220. queryParam: {
  221. problem: '',
  222. statusId: undefined,
  223. startTime: undefined,
  224. endTime: undefined,
  225. },
  226. });
  227. const handleSizeChange = (value: number) => {
  228. pagination.pageSize = value;
  229. tableQuery.pageSize = value;
  230. getTableData();
  231. };
  232. const handleCurrentChange = (value: number) => {
  233. pagination.pageNumber = value;
  234. tableQuery.pageNumber = value;
  235. getTableData();
  236. };
  237. function applyPlanDateRange() {
  238. if (planDateRange.value && planDateRange.value.length === 2) {
  239. tableQuery.queryParam.startTime = planDateRange.value[0];
  240. tableQuery.queryParam.endTime = planDateRange.value[1];
  241. } else {
  242. tableQuery.queryParam.startTime = undefined;
  243. tableQuery.queryParam.endTime = undefined;
  244. }
  245. }
  246. async function getTableData() {
  247. applyPlanDateRange();
  248. tableConfig.loading = true;
  249. try {
  250. const res = await queryDrawLessonsAdminPage(tableQuery);
  251. if (res?.records) {
  252. tableData.value = res.records.map((item: any) => ({
  253. id: item.id,
  254. dangerId: item.dangerId,
  255. problem: item.problem,
  256. statusId: item.statusId,
  257. statusName: item.statusName,
  258. associationOneThree: item.associationOneThree,
  259. associationOtObligationDeptName: item.associationOtObligationDeptName,
  260. issueCount: item.issueCount ?? item.sendCount ?? 0,
  261. feedbackCount: item.feedbackCount ?? 0,
  262. feedbackRatio: item.feedbackRatio,
  263. associationOtTimeLimit: item.associationOtTimeLimit,
  264. }));
  265. pagination.total = (res as any).totalRow ?? (res as any).total ?? 0;
  266. }
  267. } catch (e) {
  268. console.error('获取举一反三列表失败:', e);
  269. tableData.value = [];
  270. pagination.total = 0;
  271. } finally {
  272. tableConfig.loading = false;
  273. }
  274. }
  275. const handleSearch = () => {
  276. pagination.pageNumber = 1;
  277. tableQuery.pageNumber = 1;
  278. getTableData();
  279. };
  280. const handleReset = () => {
  281. tableQuery.queryParam.problem = '';
  282. tableQuery.queryParam.statusId = undefined;
  283. tableQuery.queryParam.startTime = undefined;
  284. tableQuery.queryParam.endTime = undefined;
  285. planDateRange.value = null;
  286. handleSearch();
  287. };
  288. const { urlPrefix } = useGlobSetting();
  289. const exportApiUrl = ref(urlJoin(urlPrefix, '/api/production/drawLessons/admin/queryPage'));
  290. const handleDownload = async () => {
  291. try {
  292. // 后端如有专门导出接口,可在此替换
  293. await queryDrawLessonsAdminPage(tableQuery);
  294. ElMessage.success('导出逻辑待实现');
  295. } catch (e) {
  296. console.error('导出举一反三失败:', e);
  297. ElMessage.error('导出失败,请重试');
  298. }
  299. };
  300. const handleCreate = () => {
  301. router.push({
  302. name: 'oneByOneManagementItem',
  303. query: {
  304. operate: 'one-by-one-create',
  305. },
  306. });
  307. };
  308. const handleEdit = (id: number) => {
  309. router.push({
  310. name: 'oneByOneManagementItem',
  311. query: {
  312. id,
  313. operate: 'one-by-one-edit',
  314. },
  315. });
  316. };
  317. const handleDelete = async (id: number) => {
  318. try {
  319. await deleteDrawLessons(id);
  320. ElMessage.success('删除成功');
  321. getTableData();
  322. } catch (e) {
  323. console.error('删除举一反三记录失败:', e);
  324. ElMessage.error('删除失败,请重试');
  325. }
  326. };
  327. const handleView = (id: number) => {
  328. router.push({
  329. name: 'oneByOneManagementItem',
  330. query: {
  331. id: String(id),
  332. operate: 'one-by-one-view',
  333. },
  334. });
  335. };
  336. /** 审核:跳转审核详情页 */
  337. const handleAudit = (id: number) => {
  338. router.push({
  339. name: 'oneByOneManagementItem',
  340. query: {
  341. id: String(id),
  342. operate: 'one-by-one-audit-detail',
  343. },
  344. });
  345. };
  346. /** 通知对象:跳转通知对象页(与安全考核管理(管)的考核对象一致) */
  347. const handleNotifyTarget = (row: { id: number }) => {
  348. router.push({
  349. name: 'oneByOneManagementItem',
  350. query: {
  351. id: String(row.id),
  352. operate: 'one-by-one-notify-target',
  353. },
  354. });
  355. };
  356. /** 发送:弹出「下发举一反三」弹窗,保存时调用 api/drawLessons/admin/issue */
  357. const showIssueDialog = ref(false);
  358. const currentIssueRow = ref<{
  359. id: number;
  360. dangerId?: number;
  361. associationOneThree?: string;
  362. } | null>(null);
  363. const issueFormRef = ref<FormInstance>();
  364. const issueForm = ref({
  365. groupDeptId: undefined as number | undefined,
  366. groupDeptName: '',
  367. planStartDate: '',
  368. planEndTime: '',
  369. });
  370. const issueRules: FormRules = {
  371. groupDeptId: [{ required: true, message: '请选择分组名称', trigger: 'change' }],
  372. planStartDate: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
  373. planEndTime: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }],
  374. };
  375. const groupOptions = ref<Array<{ id: number; deptName: string }>>([]);
  376. function flattenDeptTree(nodes: DeptTree[] | undefined): Array<{ id: number; deptName: string }> {
  377. if (!nodes?.length) return [];
  378. const list: Array<{ id: number; deptName: string }> = [];
  379. const walk = (items: DeptTree[]) => {
  380. items.forEach((n) => {
  381. if (n.id != null) list.push({ id: n.id, deptName: n.deptName ?? '' });
  382. if (n.children?.length) walk(n.children);
  383. });
  384. };
  385. walk(nodes);
  386. return list;
  387. }
  388. async function loadGroupOptions() {
  389. try {
  390. const res = await getAllDepartments();
  391. const tree = (res as DeptTree[]) ?? [];
  392. groupOptions.value = flattenDeptTree(Array.isArray(tree) && tree[0]?.children ? tree[0].children : tree);
  393. } catch (e) {
  394. console.error('获取分组列表失败:', e);
  395. groupOptions.value = [];
  396. }
  397. }
  398. function onIssueGroupChange(deptId: number) {
  399. const d = groupOptions.value.find((x) => x.id === deptId);
  400. issueForm.value.groupDeptName = d?.deptName ?? '';
  401. }
  402. function resetIssueForm() {
  403. issueForm.value = {
  404. groupDeptId: undefined,
  405. groupDeptName: '',
  406. planStartDate: '',
  407. planEndTime: '',
  408. };
  409. currentIssueRow.value = null;
  410. }
  411. function handleSend(row: { id: number; dangerId?: number; associationOneThree?: string }) {
  412. currentIssueRow.value = { id: row.id, dangerId: row.dangerId, associationOneThree: row.associationOneThree };
  413. issueForm.value = { groupDeptId: undefined, groupDeptName: '', planStartDate: '', planEndTime: '' };
  414. showIssueDialog.value = true;
  415. }
  416. async function handleIssueSubmit() {
  417. await issueFormRef.value?.validate?.().catch(() => {});
  418. if (!currentIssueRow.value) return;
  419. try {
  420. await issueDrawLessons({
  421. associationOtId: currentIssueRow.value.id,
  422. dangerId: currentIssueRow.value.dangerId,
  423. associationOtObligationDeptId: issueForm.value.groupDeptId,
  424. associationOtObligationDeptName: issueForm.value.groupDeptName,
  425. associationOneThree: currentIssueRow.value.associationOneThree,
  426. associationOtTimeLimit: issueForm.value.planEndTime || undefined,
  427. planStartDate: issueForm.value.planStartDate || undefined,
  428. planEndTime: issueForm.value.planEndTime || undefined,
  429. });
  430. ElMessage.success('下发成功');
  431. showIssueDialog.value = false;
  432. getTableData();
  433. } catch (e) {
  434. console.error('下发失败:', e);
  435. ElMessage.error('下发失败,请重试');
  436. }
  437. }
  438. /** 作废:调用作废接口变更为已作废状态 */
  439. const handleCancel = async (id: number) => {
  440. try {
  441. await voidDrawLessons(id);
  442. ElMessage.success('已作废');
  443. getTableData();
  444. } catch (e) {
  445. console.error('作废失败:', e);
  446. ElMessage.error('作废失败,请重试');
  447. }
  448. };
  449. onMounted(() => {
  450. loadGroupOptions();
  451. getTableData();
  452. });
  453. </script>
  454. <style scoped lang="scss">
  455. @use '@/styles/page-details-layout.scss' as *;
  456. @use '@/styles/page-main-layout.scss' as *;
  457. @use '@/styles/basic-table-action.scss' as *;
  458. @use '@/views/traffic/violation/style/act-search-table.scss' as *;
  459. </style>