department.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. <template>
  2. <div>
  3. <div class="mb-4 flex item-center justify-between">
  4. <div>
  5. <el-button type="primary" @click="showEditDepartment"
  6. >新建部门</el-button
  7. >
  8. <el-button
  9. @click="
  10. importUploadState = false;
  11. importDialogVisible = true;
  12. "
  13. >一键导入部门与人员信息</el-button
  14. >
  15. <el-button
  16. @click="
  17. visibleLastTimeImport = true;
  18. getImportProgress();
  19. "
  20. >最近1次导入信息</el-button
  21. >
  22. </div>
  23. <div class="text-right w-1/2 flex items-center">
  24. <el-select v-model="filter.type" placeholder="">
  25. <el-option label="搜部门" :value="1"> </el-option>
  26. <el-option label="搜人员" :value="2"> </el-option>
  27. </el-select>
  28. <el-input
  29. v-if="filter.type == 1"
  30. v-model="filter.key"
  31. class="w-full"
  32. placeholder="请输入关键词"
  33. clearable
  34. >
  35. </el-input>
  36. <SearchArchivesSelect
  37. v-else
  38. style="width: 100%"
  39. v-model="filter.archivesId"
  40. :channelIds="props.channelId"
  41. placeholder="请选择档案"
  42. @change="handleChangeArchivesId"
  43. />
  44. <div class="ml-2 flex">
  45. <el-button type="primary" @click="onSearch">搜索</el-button>
  46. <el-button type="primary" plain @click="onReset">重置</el-button>
  47. </div>
  48. </div>
  49. </div>
  50. <el-table :data="departments" row-key="id" default-expand-all>
  51. <el-table-column prop="name" label="部门名称">
  52. <template #default="{ row }">
  53. <template v-if="filter.type == 1">
  54. <span v-html="hightLight(filter.key, row.name)"></span>
  55. </template>
  56. <template v-if="filter.type == 2">
  57. <span
  58. v-if="filter.archivesId"
  59. :style="{
  60. color:
  61. currentArchivesInfo?.departmentId == row.id ? '#67C23A' : ''
  62. }"
  63. >{{ row.name }}</span
  64. >
  65. <span v-else>{{ row.name }}</span>
  66. </template>
  67. </template>
  68. </el-table-column>
  69. <el-table-column prop="archivesTotal" label="部门人数"></el-table-column>
  70. <el-table-column label="操作">
  71. <template #default="{ row }">
  72. <el-button type="primary" link @click="showEditDepartment(row)"
  73. >编辑</el-button
  74. >
  75. <el-button
  76. type="primary"
  77. link
  78. @click="showPersonnelManagementVisible(row)"
  79. >人员管理</el-button
  80. >
  81. <el-button type="danger" link @click="deleteDepartment(row)"
  82. >删除</el-button
  83. >
  84. </template>
  85. </el-table-column>
  86. </el-table>
  87. <el-pagination
  88. background
  89. class="justify-end mt-4"
  90. layout="total, prev, pager, next"
  91. :page-size="10"
  92. :total="totalDepartments"
  93. @change="handleCurrentChange"
  94. ></el-pagination>
  95. <el-dialog v-model="editDepartmentVisible" title="编辑部门">
  96. <h3 class="my-4">{{ props.channelName }}</h3>
  97. <el-form :label-width="100">
  98. <el-form-item label="部门名称" required>
  99. <el-input
  100. v-model="editDepartment.name"
  101. placeholder="请输入部门名称"
  102. ></el-input>
  103. </el-form-item>
  104. <el-form-item label="部门所属" required>
  105. <el-select
  106. v-model="editDepartment.departmentId"
  107. filterable
  108. placeholder="请选择"
  109. >
  110. <el-option label="无所属部门" :value="undefined"></el-option>
  111. <el-option
  112. v-for="item in treeDepartmentData"
  113. :key="item.id"
  114. :label="item.name"
  115. :value="item.id"
  116. >
  117. </el-option>
  118. </el-select>
  119. </el-form-item>
  120. <el-form-item label="排序">
  121. <el-input
  122. v-model="editDepartment.sort"
  123. type="number"
  124. placeholder="请输入值,值越大,在同属部门下排序越靠前"
  125. ></el-input>
  126. </el-form-item>
  127. </el-form>
  128. <template #footer>
  129. <el-button
  130. type="primary"
  131. :disabled="!editDepartment.name"
  132. @click="saveDepartment"
  133. >保存</el-button
  134. >
  135. </template>
  136. </el-dialog>
  137. <el-dialog v-model="visibleLastTimeImport">
  138. <template v-if="!importProgress.errorList.length">
  139. <div class="flex justify-center items-center" style="height: 300px">
  140. <div>
  141. <div class="flex items-center">
  142. <SuccessFilled
  143. class="text-green-600"
  144. style="height: 30px; width: 30px"
  145. ></SuccessFilled>
  146. <p class="font-bold text-blue-500 ml-2">
  147. 共找到{{ importProgress.total }}条数据,{{
  148. importProgress.progressTotal -
  149. importProgress.errorList.length
  150. }}条数据已成功处理
  151. </p>
  152. </div>
  153. <el-button
  154. class="w-full mt-10"
  155. type="primary"
  156. plain
  157. @click="
  158. visibleLastTimeImport = false;
  159. getDepartments();
  160. "
  161. >我知道了</el-button
  162. >
  163. </div>
  164. </div>
  165. </template>
  166. <template v-else>
  167. <div class="flex justify-between items-center mb-4 space-x-4">
  168. <p class="font-bold text-blue-500">
  169. 共找到{{ importProgress.total }}条数据,{{
  170. importProgress.progressTotal - importProgress.errorList.length
  171. }}条数据已成功处理<br />
  172. <span>
  173. {{
  174. importProgress.errorList.length
  175. }}条数据处理有误,请下载错误数据,修改后再上传本部分错误数据,正确数据无需再上传
  176. </span>
  177. </p>
  178. <el-button
  179. type="danger"
  180. v-if="importProgress.errorList.length"
  181. plain
  182. @click="downloadImportErrorData"
  183. >下载错误数据</el-button
  184. >
  185. <!-- <el-button type="warning" link @click="importUploadState = false"
  186. >重新上传</el-button
  187. > -->
  188. </div>
  189. <el-table :data="importProgress.errorList">
  190. <el-table-column
  191. v-for="(col, colIdx) in importDataHeader"
  192. :key="colIdx"
  193. >
  194. <template #header>{{ col }}</template>
  195. <template #default="{ $index }">
  196. <span>{{ importProgress.errorList[$index][colIdx] }}</span>
  197. </template>
  198. </el-table-column>
  199. </el-table>
  200. </template>
  201. </el-dialog>
  202. <el-dialog v-model="importDialogVisible" title="导入部门信息">
  203. <div
  204. v-if="!importUploadState"
  205. class="p-4 py-10 flex items-center justify-center flex-col"
  206. >
  207. <p class="my-4 text-gray-600 text-sm">
  208. 请下载模板文件,按照模板文件数据格式填充数据
  209. </p>
  210. <el-button
  211. type="primary"
  212. link
  213. class="my-6 px-4 py-2 bg-blue-500 text-white rounded-md"
  214. download="部门导入模板"
  215. :href="`${VITE_APP_OSS}/archives-import-template.xlsx`"
  216. tag="a"
  217. >一键导入excel模板下载</el-button
  218. >
  219. <el-upload
  220. ref="uploadRef"
  221. action="#"
  222. :show-file-list="false"
  223. :limit="2"
  224. :http-request="handleUpload"
  225. >
  226. <!-- :auto-upload="false -->
  227. <el-button type="primary"
  228. >上传名单文件,请用模板文件制作名单,否则会失败</el-button
  229. >
  230. </el-upload>
  231. <!-- <el-image
  232. class="w-full mt-3"
  233. :src="errorRemakeImg"
  234. :preview-src-list="[errorRemakeImg]"
  235. ></el-image> -->
  236. </div>
  237. <template v-else>
  238. <!-- <div
  239. v-if="importProgress.total != importProgress.progressTotal"
  240. class="flex mb-4"
  241. >
  242. <span>尚有数据未处理完成...</span>
  243. <el-button type="primary" link @click="refreshImportData"
  244. >点击刷新</el-button
  245. >
  246. </div> -->
  247. <div
  248. class="flex items-center justify-center"
  249. v-if="importUploadStatus == 'loading'"
  250. >
  251. 数据上传中,请稍后...
  252. <div class="ml-4">
  253. <el-button type="primary" plain @click="refreshImportData"
  254. >点击刷新</el-button
  255. >
  256. </div>
  257. </div>
  258. <div
  259. v-else-if="importUploadStatus == 'error'"
  260. class="flex justify-center items-center flex-col"
  261. >
  262. <div>
  263. <div class="text-red-500">数据上传失败,请重新上传</div>
  264. <el-button
  265. class="w-full mt-4"
  266. type="primary"
  267. plain
  268. @click="importUploadState = false"
  269. >重新上传</el-button
  270. >
  271. </div>
  272. </div>
  273. </template>
  274. </el-dialog>
  275. <el-dialog v-model="personnelManagementVisible" title="人员管理">
  276. <div class="flex justify-between items-center">
  277. <span
  278. >部门名称:<b>
  279. {{ personnelManagement.row?.name }}
  280. (所属渠道:{{ props.channelName }})</b
  281. ></span
  282. >
  283. <div class="space-x-4">
  284. <el-select
  285. v-model="appendPersonnelIds"
  286. multiple
  287. filterable
  288. remote
  289. :remote-method="searchArchives"
  290. >
  291. <el-option
  292. v-for="(item, index) in searchArchivesList"
  293. :key="index"
  294. :label="item.name"
  295. :value="item.id"
  296. ></el-option>
  297. </el-select>
  298. <el-button
  299. type="primary"
  300. :disabled="!appendPersonnelIds.length"
  301. @click="appendPersonnel"
  302. >确定</el-button
  303. >
  304. </div>
  305. </div>
  306. <el-divider></el-divider>
  307. <el-table :data="personnelManagement.list">
  308. <el-table-column label="姓名" prop="name"></el-table-column>
  309. <el-table-column
  310. label="性别"
  311. prop="gender"
  312. :formatter="v => (v.gender == 1 ? '男' : '女')"
  313. ></el-table-column>
  314. <el-table-column label="年龄" prop="birthday">
  315. <template #default="{ row }">
  316. {{ formatAge(row.birthday) }}
  317. </template>
  318. </el-table-column>
  319. <el-table-column label="档案编号" prop="id"></el-table-column>
  320. <el-table-column label="操作">
  321. <template #default="{ row }">
  322. <el-button type="danger" link @click="removePersonnel(row)"
  323. >移除</el-button
  324. >
  325. </template>
  326. </el-table-column>
  327. </el-table>
  328. <el-pagination
  329. class="justify-end mt-4"
  330. :current-page="personnelManagement.filter.page"
  331. :page-size="personnelManagement.filter.pageSize"
  332. :total="personnelManagement.total"
  333. background
  334. layout="total, prev, pager, next, jumper"
  335. @current-change="
  336. page => {
  337. personnelManagement.filter.page = page;
  338. getPersonnelList();
  339. }
  340. "
  341. ></el-pagination>
  342. </el-dialog>
  343. </div>
  344. </template>
  345. <script setup>
  346. import SearchArchivesSelect from "@/components/Archives/SearchArchivesSelect.vue";
  347. import { SuccessFilled } from "@element-plus/icons-vue";
  348. import { onMounted, ref, nextTick } from "vue";
  349. // 导入ElMessage和ElMessageBox组件
  350. import {
  351. ElMessage,
  352. ElMessageBox,
  353. ElLoading,
  354. progressProps
  355. } from "element-plus";
  356. // 导入route、router并定义
  357. import { useRoute, useRouter } from "vue-router";
  358. import errorRemake from "@/assets/errorRemake.png";
  359. import { request, formatAge } from "@/utils";
  360. import dayjs from "dayjs";
  361. import { watch } from "vue";
  362. const { VITE_APP_OSS } = import.meta.env;
  363. const [route, router] = [useRoute(), useRouter()];
  364. const props = defineProps({
  365. channelId: {
  366. default: undefined
  367. },
  368. channelName: {
  369. type: String,
  370. default: ""
  371. }
  372. });
  373. watch(
  374. () => props.channelId,
  375. (n, o) => {
  376. if (n) {
  377. try {
  378. nextTick(() => {
  379. filter.value.channelId = n;
  380. onSearch();
  381. });
  382. } catch (error) {
  383. console.log(error);
  384. }
  385. }
  386. },
  387. {
  388. deep: true,
  389. immediate: true
  390. }
  391. );
  392. const filter = ref({
  393. page: 1,
  394. pageSize: 10,
  395. key: "",
  396. type: 2,
  397. archivesId: "",
  398. channelId: undefined
  399. });
  400. const currentArchivesInfo = ref({ departmentId: "" });
  401. const errorRemakeImg = errorRemake;
  402. const departments = ref([{}]);
  403. const treeDepartmentData = ref([]);
  404. const totalDepartments = ref(0);
  405. const mapTree = (org, parentId) => {
  406. const haveChildren =
  407. Array.isArray(org.subDepartmentList) && org.subDepartmentList.length > 0;
  408. return {
  409. parentId,
  410. departmentId: parentId,
  411. ...org,
  412. children: haveChildren
  413. ? org.subDepartmentList?.map(r => mapTree(r, org.id))
  414. : []
  415. };
  416. };
  417. let filterTree = (value, arr) => {
  418. let newarr = [];
  419. arr.forEach(element => {
  420. if (element.name.indexOf(value) > -1) {
  421. // 判断条件
  422. newarr.push({ ...element, children: element.children });
  423. } else {
  424. if (element.children && element.children.length > 0) {
  425. let result = filterTree(value, element.children);
  426. if (result && result.length > 0) {
  427. let obj = {
  428. ...element,
  429. children: result
  430. };
  431. newarr.push(obj);
  432. }
  433. }
  434. }
  435. });
  436. return newarr;
  437. };
  438. const getFlatArr = (arr, key = "children") => {
  439. return arr.reduce((val, item) => {
  440. let flatArr = [...val, item];
  441. // 可以在此处限制各种需要的条件,在展示字段前加空格,——之类的,以及其它情况
  442. if (item[key]) {
  443. flatArr = [...flatArr, ...getFlatArr(item[key])];
  444. }
  445. return flatArr;
  446. }, []);
  447. };
  448. const handleChangeArchivesId = (info, options) => {
  449. try {
  450. currentArchivesInfo.value = options.archivesChannels.filter(
  451. v => v.id == props.channelId
  452. )[0];
  453. } catch (error) {
  454. currentArchivesInfo.value = {
  455. departmentId: ""
  456. };
  457. }
  458. };
  459. const getDepartments = async () => {
  460. const { data } = await request.get(
  461. `/archivesService/mechanism/department/list`,
  462. {
  463. params: filter.value
  464. }
  465. );
  466. if (!data.list?.length) {
  467. departments.value = [];
  468. treeDepartmentData.value = [];
  469. return;
  470. }
  471. let array = [];
  472. array = data.list?.map(org => mapTree(org));
  473. console.log("array", array);
  474. departments.value = filterTree(
  475. filter.value.type == 1 ? filter.value.key : "",
  476. array
  477. );
  478. treeDepartmentData.value = getFlatArr(array);
  479. totalDepartments.value = data.total;
  480. };
  481. const hightLight = (keyWord, suggtion) => {
  482. // 使用 regexp 构造函数,因为这里要匹配的是一个变量
  483. const reg = new RegExp(keyWord, "ig");
  484. const newSrt = String(suggtion).replace(reg, function (p) {
  485. return `<span style="color: #67C23A">${p}</span>`;
  486. });
  487. return newSrt;
  488. };
  489. const onSearch = () => {
  490. getDepartments();
  491. };
  492. const onReset = () => {
  493. filter.value.page = 1;
  494. filter.value.pageSize = 10;
  495. filter.value.key = "";
  496. filter.value.type = 2;
  497. filter.value.archivesId = "";
  498. onSearch();
  499. };
  500. function handleCurrentChange(page) {
  501. filter.value.page = page;
  502. getDepartments();
  503. }
  504. const deleteDepartment = async row => {
  505. await ElMessageBox.confirm(`确认要删除这个部门吗?`, "提示");
  506. await request.post(`/archivesService/mechanism/department/delete`, {
  507. id: row.id
  508. });
  509. ElMessage.success("删除成功");
  510. await getDepartments();
  511. };
  512. const editDepartmentVisible = ref(false);
  513. const editDepartment = ref({
  514. id: "",
  515. name: "",
  516. departmentId: undefined,
  517. sort: undefined
  518. });
  519. const showEditDepartment = department => {
  520. if (department) {
  521. editDepartment.value = { ...department };
  522. } else {
  523. editDepartment.value = {
  524. id: "",
  525. name: "",
  526. departmentId: undefined,
  527. sort: undefined
  528. };
  529. }
  530. editDepartmentVisible.value = true;
  531. };
  532. const saveDepartment = async () => {
  533. await request.post(
  534. `/archivesService/mechanism/department/${
  535. editDepartment.value.id ? "/update/name" : "/create"
  536. }`,
  537. {
  538. ...editDepartment.value,
  539. sort: Number(editDepartment.value.sort),
  540. channelId: props.channelId
  541. }
  542. );
  543. ElMessage.success("保存成功");
  544. editDepartmentVisible.value = false;
  545. getDepartments();
  546. };
  547. const personnelManagementVisible = ref(false);
  548. const personnelManagement = ref({
  549. filter: {
  550. page: 1,
  551. pageSize: 10,
  552. needSubArchives: 1,
  553. departmentId: 0
  554. },
  555. row: undefined,
  556. list: [],
  557. total: 0
  558. });
  559. const showPersonnelManagementVisible = row => {
  560. personnelManagement.value.row = row;
  561. personnelManagement.value.filter.page = 1;
  562. personnelManagement.value.filter.departmentId = row.id;
  563. personnelManagementVisible.value = true;
  564. getPersonnelList();
  565. };
  566. const getPersonnelList = async () => {
  567. const { data } = await request.get(
  568. "/archivesService/mechanism/archives/paginate",
  569. {
  570. params: {
  571. ...personnelManagement.value.filter
  572. }
  573. }
  574. );
  575. personnelManagement.value.list = data.list;
  576. personnelManagement.value.total = data.total;
  577. };
  578. const removePersonnel = async row => {
  579. await ElMessageBox.confirm(`确认要移除这个人吗?`, "提示");
  580. await request.post(`/archivesService/mechanism/department/removeArchives`, {
  581. archivesId: row.id,
  582. departmentId: personnelManagement.value.row.id
  583. });
  584. ElMessage.success("删除成功");
  585. getDepartments();
  586. await getPersonnelList();
  587. };
  588. const appendPersonnelIds = ref([]);
  589. const appendPersonnel = async () => {
  590. await request.post(`/archivesService/mechanism/department/pushArchives`, {
  591. departmentId: personnelManagement.value.row.id,
  592. archivesIds: appendPersonnelIds.value
  593. });
  594. ElMessage.success("添加成功");
  595. appendPersonnelIds.value = [];
  596. getDepartments();
  597. getPersonnelList();
  598. };
  599. const searchArchivesList = ref([]);
  600. const searchArchives = async q => {
  601. if (!q) return;
  602. const { data } = await request.get(
  603. "/archivesService/mechanism/archives/paginate",
  604. {
  605. params: {
  606. page: 1,
  607. pageSize: 999,
  608. needSubArchives: 1,
  609. departmentId: 0,
  610. inDepartment: 2,
  611. keyword: q,
  612. channelId: props.channelId
  613. }
  614. }
  615. );
  616. searchArchivesList.value = data.list;
  617. };
  618. const importDialogVisible = ref(false);
  619. const importUploadState = ref(false);
  620. const visibleLastTimeImport = ref(false);
  621. const importUploadStatus = ref("");
  622. const importDataHeader = ref([]);
  623. const importProgress = ref({
  624. total: 0,
  625. progressTotal: 0,
  626. errorList: []
  627. });
  628. const handleUpload = async opt => {
  629. console.log(opt);
  630. importUploadStatus.value = "loading";
  631. const loading = ElLoading.service({
  632. lock: true,
  633. text: "开始上传",
  634. background: "rgba(255, 255, 255, 0.7)"
  635. });
  636. const fd = new FormData();
  637. fd.append("file", opt.file);
  638. fd.append("channelId", props.channelId);
  639. try {
  640. await request.post("/archivesService/mechanism/archives/import", fd, {
  641. headers: {
  642. "Content-Type": "multipart/form-data"
  643. }
  644. });
  645. ElMessage.success("上传成功");
  646. await getImportProgress();
  647. if (importProgress.value?.status > 1) {
  648. importDialogVisible.value = false;
  649. visibleLastTimeImport.value = true;
  650. }
  651. // importUploadState.value = true;
  652. } catch (e) {
  653. console.log("error", e);
  654. importUploadState.value = true;
  655. importUploadStatus.value = "error";
  656. } finally {
  657. loading.close();
  658. }
  659. };
  660. const getImportProgress = async () => {
  661. const { data } = await request.get(
  662. "/archivesService/mechanism/archives/import/progress"
  663. );
  664. importProgress.value = {
  665. total: data.progress.total,
  666. progressTotal: data.progress.progressTotal,
  667. errorList: data.progress.errorList.slice(2),
  668. status: data.progress.status
  669. };
  670. importDataHeader.value = data.progress.errorList[1];
  671. // importUploadState.value = !!importProgress.value.total;
  672. };
  673. const refreshImportData = async () => {
  674. await getImportProgress();
  675. if (importProgress.value?.importProgress > 1) {
  676. importDialogVisible.value = false;
  677. visibleLastTimeImport.value = true;
  678. }
  679. ElMessage.success("刷新成功");
  680. };
  681. const downloadImportErrorData = async () => {
  682. try {
  683. const response = await request.post(
  684. "/archivesService/mechanism/archives/import/error/download",
  685. {},
  686. {
  687. responseType: "blob"
  688. }
  689. );
  690. console.log(response);
  691. const link = document.createElement("a");
  692. const blob = new Blob([response], {
  693. type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  694. });
  695. const body = document.body;
  696. link.href = window.URL.createObjectURL(blob);
  697. link.download = `错误数据-${dayjs(Date.now()).format(
  698. "YYYYMMDDHHmmss"
  699. )}.xlsx`;
  700. body.appendChild(link);
  701. link.click();
  702. body.removeChild(link);
  703. window.URL.revokeObjectURL(link.href);
  704. ElMessage.success("下载成功!");
  705. } catch (error) {
  706. ElMessage.error("下载失败,请重试!");
  707. console.error("Export error:", error);
  708. }
  709. };
  710. // onMounted(onSearch);
  711. </script>
  712. <style lang="scss"></style>