customthree-tree-select.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. <template>
  2. <view
  3. :class="['select-list', { disabled }, { active: selectList.length }]"
  4. @click="open"
  5. >
  6. <view class="left">
  7. <view v-if="selectList.length" class="select-items">
  8. <view
  9. class="select-item"
  10. v-for="item in selectedListBaseinfo"
  11. :key="item[dataValue]"
  12. >
  13. <view class="name">
  14. <text>{{ item[dataLabel] }}</text>
  15. </view>
  16. <view
  17. v-if="!disabled && !item.disabled"
  18. class="close"
  19. @click.stop="removeSelectedItem(item)"
  20. >
  21. <uni-icons type="closeempty" size="16" color="#999"></uni-icons>
  22. </view>
  23. </view>
  24. </view>
  25. <view v-else style="color: #6a6a6a" class="no-data">
  26. <text>{{ placeholder }}</text>
  27. </view>
  28. </view>
  29. <view class="right">
  30. <uni-icons
  31. v-if="!selectList.length || !clearable"
  32. type="bottom"
  33. size="14"
  34. color="#999"
  35. ></uni-icons>
  36. <view @click.stop>
  37. <uni-icons
  38. v-if="selectList.length && clearable"
  39. type="clear"
  40. size="24"
  41. color="#c0c4cc"
  42. @click="clearSelectList"
  43. ></uni-icons>
  44. </view>
  45. </view>
  46. </view>
  47. <uni-popup
  48. ref="popup"
  49. :animation="animation"
  50. :is-mask-click="isMaskClick"
  51. :mask-background-color="maskBackgroundColor"
  52. :background-color="backgroundColor"
  53. :safe-area="safeArea"
  54. type="bottom"
  55. @change="change"
  56. @maskClick="maskClick"
  57. >
  58. <view class="popup-content" :style="{ height: contentHeight }">
  59. <view class="title">
  60. <view
  61. v-if="mutiple && canSelectAll"
  62. class="left"
  63. @click="handleSelectAll"
  64. >
  65. <text>{{ isSelectedAll ? '取消全选' : '全选' }}</text>
  66. </view>
  67. <view class="center">
  68. <text>{{ placeholder }}</text>
  69. </view>
  70. <view class="right" :style="{ color: confirmTextColor }" @click="close">
  71. <text>{{ confirmText }}</text>
  72. </view>
  73. </view>
  74. <view v-if="search" class="search-box">
  75. <uni-easyinput
  76. :maxlength="-1"
  77. prefixIcon="search"
  78. placeholder="搜索"
  79. v-model="searchStr"
  80. confirm-type="search"
  81. @confirm="handleSearch(false)"
  82. @clear="handleSearch(true)"
  83. >
  84. </uni-easyinput>
  85. <button
  86. type="primary"
  87. size="mini"
  88. class="search-btn"
  89. @click="handleSearch(false)"
  90. >
  91. 搜索
  92. </button>
  93. </view>
  94. <view v-if="treeData.length" class="select-content">
  95. <scroll-view
  96. class="scroll-view-box"
  97. :scroll-top="scrollTop"
  98. scroll-y="true"
  99. @touchmove.stop
  100. >
  101. <view v-if="!filterTreeData.length" class="no-data center">
  102. <text>暂无数据</text>
  103. </view>
  104. <data-select-item
  105. v-for="item in filterTreeData"
  106. :key="item[dataValue]"
  107. :node="item"
  108. :dataLabel="dataLabel"
  109. :dataValue="dataValue"
  110. :dataChildren="dataChildren"
  111. :choseParent="choseParent"
  112. :border="border"
  113. :linkage="linkage"
  114. :lazyLoadChildren="lazyLoadChildren"
  115. ></data-select-item>
  116. <view class="sentry" />
  117. </scroll-view>
  118. </view>
  119. <view v-else class="no-data center">
  120. <text>暂无数据</text>
  121. </view>
  122. </view>
  123. </uni-popup>
  124. </template>
  125. <script lang="ts" setup>
  126. import { ref, computed, watch, onMounted, nextTick, provide } from 'vue'
  127. import dataSelectItem from './data-select-item.vue'
  128. import { isString, paging } from './utils'
  129. const props = defineProps({
  130. canSelectAll: {
  131. type: Boolean,
  132. default: false
  133. },
  134. safeArea: {
  135. type: Boolean,
  136. default: true
  137. },
  138. search: {
  139. type: Boolean,
  140. default: false
  141. },
  142. clearResetSearch: {
  143. type: Boolean,
  144. default: false
  145. },
  146. animation: {
  147. type: Boolean,
  148. default: true
  149. },
  150. 'is-mask-click': {
  151. type: Boolean,
  152. default: true
  153. },
  154. 'mask-background-color': {
  155. type: String,
  156. default: 'rgba(0,0,0,0.4)'
  157. },
  158. 'background-color': {
  159. type: String,
  160. default: 'none'
  161. },
  162. 'safe-area': {
  163. type: Boolean,
  164. default: true
  165. },
  166. choseParent: {
  167. type: Boolean,
  168. default: true
  169. },
  170. placeholder: {
  171. type: String,
  172. default: '请选择'
  173. },
  174. confirmText: {
  175. type: String,
  176. default: '完成'
  177. },
  178. confirmTextColor: {
  179. type: String,
  180. default: '#007aff'
  181. },
  182. listData: {
  183. type: Array,
  184. default: () => []
  185. },
  186. dataLabel: {
  187. type: String,
  188. default: 'name'
  189. },
  190. dataValue: {
  191. type: String,
  192. default: 'id'
  193. },
  194. dataChildren: {
  195. type: String,
  196. default: 'children'
  197. },
  198. linkage: {
  199. type: Boolean,
  200. default: false
  201. },
  202. removeLinkage: {
  203. type: Boolean,
  204. default: true
  205. },
  206. clearable: {
  207. type: Boolean,
  208. default: false
  209. },
  210. mutiple: {
  211. type: Boolean,
  212. default: false
  213. },
  214. disabled: {
  215. type: Boolean,
  216. default: false
  217. },
  218. showChildren: {
  219. type: Boolean,
  220. default: true
  221. },
  222. border: {
  223. type: Boolean,
  224. default: false
  225. },
  226. lazyLoadChildren: {
  227. type: Boolean,
  228. default: false
  229. },
  230. load: {
  231. type: Function,
  232. default: function () {}
  233. },
  234. modelValue: {
  235. type: [Array, String],
  236. default: () => []
  237. }
  238. })
  239. const emits = defineEmits([
  240. 'update:modelValue',
  241. 'change',
  242. 'maskClick',
  243. 'select-change',
  244. 'removeSelect'
  245. ])
  246. const contentHeight = ref('500px')
  247. const treeData = ref([])
  248. const filterTreeData = ref([])
  249. const clearTimerList = ref([])
  250. const selectedListBaseinfo = ref([])
  251. const showPopup = ref(false)
  252. const isSelectedAll = ref(false)
  253. const scrollTop = ref(0)
  254. const searchStr = ref('')
  255. const popup = ref<any>(null)
  256. const partCheckedSet = new Set()
  257. provide('nodeFn', {
  258. nodeClick: handleNodeClick,
  259. nameClick: handleHideChildren,
  260. loadNode: props.load,
  261. initData: initData,
  262. addNode: addNode
  263. })
  264. const selectList = computed(() => {
  265. const newVal = props.modelValue === null ? '' : props.modelValue
  266. return isString(newVal)
  267. ? newVal.length
  268. ? newVal.split(',')
  269. : []
  270. : newVal.map((item: any) => item.toString())
  271. })
  272. onMounted(() => {
  273. getContentHeight(uni.getSystemInfoSync())
  274. })
  275. function getContentHeight({ screenHeight }: { screenHeight: number }) {
  276. contentHeight.value = `${Math.floor(screenHeight * 0.7)}px`
  277. }
  278. watch(
  279. () => props.listData,
  280. (newVal: any[]) => {
  281. if (newVal) {
  282. treeData.value = initData(newVal)
  283. const ids = props.modelValue
  284. ? Array.isArray(props.modelValue)
  285. ? props.modelValue
  286. : props.modelValue.split(',')
  287. : []
  288. changeStatus(treeData.value, ids, true)
  289. filterTreeData.value.length && changeStatus(filterTreeData.value, ids)
  290. if (showPopup.value) {
  291. resetClearTimerList()
  292. renderTree(treeData.value)
  293. }
  294. }
  295. },
  296. { immediate: true, deep: true }
  297. )
  298. watch(
  299. () => props.modelValue,
  300. (newVal: any[] | string) => {
  301. const ids = newVal
  302. ? Array.isArray(newVal)
  303. ? newVal
  304. : newVal.split(',')
  305. : []
  306. changeStatus(treeData.value, ids, true)
  307. filterTreeData.value.length && changeStatus(filterTreeData.value, ids)
  308. },
  309. { immediate: true }
  310. )
  311. // 搜索完成返回顶部
  312. function goTop() {
  313. scrollTop.val = 10
  314. nextTick(() => {
  315. scrollTop.value = 0
  316. })
  317. }
  318. // 处理搜索
  319. function handleSearch(isClear = false) {
  320. resetClearTimerList()
  321. if (isClear) {
  322. // 点击清空按钮并且设置清空按钮会重置搜索
  323. if (props.clearResetSearch) {
  324. renderTree(treeData.value)
  325. }
  326. } else {
  327. renderTree(searchValue(searchStr.value, treeData.value))
  328. }
  329. goTop()
  330. uni.hideKeyboard()
  331. }
  332. // 具体搜索方法
  333. function searchValue(str: any, arr: any[]) {
  334. const res: any = []
  335. arr.forEach((item) => {
  336. if (item.visible) {
  337. if (
  338. item[props.dataLabel]
  339. .toString()
  340. .toLowerCase()
  341. .indexOf(str.toLowerCase()) > -1
  342. ) {
  343. res.push(item)
  344. } else {
  345. if (item[props.dataChildren]?.length) {
  346. const data = searchValue(str, item[props.dataChildren])
  347. if (data?.length) {
  348. if (str && !item.showChildren && item[props.dataChildren]?.length) {
  349. item.showChildren = true
  350. }
  351. res.push({
  352. ...item,
  353. [props.dataChildren]: data
  354. })
  355. }
  356. }
  357. }
  358. }
  359. })
  360. return res
  361. }
  362. // 打开弹窗
  363. async function open() {
  364. // disaled模式下禁止打开弹窗
  365. if (props.disabled) return
  366. showPopup.value = true
  367. popup.value.open()
  368. renderTree(treeData.value)
  369. }
  370. // 关闭弹窗
  371. function close() {
  372. popup.value.close()
  373. }
  374. // 弹窗状态变化 包括点击回显框和遮罩
  375. function change(data: any) {
  376. if (!data.show) {
  377. resetClearTimerList()
  378. searchStr.value = ''
  379. showPopup.value = false
  380. }
  381. emits('change', data)
  382. }
  383. // 点击遮罩
  384. function maskClick() {
  385. emits('maskClick')
  386. }
  387. // 初始化数据
  388. function initData(arr: any[], parentVisible?: undefined | boolean) {
  389. if (!Array.isArray(arr)) return []
  390. const res = []
  391. for (let i = 0; i < arr.length; i++) {
  392. const obj: any = {
  393. [props.dataLabel]: arr[i][props.dataLabel],
  394. [props.dataValue]: arr[i][props.dataValue]
  395. }
  396. obj.checked = selectList.value.includes(arr[i][props.dataValue].toString())
  397. obj.disabled = Boolean(arr[i].disabled)
  398. //半选
  399. obj.partChecked = Boolean(
  400. arr[i].partChecked === undefined ? false : arr[i].partChecked
  401. )
  402. obj.partChecked && obj.partCheckedSet.add(obj[props.dataValue])
  403. !obj.partChecked && (isSelectedAll.value = false)
  404. const parentVisibleState =
  405. parentVisible === undefined ? true : parentVisible
  406. const curVisibleState =
  407. arr[i].visible === undefined ? true : Boolean(arr[i].visible)
  408. if (parentVisibleState === curVisibleState) {
  409. obj.visible = parentVisibleState
  410. } else if (!parentVisibleState || !curVisibleState) {
  411. obj.visible = false
  412. } else {
  413. obj.visible = true
  414. }
  415. obj.showChildren =
  416. 'showChildren' in arr[i] && arr[i].showChildren != undefined
  417. ? arr[i].showChildren
  418. : props.showChildren
  419. if (arr[i].visible && !arr[i].disabled && !arr[i].checked) {
  420. isSelectedAll.value = false
  421. }
  422. if (arr[i][props.dataChildren]?.length) {
  423. obj[props.dataChildren] = initData(
  424. arr[i][props.dataChildren],
  425. obj.visible
  426. )
  427. }
  428. res.push(obj)
  429. }
  430. return res
  431. }
  432. function addNode(node: any, children: any[]) {
  433. getReflectNode(node, treeData.value)[props.dataChildren] = children
  434. handleHideChildren(node)
  435. }
  436. // 中断懒加载
  437. function resetClearTimerList() {
  438. const list = [...clearTimerList.value]
  439. clearTimerList.value = []
  440. list.forEach((fn) => fn())
  441. }
  442. // 懒加载
  443. function renderTree(arr: any[]) {
  444. const pagingArr = paging(arr)
  445. filterTreeData.value = pagingArr?.[0] || []
  446. lazyRenderList(pagingArr, 1)
  447. }
  448. // 懒加载具体逻辑
  449. function lazyRenderList(arr: any[], startIndex: number) {
  450. for (let i = startIndex; i < arr.length; i++) {
  451. let timer: any = null
  452. timer = setTimeout(() => {
  453. filterTreeData.value.push(...arr[i])
  454. }, i * 500)
  455. clearTimerList.push(() => clearTimeout(timer))
  456. }
  457. }
  458. // 根据 dataValue 找节点
  459. function changeStatus(list: any[], ids: any[] | string, needEmit = false) {
  460. const arr = [...list]
  461. let flag = true
  462. needEmit && (selectedListBaseinfo.value = [])
  463. while (arr.length) {
  464. const item = arr.shift()
  465. if (ids.includes(item[props.dataValue].toString())) {
  466. item.checked = true
  467. // 数据被选中清除半选状态
  468. item.partChecked = false
  469. partCheckedSet.delete(item[props.dataValue])
  470. needEmit && selectedListBaseinfo.value.push(item)
  471. } else {
  472. item.checked = false
  473. if (item.visible && !item.disabled) {
  474. flag = false
  475. }
  476. if (partCheckedSet.has(item[props.dataValue])) {
  477. // filtertreedata 更新状态
  478. item.partChecked = true
  479. } else {
  480. item.partChecked = false
  481. }
  482. }
  483. if (item[props.dataChildren]?.length) {
  484. arr.push(...item[props.dataChildren])
  485. }
  486. }
  487. isSelectedAll.value = flag
  488. needEmit && emits('select-change', [...selectedListBaseinfo.value])
  489. }
  490. // 移除选项
  491. function removeSelectedItem(node: any) {
  492. isSelectedAll.value = false
  493. if (props.linkage) {
  494. handleNodeClick(node, false)
  495. emits('removeSelect', node)
  496. } else {
  497. const emitData = selectList.value.filter(
  498. (item: any) => item !== node[props.dataValue].toString()
  499. )
  500. emits('removeSelect', node)
  501. emits(
  502. 'update:modelValue',
  503. isString(props.modelValue) ? emitData.join(',') : emitData
  504. )
  505. }
  506. }
  507. // 获取对应数据
  508. function getReflectNode(node: any, arr: any[]) {
  509. const array = [...arr]
  510. while (array.length) {
  511. const item = array.shift()
  512. if (item[props.dataValue] === node[props.dataValue]) {
  513. return item
  514. }
  515. if (item[props.dataChildren]?.length) {
  516. array.push(...item[props.dataChildren])
  517. }
  518. }
  519. return {}
  520. }
  521. // 获取某个节点孩子节点
  522. function getChildren(node: any) {
  523. if (!node[props.dataChildren]?.length) return []
  524. const res = node[props.dataChildren].reduce((pre: any, val: any) => {
  525. if (val.visible) {
  526. return [...pre, val]
  527. }
  528. return pre
  529. }, [])
  530. for (let i = 0; i < node[props.dataChildren].length; i++) {
  531. res.push(...getChildren(node[props.dataChildren][i]))
  532. }
  533. return res
  534. }
  535. // 获取某个节点祖先元素
  536. function getParentNode(target: any, arr: any[]) {
  537. let res: any[] = []
  538. for (let i = 0; i < arr.length; i++) {
  539. if (arr[i][props.dataValue] === target[props.dataValue]) {
  540. return true
  541. }
  542. if (arr[i][props.dataChildren]?.length) {
  543. const childRes = getParentNode(target, arr[i][props.dataChildren])
  544. if (typeof childRes === 'boolean' && childRes) {
  545. res = [arr[i]]
  546. } else if (Array.isArray(childRes) && childRes.length) {
  547. res = [...childRes, arr[i]]
  548. }
  549. }
  550. }
  551. return res
  552. }
  553. // 点击checkbox
  554. function handleNodeClick(data: any, status: boolean | undefined) {
  555. const node = getReflectNode(data, treeData.value)
  556. node.checked = typeof status === 'boolean' ? status : !node.checked
  557. node.partChecked = false
  558. partCheckedSet.delete(node[props.dataValue])
  559. // 如果是单选不考虑其他情况
  560. if (!props.mutiple) {
  561. let emitData: any[] = []
  562. if (node.checked) {
  563. emitData = [node[props.dataValue].toString()]
  564. }
  565. emits(
  566. 'update:modelValue',
  567. isString(props.modelValue) ? emitData.join(',') : emitData
  568. )
  569. } else {
  570. // 多选情况
  571. if (!props.linkage) {
  572. // 不需要联动
  573. let emitData = null
  574. if (node.checked) {
  575. emitData = Array.from(
  576. new Set([...selectList.value, node[props.dataValue].toString()])
  577. )
  578. } else {
  579. emitData = selectList.value.filter(
  580. (id: number | string) => id !== node[props.dataValue].toString()
  581. )
  582. }
  583. emits(
  584. 'update:modelValue',
  585. isString(props.modelValue) ? emitData.join(',') : emitData
  586. )
  587. } else {
  588. // 需要联动
  589. let emitData = [...selectList.value]
  590. const parentNodes: any = getParentNode(node, treeData.value)
  591. const childrenVal = getChildren(node).filter(
  592. (item: any) => !item.disabled
  593. )
  594. if (node.checked) {
  595. // 选中
  596. emitData = Array.from(
  597. new Set([...emitData, node[props.dataValue].toString()])
  598. )
  599. if (childrenVal.length) {
  600. emitData = Array.from(
  601. new Set([
  602. ...emitData,
  603. ...childrenVal.map((item: any) =>
  604. item[props.dataValue].toString()
  605. )
  606. ])
  607. )
  608. // 孩子节点全部选中并且清除半选状态
  609. childrenVal.forEach((childNode: any) => {
  610. childNode.partChecked = false
  611. partCheckedSet.delete(childNode[props.dataValue])
  612. })
  613. }
  614. if (parentNodes.length) {
  615. let flag = false
  616. // 有父元素 如果父元素下所有子元素全部选中,选中父元素
  617. while (parentNodes.length) {
  618. const item = parentNodes.shift()
  619. if (!item.disabled) {
  620. if (flag) {
  621. // 前一个没选中并且为半选那么之后的全为半选
  622. item.partChecked = true
  623. partCheckedSet.add(item[props.dataValue])
  624. } else {
  625. const allChecked = item[props.dataChildren]
  626. .filter((node: any) => node.visible && !node.disabled)
  627. .every((node: any) => node.checked)
  628. if (allChecked) {
  629. item.checked = true
  630. item.partChecked = false
  631. partCheckedSet.delete(item[props.dataValue])
  632. emitData = Array.from(
  633. new Set([...emitData, item[props.dataValue].toString()])
  634. )
  635. } else {
  636. item.partChecked = true
  637. partCheckedSet.add(item[props.dataValue])
  638. flag = true
  639. }
  640. }
  641. }
  642. }
  643. }
  644. } else {
  645. // 取消选中
  646. emitData = emitData.filter(
  647. (id) => id !== node[props.dataValue].toString()
  648. )
  649. if (childrenVal.length) {
  650. // 取消选中全部子节点
  651. childrenVal.forEach((childNode: any) => {
  652. emitData = emitData.filter(
  653. (id) => id !== childNode[props.dataValue].toString()
  654. )
  655. })
  656. }
  657. if (parentNodes.length) {
  658. parentNodes.forEach((parentNode: any) => {
  659. if (emitData.includes(parentNode[props.dataValue].toString())) {
  660. parentNode.checked = false
  661. }
  662. emitData = emitData.filter(
  663. (id) => id !== parentNode[props.dataValue].toString()
  664. )
  665. const hasChecked = parentNode[props.dataChildren]
  666. .filter((node: any) => node.visible && !node.disabled)
  667. .some((node: any) => node.checked || node.partChecked)
  668. parentNode.partChecked = hasChecked
  669. if (hasChecked) {
  670. partCheckedSet.add(parentNode[props.dataValue])
  671. } else {
  672. partCheckedSet.delete(parentNode[props.dataValue])
  673. }
  674. })
  675. }
  676. }
  677. emits(
  678. 'update:modelValue',
  679. isString(props.modelValue) ? emitData.join(',') : emitData
  680. )
  681. }
  682. }
  683. }
  684. // 点击名称折叠或展开
  685. function handleHideChildren(node: any) {
  686. const status = !node.showChildren
  687. getReflectNode(node, treeData.value).showChildren = status
  688. getReflectNode(node, filterTreeData.value).showChildren = status
  689. }
  690. // 全部选中
  691. function handleSelectAll() {
  692. isSelectedAll.value = !isSelectedAll.value
  693. if (isSelectedAll.value) {
  694. if (!props.mutiple) {
  695. uni.showToast({
  696. title: '单选模式下不能全选',
  697. icon: 'none',
  698. duration: 1000
  699. })
  700. return
  701. }
  702. let emitData: any[] = []
  703. treeData.value.forEach((item: any) => {
  704. if (item.visible || (item.disabled && item.checked)) {
  705. emitData = Array.from(
  706. new Set([...emitData, item[props.dataValue].toString()])
  707. )
  708. if (item[props.dataChildren]?.length) {
  709. emitData = Array.from(
  710. new Set([
  711. ...emitData,
  712. ...getChildren(item)
  713. .filter(
  714. (item: any) =>
  715. !item.disabled || (item.disabled && item.checked)
  716. )
  717. .map((item: any) => item[props.dataValue].toString())
  718. ])
  719. )
  720. }
  721. }
  722. })
  723. emits(
  724. 'update:modelValue',
  725. isString(props.modelValue) ? emitData.join(',') : emitData
  726. )
  727. } else {
  728. clearSelectList()
  729. }
  730. }
  731. // 清空选项
  732. function clearSelectList() {
  733. if (props.disabled) return
  734. partCheckedSet.clear()
  735. const emitData: any[] = []
  736. selectedListBaseinfo.value.forEach((node: any) => {
  737. if (node.visible && node.checked && node.disabled) {
  738. emitData.push(node[props.dataValue])
  739. }
  740. })
  741. emits(
  742. 'update:modelValue',
  743. isString(props.modelValue) ? emitData.join(',') : emitData
  744. )
  745. }
  746. </script>
  747. <style lang="scss" scoped>
  748. $primary-color: #007aff;
  749. $col-sm: 4px;
  750. $col-base: 8px;
  751. $col-lg: 12px;
  752. $row-sm: 5px;
  753. $row-base: 10px;
  754. $row-lg: 15px;
  755. $radius-sm: 3px;
  756. $radius-base: 6px;
  757. .select-list {
  758. padding-left: $row-base;
  759. min-height: 35px;
  760. border: 1px solid #e5e5e5;
  761. border-radius: $radius-sm;
  762. display: flex;
  763. justify-content: space-between;
  764. align-items: center;
  765. &.active {
  766. padding: calc(#{$col-sm} / 2) 0 calc(#{$col-sm} / 2) $row-base;
  767. }
  768. .left {
  769. flex: 1;
  770. .select-items {
  771. display: flex;
  772. flex-wrap: wrap;
  773. }
  774. .select-item {
  775. margin: $col-sm $row-base $col-sm 0;
  776. padding: $col-sm $row-sm;
  777. max-width: auto;
  778. height: auto;
  779. background-color: #eaeaea;
  780. border-radius: $radius-sm;
  781. color: #333;
  782. display: flex;
  783. align-items: center;
  784. .name {
  785. flex: 1;
  786. padding-right: $row-base;
  787. font-size: 14px;
  788. }
  789. .close {
  790. width: 18px;
  791. height: 18px;
  792. display: flex;
  793. justify-content: center;
  794. align-items: center;
  795. overflow: hidden;
  796. }
  797. }
  798. }
  799. .right {
  800. margin-right: $row-sm;
  801. display: flex;
  802. justify-content: flex-end;
  803. align-items: center;
  804. }
  805. &.disabled {
  806. background-color: #f5f7fa;
  807. .left {
  808. .select-item {
  809. .name {
  810. padding: 0;
  811. }
  812. }
  813. }
  814. }
  815. }
  816. .popup-content {
  817. flex: 1;
  818. background-color: #fff;
  819. border-top-left-radius: 20px;
  820. border-top-right-radius: 20px;
  821. display: flex;
  822. flex-direction: column;
  823. .title {
  824. padding: $col-base 3rem;
  825. border-bottom: 1px solid $uni-border-color;
  826. font-size: 14px;
  827. display: flex;
  828. justify-content: space-between;
  829. position: relative;
  830. .left {
  831. position: absolute;
  832. left: 10px;
  833. }
  834. .center {
  835. flex: 1;
  836. text-align: center;
  837. }
  838. .right {
  839. position: absolute;
  840. right: 10px;
  841. }
  842. }
  843. .search-box {
  844. margin: $col-base $row-base 0;
  845. background-color: #fff;
  846. display: flex;
  847. align-items: center;
  848. .search-btn {
  849. margin-left: $row-base;
  850. height: 35px;
  851. line-height: 35px;
  852. }
  853. }
  854. .select-content {
  855. margin: $col-base $row-base;
  856. flex: 1;
  857. overflow: hidden;
  858. position: relative;
  859. }
  860. .scroll-view-box {
  861. touch-action: none;
  862. flex: 1;
  863. position: absolute;
  864. top: 0;
  865. right: 0;
  866. bottom: 0;
  867. left: 0;
  868. }
  869. .sentry {
  870. height: 48px;
  871. }
  872. }
  873. .no-data {
  874. width: auto;
  875. color: #999;
  876. font-size: 12px;
  877. }
  878. .no-data.center {
  879. text-align: center;
  880. }
  881. </style>