selectUserDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <template>
  2. <u-popup
  3. :show="visible"
  4. mode="bottom"
  5. :round="10"
  6. :closeOnClickOverlay="false"
  7. @close="close"
  8. class="select-user-popup"
  9. >
  10. <view class="popup-content">
  11. <!-- 头部 -->
  12. <view class="popup-header">
  13. <text class="popup-title">选择人员</text>
  14. <view class="close-btn" @click="close">×</view>
  15. </view>
  16. <!-- 搜索行 -->
  17. <view class="search-row">
  18. <input
  19. v-model="searchVal"
  20. placeholder="请输入名称/工号/账号"
  21. class="search-input"
  22. @confirm="doSearch"
  23. />
  24. <view class="search-btn" @click="doSearch">
  25. <text>🔍</text>
  26. <text>搜索</text>
  27. </view>
  28. </view>
  29. <!-- 部门选择行 -->
  30. <view class="dept-row">
  31. <view class="dept-btn" @click="showTreePicker">
  32. <text>🏢</text>
  33. <text>{{ deptName || "全部部门" }}</text>
  34. <text class="arrow">▾</text>
  35. </view>
  36. <!-- 清空部门筛选 -->
  37. <view class="clear-dept" v-if="categoryLevelId" @click="clearDept">
  38. <text>✕</text>
  39. </view>
  40. </view>
  41. <!-- 列表 -->
  42. <scroll-view class="popup-body" scroll-y @scrolltolower="scrolltolower">
  43. <view
  44. v-for="(item, index) in listData"
  45. :key="item.id"
  46. class="list-item"
  47. :class="{ active: isSelected(item.id) }"
  48. @click="toggleItem(item, index)"
  49. >
  50. <!-- 自定义选中图标 -->
  51. <view class="item-check">
  52. <view class="check-box" :class="{ checked: isSelected(item.id) }">
  53. <text v-if="isSelected(item.id)" class="check-mark">✓</text>
  54. </view>
  55. </view>
  56. <view class="item-info">
  57. <view class="item-top">
  58. <text class="item-name">{{ item.name }}</text>
  59. <text class="item-code">{{ item.loginName }}</text>
  60. </view>
  61. <view class="item-bottom">
  62. <text>工号:{{ item.jobNumber || "-" }}</text>
  63. <text>部门:{{ item.groupName || "-" }}</text>
  64. </view>
  65. </view>
  66. </view>
  67. <view style="height: 20rpx"></view>
  68. <view v-if="loading" class="load-more">加载中...</view>
  69. <view v-if="isEnd && listData.length > 0" class="load-more"
  70. >没有更多了</view
  71. >
  72. <u-empty
  73. class="no-data"
  74. v-if="!loading && listData.length === 0"
  75. text="暂无数据"
  76. />
  77. </scroll-view>
  78. <!-- 底部 -->
  79. <view class="footer">
  80. <view class="bottom-left" v-if="isAll == 1" @click="toggleSelectAll">
  81. <!-- 自定义全选复选框 -->
  82. <view class="check-box" :class="{ checked: seletedAll }">
  83. <text v-if="seletedAll" class="check-mark">✓</text>
  84. </view>
  85. <text class="select-all-text">{{
  86. seletedAll ? "取消全选" : "全选"
  87. }}</text>
  88. </view>
  89. <view class="bottom-left" v-else>
  90. <text class="select-all-text">单选模式</text>
  91. </view>
  92. <view
  93. class="confirm-btn"
  94. :class="{ disabled: selectedIds.length === 0 }"
  95. @click="confirmSelect"
  96. >
  97. 确定 ({{ selectedIds.length }})
  98. </view>
  99. </view>
  100. </view>
  101. <!-- 部门树选择器 -->
  102. <ba-tree-picker
  103. ref="treePicker"
  104. key="verify"
  105. :multiple="false"
  106. @select-change="onDeptConfirm"
  107. title="选择部门"
  108. :localdata="classificationList"
  109. valueKey="id"
  110. textKey="name"
  111. />
  112. <u-toast ref="uToast" />
  113. </u-popup>
  114. </template>
  115. <script>
  116. import { listOrganizations, getUserPage } from "@/api/myTicket/index.js";
  117. import baTreePicker from "@/components/ba-tree-picker/ba-tree-picker.vue";
  118. export default {
  119. components: {
  120. baTreePicker,
  121. },
  122. data() {
  123. return {
  124. visible: false,
  125. // 列表数据
  126. listData: [],
  127. selectedIds: [], // 选中项的 id 数组
  128. page: 1,
  129. size: 20,
  130. isEnd: true,
  131. loading: false,
  132. searchVal: "",
  133. // 部门筛选
  134. classificationList: [],
  135. categoryLevelId: null,
  136. deptName: "全部部门",
  137. // 模式:1多选,2单选
  138. isAll: "1",
  139. seletedAll: false,
  140. };
  141. },
  142. watch: {
  143. selectedIds: {
  144. handler(val) {
  145. // 更新全选状态
  146. if (this.listData.length > 0) {
  147. this.seletedAll =
  148. val.length === this.listData.length && val.length > 0;
  149. }
  150. },
  151. deep: true,
  152. immediate: true,
  153. },
  154. },
  155. methods: {
  156. // ============ 外部调用 ============
  157. open(isAll) {
  158. this.isAll = isAll || "1";
  159. this.visible = true;
  160. // 重置状态
  161. this.listData = [];
  162. this.selectedIds = [];
  163. this.searchVal = "";
  164. this.categoryLevelId = null;
  165. this.isEnd = false;
  166. this.seletedAll = false;
  167. // 加载数据
  168. this.getClassify();
  169. },
  170. close() {
  171. this.visible = false;
  172. },
  173. // ============ 数据加载 ============
  174. async getClassify() {
  175. try {
  176. const res = await listOrganizations(1);
  177. this.classificationList = res || [];
  178. // ========== 关键改动:默认选中第一个部门 ==========
  179. if (this.classificationList.length > 0) {
  180. const firstDept = this.classificationList[0];
  181. this.categoryLevelId = firstDept.id;
  182. this.deptName = firstDept.name;
  183. } else {
  184. this.categoryLevelId = null;
  185. this.deptName = "全部部门";
  186. }
  187. this.page = 1;
  188. this.getList();
  189. } catch (e) {
  190. console.error("获取部门列表失败", e);
  191. }
  192. },
  193. async getList() {
  194. if (this.loading || this.isEnd) return;
  195. this.loading = true;
  196. try {
  197. const params = {
  198. pageNum: this.page,
  199. size: this.size,
  200. name: this.searchVal || undefined,
  201. groupId: this.categoryLevelId || undefined,
  202. };
  203. const res = await getUserPage(params);
  204. const list = res.list || [];
  205. if (this.page === 1) {
  206. this.listData = list;
  207. // 如果全选状态开启,自动选中所有
  208. if (this.seletedAll) {
  209. this.selectedIds = this.listData.map((item) => item.id);
  210. }
  211. } else {
  212. this.listData = this.listData.concat(list);
  213. // 追加数据时,如果全选开启,新数据自动选中
  214. if (this.seletedAll) {
  215. const newIds = list.map((item) => item.id);
  216. this.selectedIds = [...this.selectedIds, ...newIds];
  217. }
  218. }
  219. this.isEnd =
  220. list.length < this.size || this.listData.length >= res.count;
  221. this.page += 1;
  222. } catch (e) {
  223. console.error("获取人员列表失败", e);
  224. this.$refs.uToast.show({ type: "error", message: "加载失败,请重试" });
  225. } finally {
  226. this.loading = false;
  227. }
  228. },
  229. // ============ 搜索 ============
  230. doSearch() {
  231. this.page = 1;
  232. this.isEnd = false;
  233. this.selectedIds = [];
  234. this.getList();
  235. },
  236. // ============ 滚动加载更多 ============
  237. scrolltolower() {
  238. if (!this.isEnd && !this.loading) {
  239. this.getList();
  240. }
  241. },
  242. // ============ 部门树选择 ============
  243. showTreePicker() {
  244. this.$refs.treePicker._show();
  245. },
  246. onDeptConfirm(data) {
  247. const id = data[0];
  248. this.categoryLevelId = id;
  249. // 查找部门名称
  250. const findDept = (list) => {
  251. for (const item of list) {
  252. if (item.id === id) {
  253. return item.name;
  254. }
  255. if (item.children && item.children.length > 0) {
  256. const found = findDept(item.children);
  257. if (found) return found;
  258. }
  259. }
  260. return null;
  261. };
  262. this.deptName = findDept(this.classificationList) || "全部部门";
  263. this.page = 1;
  264. this.isEnd = false;
  265. this.selectedIds = [];
  266. this.getList();
  267. },
  268. // ============ 清空部门筛选 ============
  269. clearDept() {
  270. this.categoryLevelId = null;
  271. this.deptName = "全部部门";
  272. this.page = 1;
  273. this.isEnd = false;
  274. this.selectedIds = [];
  275. this.getList();
  276. },
  277. // ============ 判断是否选中 ============
  278. isSelected(id) {
  279. return this.selectedIds.includes(id);
  280. },
  281. // ============ 点击切换选中 ============
  282. toggleItem(item, index) {
  283. if (item.disabled) return;
  284. // 单选模式:只能选一个
  285. if (this.isAll !== "1") {
  286. if (this.selectedIds.includes(item.id)) {
  287. this.selectedIds = [];
  288. } else {
  289. this.selectedIds = [item.id];
  290. }
  291. return;
  292. }
  293. // 多选模式:切换
  294. const idx = this.selectedIds.indexOf(item.id);
  295. if (idx > -1) {
  296. this.selectedIds.splice(idx, 1);
  297. } else {
  298. this.selectedIds.push(item.id);
  299. }
  300. // 触发响应式更新
  301. this.selectedIds = [...this.selectedIds];
  302. },
  303. // ============ 全选 ============
  304. toggleSelectAll() {
  305. if (this.seletedAll) {
  306. this.selectedIds = [];
  307. } else {
  308. this.selectedIds = this.listData.map((item) => item.id);
  309. }
  310. this.seletedAll = !this.seletedAll;
  311. },
  312. // ============ 确认选择 ============
  313. confirmSelect() {
  314. if (this.selectedIds.length === 0) {
  315. this.$refs.uToast.show({ type: "warning", message: "请至少选择一人" });
  316. return;
  317. }
  318. const selected = this.listData.filter((item) =>
  319. this.selectedIds.includes(item.id),
  320. );
  321. this.$emit("confirm", selected);
  322. this.close();
  323. },
  324. },
  325. };
  326. </script>
  327. <style lang="scss" scoped>
  328. .select-user-popup {
  329. /deep/ .u-popup__content {
  330. border-radius: 32rpx 32rpx 0 0;
  331. overflow: hidden;
  332. }
  333. .popup-content {
  334. height: 75vh;
  335. display: flex;
  336. flex-direction: column;
  337. background: #f5f7fb;
  338. }
  339. // ===== 头部 =====
  340. .popup-header {
  341. display: flex;
  342. justify-content: space-between;
  343. align-items: center;
  344. padding: 30rpx 32rpx;
  345. background: #fff;
  346. border-bottom: 2rpx solid #eef2f6;
  347. flex-shrink: 0;
  348. .popup-title {
  349. font-size: 36rpx;
  350. font-weight: bold;
  351. color: #1f2b3c;
  352. }
  353. .close-btn {
  354. width: 60rpx;
  355. height: 60rpx;
  356. display: flex;
  357. align-items: center;
  358. justify-content: center;
  359. font-size: 52rpx;
  360. color: #8e9aae;
  361. line-height: 1;
  362. }
  363. }
  364. // ===== 搜索行 =====
  365. .search-row {
  366. display: flex;
  367. align-items: center;
  368. padding: 16rpx 24rpx 8rpx;
  369. background: #fff;
  370. flex-shrink: 0;
  371. gap: 16rpx;
  372. .search-input {
  373. flex: 1;
  374. height: 70rpx;
  375. background: #f5f7fb;
  376. border-radius: 48rpx;
  377. padding: 0 24rpx;
  378. font-size: 28rpx;
  379. border: 2rpx solid transparent;
  380. &:focus {
  381. border-color: #2979ff;
  382. }
  383. }
  384. .search-btn {
  385. display: flex;
  386. align-items: center;
  387. justify-content: center;
  388. height: 70rpx;
  389. padding: 0 28rpx;
  390. background: #2979ff;
  391. border-radius: 48rpx;
  392. color: #fff;
  393. font-size: 28rpx;
  394. gap: 8rpx;
  395. flex-shrink: 0;
  396. .text {
  397. font-size: 26rpx;
  398. }
  399. }
  400. }
  401. // ===== 部门选择行 =====
  402. .dept-row {
  403. display: flex;
  404. align-items: center;
  405. padding: 8rpx 24rpx 16rpx;
  406. background: #fff;
  407. border-bottom: 2rpx solid #eef2f6;
  408. flex-shrink: 0;
  409. gap: 16rpx;
  410. .dept-btn {
  411. display: inline-flex;
  412. align-items: center;
  413. height: 60rpx;
  414. padding: 0 24rpx;
  415. background: #e8edf4;
  416. border-radius: 48rpx;
  417. color: #2979ff;
  418. font-size: 26rpx;
  419. gap: 8rpx;
  420. .arrow {
  421. font-size: 20rpx;
  422. margin-left: 4rpx;
  423. }
  424. }
  425. .clear-dept {
  426. display: flex;
  427. align-items: center;
  428. justify-content: center;
  429. width: 44rpx;
  430. height: 44rpx;
  431. border-radius: 50%;
  432. background: #f0f0f0;
  433. color: #999;
  434. font-size: 24rpx;
  435. }
  436. }
  437. // ===== 列表 =====
  438. .popup-body {
  439. flex: 1;
  440. overflow-y: auto;
  441. padding: 0 24rpx;
  442. background: #f5f7fb;
  443. .list-item {
  444. display: flex;
  445. align-items: center;
  446. padding: 24rpx 20rpx;
  447. background: #fff;
  448. border-radius: 24rpx;
  449. margin-top: 20rpx;
  450. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  451. transition: all 0.2s;
  452. border: 3rpx solid transparent;
  453. &.active {
  454. border-color: #2979ff;
  455. background: #f0f7ff;
  456. }
  457. &:active {
  458. transform: scale(0.99);
  459. }
  460. .item-check {
  461. margin-right: 20rpx;
  462. flex-shrink: 0;
  463. }
  464. // 自定义复选框
  465. .check-box {
  466. width: 40rpx;
  467. height: 40rpx;
  468. border-radius: 50%;
  469. border: 3rpx solid #ccc;
  470. display: flex;
  471. align-items: center;
  472. justify-content: center;
  473. transition: all 0.2s;
  474. background: #fff;
  475. &.checked {
  476. background: #2979ff;
  477. border-color: #2979ff;
  478. }
  479. .check-mark {
  480. color: #fff;
  481. font-size: 24rpx;
  482. font-weight: bold;
  483. }
  484. }
  485. .item-info {
  486. flex: 1;
  487. min-width: 0;
  488. .item-top {
  489. display: flex;
  490. justify-content: space-between;
  491. align-items: center;
  492. margin-bottom: 8rpx;
  493. .item-name {
  494. font-size: 30rpx;
  495. font-weight: 600;
  496. color: #1f2b3c;
  497. }
  498. .item-code {
  499. font-size: 24rpx;
  500. color: #8e9aae;
  501. }
  502. }
  503. .item-bottom {
  504. display: flex;
  505. justify-content: space-between;
  506. font-size: 24rpx;
  507. color: #8e9aae;
  508. text {
  509. flex: 1;
  510. overflow: hidden;
  511. text-overflow: ellipsis;
  512. white-space: nowrap;
  513. }
  514. }
  515. }
  516. }
  517. .no-data {
  518. margin-top: 40vh;
  519. }
  520. .load-more {
  521. text-align: center;
  522. font-size: 26rpx;
  523. color: #aaa;
  524. padding: 30rpx 0 60rpx;
  525. }
  526. }
  527. // ===== 底部 =====
  528. .footer {
  529. display: flex;
  530. justify-content: space-between;
  531. align-items: center;
  532. padding: 16rpx 24rpx;
  533. background: #fff;
  534. border-top: 2rpx solid #eef2f6;
  535. flex-shrink: 0;
  536. min-height: 90rpx;
  537. .bottom-left {
  538. display: flex;
  539. align-items: center;
  540. gap: 12rpx;
  541. .check-box {
  542. width: 40rpx;
  543. height: 40rpx;
  544. border-radius: 4rpx;
  545. border: 3rpx solid #ccc;
  546. display: flex;
  547. align-items: center;
  548. justify-content: center;
  549. transition: all 0.2s;
  550. background: #fff;
  551. &.checked {
  552. background: #2979ff;
  553. border-color: #2979ff;
  554. }
  555. .check-mark {
  556. color: #fff;
  557. font-size: 28rpx;
  558. font-weight: bold;
  559. }
  560. }
  561. .select-all-text {
  562. font-size: 28rpx;
  563. color: #333;
  564. }
  565. }
  566. .confirm-btn {
  567. height: 72rpx;
  568. padding: 0 40rpx;
  569. border-radius: 48rpx;
  570. background: #2979ff;
  571. color: #fff;
  572. font-size: 28rpx;
  573. display: flex;
  574. align-items: center;
  575. justify-content: center;
  576. &.disabled {
  577. background: #ccc;
  578. color: #999;
  579. }
  580. }
  581. }
  582. }
  583. </style>