oneByOneManagement.vue 19 KB

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