safetyOrganizationSystemManagement.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <template>
  2. <!-- 旧版本,可舍弃,暂时为了兼容测试 -->
  3. <div class="safety-platform-container">
  4. <header class="safety-platform-container__header">
  5. <div class="breadcrumb-title"> 安全组织体系管理 </div>
  6. </header>
  7. <main class="safety-platform-container__main flex platform-main">
  8. <div class="nav">
  9. <el-button type="primary" :icon="Plus" @click="addTeam('parent')"> 添加组织 </el-button>
  10. <div class="collapse-wrapper">
  11. <!-- 组织树 -->
  12. <el-collapse v-model="activeName" accordion v-if="fetchSafetyOrganizationList.length > 0">
  13. <CollapseItem
  14. v-for="item in fetchSafetyOrganizationList"
  15. :key="item.id"
  16. :data="item"
  17. :level="level"
  18. @click-node="querySafetyTeamData"
  19. @create-node="handleCreateSafetySystem"
  20. @edit-node="handleEditSafetySystem"
  21. @delete-node="handleDelSafetySystem"
  22. />
  23. </el-collapse>
  24. <div v-else>
  25. <el-empty description="未添加组织" />
  26. </div>
  27. </div>
  28. <!-- 添加、编辑组织弹窗 -->
  29. <AddSafetySystem
  30. v-model:visible="addSafetySystemVisible"
  31. :data="addSafetyOrganizationSystemFormData"
  32. @confirmAddSafetySystem="confirmAddSafetySystemCallback"
  33. />
  34. </div>
  35. <div class="search-table-container table-content">
  36. <div class="chart">
  37. <!-- 架构图 -->
  38. <OrgChart :treeData="treeData" @node-click="handleNodeClick" />
  39. </div>
  40. <TeamDetailDrawer ref="teamDetailDrawerRef" :selected-team-id="selectedTeamId" />
  41. <section class="content">
  42. <div>
  43. <p class="label-title">组织信息</p>
  44. <el-form :model="safetyOrgUser" ref="formRef" :rules="safetyOrgUserRules">
  45. <el-form-item label="组织人数" prop="userNum">
  46. <el-input placeholder="请输入组织人数" type="number" v-model="safetyOrgUser.userNum" style="width:450px" />
  47. </el-form-item>
  48. <el-form-item label="组织职责" prop="depResp">
  49. <el-input placeholder="请填写组织职责" v-model="safetyOrgUser.depResp" :autosize="{ minRows: 2, maxRows: 6 }" :maxlength="300" show-word-limit type="textarea" style="width:450px" />
  50. </el-form-item>
  51. <el-form-item>
  52. <el-button type="primary" @click="handleSave"> 保存 </el-button>
  53. </el-form-item>
  54. </el-form>
  55. </div>
  56. <div class="mb-4">
  57. <p class="label-title">人员信息</p>
  58. <el-button type="primary" :icon="Plus" @click="handleCreate"> 添加 </el-button>
  59. <el-button plain @click="handleImport">导入</el-button>
  60. </div>
  61. <header class="mb-4">
  62. <div class="act-search">
  63. <section class="select-box">
  64. <div class="select-box--item">
  65. <span>搜索工号/姓名:</span>
  66. <el-input
  67. v-model="tableQuery.queryParam.keyword"
  68. placeholder="搜索工号/姓名"
  69. class="act-search-input"
  70. />
  71. </div>
  72. <div class="select-box--item">
  73. <span>状态:</span>
  74. <el-select v-model="tableQuery.queryParam.status" placeholder="请选择状态" clearable>
  75. <el-option label="启用" :value="1" />
  76. <el-option label="禁用" :value="2" />
  77. </el-select>
  78. </div>
  79. <div class="select-box--item">
  80. <span>日期范围:</span>
  81. <el-date-picker
  82. v-model="dateRange"
  83. @change="onchangeDateRange"
  84. type="daterange"
  85. range-separator="至"
  86. start-placeholder="开始日期"
  87. end-placeholder="结束日期"
  88. value-format="YYYY-MM-DD"
  89. format="YYYY-MM-DD"
  90. />
  91. </div>
  92. </section>
  93. <section class="search-btn">
  94. <el-button type="primary" @click="handleSearch">查询</el-button>
  95. <el-button @click="handleReset">重置</el-button>
  96. <el-button plain @click="handleDownload">导出</el-button>
  97. </section>
  98. </div>
  99. </header>
  100. <div class="batch-table">
  101. <BasicTable
  102. ref="basicTableRef"
  103. :tableData="tableData"
  104. :tableConfig="tableConfig"
  105. @update:pageSize="handleSizeChange"
  106. @update:pageNumber="handleCurrentChange"
  107. >
  108. <template #status="scope">
  109. <span>
  110. {{ scope.row.status === 1 ? '启用' : scope.row.status === 2 ? '禁用' : '-' }}
  111. </span>
  112. </template>
  113. <template #action="scope">
  114. <div class="action-container--div" style="justify-content: left">
  115. <ActionButton text="编辑" @click="handleEdit(scope.row.id)" />
  116. <ActionButton
  117. text="删除"
  118. :popconfirm="{
  119. title: '确定要删除?',
  120. }"
  121. @confirm="handleDelete(scope.row.id)"
  122. />
  123. <ActionButton text="查看" @click="handleView(scope.row.id)" />
  124. </div>
  125. </template>
  126. </BasicTable>
  127. </div>
  128. </section>
  129. </div>
  130. </main>
  131. <BatchImport
  132. v-if="batchImportVisible"
  133. :visible="batchImportVisible"
  134. :import-api-url="importApiUrl"
  135. :template-url="templateUrl"
  136. template-name="安全组织体系管理导入模版"
  137. :show-template="true"
  138. @close="batchImportVisible = false"
  139. @update="handleUpdate"
  140. />
  141. </div>
  142. </template>
  143. <script setup lang="ts">
  144. import { onMounted, reactive, ref } from 'vue';
  145. import { ElMessage, ElMessageBox } from 'element-plus';
  146. import BasicTable from '@/components/BasicTable.vue';
  147. import useTableConfig from '@/hooks/useTableConfigHook';
  148. import ActionButton from '@/components/ActionButton.vue';
  149. import { TABLE_OPTIONS, TABLE_COLUMNS } from './configs/tables';
  150. import { useRouter, useRoute } from 'vue-router';
  151. import type { QueryPageRequest } from '@/types/basic-query';
  152. import {
  153. getSafetySystemList,
  154. addSafetySystem,
  155. updateSafetySystem,
  156. deleteSafetySystem,
  157. fetchTableList,
  158. delEmployee,
  159. safetyOrgUserSave,
  160. safetyOrgUserDetail,
  161. exportSafetyOrganizationSystemManagement
  162. } from '@/api/safety-organization-management';
  163. import { downloadByData } from '@/utils/file/download';
  164. import BatchImport from '@/components/batch-import/BatchImport.vue';
  165. import { useGlobSetting } from '@/hooks/setting';
  166. import urlJoin from 'url-join';
  167. import AddSafetySystem from './components/addSafetySystem.vue';
  168. import {SafetyOrgUserRules} from "./configs/form"
  169. import {
  170. Delete,
  171. Edit,
  172. Plus,
  173. } from '@element-plus/icons-vue'
  174. import OrgChart from './components/orgChart.vue';
  175. import CollapseItem from './components/collapseItem.vue'
  176. import TeamDetailDrawer from './components/TeamDetailDrawer.vue';
  177. const position = ref('left')
  178. const route = useRoute();
  179. const router = useRouter();
  180. // 表格
  181. const basicTableRef = ref<InstanceType<typeof BasicTable>>();
  182. const { tableConfig, pagination } = useTableConfig(TABLE_COLUMNS, TABLE_OPTIONS);
  183. const tableData = ref<any[]>([]);
  184. const fetchSafetyOrganizationList = ref<any[]>([]);
  185. // 架构数据类型
  186. type OrganizationTreeType = {
  187. id: string;
  188. data: { name: string };
  189. children?: OrganizationTreeType[];
  190. };
  191. const treeData = ref<OrganizationTreeType>({
  192. id: 'root',
  193. data: { name: '请添加组织' },
  194. children: [],
  195. });
  196. const teamDetailDrawerRef = ref<InstanceType<typeof TeamDetailDrawer>>();
  197. const selectedTeamId = ref<number | null>(null);
  198. const handleNodeClick = (nodeData: any) => {
  199. const id = nodeData?.id?.replace('org-', '')
  200. console.log(nodeData, 'canshu')
  201. selectedTeamId.value = Number(id);
  202. teamDetailDrawerRef.value?.drawerShow();
  203. };
  204. const activeName = ref('');
  205. // 日期范围(用于日期选择器)
  206. const dateRange = ref<[string, string] | string>('');
  207. const level = ref(1)
  208. const tableQuery = reactive<QueryPageRequest<any>>({
  209. pageNumber: pagination.pageNumber,
  210. pageSize: pagination.pageSize,
  211. queryParam: {
  212. classifyName: '',
  213. keyword: '',
  214. status: '',
  215. startTime: '',
  216. endTime: '',
  217. },
  218. });
  219. const safetyOrgUser = reactive({
  220. id: 0,
  221. userNum: '',
  222. depResp: ''
  223. })
  224. const formRef = ref()
  225. const safetyOrgUserRules = ref(SafetyOrgUserRules)
  226. // 校验员工数量和职责
  227. const handleValidate = async () => {
  228. if (!formRef.value) return;
  229. const res = await formRef.value.validateField();
  230. return res;
  231. };
  232. // 保存员工数量和职责
  233. const handleSave = async ()=>{
  234. const res = await handleValidate()
  235. if (!res) return;
  236. try {
  237. console.log(safetyOrgUser, 'canshu')
  238. safetyOrgUser.id = Number(safetyOrgUser.id)
  239. await safetyOrgUserSave(safetyOrgUser)
  240. ElMessage.success('保存成功');
  241. } catch (error) {
  242. ElMessage.error('保存失败');
  243. }
  244. }
  245. // 查询组织详情
  246. const safetyOrgDetail = async (id)=>{
  247. try {
  248. const res = await safetyOrgUserDetail(id)
  249. Object.assign(safetyOrgUser, {
  250. id,
  251. userNum: res.userNum,
  252. depResp: res.depResp
  253. })
  254. } catch (error) {
  255. ElMessage.error('获取详情失败');
  256. }
  257. }
  258. const handleSizeChange = (value: number) => {
  259. pagination.pageSize = value;
  260. tableQuery.pageSize = value;
  261. getTableData();
  262. };
  263. const handleCurrentChange = (value: number) => {
  264. pagination.pageNumber = value;
  265. tableQuery.pageNumber = value;
  266. getTableData();
  267. };
  268. async function getTableData() {
  269. tableConfig.loading = true;
  270. try {
  271. const res = await fetchTableList(tableQuery);
  272. if (res) {
  273. tableData.value = res.records
  274. pagination.total = res.totalRow;
  275. }
  276. } catch (e) {
  277. console.error('获取列表失败:', e);
  278. tableData.value = [];
  279. pagination.total = 0;
  280. } finally {
  281. tableConfig.loading = false;
  282. }
  283. }
  284. interface addSafetyOrganizationSystemFormDataType {
  285. type: String;
  286. orgName?: String;
  287. orgId?: String | number;
  288. action?: String;
  289. parentid?: String | number;
  290. }
  291. const addSafetySystemVisible = ref(false);
  292. const addSafetyOrganizationSystemFormData = ref<addSafetyOrganizationSystemFormDataType>({
  293. type: '',
  294. orgName: '',
  295. orgId: '',
  296. action: '',
  297. parentid: '',
  298. });
  299. /**
  300. * 递归给树形结构添加 id 、data 、children 字段
  301. * @param {Array} tree 原始树形数组
  302. * @returns 格式化后的标准树结构
  303. */
  304. const formatTreeData = (tree)=> {
  305. if (!tree || !Array.isArray(tree)) return [];
  306. return tree.map(item => {
  307. // 给每一层节点都加上 id 和 data
  308. const formattedItem = {
  309. children: item.children || [],
  310. id: `org-${item.orgId}`,
  311. data: {
  312. name: item.orgName
  313. }
  314. };
  315. // 递归处理子节点
  316. if (formattedItem.children && formattedItem.children.length > 0) {
  317. formattedItem.children = formatTreeData(formattedItem.children);
  318. }
  319. return formattedItem;
  320. });
  321. }
  322. function convertData(leaderTeams): OrganizationTreeType {
  323. return {
  324. id: `org-${leaderTeams.orgId}`,
  325. data: {
  326. name: leaderTeams.orgName
  327. },
  328. children: leaderTeams.children?.map((child) => convertData(child)),
  329. };
  330. }
  331. // 获取组织列表
  332. const fetchSafetyOrganizationTeamList = async () => {
  333. try {
  334. const res = await getSafetySystemList();
  335. fetchSafetyOrganizationList.value = res;
  336. // 默认选择第一个组织
  337. if(res[0].orgId){
  338. treeNodePreview(res[0])
  339. // activeName.value = String(res[0].orgId)
  340. tableQuery.queryParam.classifyName = res[0].orgId
  341. }
  342. } catch (error) {
  343. ElMessage.error('获取组织列表失败');
  344. }
  345. };
  346. // 给架构图赋值
  347. const treeNodePreview = (data)=>{
  348. let TreeNode = convertData(data)
  349. treeData.value = TreeNode
  350. }
  351. // 一级新增
  352. const addTeam = (type) => {
  353. addSafetyOrganizationSystemFormData.value = {
  354. type,
  355. action: 'add',
  356. };
  357. addSafetySystemVisible.value = true;
  358. };
  359. // 子级新增
  360. const handleCreateSafetySystem = async (type, value) => {
  361. // console.log('新增参数--',type, value)
  362. addSafetyOrganizationSystemFormData.value = {
  363. type:'children',
  364. action: 'add',
  365. orgName: value.orgName,
  366. orgId: value.orgId,
  367. };
  368. addSafetySystemVisible.value = true;
  369. // 打开某一个
  370. // activeName.value = value.orgId;
  371. };
  372. // 编辑
  373. const handleEditSafetySystem = (type, value, parentid) => {
  374. // console.log('编辑参数--', type, value, parentid)
  375. addSafetySystemVisible.value = true;
  376. addSafetyOrganizationSystemFormData.value = {
  377. type,
  378. action: 'edit',
  379. orgName: value.orgName,
  380. orgId: value.orgId,
  381. parentid,
  382. };
  383. };
  384. // 查询
  385. const querySafetyTeamData = (value) => {
  386. // console.log('查询', value);
  387. tableQuery.queryParam.classifyName = value.orgId;
  388. // activeName.value = String(value.orgId)
  389. treeNodePreview(value)
  390. safetyOrgDetail(value.orgId)
  391. getTableData();
  392. };
  393. // 定义组织数据类型
  394. interface SafetySystemFormData {
  395. value: string; // 输入的组织名称
  396. action: 'add' | 'edit'; // 操作类型:新增或编辑
  397. orgId?: string | number; // 组织ID(编辑时必传)
  398. parentid?: string | number; // 父组织ID(新增子组织时必传)
  399. type?: 'children' | 'parent'; // 组织类型(子组织或根组织)
  400. }
  401. // 保存弹窗回调
  402. const confirmAddSafetySystemCallback = async (formData: SafetySystemFormData) => {
  403. try {
  404. if (!formData.value?.trim()) {
  405. ElMessage.warning('请输入有效的组织名称!');
  406. return;
  407. }
  408. // 新增时,传orgName(组织名称)、parentid(父组织ID)
  409. // 编辑时,传当前ID和orgName
  410. const requestData = {
  411. orgName: formData.value.trim(),
  412. id: formData.action === 'edit' ? formData.orgId : undefined,
  413. // 第一级不需要传parentid
  414. parentid: formData.action === 'add' ? formData.parentid : undefined,
  415. };
  416. console.log(formData, '参数--formData')
  417. if (formData.action === 'add') {
  418. if (formData.type === 'children' && formData.orgId) {
  419. requestData.parentid = formData.orgId;
  420. }
  421. await addSafetySystem(requestData);
  422. ElMessage.success('新增组织成功!');
  423. } else {
  424. // 如果是子类,补充父级ID
  425. if (formData.type === 'children' && formData.parentid) {
  426. requestData.parentid = formData.parentid;
  427. }
  428. await updateSafetySystem(requestData);
  429. ElMessage.success('编辑组织成功!');
  430. }
  431. // 刷新列表
  432. fetchSafetyOrganizationTeamList();
  433. } catch (error) {
  434. console.error('操作失败:', error);
  435. ElMessage.error(formData.action === 'add' ? '新增组织失败!' : '编辑组织失败!');
  436. }
  437. };
  438. // 删除
  439. const handleDelSafetySystem = async (type, value) => {
  440. // console.log('删除', type, value)
  441. ElMessageBox.confirm('确认删除该组织吗?', '警告', { type: 'warning' }).then(async () => {
  442. try {
  443. if(value.children.length > 0){
  444. ElMessage.error('当前一级组织存在子级数据,无法删除,请先删除子级组织')
  445. return
  446. }
  447. await deleteSafetySystem(value.orgId);
  448. ElMessage.success('删除成功');
  449. // 刷新组织列表
  450. fetchSafetyOrganizationTeamList();
  451. handleReset();
  452. } catch (error) {
  453. ElMessage.error(error || '删除失败');
  454. }
  455. });
  456. };
  457. // 定义组织数据类型
  458. interface SafetySystemFormData {
  459. value: string; // 输入的组织名称
  460. action: 'add' | 'edit'; // 操作类型:新增或编辑
  461. orgId?: string | number; // 组织ID(编辑时必传)
  462. parentid?: string | number; // 父组织ID(新增子组织时必传)
  463. type?: 'children' | 'parent'; // 组织类型(子组织或根组织)
  464. }
  465. // 时间查询
  466. const onchangeDateRange = () => {
  467. if (dateRange.value && Array.isArray(dateRange.value) && dateRange.value.length === 2) {
  468. tableQuery.queryParam.startTime = dateRange.value[0] || '';
  469. tableQuery.queryParam.endTime = dateRange.value[1] || '';
  470. } else {
  471. tableQuery.queryParam.startTime = '';
  472. tableQuery.queryParam.endTime = '';
  473. }
  474. getTableData();
  475. };
  476. const handleSearch = () => {
  477. pagination.pageNumber = 1;
  478. tableQuery.pageNumber = 1;
  479. getTableData();
  480. };
  481. const handleReset = () => {
  482. pagination.pageNumber = 1;
  483. tableQuery.queryParam = {
  484. classifyName: '',
  485. keyword: '',
  486. status: '', // 重置为默认启用状态
  487. startTime: '',
  488. endTime: '',
  489. };
  490. dateRange.value = '';
  491. handleSearch();
  492. };
  493. // 批量导入
  494. const batchImportVisible = ref(false);
  495. const { urlPrefix } = useGlobSetting();
  496. const importApiUrl = ref(urlJoin(urlPrefix, '/safetyorguser/importSafetyOrgUser'));
  497. const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/安全组织体系管理导入模版.xlsx');
  498. const handleImport = () => {
  499. batchImportVisible.value = true;
  500. };
  501. const handleUpdate = () => {
  502. batchImportVisible.value = false;
  503. getTableData();
  504. };
  505. const handleDownload = async () => {
  506. try {
  507. const response = await exportSafetyOrganizationSystemManagement(tableQuery.queryParam);
  508. if (response) {
  509. const fileName = `安全组织体系管理_${new Date().toISOString().split('T')[0]}.xlsx`;
  510. downloadByData(response, fileName);
  511. ElMessage.success('导出成功');
  512. }
  513. } catch (e) {
  514. console.error('导出安全组织体系管理失败:', e);
  515. ElMessage.error('导出失败,请重试');
  516. }
  517. };
  518. const handleCreate = () => {
  519. router.push({
  520. name: 'SecurityOrganizationalStructureItem',
  521. query: {
  522. operate: 'employee-create',
  523. },
  524. });
  525. };
  526. const handleEdit = (id: number) => {
  527. router.push({
  528. name: 'SecurityOrganizationalStructureItem',
  529. query: {
  530. id,
  531. operate: 'employee-edit',
  532. },
  533. });
  534. };
  535. const handleDelete = async (id: number) => {
  536. try {
  537. await delEmployee(id);
  538. ElMessage.success('删除成功');
  539. getTableData();
  540. } catch (e) {
  541. console.error('删除员工失败:', e);
  542. ElMessage.error('删除失败,请重试');
  543. }
  544. };
  545. const handleView = (id: number) => {
  546. router.push({
  547. name: 'SecurityOrganizationalStructureItem',
  548. query: {
  549. id,
  550. operate: 'employee-view',
  551. },
  552. });
  553. };
  554. onMounted(async () => {
  555. fetchSafetyOrganizationTeamList();
  556. // 默认第一个架构组织的员工数据
  557. const res = await getSafetySystemList();
  558. const orgId = res[0]?.orgId;
  559. tableQuery.queryParam.classifyName = orgId || undefined
  560. getTableData();
  561. if(orgId){
  562. safetyOrgDetail(orgId)
  563. }
  564. });
  565. </script>
  566. <style scoped lang="scss">
  567. @use '@/styles/page-details-layout.scss' as *;
  568. @use '@/styles/page-main-layout.scss' as *;
  569. @use '@/styles/basic-table-action.scss' as *;
  570. @use '@/views/traffic/violation/style/act-search-table.scss' as *;
  571. .platform-main {
  572. width:100%;
  573. }
  574. .table-content {
  575. flex:1;
  576. width: 0;
  577. min-width: 0;
  578. }
  579. .mb-4{
  580. margin-bottom: 16px;
  581. }
  582. .nav {
  583. flex: 0 0 300px;
  584. margin-right: 15px;
  585. padding-right: 15px;
  586. border-right: 1px solid #eee;
  587. overflow-y: auto;
  588. :deep(.collapse-title) {
  589. flex: 1 0 90%;
  590. order: 1;
  591. .el-collapse-item__header {
  592. flex: 1 0 auto;
  593. order: -1;
  594. }
  595. }
  596. .collapse-wrapper {
  597. margin-top: 10px;
  598. .title-wrapper {
  599. display: flex;
  600. justify-content: space-between;
  601. align-items: center;
  602. width: 100%;
  603. .handler {
  604. flex: 1;
  605. display: flex;
  606. justify-content: flex-end;
  607. align-items: center;
  608. padding-right: 15px;
  609. }
  610. }
  611. .collapse-item-content {
  612. ul {
  613. padding-left: 40px;
  614. li {
  615. display: flex;
  616. justify-content: space-between;
  617. align-items: center;
  618. width: 100%;
  619. padding: 6px 0;
  620. border-bottom: 1px solid #eeeeeed1;
  621. span {
  622. cursor: pointer;
  623. }
  624. }
  625. }
  626. }
  627. }
  628. }
  629. .label-title{
  630. margin-bottom:16px;
  631. }
  632. .chart {
  633. height:260px;
  634. background-color: #f1f7ff;
  635. border-radius: 4px;
  636. overflow: hidden;
  637. }
  638. .content {
  639. height: calc(100% - 260px - 32px);
  640. overflow-y: auto;
  641. }
  642. </style>