| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952 |
- <template>
- <view
- :class="['select-list', { disabled }, { active: selectList.length }]"
- @click="open"
- >
- <view class="left">
- <view v-if="selectList.length" class="select-items">
- <view
- class="select-item"
- v-for="item in selectedListBaseinfo"
- :key="item[dataValue]"
- >
- <view class="name">
- <text>{{ item[dataLabel] }}</text>
- </view>
- <view
- v-if="!disabled && !item.disabled"
- class="close"
- @click.stop="removeSelectedItem(item)"
- >
- <uni-icons type="closeempty" size="16" color="#999"></uni-icons>
- </view>
- </view>
- </view>
- <view v-else style="color: #6a6a6a" class="no-data">
- <text>{{ placeholder }}</text>
- </view>
- </view>
- <view class="right">
- <uni-icons
- v-if="!selectList.length || !clearable"
- type="bottom"
- size="14"
- color="#999"
- ></uni-icons>
- <view @click.stop>
- <uni-icons
- v-if="selectList.length && clearable"
- type="clear"
- size="24"
- color="#c0c4cc"
- @click="clearSelectList"
- ></uni-icons>
- </view>
- </view>
- </view>
- <uni-popup
- ref="popup"
- :animation="animation"
- :is-mask-click="isMaskClick"
- :mask-background-color="maskBackgroundColor"
- :background-color="backgroundColor"
- :safe-area="safeArea"
- type="bottom"
- @change="change"
- @maskClick="maskClick"
- >
- <view class="popup-content" :style="{ height: contentHeight }">
- <view class="title">
- <view
- v-if="mutiple && canSelectAll"
- class="left"
- @click="handleSelectAll"
- >
- <text>{{ isSelectedAll ? '取消全选' : '全选' }}</text>
- </view>
- <view class="center">
- <text>{{ placeholder }}</text>
- </view>
- <view class="right" :style="{ color: confirmTextColor }" @click="close">
- <text>{{ confirmText }}</text>
- </view>
- </view>
- <view v-if="search" class="search-box">
- <uni-easyinput
- :maxlength="-1"
- prefixIcon="search"
- placeholder="搜索"
- v-model="searchStr"
- confirm-type="search"
- @confirm="handleSearch(false)"
- @clear="handleSearch(true)"
- >
- </uni-easyinput>
- <button
- type="primary"
- size="mini"
- class="search-btn"
- @click="handleSearch(false)"
- >
- 搜索
- </button>
- </view>
- <view v-if="treeData.length" class="select-content">
- <scroll-view
- class="scroll-view-box"
- :scroll-top="scrollTop"
- scroll-y="true"
- @touchmove.stop
- >
- <view v-if="!filterTreeData.length" class="no-data center">
- <text>暂无数据</text>
- </view>
- <data-select-item
- v-for="item in filterTreeData"
- :key="item[dataValue]"
- :node="item"
- :dataLabel="dataLabel"
- :dataValue="dataValue"
- :dataChildren="dataChildren"
- :choseParent="choseParent"
- :border="border"
- :linkage="linkage"
- :lazyLoadChildren="lazyLoadChildren"
- ></data-select-item>
- <view class="sentry" />
- </scroll-view>
- </view>
- <view v-else class="no-data center">
- <text>暂无数据</text>
- </view>
- </view>
- </uni-popup>
- </template>
- <script lang="ts" setup>
- import { ref, computed, watch, onMounted, nextTick, provide } from 'vue'
- import dataSelectItem from './data-select-item.vue'
- import { isString, paging } from './utils'
- const props = defineProps({
- canSelectAll: {
- type: Boolean,
- default: false
- },
- safeArea: {
- type: Boolean,
- default: true
- },
- search: {
- type: Boolean,
- default: false
- },
- clearResetSearch: {
- type: Boolean,
- default: false
- },
- animation: {
- type: Boolean,
- default: true
- },
- 'is-mask-click': {
- type: Boolean,
- default: true
- },
- 'mask-background-color': {
- type: String,
- default: 'rgba(0,0,0,0.4)'
- },
- 'background-color': {
- type: String,
- default: 'none'
- },
- 'safe-area': {
- type: Boolean,
- default: true
- },
- choseParent: {
- type: Boolean,
- default: true
- },
- placeholder: {
- type: String,
- default: '请选择'
- },
- confirmText: {
- type: String,
- default: '完成'
- },
- confirmTextColor: {
- type: String,
- default: '#007aff'
- },
- listData: {
- type: Array,
- default: () => []
- },
- dataLabel: {
- type: String,
- default: 'name'
- },
- dataValue: {
- type: String,
- default: 'id'
- },
- dataChildren: {
- type: String,
- default: 'children'
- },
- linkage: {
- type: Boolean,
- default: false
- },
- removeLinkage: {
- type: Boolean,
- default: true
- },
- clearable: {
- type: Boolean,
- default: false
- },
- mutiple: {
- type: Boolean,
- default: false
- },
- disabled: {
- type: Boolean,
- default: false
- },
- showChildren: {
- type: Boolean,
- default: true
- },
- border: {
- type: Boolean,
- default: false
- },
- lazyLoadChildren: {
- type: Boolean,
- default: false
- },
- load: {
- type: Function,
- default: function () {}
- },
- modelValue: {
- type: [Array, String],
- default: () => []
- }
- })
- const emits = defineEmits([
- 'update:modelValue',
- 'change',
- 'maskClick',
- 'select-change',
- 'removeSelect'
- ])
- const contentHeight = ref('500px')
- const treeData = ref([])
- const filterTreeData = ref([])
- const clearTimerList = ref([])
- const selectedListBaseinfo = ref([])
- const showPopup = ref(false)
- const isSelectedAll = ref(false)
- const scrollTop = ref(0)
- const searchStr = ref('')
- const popup = ref<any>(null)
- const partCheckedSet = new Set()
- provide('nodeFn', {
- nodeClick: handleNodeClick,
- nameClick: handleHideChildren,
- loadNode: props.load,
- initData: initData,
- addNode: addNode
- })
- const selectList = computed(() => {
- const newVal = props.modelValue === null ? '' : props.modelValue
- return isString(newVal)
- ? newVal.length
- ? newVal.split(',')
- : []
- : newVal.map((item: any) => item.toString())
- })
- onMounted(() => {
- getContentHeight(uni.getSystemInfoSync())
- })
- function getContentHeight({ screenHeight }: { screenHeight: number }) {
- contentHeight.value = `${Math.floor(screenHeight * 0.7)}px`
- }
- watch(
- () => props.listData,
- (newVal: any[]) => {
- if (newVal) {
- treeData.value = initData(newVal)
- const ids = props.modelValue
- ? Array.isArray(props.modelValue)
- ? props.modelValue
- : props.modelValue.split(',')
- : []
- changeStatus(treeData.value, ids, true)
- filterTreeData.value.length && changeStatus(filterTreeData.value, ids)
- if (showPopup.value) {
- resetClearTimerList()
- renderTree(treeData.value)
- }
- }
- },
- { immediate: true, deep: true }
- )
- watch(
- () => props.modelValue,
- (newVal: any[] | string) => {
- const ids = newVal
- ? Array.isArray(newVal)
- ? newVal
- : newVal.split(',')
- : []
- changeStatus(treeData.value, ids, true)
- filterTreeData.value.length && changeStatus(filterTreeData.value, ids)
- },
- { immediate: true }
- )
- // 搜索完成返回顶部
- function goTop() {
- scrollTop.val = 10
- nextTick(() => {
- scrollTop.value = 0
- })
- }
- // 处理搜索
- function handleSearch(isClear = false) {
- resetClearTimerList()
- if (isClear) {
- // 点击清空按钮并且设置清空按钮会重置搜索
- if (props.clearResetSearch) {
- renderTree(treeData.value)
- }
- } else {
- renderTree(searchValue(searchStr.value, treeData.value))
- }
- goTop()
- uni.hideKeyboard()
- }
- // 具体搜索方法
- function searchValue(str: any, arr: any[]) {
- const res: any = []
- arr.forEach((item) => {
- if (item.visible) {
- if (
- item[props.dataLabel]
- .toString()
- .toLowerCase()
- .indexOf(str.toLowerCase()) > -1
- ) {
- res.push(item)
- } else {
- if (item[props.dataChildren]?.length) {
- const data = searchValue(str, item[props.dataChildren])
- if (data?.length) {
- if (str && !item.showChildren && item[props.dataChildren]?.length) {
- item.showChildren = true
- }
- res.push({
- ...item,
- [props.dataChildren]: data
- })
- }
- }
- }
- }
- })
- return res
- }
- // 打开弹窗
- async function open() {
- // disaled模式下禁止打开弹窗
- if (props.disabled) return
- showPopup.value = true
- popup.value.open()
- renderTree(treeData.value)
- }
- // 关闭弹窗
- function close() {
- popup.value.close()
- }
- // 弹窗状态变化 包括点击回显框和遮罩
- function change(data: any) {
- if (!data.show) {
- resetClearTimerList()
- searchStr.value = ''
- showPopup.value = false
- }
- emits('change', data)
- }
- // 点击遮罩
- function maskClick() {
- emits('maskClick')
- }
- // 初始化数据
- function initData(arr: any[], parentVisible?: undefined | boolean) {
- if (!Array.isArray(arr)) return []
- const res = []
- for (let i = 0; i < arr.length; i++) {
- const obj: any = {
- [props.dataLabel]: arr[i][props.dataLabel],
- [props.dataValue]: arr[i][props.dataValue]
- }
- obj.checked = selectList.value.includes(arr[i][props.dataValue].toString())
- obj.disabled = Boolean(arr[i].disabled)
- //半选
- obj.partChecked = Boolean(
- arr[i].partChecked === undefined ? false : arr[i].partChecked
- )
- obj.partChecked && obj.partCheckedSet.add(obj[props.dataValue])
- !obj.partChecked && (isSelectedAll.value = false)
- const parentVisibleState =
- parentVisible === undefined ? true : parentVisible
- const curVisibleState =
- arr[i].visible === undefined ? true : Boolean(arr[i].visible)
- if (parentVisibleState === curVisibleState) {
- obj.visible = parentVisibleState
- } else if (!parentVisibleState || !curVisibleState) {
- obj.visible = false
- } else {
- obj.visible = true
- }
- obj.showChildren =
- 'showChildren' in arr[i] && arr[i].showChildren != undefined
- ? arr[i].showChildren
- : props.showChildren
- if (arr[i].visible && !arr[i].disabled && !arr[i].checked) {
- isSelectedAll.value = false
- }
- if (arr[i][props.dataChildren]?.length) {
- obj[props.dataChildren] = initData(
- arr[i][props.dataChildren],
- obj.visible
- )
- }
- res.push(obj)
- }
- return res
- }
- function addNode(node: any, children: any[]) {
- getReflectNode(node, treeData.value)[props.dataChildren] = children
- handleHideChildren(node)
- }
- // 中断懒加载
- function resetClearTimerList() {
- const list = [...clearTimerList.value]
- clearTimerList.value = []
- list.forEach((fn) => fn())
- }
- // 懒加载
- function renderTree(arr: any[]) {
- const pagingArr = paging(arr)
- filterTreeData.value = pagingArr?.[0] || []
- lazyRenderList(pagingArr, 1)
- }
- // 懒加载具体逻辑
- function lazyRenderList(arr: any[], startIndex: number) {
- for (let i = startIndex; i < arr.length; i++) {
- let timer: any = null
- timer = setTimeout(() => {
- filterTreeData.value.push(...arr[i])
- }, i * 500)
- clearTimerList.push(() => clearTimeout(timer))
- }
- }
- // 根据 dataValue 找节点
- function changeStatus(list: any[], ids: any[] | string, needEmit = false) {
- const arr = [...list]
- let flag = true
- needEmit && (selectedListBaseinfo.value = [])
- while (arr.length) {
- const item = arr.shift()
- if (ids.includes(item[props.dataValue].toString())) {
- item.checked = true
- // 数据被选中清除半选状态
- item.partChecked = false
- partCheckedSet.delete(item[props.dataValue])
- needEmit && selectedListBaseinfo.value.push(item)
- } else {
- item.checked = false
- if (item.visible && !item.disabled) {
- flag = false
- }
- if (partCheckedSet.has(item[props.dataValue])) {
- // filtertreedata 更新状态
- item.partChecked = true
- } else {
- item.partChecked = false
- }
- }
- if (item[props.dataChildren]?.length) {
- arr.push(...item[props.dataChildren])
- }
- }
- isSelectedAll.value = flag
- needEmit && emits('select-change', [...selectedListBaseinfo.value])
- }
- // 移除选项
- function removeSelectedItem(node: any) {
- isSelectedAll.value = false
- if (props.linkage) {
- handleNodeClick(node, false)
- emits('removeSelect', node)
- } else {
- const emitData = selectList.value.filter(
- (item: any) => item !== node[props.dataValue].toString()
- )
- emits('removeSelect', node)
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- }
- }
- // 获取对应数据
- function getReflectNode(node: any, arr: any[]) {
- const array = [...arr]
- while (array.length) {
- const item = array.shift()
- if (item[props.dataValue] === node[props.dataValue]) {
- return item
- }
- if (item[props.dataChildren]?.length) {
- array.push(...item[props.dataChildren])
- }
- }
- return {}
- }
- // 获取某个节点孩子节点
- function getChildren(node: any) {
- if (!node[props.dataChildren]?.length) return []
- const res = node[props.dataChildren].reduce((pre: any, val: any) => {
- if (val.visible) {
- return [...pre, val]
- }
- return pre
- }, [])
- for (let i = 0; i < node[props.dataChildren].length; i++) {
- res.push(...getChildren(node[props.dataChildren][i]))
- }
- return res
- }
- // 获取某个节点祖先元素
- function getParentNode(target: any, arr: any[]) {
- let res: any[] = []
- for (let i = 0; i < arr.length; i++) {
- if (arr[i][props.dataValue] === target[props.dataValue]) {
- return true
- }
- if (arr[i][props.dataChildren]?.length) {
- const childRes = getParentNode(target, arr[i][props.dataChildren])
- if (typeof childRes === 'boolean' && childRes) {
- res = [arr[i]]
- } else if (Array.isArray(childRes) && childRes.length) {
- res = [...childRes, arr[i]]
- }
- }
- }
- return res
- }
- // 点击checkbox
- function handleNodeClick(data: any, status: boolean | undefined) {
- const node = getReflectNode(data, treeData.value)
- node.checked = typeof status === 'boolean' ? status : !node.checked
- node.partChecked = false
- partCheckedSet.delete(node[props.dataValue])
- // 如果是单选不考虑其他情况
- if (!props.mutiple) {
- let emitData: any[] = []
- if (node.checked) {
- emitData = [node[props.dataValue].toString()]
- }
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- } else {
- // 多选情况
- if (!props.linkage) {
- // 不需要联动
- let emitData = null
- if (node.checked) {
- emitData = Array.from(
- new Set([...selectList.value, node[props.dataValue].toString()])
- )
- } else {
- emitData = selectList.value.filter(
- (id: number | string) => id !== node[props.dataValue].toString()
- )
- }
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- } else {
- // 需要联动
- let emitData = [...selectList.value]
- const parentNodes: any = getParentNode(node, treeData.value)
- const childrenVal = getChildren(node).filter(
- (item: any) => !item.disabled
- )
- if (node.checked) {
- // 选中
- emitData = Array.from(
- new Set([...emitData, node[props.dataValue].toString()])
- )
- if (childrenVal.length) {
- emitData = Array.from(
- new Set([
- ...emitData,
- ...childrenVal.map((item: any) =>
- item[props.dataValue].toString()
- )
- ])
- )
- // 孩子节点全部选中并且清除半选状态
- childrenVal.forEach((childNode: any) => {
- childNode.partChecked = false
- partCheckedSet.delete(childNode[props.dataValue])
- })
- }
- if (parentNodes.length) {
- let flag = false
- // 有父元素 如果父元素下所有子元素全部选中,选中父元素
- while (parentNodes.length) {
- const item = parentNodes.shift()
- if (!item.disabled) {
- if (flag) {
- // 前一个没选中并且为半选那么之后的全为半选
- item.partChecked = true
- partCheckedSet.add(item[props.dataValue])
- } else {
- const allChecked = item[props.dataChildren]
- .filter((node: any) => node.visible && !node.disabled)
- .every((node: any) => node.checked)
- if (allChecked) {
- item.checked = true
- item.partChecked = false
- partCheckedSet.delete(item[props.dataValue])
- emitData = Array.from(
- new Set([...emitData, item[props.dataValue].toString()])
- )
- } else {
- item.partChecked = true
- partCheckedSet.add(item[props.dataValue])
- flag = true
- }
- }
- }
- }
- }
- } else {
- // 取消选中
- emitData = emitData.filter(
- (id) => id !== node[props.dataValue].toString()
- )
- if (childrenVal.length) {
- // 取消选中全部子节点
- childrenVal.forEach((childNode: any) => {
- emitData = emitData.filter(
- (id) => id !== childNode[props.dataValue].toString()
- )
- })
- }
- if (parentNodes.length) {
- parentNodes.forEach((parentNode: any) => {
- if (emitData.includes(parentNode[props.dataValue].toString())) {
- parentNode.checked = false
- }
- emitData = emitData.filter(
- (id) => id !== parentNode[props.dataValue].toString()
- )
- const hasChecked = parentNode[props.dataChildren]
- .filter((node: any) => node.visible && !node.disabled)
- .some((node: any) => node.checked || node.partChecked)
- parentNode.partChecked = hasChecked
- if (hasChecked) {
- partCheckedSet.add(parentNode[props.dataValue])
- } else {
- partCheckedSet.delete(parentNode[props.dataValue])
- }
- })
- }
- }
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- }
- }
- }
- // 点击名称折叠或展开
- function handleHideChildren(node: any) {
- const status = !node.showChildren
- getReflectNode(node, treeData.value).showChildren = status
- getReflectNode(node, filterTreeData.value).showChildren = status
- }
- // 全部选中
- function handleSelectAll() {
- isSelectedAll.value = !isSelectedAll.value
- if (isSelectedAll.value) {
- if (!props.mutiple) {
- uni.showToast({
- title: '单选模式下不能全选',
- icon: 'none',
- duration: 1000
- })
- return
- }
- let emitData: any[] = []
- treeData.value.forEach((item: any) => {
- if (item.visible || (item.disabled && item.checked)) {
- emitData = Array.from(
- new Set([...emitData, item[props.dataValue].toString()])
- )
- if (item[props.dataChildren]?.length) {
- emitData = Array.from(
- new Set([
- ...emitData,
- ...getChildren(item)
- .filter(
- (item: any) =>
- !item.disabled || (item.disabled && item.checked)
- )
- .map((item: any) => item[props.dataValue].toString())
- ])
- )
- }
- }
- })
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- } else {
- clearSelectList()
- }
- }
- // 清空选项
- function clearSelectList() {
- if (props.disabled) return
- partCheckedSet.clear()
- const emitData: any[] = []
- selectedListBaseinfo.value.forEach((node: any) => {
- if (node.visible && node.checked && node.disabled) {
- emitData.push(node[props.dataValue])
- }
- })
- emits(
- 'update:modelValue',
- isString(props.modelValue) ? emitData.join(',') : emitData
- )
- }
- </script>
- <style lang="scss" scoped>
- $primary-color: #007aff;
- $col-sm: 4px;
- $col-base: 8px;
- $col-lg: 12px;
- $row-sm: 5px;
- $row-base: 10px;
- $row-lg: 15px;
- $radius-sm: 3px;
- $radius-base: 6px;
- .select-list {
- padding-left: $row-base;
- min-height: 35px;
- border: 1px solid #e5e5e5;
- border-radius: $radius-sm;
- display: flex;
- justify-content: space-between;
- align-items: center;
- &.active {
- padding: calc(#{$col-sm} / 2) 0 calc(#{$col-sm} / 2) $row-base;
- }
- .left {
- flex: 1;
- .select-items {
- display: flex;
- flex-wrap: wrap;
- }
- .select-item {
- margin: $col-sm $row-base $col-sm 0;
- padding: $col-sm $row-sm;
- max-width: auto;
- height: auto;
- background-color: #eaeaea;
- border-radius: $radius-sm;
- color: #333;
- display: flex;
- align-items: center;
- .name {
- flex: 1;
- padding-right: $row-base;
- font-size: 14px;
- }
- .close {
- width: 18px;
- height: 18px;
- display: flex;
- justify-content: center;
- align-items: center;
- overflow: hidden;
- }
- }
- }
- .right {
- margin-right: $row-sm;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- }
- &.disabled {
- background-color: #f5f7fa;
- .left {
- .select-item {
- .name {
- padding: 0;
- }
- }
- }
- }
- }
- .popup-content {
- flex: 1;
- background-color: #fff;
- border-top-left-radius: 20px;
- border-top-right-radius: 20px;
- display: flex;
- flex-direction: column;
- .title {
- padding: $col-base 3rem;
- border-bottom: 1px solid $uni-border-color;
- font-size: 14px;
- display: flex;
- justify-content: space-between;
- position: relative;
- .left {
- position: absolute;
- left: 10px;
- }
- .center {
- flex: 1;
- text-align: center;
- }
- .right {
- position: absolute;
- right: 10px;
- }
- }
- .search-box {
- margin: $col-base $row-base 0;
- background-color: #fff;
- display: flex;
- align-items: center;
- .search-btn {
- margin-left: $row-base;
- height: 35px;
- line-height: 35px;
- }
- }
- .select-content {
- margin: $col-base $row-base;
- flex: 1;
- overflow: hidden;
- position: relative;
- }
- .scroll-view-box {
- touch-action: none;
- flex: 1;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
- .sentry {
- height: 48px;
- }
- }
- .no-data {
- width: auto;
- color: #999;
- font-size: 12px;
- }
- .no-data.center {
- text-align: center;
- }
- </style>
|