cascade-picker.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <template name="cascade-picker">
  2. <view
  3. class="aui-picker"
  4. v-if="SHOW"
  5. :class="{ 'aui-picker-in': FADE == 1, 'aui-picker-out': FADE == 0 }"
  6. >
  7. <view class="aui-mask" @click.stop="close"></view>
  8. <view class="aui-picker-main">
  9. <view class="aui-picker-header">
  10. <view class="aui-picker-header-icon" @click.stop="close">取消</view>
  11. <view class="aui-picker-title" v-if="title">{{ title }}</view>
  12. <view
  13. class="aui-picker-header-icon aui-picker-confirm"
  14. @click.stop="_confirm"
  15. >确认</view
  16. >
  17. </view>
  18. <view class="aui-picker-nav">
  19. <view
  20. class="aui-picker-navitem"
  21. v-if="nav.length > 0"
  22. v-for="(item, index) in nav"
  23. :key="index"
  24. :data-index="index"
  25. :class="[
  26. index == navCurrentIndex ? 'active' : '',
  27. 'aui-picker-navitem-' + index,
  28. ]"
  29. :style="{ margin: nav.length > 2 ? '0 25rpx 0 0' : '0 45rpx 0 0' }"
  30. @click.stop="_changeNav($event)"
  31. >{{ item.name }}
  32. </view>
  33. <view
  34. class="aui-picker-navitem"
  35. :key="nav.length"
  36. :data-index="nav.length"
  37. :class="[
  38. nav.length == navCurrentIndex ? 'active' : '',
  39. 'aui-picker-navitem-' + nav.length,
  40. ]"
  41. :style="{ margin: nav.length > 2 ? '0 25rpx 0 0' : '0 45rpx 0 0' }"
  42. @click.stop="_changeNav($event)"
  43. >请选择
  44. </view>
  45. <view
  46. class="aui-picker-navborder"
  47. :style="{ left: navBorderLeft + 'px' }"
  48. ></view>
  49. </view>
  50. <view class="aui-picker-content">
  51. <view class="aui-picker-lists">
  52. <view
  53. class="aui-picker-list"
  54. v-for="(list, index) in queryItems.length + 1"
  55. :key="index"
  56. :data-index="index"
  57. :class="[index == navCurrentIndex ? 'active' : '']"
  58. >
  59. <view class="aui-picker-list-warp" v-if="index == 0">
  60. <view
  61. class="aui-picker-item"
  62. v-for="(item, key) in items"
  63. v-if="item.pid == '0'"
  64. :key="key"
  65. :data-pindex="index"
  66. :data-index="key"
  67. :data-id="item.id"
  68. :data-pid="item.pid"
  69. :data-name="item.name"
  70. :data-type="item.type"
  71. :class="{
  72. active: result.length > index && result[index].id == item.id,
  73. }"
  74. :style="{
  75. background:
  76. touchConfig.index == key && touchConfig.pindex == index
  77. ? touchConfig.style.background
  78. : '',
  79. }"
  80. @click.stop="_chooseItem($event)"
  81. @touchstart="_btnTouchStart($event)"
  82. @touchmove="_btnTouchEnd($event)"
  83. @touchend="_btnTouchEnd($event)"
  84. >{{ item.name }}
  85. </view>
  86. </view>
  87. <view class="aui-picker-list-warp" v-else>
  88. <view
  89. class="aui-picker-item"
  90. v-for="(item, key) in queryItems[index - 1]"
  91. :key="key"
  92. :data-pindex="index"
  93. :data-index="key"
  94. :data-id="item.id"
  95. :data-pid="item.pid"
  96. :data-name="item.name"
  97. :data-type="item.type"
  98. :class="{
  99. active: result.length > index && result[index].id == item.id,
  100. }"
  101. :style="{
  102. background:
  103. touchConfig.index == key && touchConfig.pindex == index
  104. ? touchConfig.style.background
  105. : '',
  106. }"
  107. @click.stop="_chooseItem($event)"
  108. @touchstart="_btnTouchStart($event)"
  109. @touchmove="_btnTouchEnd($event)"
  110. @touchend="_btnTouchEnd($event)"
  111. >{{ item.name }}
  112. </view>
  113. </view>
  114. </view>
  115. </view>
  116. </view>
  117. </view>
  118. </view>
  119. </template>
  120. <script>
  121. export default {
  122. name: "cascade-picker",
  123. props: {
  124. title: {
  125. //标题
  126. type: String,
  127. default: "",
  128. },
  129. layer: {
  130. //控制几级联动,默认无限级(跟随数据有无下级)
  131. type: Number,
  132. default: null,
  133. },
  134. data: {
  135. //数据 如:[{id: '', name: '', children: [{id: '', name: ''}]}]
  136. type: Array,
  137. default() {
  138. return [
  139. // [{id: '', name: '', children: [{id: '', name: ''}]}]
  140. ];
  141. },
  142. },
  143. value: {
  144. // 新增初始选中值
  145. type: Array,
  146. default: () => [],
  147. },
  148. },
  149. data() {
  150. return {
  151. SHOW: false,
  152. FADE: -1,
  153. nav: [],
  154. items: [], // 第一级数据列表
  155. queryItems: [], // 各级数据列表
  156. navCurrentIndex: 0, // 当前选中的导航项索引
  157. navBorderLeft: 26, // 导航栏的边框左侧位置
  158. result: [],
  159. touchConfig: {
  160. index: -1,
  161. pindex: -1,
  162. style: {
  163. color: "#214579",
  164. background: "#EFEFEF",
  165. },
  166. },
  167. selectedData: [], // 用于回显数据的属性
  168. };
  169. },
  170. watch: {
  171. data() {
  172. const _this = this;
  173. const data = _this.data;
  174. _this.items = _this._flatten(data, "0");
  175. console.log(_this.items, "_this.items");
  176. },
  177. },
  178. methods: {
  179. // 打开
  180. open() {
  181. const _this = this;
  182. _this.reset(); //打开时重置picker
  183. // _this._restoreSelectedData()
  184. return new Promise(function (resolve, reject) {
  185. _this.SHOW = true;
  186. _this.FADE = 1;
  187. resolve();
  188. });
  189. },
  190. // 关闭
  191. close() {
  192. const _this = this;
  193. return new Promise(function (resolve, reject) {
  194. _this.FADE = 0;
  195. const _hidetimer = setTimeout(() => {
  196. _this.SHOW = false;
  197. _this.FADE = -1;
  198. clearTimeout(_hidetimer);
  199. resolve();
  200. }, 100);
  201. });
  202. },
  203. //重置
  204. reset() {
  205. const _this = this;
  206. _this.queryItems = [];
  207. _this.nav = [];
  208. _this.navBorderLeft = 26;
  209. _this.navCurrentIndex = 0;
  210. _this.result = [];
  211. },
  212. //导航栏切换
  213. _changeNav(e) {
  214. const _this = this;
  215. const index = Number(e.currentTarget.dataset.index);
  216. _this.navCurrentIndex = index;
  217. const _el = uni
  218. .createSelectorQuery()
  219. .in(this)
  220. .select(".aui-picker-navitem-" + index);
  221. _el
  222. .boundingClientRect((data) => {
  223. _this.navBorderLeft = data.left + 10;
  224. })
  225. .exec();
  226. },
  227. //数据选择
  228. _chooseItem(e) {
  229. const _this = this;
  230. const id = e.currentTarget.dataset.id;
  231. const name = e.currentTarget.dataset.name;
  232. const pid = e.currentTarget.dataset.pid;
  233. const type = e.currentTarget.dataset.type;
  234. const _arr = [];
  235. // _this.result[_this.navCurrentIndex] = {
  236. // id: id,
  237. // name: name,
  238. // pid: pid,
  239. // type: type,
  240. // };
  241. const item = {
  242. id: e.currentTarget.dataset.id,
  243. name: e.currentTarget.dataset.name,
  244. pid: e.currentTarget.dataset.pid,
  245. type: e.currentTarget.dataset.type,
  246. };
  247. // 清除后续层级数据
  248. _this.result = _this.result.slice(0, _this.navCurrentIndex + 1);
  249. _this.result[_this.navCurrentIndex] = item;
  250. _this.queryItems = _this.queryItems.slice(0, _this.navCurrentIndex);
  251. _this.nav = _this.nav.slice(0, _this.navCurrentIndex);
  252. if (
  253. (!_this._isDefine(_this.layer) &&
  254. _this._isDefine(_this._deepQuery(_this.data, id).children)) ||
  255. (_this.navCurrentIndex < Number(_this.layer) - 1 &&
  256. _this._isDefine(_this._deepQuery(_this.data, id).children))
  257. ) {
  258. //有下级数据
  259. _this
  260. ._deepQuery(_this.data, id)
  261. .children.forEach(function (item, index) {
  262. _arr.push({
  263. id: item.id,
  264. name: item.name,
  265. pid: id,
  266. type: item.type,
  267. });
  268. });
  269. if (_this.navCurrentIndex == _this.queryItems.length) {
  270. //选择数据
  271. _this.queryItems.push(_arr);
  272. _this.nav.push({ name: name });
  273. } else {
  274. //重新选择数据
  275. _this.queryItems.splice(_this.navCurrentIndex + 1, 1);
  276. _this.nav.splice(_this.navCurrentIndex + 1, 1);
  277. _this.queryItems.splice(_this.navCurrentIndex, 1, _arr);
  278. _this.nav.splice(_this.navCurrentIndex, 1, { name: name });
  279. }
  280. _this.navCurrentIndex = _this.navCurrentIndex + 1;
  281. const _el = uni
  282. .createSelectorQuery()
  283. .in(this)
  284. .select(".aui-picker-navitem-" + _this.navCurrentIndex);
  285. setTimeout(() => {
  286. _el
  287. .boundingClientRect((data) => {
  288. _this.navBorderLeft = data.left + 10;
  289. })
  290. .exec();
  291. }, 100);
  292. } else {
  293. //无下级数据且最后一级数据的type为1时,则可以确认关闭
  294. // console.log(_this.result,type,"typetypetypetypetype",e.currentTarget.dataset)
  295. _this._confirm();
  296. }
  297. },
  298. _hasChildren(id) {
  299. return this._deepQuery(this.data, id)?.children?.length > 0;
  300. },
  301. _confirm() {
  302. const _this = this;
  303. const lastItem = _this.result[_this.result.length - 1];
  304. console.log(lastItem);
  305. console.log(_this.result);
  306. if (lastItem && lastItem.type === 1) {
  307. _this.close().then(() => {
  308. _this.selectedData = [..._this.result]; // 保存选择的数据
  309. _this.$emit("callback", { status: 0, data: _this.result });
  310. });
  311. } else {
  312. uni.$u.toast("请选择最后一级分类");
  313. }
  314. },
  315. //递归遍历——将树形结构数据转化为数组格式
  316. _flatten(tree, pid) {
  317. return tree.reduce(
  318. (arr, { id, name, type, children = [] }) =>
  319. arr.concat([{ id, name, pid, type }], this._flatten(children, id)),
  320. []
  321. );
  322. },
  323. //根据id查询对应的数据(如查询id=10100对应的对象)
  324. _deepQuery(tree, id) {
  325. let isGet = false;
  326. let retNode = null;
  327. function deepSearch(tree, id) {
  328. for (let i = 0; i < tree.length; i++) {
  329. if (tree[i].children && tree[i].children.length > 0) {
  330. deepSearch(tree[i].children, id);
  331. }
  332. if (id === tree[i].id || isGet) {
  333. isGet || (retNode = tree[i]);
  334. isGet = true;
  335. break;
  336. }
  337. }
  338. }
  339. deepSearch(tree, id);
  340. return retNode;
  341. },
  342. /***判断字符串是否为空
  343. @param {string} str 变量
  344. @example: aui.isDefine("变量");
  345. */
  346. _isDefine(str) {
  347. if (
  348. str == null ||
  349. str == "" ||
  350. str == "undefined" ||
  351. str == undefined ||
  352. str == "null" ||
  353. str == "(null)" ||
  354. str == "NULL" ||
  355. typeof str == "undefined"
  356. ) {
  357. return false;
  358. } else {
  359. str = str + "";
  360. str = str.replace(/\s/g, "");
  361. if (str == "") {
  362. return false;
  363. }
  364. return true;
  365. }
  366. },
  367. _btnTouchStart(e) {
  368. const _this = this,
  369. index = Number(e.currentTarget.dataset.index),
  370. pindex = Number(e.currentTarget.dataset.pindex);
  371. _this.touchConfig.index = index;
  372. _this.touchConfig.pindex = pindex;
  373. },
  374. _btnTouchEnd(e) {
  375. const _this = this,
  376. index = Number(e.currentTarget.dataset.index),
  377. pindex = Number(e.currentTarget.dataset.pindex);
  378. _this.touchConfig.index = -1;
  379. _this.touchConfig.pindex = -1;
  380. },
  381. // 恢复选择的数据
  382. _restoreSelectedData() {
  383. const _this = this;
  384. if (_this.selectedData.length === 0) return;
  385. // 验证数据有效性
  386. const isValid = _this.selectedData.every((item) =>
  387. _this._deepQuery(_this.data, item.id)
  388. );
  389. if (!isValid) {
  390. uni.showToast({ title: "无效的初始数据", icon: "none" });
  391. return;
  392. }
  393. _this.reset();
  394. _this.result = [..._this.selectedData];
  395. // 构建各级数据
  396. _this.selectedData.forEach((item, index) => {
  397. if (index === 0) return;
  398. const parent = _this._deepQuery(_this.data, item.pid);
  399. if (parent && parent.children) {
  400. _this.queryItems[index - 1] = parent.children.map((child) => ({
  401. id: child.id,
  402. name: child.name,
  403. pid: child.pid,
  404. type: child.type,
  405. }));
  406. _this.nav[index - 1] = { name: parent.name };
  407. }
  408. });
  409. _this.navCurrentIndex = _this.selectedData.length - 1;
  410. // 计算导航栏位置
  411. setTimeout(() => {
  412. const _el = uni
  413. .createSelectorQuery()
  414. .in(_this)
  415. .select(`.aui-picker-navitem-${_this.navCurrentIndex}`);
  416. _el
  417. .boundingClientRect((data) => {
  418. _this.navBorderLeft = data.left + 10;
  419. })
  420. .exec();
  421. }, 100);
  422. // 更新选中状态
  423. _this.$nextTick(() => {
  424. _this._updateSelectedStyle();
  425. });
  426. },
  427. // 更新选中样式
  428. _updateSelectedStyle() {
  429. const _this = this;
  430. _this.result.forEach((item, index) => {
  431. const list = index === 0 ? _this.items : _this.queryItems[index - 1];
  432. const targetItem = list.find((i) => i.id === item.id);
  433. if (targetItem) {
  434. targetItem.active = true;
  435. }
  436. });
  437. },
  438. },
  439. };
  440. </script>
  441. <style lang="scss" scoped>
  442. $custom-content-color: #1f7cff;
  443. /* ====================
  444. 多级联动弹窗
  445. =====================*/
  446. .aui-picker {
  447. width: 100vw;
  448. height: 100vh;
  449. //opacity: 0;
  450. position: fixed;
  451. top: 0;
  452. left: 0;
  453. z-index: 999;
  454. /* display: none; */
  455. }
  456. // 遮罩层
  457. .aui-mask {
  458. width: 100%;
  459. height: 100%;
  460. background: rgba(0, 0, 0, 0.3);
  461. position: absolute;
  462. left: 0;
  463. top: 0;
  464. z-index: 999;
  465. }
  466. .aui-picker.aui-picker-in {
  467. -moz-animation: aui-fade-in 0.1s ease-out forwards;
  468. -ms-animation: aui-fade-in 0.1s ease-out forwards;
  469. -webkit-animation: aui-fade-in 0.1s ease-out forwards;
  470. animation: aui-fade-in 0.1s ease-out forwards;
  471. }
  472. .aui-picker.aui-picker-out {
  473. -moz-animation: aui-fade-out 0.1s ease-out forwards;
  474. -ms-animation: aui-fade-out 0.1s ease-out forwards;
  475. -webkit-animation: aui-fade-out 0.1s ease-out forwards;
  476. animation: aui-fade-out 0.1s ease-out forwards;
  477. }
  478. .aui-picker-main {
  479. width: 100vw;
  480. height: 50vh;
  481. background: #fff;
  482. position: absolute;
  483. left: 0rpx;
  484. bottom: 0;
  485. z-index: 999;
  486. }
  487. .aui-picker.aui-picker-in .aui-picker-main {
  488. -moz-animation: aui-slide-up-screen 0.2s ease-out forwards;
  489. -ms-animation: aui-slide-up-screen 0.2s ease-out forwards;
  490. -webkit-animation: aui-slide-up-screen 0.2s ease-out forwards;
  491. animation: aui-slide-up-screen 0.2s ease-out forwards;
  492. }
  493. .aui-picker.aui-picker-out .aui-picker-main {
  494. -moz-animation: aui-slide-down-screen 0.2s ease-out forwards;
  495. -ms-animation: aui-slide-down-screen 0.2s ease-out forwards;
  496. -webkit-animation: aui-slide-down-screen 0.2s ease-out forwards;
  497. animation: aui-slide-down-screen 0.2s ease-out forwards;
  498. }
  499. .aui-picker-header {
  500. width: 100%;
  501. min-height: 70rpx;
  502. position: relative;
  503. z-index: 999;
  504. display: flex;
  505. justify-content: space-between;
  506. align-items: center;
  507. &-icon {
  508. font-size: 26rpx;
  509. color: #aaa;
  510. padding: 0 26rpx;
  511. }
  512. .aui-picker-confirm {
  513. height: 70rpx;
  514. line-height: 70rpx;
  515. text-align: center;
  516. font-size: 26rpx;
  517. color: $custom-content-color;
  518. padding: 0 25rpx;
  519. }
  520. }
  521. .aui-picker-header::after {
  522. content: "";
  523. width: 100%;
  524. height: 2rpx;
  525. background: rgba(100, 100, 100, 0.3);
  526. -moz-transform: scaleY(0.3);
  527. -ms-transform: scaleY(0.3);
  528. -webkit-transform: scaleY(0.3);
  529. transform: scaleY(0.3);
  530. position: absolute;
  531. left: 0;
  532. bottom: 0;
  533. z-index: 999;
  534. }
  535. .aui-picker-title {
  536. line-height: 45rpx;
  537. text-align: center;
  538. font-size: 26rpx;
  539. color: #333;
  540. padding: 15rpx;
  541. font-weight: bold;
  542. box-sizing: border-box;
  543. position: absolute;
  544. left: 60rpx;
  545. right: 60rpx;
  546. top: 0;
  547. }
  548. .aui-picker-content {
  549. width: 100%;
  550. height: -webkit-calc(100% - 110rpx);
  551. height: calc(100% - 110rpx);
  552. }
  553. .aui-picker-nav {
  554. width: 100%;
  555. height: 60rpx;
  556. text-align: left;
  557. padding: 0 26rpx;
  558. margin: 0 0 2rpx 0;
  559. justify-content: flex-start;
  560. white-space: nowrap;
  561. box-sizing: border-box;
  562. position: relative;
  563. overflow-x: scroll;
  564. overflow-y: hidden;
  565. }
  566. .aui-picker-nav::after {
  567. content: "";
  568. width: 100%;
  569. height: 2rpx;
  570. background: rgba(100, 100, 100, 0.3);
  571. -moz-transform: scaleY(0.3);
  572. -ms-transform: scaleY(0.3);
  573. -webkit-transform: scaleY(0.3);
  574. transform: scaleY(0.3);
  575. position: absolute;
  576. left: 0;
  577. bottom: 0;
  578. z-index: 999;
  579. }
  580. .aui-picker-navitem {
  581. width: 110rpx;
  582. line-height: 50rpx;
  583. font-size: 25rpx;
  584. margin: 0 70rpx 0 0;
  585. text-align: center;
  586. display: inline-block;
  587. overflow: hidden;
  588. white-space: nowrap;
  589. text-overflow: ellipsis;
  590. }
  591. .aui-picker-navitem.active {
  592. color: $custom-content-color;
  593. }
  594. .aui-picker-navborder {
  595. width: 90rpx;
  596. height: 6rpx;
  597. background: $custom-content-color;
  598. border-radius: 10rpx;
  599. transition: left 0.15s;
  600. position: absolute;
  601. left: 60rpx;
  602. bottom: 0;
  603. }
  604. .aui-picker-lists {
  605. width: 100%;
  606. height: 100%;
  607. justify-content: space-around;
  608. white-space: nowrap;
  609. }
  610. .aui-picker-list {
  611. width: 100%;
  612. height: 100%;
  613. overflow: hidden;
  614. overflow-y: scroll;
  615. display: none;
  616. vertical-align: top;
  617. }
  618. .aui-picker-list.active {
  619. display: inline-block;
  620. }
  621. .aui-picker-list-warp {
  622. width: 100%;
  623. height: auto;
  624. box-sizing: border-box;
  625. padding: 20rpx 0;
  626. display: inline-block;
  627. }
  628. .aui-picker-item {
  629. width: 100%;
  630. height: 70rpx;
  631. line-height: 70rpx;
  632. padding: 0 30rpx;
  633. box-sizing: border-box;
  634. font-size: 25rpx;
  635. color: #333;
  636. position: relative;
  637. }
  638. .aui-picker-item.active {
  639. color: $custom-content-color;
  640. }
  641. .aui-picker-item.active::after {
  642. content: "✔";
  643. font-size: 25rpx;
  644. color: $custom-content-color;
  645. position: absolute;
  646. top: 0rpx;
  647. right: 20rpx;
  648. }
  649. </style>