Ver código fonte

增加级联下拉框组件

leaf 5 meses atrás
pai
commit
e01477cb8f

+ 79 - 0
uni_modules/uni-data-picker/changelog.md

@@ -0,0 +1,79 @@
+## 2.0.1(2024-08-22)
+- 修复 uni-app-x v-model 没有更新传入值的 bug
+## 2.0.0(2023-12-11)
+- 新增 支持 uni-app-x
+## 1.1.2(2023-04-11)
+- 修复 更改 modelValue 报错的 bug
+- 修复 v-for 未使用 key 值控制台 warning
+## 1.1.1(2023-02-21)
+- 修复代码合并时引发 value 属性为空时不渲染数据的问题
+## 1.1.0(2023-02-15)
+- 修复 localdata 不支持动态更新的bug
+## 1.0.9(2023-02-15)
+- 修复 localdata 不支持动态更新的bug
+## 1.0.8(2022-09-16)
+- 可以使用 uni-scss 控制主题色
+## 1.0.7(2022-07-06)
+- 优化 pc端图标位置不正确的问题
+## 1.0.6(2022-07-05)
+- 优化 显示样式
+## 1.0.5(2022-07-04)
+- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
+## 1.0.4(2022-04-19)
+- 修复 字节小程序 本地数据无法选择下一级的Bug
+## 1.0.3(2022-02-25)
+- 修复 nvue 不支持的 v-show 的 bug
+## 1.0.2(2022-02-25)
+- 修复 条件编译 nvue 不支持的 css 样式
+## 1.0.1(2021-11-23)
+- 修复 由上个版本引发的map、v-model等属性不生效的bug
+## 1.0.0(2021-11-19)
+- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-picker](https://uniapp.dcloud.io/component/uniui/uni-data-picker)
+## 0.4.9(2021-10-28)
+- 修复 VUE2 v-model 概率无效的 bug
+## 0.4.8(2021-10-27)
+- 修复 v-model 概率无效的 bug
+## 0.4.7(2021-10-25)
+- 新增 属性 spaceInfo 服务空间配置 HBuilderX 3.2.11+
+- 修复 树型 uniCloud 数据类型为 int 时报错的 bug
+## 0.4.6(2021-10-19)
+- 修复 非 VUE3 v-model 为 0 时无法选中的 bug
+## 0.4.5(2021-09-26)
+- 新增 清除已选项的功能(通过 clearIcon 属性配置是否显示按钮),同时提供 clear 方法以供调用,二者等效
+- 修复 readonly 为 true 时报错的 bug
+## 0.4.4(2021-09-26)
+- 修复 上一版本造成的 map 属性失效的 bug
+- 新增 ellipsis 属性,支持配置 tab 选项长度过长时是否自动省略
+## 0.4.3(2021-09-24)
+- 修复 某些情况下级联未触发的 bug
+## 0.4.2(2021-09-23)
+- 新增 提供 show 和 hide 方法,开发者可以通过 ref 调用
+- 新增 选项内容过长自动添加省略号
+## 0.4.1(2021-09-15)
+- 新增 map 属性 字段映射,将 text/value 映射到数据中的其他字段
+## 0.4.0(2021-07-13)
+- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 0.3.5(2021-06-04)
+- 修复 无法加载云端数据的问题
+## 0.3.4(2021-05-28)
+- 修复 v-model 无效问题
+- 修复 loaddata 为空数据组时加载时间过长问题
+- 修复 上个版本引出的本地数据无法选择带有 children 的 2 级节点
+## 0.3.3(2021-05-12)
+- 新增 组件示例地址
+## 0.3.2(2021-04-22)
+- 修复 非树形数据有 where 属性查询报错的问题
+## 0.3.1(2021-04-15)
+- 修复 本地数据概率无法回显时问题
+## 0.3.0(2021-04-07)
+- 新增 支持云端非树形表结构数据
+- 修复 根节点 parent_field 字段等于 null 时选择界面错乱问题
+## 0.2.0(2021-03-15)
+- 修复 nodeclick、popupopened、popupclosed 事件无法触发的问题
+## 0.1.9(2021-03-09)
+- 修复 微信小程序某些情况下无法选择的问题
+## 0.1.8(2021-02-05)
+- 优化 部分样式在 nvue 上的兼容表现
+## 0.1.7(2021-02-05)
+- 调整为 uni_modules 目录规范

+ 45 - 0
uni_modules/uni-data-picker/components/uni-data-picker/keypress.js

@@ -0,0 +1,45 @@
+// #ifdef H5
+export default {
+  name: 'Keypress',
+  props: {
+    disable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  mounted () {
+    const keyNames = {
+      esc: ['Esc', 'Escape'],
+      tab: 'Tab',
+      enter: 'Enter',
+      space: [' ', 'Spacebar'],
+      up: ['Up', 'ArrowUp'],
+      left: ['Left', 'ArrowLeft'],
+      right: ['Right', 'ArrowRight'],
+      down: ['Down', 'ArrowDown'],
+      delete: ['Backspace', 'Delete', 'Del']
+    }
+    const listener = ($event) => {
+      if (this.disable) {
+        return
+      }
+      const keyName = Object.keys(keyNames).find(key => {
+        const keyName = $event.key
+        const value = keyNames[key]
+        return value === keyName || (Array.isArray(value) && value.includes(keyName))
+      })
+      if (keyName) {
+        // 避免和其他按键事件冲突
+        setTimeout(() => {
+          this.$emit(keyName, {})
+        }, 0)
+      }
+    }
+    document.addEventListener('keyup', listener)
+    this.$once('hook:beforeDestroy', () => {
+      document.removeEventListener('keyup', listener)
+    })
+  },
+	render: () => {}
+}
+// #endif

+ 381 - 0
uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.uvue

@@ -0,0 +1,381 @@
+<template>
+  <view class="uni-data-tree">
+    <view class="uni-data-tree-input" @click="handleInput">
+      <slot :data="selectedPaths" :error="error">
+        <view class="input-value" :class="{'input-value-border': border}">
+          <text v-if="error!=null" class="error-text">{{error!.errMsg}}</text>
+          <scroll-view v-if="selectedPaths.length" class="selected-path" scroll-x="true">
+            <view class="selected-list">
+              <template v-for="(item, index) in selectedPaths">
+                <text class="text-color">{{item[mappingTextName]}}</text>
+                <text v-if="index<selectedPaths.length-1" class="input-split-line">{{split}}</text>
+              </template>
+            </view>
+          </scroll-view>
+          <text v-else-if="error==null&&!loading" class="placeholder">{{placeholder}}</text>
+          <view v-if="!readonly" class="arrow-area">
+            <view class="input-arrow"></view>
+          </view>
+        </view>
+      </slot>
+      <view v-if="loading && !isOpened" class="selected-loading">
+        <slot name="picker-loading" :loading="loading"></slot>
+      </view>
+    </view>
+    <view class="uni-data-tree-cover" v-if="isOpened" @click="handleClose"></view>
+    <view class="uni-data-tree-dialog" v-if="isOpened">
+      <view class="uni-popper__arrow"></view>
+      <view class="dialog-caption">
+        <view class="dialog-title-view">
+          <text class="dialog-title">{{popupTitle}}</text>
+        </view>
+        <view class="dialog-close" @click="handleClose">
+          <view class="dialog-close-plus" data-id="close"></view>
+          <view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
+        </view>
+      </view>
+      <view ref="pickerView" class="uni-data-pickerview">
+        <view v-if="error!=null" class="error">
+          <text class="error-text">{{error!.errMsg}}</text>
+        </view>
+        <scroll-view v-if="!isCloudDataList" :scroll-x="true">
+          <view class="selected-node-list">
+            <template v-for="(item, index) in selectedNodes">
+              <text class="selected-node-item" :class="{'selected-node-item-active':index==selectedIndex}"
+                @click="onTabSelect(index)">
+                {{item[mappingTextName]}}
+              </text>
+            </template>
+          </view>
+        </scroll-view>
+        <list-view class="list-view" :scroll-y="true">
+          <list-item class="list-item" v-for="(item, _) in currentDataList" @click="onNodeClick(item)">
+            <text class="item-text" :class="{'item-text-disabled': item['disable']}">{{item[mappingTextName]}}</text>
+            <text class="check" v-if="item[mappingValueName] == selectedNodes[selectedIndex][mappingValueName]"></text>
+          </list-item>
+        </list-view>
+        <view class="loading-cover" v-if="loading">
+          <slot name="pickerview-loading" :loading="loading"></slot>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { dataPicker } from "../uni-data-pickerview/uni-data-picker.uts"
+
+  /**
+   * DataPicker 级联选择
+   * @description 支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=3796
+   * @property {String} popup-title 弹出窗口标题
+   * @property {Array} localdata 本地数据,参考
+   * @property {Boolean} border = [true|false] 是否有边框
+   * @property {Boolean} readonly = [true|false] 是否仅读
+   * @property {Boolean} preload = [true|false] 是否预加载数据
+   * @value true 开启预加载数据,点击弹出窗口后显示已加载数据
+   * @value false 关闭预加载数据,点击弹出窗口后开始加载数据
+   * @property {Boolean} step-searh = [true|false] 是否分布查询
+   * @value true 启用分布查询,仅查询当前选中节点
+   * @value false 关闭分布查询,一次查询出所有数据
+   * @property {String|DBFieldString} self-field 分布查询当前字段名称
+   * @property {String|DBFieldString} parent-field 分布查询父字段名称
+   * @property {String|DBCollectionString} collection 表名
+   * @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
+   * @property {String} orderby 排序字段及正序倒叙设置
+   * @property {String|JQLString} where 查询条件
+   * @event {Function} popupshow 弹出的选择窗口打开时触发此事件
+   * @event {Function} popuphide 弹出的选择窗口关闭时触发此事件
+   */
+  export default {
+    name: 'UniDataPicker',
+    emits: ['popupopened', 'popupclosed', 'nodeclick', 'change', 'input', 'update:modelValue', 'inputclick'],
+    mixins: [dataPicker],
+    props: {
+      popupTitle: {
+        type: String,
+        default: '请选择'
+      },
+      placeholder: {
+        type: String,
+        default: '请选择'
+      },
+      heightMobile: {
+        type: String,
+        default: ''
+      },
+      readonly: {
+        type: Boolean,
+        default: false
+      },
+      clearIcon: {
+        type: Boolean,
+        default: true
+      },
+      border: {
+        type: Boolean,
+        default: true
+      },
+      split: {
+        type: String,
+        default: '/'
+      },
+      ellipsis: {
+        type: Boolean,
+        default: true
+      }
+    },
+    data() {
+      return {
+        isOpened: false
+      }
+    },
+    computed: {
+      isShowClearIcon() : boolean {
+        if (this.readonly) {
+          return false
+        }
+
+        if (this.clearIcon && this.selectedPaths.length > 0) {
+          return true
+        }
+
+        return false
+      }
+    },
+    created() {
+      this.load()
+    },
+    methods: {
+      clear() {
+      },
+      load() {
+        if (this.isLocalData) {
+          this.loadLocalData()
+        } else if (this.isCloudDataList || this.isCloudDataTree) {
+          this.loadCloudDataPath()
+        }
+      },
+      show() {
+        this.isOpened = true
+        this.$emit('popupopened')
+        if (!this.hasCloudTreeData) {
+          this.loadData()
+        }
+      },
+      hide() {
+        this.isOpened = false
+        this.$emit('popupclosed')
+      },
+      handleInput() {
+        if (this.readonly) {
+          this.$emit('inputclick')
+        } else {
+          this.show()
+        }
+      },
+      handleClose() {
+        this.hide()
+      },
+      onFinish() {
+        this.selectedPaths = this.getChangeNodes()
+        this.$emit('update:modelValue', this.selectedPaths)
+        this.$emit('change', this.selectedPaths)
+        this.hide()
+      }
+    }
+  }
+</script>
+
+<style>
+  @import url("../uni-data-pickerview/uni-data-pickerview.css");
+
+  .uni-data-tree {
+    position: relative;
+  }
+
+  .uni-data-tree-input {
+    position: relative;
+  }
+
+  .selected-loading {
+    display: flex;
+    justify-content: center;
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+  }
+
+  .error-text {
+    flex: 1;
+    font-size: 12px;
+    color: #DD524D;
+  }
+
+  .input-value {
+    flex-direction: row;
+    align-items: center;
+    flex-wrap: nowrap;
+    padding: 5px 5px;
+    padding-right: 5px;
+    overflow: hidden;
+    min-height: 28px;
+  }
+
+  .input-value-border {
+    border: 1px solid #e5e5e5;
+    border-radius: 5px;
+  }
+
+  .selected-path {
+    flex: 1;
+    flex-direction: row;
+    overflow: hidden;
+  }
+
+  .load-more {
+    width: 40px;
+  }
+
+  .selected-list {
+    flex-direction: row;
+    flex-wrap: nowrap;
+  }
+
+  .selected-item {
+    flex-direction: row;
+    flex-wrap: nowrap;
+  }
+
+  .text-color {
+    font-size: 14px;
+    color: #333;
+  }
+
+  .placeholder {
+    color: grey;
+    font-size: 14px;
+  }
+
+  .input-split-line {
+    opacity: .5;
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+
+  .arrow-area {
+    position: relative;
+    padding: 0 12px;
+    margin-left: auto;
+    justify-content: center;
+    transform: rotate(-45deg);
+    transform-origin: center;
+  }
+
+  .input-arrow {
+    width: 8px;
+    height: 8px;
+    border-left: 2px solid #999;
+    border-bottom: 2px solid #999;
+  }
+
+  .uni-data-tree-cover {
+    position: fixed;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, .4);
+    flex-direction: column;
+    z-index: 100;
+  }
+
+  .uni-data-tree-dialog {
+    position: fixed;
+    left: 0;
+    top: 20%;
+    right: 0;
+    bottom: 0;
+    background-color: #FFFFFF;
+    border-top-left-radius: 10px;
+    border-top-right-radius: 10px;
+    flex-direction: column;
+    z-index: 102;
+    overflow: hidden;
+  }
+
+  .dialog-caption {
+    position: relative;
+    flex-direction: row;
+  }
+
+  .dialog-title-view {
+    flex: 1;
+  }
+
+  .dialog-title {
+    align-self: center;
+    padding: 0 10px;
+    line-height: 44px;
+  }
+
+  .dialog-close {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    flex-direction: row;
+    align-items: center;
+    padding: 0 15px;
+  }
+
+  .dialog-close-plus {
+    width: 16px;
+    height: 2px;
+    background-color: #666;
+    border-radius: 2px;
+    transform: rotate(45deg);
+  }
+
+  .dialog-close-rotate {
+    position: absolute;
+    transform: rotate(-45deg);
+  }
+
+  .uni-data-pickerview {
+    flex: 1;
+  }
+
+  .icon-clear {
+    display: flex;
+    align-items: center;
+  }
+
+  /* #ifdef H5 */
+  @media all and (min-width: 768px) {
+    .uni-data-tree-cover {
+      background-color: transparent;
+    }
+
+    .uni-data-tree-dialog {
+      position: absolute;
+      top: 55px;
+      height: auto;
+      min-height: 400px;
+      max-height: 50vh;
+      background-color: #fff;
+      border: 1px solid #EBEEF5;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+      border-radius: 4px;
+      overflow: unset;
+    }
+
+    .dialog-caption {
+      display: none;
+    }
+  }
+  /* #endif */
+</style>

+ 551 - 0
uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue

@@ -0,0 +1,551 @@
+<template>
+  <view class="uni-data-tree">
+    <view class="uni-data-tree-input" @click="handleInput">
+      <slot :options="options" :data="inputSelected" :error="errorMessage">
+        <view class="input-value" :class="{'input-value-border': border}">
+          <text v-if="errorMessage" class="selected-area error-text">{{errorMessage}}</text>
+          <view v-else-if="loading && !isOpened" class="selected-area">
+            <uni-load-more class="load-more" :contentText="loadMore" status="loading"></uni-load-more>
+          </view>
+          <scroll-view v-else-if="inputSelected.length" class="selected-area" scroll-x="true">
+            <view class="selected-list">
+              <view class="selected-item" v-for="(item,index) in inputSelected" :key="index">
+                <text class="text-color">{{item.text}}</text><text v-if="index<inputSelected.length-1"
+                  class="input-split-line">{{split}}</text>
+              </view>
+            </view>
+          </scroll-view>
+          <text v-else class="selected-area placeholder">{{placeholder}}</text>
+          <view v-if="clearIcon && !readonly && inputSelected.length" class="icon-clear" @click.stop="clear">
+            <uni-icons type="clear" color="#c0c4cc" size="24"></uni-icons>
+          </view>
+          <view class="arrow-area" v-if="(!clearIcon || !inputSelected.length) && !readonly ">
+            <view class="input-arrow"></view>
+          </view>
+        </view>
+      </slot>
+    </view>
+    <view class="uni-data-tree-cover" v-if="isOpened" @click="handleClose"></view>
+    <view class="uni-data-tree-dialog" v-if="isOpened">
+      <view class="uni-popper__arrow"></view>
+      <view class="dialog-caption">
+        <view class="title-area">
+          <text class="dialog-title">{{popupTitle}}</text>
+        </view>
+        <view class="dialog-close" @click="handleClose">
+          <view class="dialog-close-plus" data-id="close"></view>
+          <view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
+        </view>
+      </view>
+      <data-picker-view class="picker-view" ref="pickerView" v-model="dataValue" :localdata="localdata"
+        :preload="preload" :collection="collection" :field="field" :orderby="orderby" :where="where"
+        :step-searh="stepSearh" :self-field="selfField" :parent-field="parentField" :managed-mode="true" :map="map"
+        :ellipsis="ellipsis" @change="onchange" @datachange="ondatachange" @nodeclick="onnodeclick">
+      </data-picker-view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import dataPicker from "../uni-data-pickerview/uni-data-picker.js"
+  import DataPickerView from "../uni-data-pickerview/uni-data-pickerview.vue"
+
+  /**
+   * DataPicker 级联选择
+   * @description 支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=3796
+   * @property {String} popup-title 弹出窗口标题
+   * @property {Array} localdata 本地数据,参考
+   * @property {Boolean} border = [true|false] 是否有边框
+   * @property {Boolean} readonly = [true|false] 是否仅读
+   * @property {Boolean} preload = [true|false] 是否预加载数据
+   * @value true 开启预加载数据,点击弹出窗口后显示已加载数据
+   * @value false 关闭预加载数据,点击弹出窗口后开始加载数据
+   * @property {Boolean} step-searh = [true|false] 是否分布查询
+   * @value true 启用分布查询,仅查询当前选中节点
+   * @value false 关闭分布查询,一次查询出所有数据
+   * @property {String|DBFieldString} self-field 分布查询当前字段名称
+   * @property {String|DBFieldString} parent-field 分布查询父字段名称
+   * @property {String|DBCollectionString} collection 表名
+   * @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
+   * @property {String} orderby 排序字段及正序倒叙设置
+   * @property {String|JQLString} where 查询条件
+   * @event {Function} popupshow 弹出的选择窗口打开时触发此事件
+   * @event {Function} popuphide 弹出的选择窗口关闭时触发此事件
+   */
+  export default {
+    name: 'UniDataPicker',
+    emits: ['popupopened', 'popupclosed', 'nodeclick', 'input', 'change', 'update:modelValue','inputclick'],
+    mixins: [dataPicker],
+    components: {
+      DataPickerView
+    },
+    props: {
+      options: {
+        type: [Object, Array],
+        default () {
+          return {}
+        }
+      },
+      popupTitle: {
+        type: String,
+        default: '请选择'
+      },
+      placeholder: {
+        type: String,
+        default: '请选择'
+      },
+      heightMobile: {
+        type: String,
+        default: ''
+      },
+      readonly: {
+        type: Boolean,
+        default: false
+      },
+      clearIcon: {
+        type: Boolean,
+        default: true
+      },
+      border: {
+        type: Boolean,
+        default: true
+      },
+      split: {
+        type: String,
+        default: '/'
+      },
+      ellipsis: {
+        type: Boolean,
+        default: true
+      }
+    },
+    data() {
+      return {
+        isOpened: false,
+        inputSelected: []
+      }
+    },
+    created() {
+      this.$nextTick(() => {
+        this.load();
+      })
+    },
+    watch: {
+			localdata: {
+				handler() {
+					this.load()
+				},
+        deep: true
+			},
+    },
+    methods: {
+      clear() {
+        this._dispatchEvent([]);
+      },
+      onPropsChange() {
+        this._treeData = [];
+        this.selectedIndex = 0;
+
+        this.load();
+      },
+      load() {
+        if (this.readonly) {
+          this._processReadonly(this.localdata, this.dataValue);
+          return;
+        }
+
+        // 回显本地数据
+        if (this.isLocalData) {
+          this.loadData();
+          this.inputSelected = this.selected.slice(0);
+        } else if (this.isCloudDataList || this.isCloudDataTree) { // 回显 Cloud 数据
+          this.loading = true;
+          this.getCloudDataValue().then((res) => {
+            this.loading = false;
+            this.inputSelected = res;
+          }).catch((err) => {
+            this.loading = false;
+            this.errorMessage = err;
+          })
+        }
+      },
+      show() {
+        this.isOpened = true
+        setTimeout(() => {
+          this.$refs.pickerView.updateData({
+            treeData: this._treeData,
+            selected: this.selected,
+            selectedIndex: this.selectedIndex
+          })
+        }, 200)
+        this.$emit('popupopened')
+      },
+      hide() {
+        this.isOpened = false
+        this.$emit('popupclosed')
+      },
+      handleInput() {
+        if (this.readonly) {
+					this.$emit('inputclick')
+          return
+        }
+        this.show()
+      },
+      handleClose(e) {
+        this.hide()
+      },
+      onnodeclick(e) {
+        this.$emit('nodeclick', e)
+      },
+      ondatachange(e) {
+        this._treeData = this.$refs.pickerView._treeData
+      },
+      onchange(e) {
+        this.hide()
+        this.$nextTick(() => {
+          this.inputSelected = e;
+        })
+        this._dispatchEvent(e)
+      },
+      _processReadonly(dataList, value) {
+        var isTree = dataList.findIndex((item) => {
+          return item.children
+        })
+        if (isTree > -1) {
+          let inputValue
+          if (Array.isArray(value)) {
+            inputValue = value[value.length - 1]
+            if (typeof inputValue === 'object' && inputValue.value) {
+              inputValue = inputValue.value
+            }
+          } else {
+            inputValue = value
+          }
+          this.inputSelected = this._findNodePath(inputValue, this.localdata)
+          return
+        }
+
+        if (!this.hasValue) {
+          this.inputSelected = []
+          return
+        }
+
+        let result = []
+        for (let i = 0; i < value.length; i++) {
+          var val = value[i]
+          var item = dataList.find((v) => {
+            return v.value == val
+          })
+          if (item) {
+            result.push(item)
+          }
+        }
+        if (result.length) {
+          this.inputSelected = result
+        }
+      },
+      _filterForArray(data, valueArray) {
+        var result = []
+        for (let i = 0; i < valueArray.length; i++) {
+          var value = valueArray[i]
+          var found = data.find((item) => {
+            return item.value == value
+          })
+          if (found) {
+            result.push(found)
+          }
+        }
+        return result
+      },
+      _dispatchEvent(selected) {
+        let item = {}
+        if (selected.length) {
+          var value = new Array(selected.length)
+          for (var i = 0; i < selected.length; i++) {
+            value[i] = selected[i].value
+          }
+          item = selected[selected.length - 1]
+        } else {
+          item.value = ''
+        }
+        if (this.formItem) {
+          this.formItem.setValue(item.value)
+        }
+
+        this.$emit('input', item.value)
+        this.$emit('update:modelValue', item.value)
+        this.$emit('change', {
+          detail: {
+            value: selected
+          }
+        })
+      }
+    }
+  }
+</script>
+
+<style>
+  .uni-data-tree {
+    flex: 1;
+    position: relative;
+    font-size: 14px;
+  }
+
+  .error-text {
+    color: #DD524D;
+  }
+
+  .input-value {
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    align-items: center;
+    flex-wrap: nowrap;
+    font-size: 14px;
+    /* line-height: 35px; */
+    padding: 0 10px;
+    padding-right: 5px;
+    overflow: hidden;
+    height: 35px;
+    /* #ifndef APP-NVUE */
+    box-sizing: border-box;
+    /* #endif */
+  }
+
+  .input-value-border {
+    border: 1px solid #e5e5e5;
+    border-radius: 5px;
+  }
+
+  .selected-area {
+    flex: 1;
+    overflow: hidden;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+  }
+
+  .load-more {
+    /* #ifndef APP-NVUE */
+    margin-right: auto;
+    /* #endif */
+    /* #ifdef APP-NVUE */
+    width: 40px;
+    /* #endif */
+  }
+
+  .selected-list {
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    flex-wrap: nowrap;
+    /* padding: 0 5px; */
+  }
+
+  .selected-item {
+    flex-direction: row;
+    /* padding: 0 1px; */
+    /* #ifndef APP-NVUE */
+    white-space: nowrap;
+    /* #endif */
+  }
+
+  .text-color {
+    color: #333;
+  }
+
+  .placeholder {
+    color: grey;
+    font-size: 12px;
+  }
+
+  .input-split-line {
+    opacity: .5;
+  }
+
+  .arrow-area {
+    position: relative;
+    width: 20px;
+    /* #ifndef APP-NVUE */
+    margin-bottom: 5px;
+    margin-left: auto;
+    display: flex;
+    /* #endif */
+    justify-content: center;
+    transform: rotate(-45deg);
+    transform-origin: center;
+  }
+
+  .input-arrow {
+    width: 7px;
+    height: 7px;
+    border-left: 1px solid #999;
+    border-bottom: 1px solid #999;
+  }
+
+  .uni-data-tree-cover {
+    position: fixed;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, .4);
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: column;
+    z-index: 100;
+  }
+
+  .uni-data-tree-dialog {
+    position: fixed;
+    left: 0;
+    /* #ifndef APP-NVUE */
+    top: 20%;
+    /* #endif */
+    /* #ifdef APP-NVUE */
+    top: 200px;
+    /* #endif */
+    right: 0;
+    bottom: 0;
+    background-color: #FFFFFF;
+    border-top-left-radius: 10px;
+    border-top-right-radius: 10px;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: column;
+    z-index: 102;
+    overflow: hidden;
+    /* #ifdef APP-NVUE */
+    width: 750rpx;
+    /* #endif */
+  }
+
+  .dialog-caption {
+    position: relative;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    /* border-bottom: 1px solid #f0f0f0; */
+  }
+
+  .title-area {
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    align-items: center;
+    /* #ifndef APP-NVUE */
+    margin: auto;
+    /* #endif */
+    padding: 0 10px;
+  }
+
+  .dialog-title {
+    /* font-weight: bold; */
+    line-height: 44px;
+  }
+
+  .dialog-close {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    align-items: center;
+    padding: 0 15px;
+  }
+
+  .dialog-close-plus {
+    width: 16px;
+    height: 2px;
+    background-color: #666;
+    border-radius: 2px;
+    transform: rotate(45deg);
+  }
+
+  .dialog-close-rotate {
+    position: absolute;
+    transform: rotate(-45deg);
+  }
+
+  .picker-view {
+    flex: 1;
+    overflow: hidden;
+  }
+
+  .icon-clear {
+    display: flex;
+    align-items: center;
+  }
+
+  /* #ifdef H5 */
+  @media all and (min-width: 768px) {
+    .uni-data-tree-cover {
+      background-color: transparent;
+    }
+
+    .uni-data-tree-dialog {
+      position: absolute;
+      top: 55px;
+      height: auto;
+      min-height: 400px;
+      max-height: 50vh;
+      background-color: #fff;
+      border: 1px solid #EBEEF5;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+      border-radius: 4px;
+      overflow: unset;
+    }
+
+    .dialog-caption {
+      display: none;
+    }
+
+    .icon-clear {
+      /* margin-right: 5px; */
+    }
+  }
+
+  /* #endif */
+
+  /* picker 弹出层通用的指示小三角, todo:扩展至上下左右方向定位 */
+  /* #ifndef APP-NVUE */
+  .uni-popper__arrow,
+  .uni-popper__arrow::after {
+    position: absolute;
+    display: block;
+    width: 0;
+    height: 0;
+    border-color: transparent;
+    border-style: solid;
+    border-width: 6px;
+  }
+
+  .uni-popper__arrow {
+    filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
+    top: -6px;
+    left: 10%;
+    margin-right: 3px;
+    border-top-width: 0;
+    border-bottom-color: #EBEEF5;
+  }
+
+  .uni-popper__arrow::after {
+    content: " ";
+    top: 1px;
+    margin-left: -6px;
+    border-top-width: 0;
+    border-bottom-color: #fff;
+  }
+
+  /* #endif */
+</style>

+ 622 - 0
uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js

@@ -0,0 +1,622 @@
+export default {
+  props: {
+    localdata: {
+      type: [Array, Object],
+      default () {
+        return []
+      }
+    },
+    spaceInfo: {
+      type: Object,
+      default () {
+        return {}
+      }
+    },
+    collection: {
+      type: String,
+      default: ''
+    },
+    action: {
+      type: String,
+      default: ''
+    },
+    field: {
+      type: String,
+      default: ''
+    },
+    orderby: {
+      type: String,
+      default: ''
+    },
+    where: {
+      type: [String, Object],
+      default: ''
+    },
+    pageData: {
+      type: String,
+      default: 'add'
+    },
+    pageCurrent: {
+      type: Number,
+      default: 1
+    },
+    pageSize: {
+      type: Number,
+      default: 500
+    },
+    getcount: {
+      type: [Boolean, String],
+      default: false
+    },
+    getone: {
+      type: [Boolean, String],
+      default: false
+    },
+    gettree: {
+      type: [Boolean, String],
+      default: false
+    },
+    manual: {
+      type: Boolean,
+      default: false
+    },
+    value: {
+      type: [Array, String, Number],
+      default () {
+        return []
+      }
+    },
+    modelValue: {
+      type: [Array, String, Number],
+      default () {
+        return []
+      }
+    },
+    preload: {
+      type: Boolean,
+      default: false
+    },
+    stepSearh: {
+      type: Boolean,
+      default: true
+    },
+    selfField: {
+      type: String,
+      default: ''
+    },
+    parentField: {
+      type: String,
+      default: ''
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    map: {
+      type: Object,
+      default () {
+        return {
+          text: "text",
+          value: "value"
+        }
+      }
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      errorMessage: '',
+      loadMore: {
+        contentdown: '',
+        contentrefresh: '',
+        contentnomore: ''
+      },
+      dataList: [],
+      selected: [],
+      selectedIndex: 0,
+      page: {
+        current: this.pageCurrent,
+        size: this.pageSize,
+        count: 0
+      }
+    }
+  },
+  computed: {
+    isLocalData() {
+      return !this.collection.length;
+    },
+    isCloudData() {
+      return this.collection.length > 0;
+    },
+    isCloudDataList() {
+      return (this.isCloudData && (!this.parentField && !this.selfField));
+    },
+    isCloudDataTree() {
+      return (this.isCloudData && this.parentField && this.selfField);
+    },
+    dataValue() {
+      let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null ||
+        this.modelValue !== undefined);
+      return isModelValue ? this.modelValue : this.value;
+    },
+    hasValue() {
+      if (typeof this.dataValue === 'number') {
+        return true
+      }
+      return (this.dataValue != null) && (this.dataValue.length > 0)
+    }
+  },
+  created() {
+    this.$watch(() => {
+      var al = [];
+      ['pageCurrent',
+        'pageSize',
+        'spaceInfo',
+        'value',
+        'modelValue',
+        'localdata',
+        'collection',
+        'action',
+        'field',
+        'orderby',
+        'where',
+        'getont',
+        'getcount',
+        'gettree'
+      ].forEach(key => {
+        al.push(this[key])
+      });
+      return al
+    }, (newValue, oldValue) => {
+      let needReset = false
+      for (let i = 2; i < newValue.length; i++) {
+        if (newValue[i] != oldValue[i]) {
+          needReset = true
+          break
+        }
+      }
+      if (newValue[0] != oldValue[0]) {
+        this.page.current = this.pageCurrent
+      }
+      this.page.size = this.pageSize
+
+      this.onPropsChange()
+    })
+    this._treeData = []
+  },
+  methods: {
+    onPropsChange() {
+      this._treeData = [];
+    },
+
+    // 填充 pickview 数据
+    async loadData() {
+      if (this.isLocalData) {
+        this.loadLocalData();
+      } else if (this.isCloudDataList) {
+        this.loadCloudDataList();
+      } else if (this.isCloudDataTree) {
+        this.loadCloudDataTree();
+      }
+    },
+
+    // 加载本地数据
+    async loadLocalData() {
+      this._treeData = [];
+      this._extractTree(this.localdata, this._treeData);
+
+      let inputValue = this.dataValue;
+      if (inputValue === undefined) {
+        return;
+      }
+
+      if (Array.isArray(inputValue)) {
+        inputValue = inputValue[inputValue.length - 1];
+        if (typeof inputValue === 'object' && inputValue[this.map.value]) {
+          inputValue = inputValue[this.map.value];
+        }
+      }
+
+      this.selected = this._findNodePath(inputValue, this.localdata);
+    },
+
+    // 加载 Cloud 数据 (单列)
+    async loadCloudDataList() {
+      if (this.loading) {
+        return;
+      }
+      this.loading = true;
+
+      try {
+        let response = await this.getCommand();
+        let responseData = response.result.data;
+
+        this._treeData = responseData;
+
+        this._updateBindData();
+        this._updateSelected();
+
+        this.onDataChange();
+      } catch (e) {
+        this.errorMessage = e;
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 加载 Cloud 数据 (树形)
+    async loadCloudDataTree() {
+      if (this.loading) {
+        return;
+      }
+      this.loading = true;
+
+      try {
+        let commandOptions = {
+          field: this._cloudDataPostField(),
+          where: this._cloudDataTreeWhere()
+        };
+        if (this.gettree) {
+          commandOptions.startwith = `${this.selfField}=='${this.dataValue}'`;
+        }
+
+        let response = await this.getCommand(commandOptions);
+        let responseData = response.result.data;
+
+        this._treeData = responseData;
+        this._updateBindData();
+        this._updateSelected();
+
+        this.onDataChange();
+      } catch (e) {
+        this.errorMessage = e;
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 加载 Cloud 数据 (节点)
+    async loadCloudDataNode(callback) {
+      if (this.loading) {
+        return;
+      }
+      this.loading = true;
+
+      try {
+        let commandOptions = {
+          field: this._cloudDataPostField(),
+          where: this._cloudDataNodeWhere()
+        };
+
+        let response = await this.getCommand(commandOptions);
+        let responseData = response.result.data;
+
+        callback(responseData);
+      } catch (e) {
+        this.errorMessage = e;
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 回显 Cloud 数据
+    getCloudDataValue() {
+      if (this.isCloudDataList) {
+        return this.getCloudDataListValue();
+      }
+
+      if (this.isCloudDataTree) {
+        return this.getCloudDataTreeValue();
+      }
+    },
+
+    // 回显 Cloud 数据 (单列)
+    getCloudDataListValue() {
+      // 根据 field's as value标识匹配 where 条件
+      let where = [];
+      let whereField = this._getForeignKeyByField();
+      if (whereField) {
+        where.push(`${whereField} == '${this.dataValue}'`)
+      }
+
+      where = where.join(' || ');
+
+      if (this.where) {
+        where = `(${this.where}) && (${where})`
+      }
+
+      return this.getCommand({
+        field: this._cloudDataPostField(),
+        where
+      }).then((res) => {
+        this.selected = res.result.data;
+        return res.result.data;
+      });
+    },
+
+    // 回显 Cloud 数据 (树形)
+    getCloudDataTreeValue() {
+      return this.getCommand({
+        field: this._cloudDataPostField(),
+        getTreePath: {
+          startWith: `${this.selfField}=='${this.dataValue}'`
+        }
+      }).then((res) => {
+        let treePath = [];
+        this._extractTreePath(res.result.data, treePath);
+        this.selected = treePath;
+        return treePath;
+      });
+    },
+
+    getCommand(options = {}) {
+      /* eslint-disable no-undef */
+      let db = uniCloud.database(this.spaceInfo)
+
+      const action = options.action || this.action
+      if (action) {
+        db = db.action(action)
+      }
+
+      const collection = options.collection || this.collection
+      db = db.collection(collection)
+
+      const where = options.where || this.where
+      if (!(!where || !Object.keys(where).length)) {
+        db = db.where(where)
+      }
+
+      const field = options.field || this.field
+      if (field) {
+        db = db.field(field)
+      }
+
+      const orderby = options.orderby || this.orderby
+      if (orderby) {
+        db = db.orderBy(orderby)
+      }
+
+      const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current
+      const size = options.pageSize !== undefined ? options.pageSize : this.page.size
+      const getCount = options.getcount !== undefined ? options.getcount : this.getcount
+      const getTree = options.gettree !== undefined ? options.gettree : this.gettree
+
+      const getOptions = {
+        getCount,
+        getTree
+      }
+      if (options.getTreePath) {
+        getOptions.getTreePath = options.getTreePath
+      }
+
+      db = db.skip(size * (current - 1)).limit(size).get(getOptions)
+
+      return db
+    },
+
+    _cloudDataPostField() {
+      let fields = [this.field];
+      if (this.parentField) {
+        fields.push(`${this.parentField} as parent_value`);
+      }
+      return fields.join(',');
+    },
+
+    _cloudDataTreeWhere() {
+      let result = []
+      let selected = this.selected
+      let parentField = this.parentField
+      if (parentField) {
+        result.push(`${parentField} == null || ${parentField} == ""`)
+      }
+      if (selected.length) {
+        for (var i = 0; i < selected.length - 1; i++) {
+          result.push(`${parentField} == '${selected[i].value}'`)
+        }
+      }
+
+      let where = []
+      if (this.where) {
+        where.push(`(${this.where})`)
+      }
+
+      if (result.length) {
+        where.push(`(${result.join(' || ')})`)
+      }
+
+      return where.join(' && ')
+    },
+
+    _cloudDataNodeWhere() {
+      let where = []
+      let selected = this.selected;
+      if (selected.length) {
+        where.push(`${this.parentField} == '${selected[selected.length - 1].value}'`);
+      }
+
+      where = where.join(' || ');
+
+      if (this.where) {
+        return `(${this.where}) && (${where})`
+      }
+
+      return where
+    },
+
+    _getWhereByForeignKey() {
+      let result = []
+      let whereField = this._getForeignKeyByField();
+      if (whereField) {
+        result.push(`${whereField} == '${this.dataValue}'`)
+      }
+
+      if (this.where) {
+        return `(${this.where}) && (${result.join(' || ')})`
+      }
+
+      return result.join(' || ')
+    },
+
+    _getForeignKeyByField() {
+      let fields = this.field.split(',');
+      let whereField = null;
+      for (let i = 0; i < fields.length; i++) {
+        const items = fields[i].split('as');
+        if (items.length < 2) {
+          continue;
+        }
+        if (items[1].trim() === 'value') {
+          whereField = items[0].trim();
+          break;
+        }
+      }
+      return whereField;
+    },
+
+    _updateBindData(node) {
+      const {
+        dataList,
+        hasNodes
+      } = this._filterData(this._treeData, this.selected)
+
+      let isleaf = this._stepSearh === false && !hasNodes
+
+      if (node) {
+        node.isleaf = isleaf
+      }
+
+      this.dataList = dataList
+      this.selectedIndex = dataList.length - 1
+
+      if (!isleaf && this.selected.length < dataList.length) {
+        this.selected.push({
+          value: null,
+          text: "请选择"
+        })
+      }
+
+      return {
+        isleaf,
+        hasNodes
+      }
+    },
+
+    _updateSelected() {
+      let dl = this.dataList
+      let sl = this.selected
+      let textField = this.map.text
+      let valueField = this.map.value
+      for (let i = 0; i < sl.length; i++) {
+        let value = sl[i].value
+        let dl2 = dl[i]
+        for (let j = 0; j < dl2.length; j++) {
+          let item2 = dl2[j]
+          if (item2[valueField] === value) {
+            sl[i].text = item2[textField]
+            break
+          }
+        }
+      }
+    },
+
+    _filterData(data, paths) {
+      let dataList = []
+      let hasNodes = true
+
+      dataList.push(data.filter((item) => {
+        return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '')
+      }))
+      for (let i = 0; i < paths.length; i++) {
+        let value = paths[i].value
+        let nodes = data.filter((item) => {
+          return item.parent_value === value
+        })
+
+        if (nodes.length) {
+          dataList.push(nodes)
+        } else {
+          hasNodes = false
+        }
+      }
+
+      return {
+        dataList,
+        hasNodes
+      }
+    },
+
+    _extractTree(nodes, result, parent_value) {
+      let list = result || []
+      let valueField = this.map.value
+      for (let i = 0; i < nodes.length; i++) {
+        let node = nodes[i]
+
+        let child = {}
+        for (let key in node) {
+          if (key !== 'children') {
+            child[key] = node[key]
+          }
+        }
+        if (parent_value !== null && parent_value !== undefined && parent_value !== '') {
+          child.parent_value = parent_value
+        }
+        result.push(child)
+
+        let children = node.children
+        if (children) {
+          this._extractTree(children, result, node[valueField])
+        }
+      }
+    },
+
+    _extractTreePath(nodes, result) {
+      let list = result || []
+      for (let i = 0; i < nodes.length; i++) {
+        let node = nodes[i]
+
+        let child = {}
+        for (let key in node) {
+          if (key !== 'children') {
+            child[key] = node[key]
+          }
+        }
+        result.push(child)
+
+        let children = node.children
+        if (children) {
+          this._extractTreePath(children, result)
+        }
+      }
+    },
+
+    _findNodePath(key, nodes, path = []) {
+      let textField = this.map.text
+      let valueField = this.map.value
+      for (let i = 0; i < nodes.length; i++) {
+        let node = nodes[i]
+        let children = node.children
+        let text = node[textField]
+        let value = node[valueField]
+
+        path.push({
+          value,
+          text
+        })
+
+        if (value === key) {
+          return path
+        }
+
+        if (children) {
+          const p = this._findNodePath(key, children, path)
+          if (p.length) {
+            return p
+          }
+        }
+
+        path.pop()
+      }
+      return []
+    }
+  }
+}

+ 692 - 0
uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.uts

@@ -0,0 +1,692 @@
+export type PaginationType = {
+  current : number,
+  size : number,
+  count : number
+}
+
+export type LoadMoreType = {
+  contentdown : string,
+  contentrefresh : string,
+  contentnomore : string
+}
+
+export type SelectedItemType = {
+  name : string,
+  value : string,
+}
+
+export type GetCommandOptions = {
+  collection ?: UTSJSONObject,
+  field ?: string,
+  orderby ?: string,
+  where ?: any,
+  pageData ?: string,
+  pageCurrent ?: number,
+  pageSize ?: number,
+  getCount ?: boolean,
+  getTree ?: any,
+  getTreePath ?: UTSJSONObject,
+  startwith ?: string,
+  limitlevel ?: number,
+  groupby ?: string,
+  groupField ?: string,
+  distinct ?: boolean,
+  pageIndistinct ?: boolean,
+  foreignKey ?: string,
+  loadtime ?: string,
+  manual ?: boolean
+}
+
+const DefaultSelectedNode = {
+  text: '请选择',
+  value: ''
+}
+
+export const dataPicker = defineMixin({
+  props: {
+    localdata: {
+      type: Array as PropType<Array<UTSJSONObject>>,
+      default: [] as Array<UTSJSONObject>
+    },
+    collection: {
+      type: Object,
+      default: ''
+    },
+    field: {
+      type: String,
+      default: ''
+    },
+    orderby: {
+      type: String,
+      default: ''
+    },
+    where: {
+      type: Object,
+      default: ''
+    },
+    pageData: {
+      type: String,
+      default: 'add'
+    },
+    pageCurrent: {
+      type: Number,
+      default: 1
+    },
+    pageSize: {
+      type: Number,
+      default: 20
+    },
+    getcount: {
+      type: Boolean,
+      default: false
+    },
+    gettree: {
+      type: Object,
+      default: ''
+    },
+    gettreepath: {
+      type: Object,
+      default: ''
+    },
+    startwith: {
+      type: String,
+      default: ''
+    },
+    limitlevel: {
+      type: Number,
+      default: 10
+    },
+    groupby: {
+      type: String,
+      default: ''
+    },
+    groupField: {
+      type: String,
+      default: ''
+    },
+    distinct: {
+      type: Boolean,
+      default: false
+    },
+    pageIndistinct: {
+      type: Boolean,
+      default: false
+    },
+    foreignKey: {
+      type: String,
+      default: ''
+    },
+    loadtime: {
+      type: String,
+      default: 'auto'
+    },
+    manual: {
+      type: Boolean,
+      default: false
+    },
+    preload: {
+      type: Boolean,
+      default: false
+    },
+    stepSearh: {
+      type: Boolean,
+      default: true
+    },
+    selfField: {
+      type: String,
+      default: ''
+    },
+    parentField: {
+      type: String,
+      default: ''
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    value: {
+      type: Object,
+      default: ''
+    },
+    modelValue: {
+      type: Object,
+      default: ''
+    },
+    defaultProps: {
+      type: Object as PropType<UTSJSONObject>,
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      error: null as UniCloudError | null,
+      treeData: [] as Array<UTSJSONObject>,
+      selectedIndex: 0,
+      selectedNodes: [] as Array<UTSJSONObject>,
+      selectedPages: [] as Array<UTSJSONObject>[],
+      selectedValue: '',
+      selectedPaths: [] as Array<UTSJSONObject>,
+      pagination: {
+        current: 1,
+        size: 20,
+        count: 0
+      } as PaginationType
+    }
+  },
+  computed: {
+    mappingTextName() : string {
+      // TODO
+      return (this.defaultProps != null) ? this.defaultProps!.getString('text', 'text') : 'text'
+    },
+    mappingValueName() : string {
+      // TODO
+      return (this.defaultProps != null) ? this.defaultProps!.getString('value', 'value') : 'value'
+    },
+    currentDataList() : Array<UTSJSONObject> {
+      if (this.selectedIndex > this.selectedPages.length - 1) {
+        return [] as Array<UTSJSONObject>
+      }
+      return this.selectedPages[this.selectedIndex]
+    },
+    isLocalData() : boolean {
+      return this.localdata.length > 0
+    },
+    isCloudData() : boolean {
+      return this._checkIsNotNull(this.collection)
+    },
+    isCloudDataList() : boolean {
+      return (this.isCloudData && (this.parentField.length == 0 && this.selfField.length == 0))
+    },
+    isCloudDataTree() : boolean {
+      return (this.isCloudData && this.parentField.length > 0 && this.selfField.length > 0)
+    },
+    dataValue() : any {
+      return this.hasModelValue ? this.modelValue : this.value
+    },
+    hasCloudTreeData() : boolean {
+      return this.treeData.length > 0
+    },
+    hasModelValue() : boolean {
+      if (typeof this.modelValue == 'string') {
+        const valueString = this.modelValue as string
+        return (valueString.length > 0)
+      } else if (Array.isArray(this.modelValue)) {
+        const valueArray = this.modelValue as Array<string>
+        return (valueArray.length > 0)
+      }
+      return false
+    },
+    hasCloudDataValue() : boolean {
+      if (typeof this.dataValue == 'string') {
+        const valueString = this.dataValue as string
+        return (valueString.length > 0)
+      }
+      return false
+    }
+  },
+  created() {
+    this.pagination.current = this.pageCurrent
+    this.pagination.size = this.pageSize
+
+    this.$watch(
+      () : any => [
+        this.pageCurrent,
+        this.pageSize,
+        this.localdata,
+        this.value,
+        this.collection,
+        this.field,
+        this.getcount,
+        this.orderby,
+        this.where,
+        this.groupby,
+        this.groupField,
+        this.distinct
+      ],
+      (newValue : Array<any>, oldValue : Array<any>) => {
+        this.pagination.size = this.pageSize
+        if (newValue[0] !== oldValue[0]) {
+          this.pagination.current = this.pageCurrent
+        }
+
+        this.onPropsChange()
+      }
+    )
+  },
+  methods: {
+    onPropsChange() {
+      this.selectedIndex = 0
+      this.selectedNodes.length = 0
+      this.selectedPages.length = 0
+      this.selectedPaths.length = 0
+
+      // 加载数据
+      this.$nextTick(() => {
+        this.loadData()
+      })
+    },
+
+    onTabSelect(index : number) {
+      this.selectedIndex = index
+    },
+
+    onNodeClick(nodeData : UTSJSONObject) {
+      if (nodeData.getBoolean('disable', false)) {
+        return
+      }
+
+      const isLeaf = this._checkIsLeafNode(nodeData)
+
+      this._trimSelectedNodes(nodeData)
+
+      this.$emit('nodeclick', nodeData)
+
+      if (this.isLocalData) {
+        if (isLeaf || !this._checkHasChildren(nodeData)) {
+          this.onFinish()
+        }
+      } else if (this.isCloudDataList) {
+        this.onFinish()
+      } else if (this.isCloudDataTree) {
+        if (isLeaf) {
+          this.onFinish()
+        } else if (!this._checkHasChildren(nodeData)) {
+          // 尝试请求一次,如果没有返回数据标记为叶子节点
+          this.loadCloudDataNode(nodeData)
+        }
+      }
+    },
+
+    getChangeNodes(): Array<UTSJSONObject> {
+      const nodes: Array<UTSJSONObject> = []
+      this.selectedNodes.forEach((node : UTSJSONObject) => {
+        const newNode: UTSJSONObject = {}
+        newNode[this.mappingTextName] = node.getString(this.mappingTextName)
+        newNode[this.mappingValueName] = node.getString(this.mappingValueName)
+        nodes.push(newNode)
+      })
+      return nodes
+    },
+
+    onFinish() { },
+
+    // 加载数据(自动判定环境)
+    loadData() {
+      if (this.isLocalData) {
+        this.loadLocalData()
+      } else if (this.isCloudDataList) {
+        this.loadCloudDataList()
+      } else if (this.isCloudDataTree) {
+        this.loadCloudDataTree()
+      }
+    },
+
+    // 加载本地数据
+    loadLocalData() {
+      this.treeData = this.localdata
+      if (Array.isArray(this.dataValue)) {
+        const value = this.dataValue as Array<UTSJSONObject>
+        this.selectedPaths = value.slice(0)
+        this._pushSelectedTreeNodes(value, this.localdata)
+      } else {
+        this._pushSelectedNodes(this.localdata)
+      }
+    },
+
+    // 加载 Cloud 数据 (单列)
+    loadCloudDataList() {
+      this._loadCloudData(null, (data : Array<UTSJSONObject>) => {
+        this.treeData = data
+        this._pushSelectedNodes(data)
+      })
+    },
+
+    // 加载 Cloud 数据 (树形)
+    loadCloudDataTree() {
+      let commandOptions = {
+        field: this._cloudDataPostField(),
+        where: this._cloudDataTreeWhere(),
+        getTree: true
+      } as GetCommandOptions
+      if (this._checkIsNotNull(this.gettree)) {
+        commandOptions.startwith = `${this.selfField}=='${this.dataValue as string}'`
+      }
+      this._loadCloudData(commandOptions, (data : Array<UTSJSONObject>) => {
+        this.treeData = data
+        if (this.selectedPaths.length > 0) {
+          this._pushSelectedTreeNodes(this.selectedPaths, data)
+        } else {
+          this._pushSelectedNodes(data)
+        }
+      })
+    },
+
+    // 加载 Cloud 数据 (节点)
+    loadCloudDataNode(nodeData : UTSJSONObject) {
+      const commandOptions = {
+        field: this._cloudDataPostField(),
+        where: this._cloudDataNodeWhere()
+      } as GetCommandOptions
+      this._loadCloudData(commandOptions, (data : Array<UTSJSONObject>) => {
+        nodeData['children'] = data
+        if (data.length == 0) {
+          nodeData['isleaf'] = true
+          this.onFinish()
+        } else {
+          this._pushSelectedNodes(data)
+        }
+      })
+    },
+
+    // 回显 Cloud Tree Path
+    loadCloudDataPath() {
+      if (!this.hasCloudDataValue) {
+        return
+      }
+
+      const command : GetCommandOptions = {}
+
+      // 单列
+      if (this.isCloudDataList) {
+        // 根据 field's as value标识匹配 where 条件
+        let where : Array<string> = [];
+        let whereField = this._getForeignKeyByField();
+        if (whereField.length > 0) {
+          where.push(`${whereField} == '${this.dataValue as string}'`)
+        }
+
+        let whereString = where.join(' || ')
+        if (this._checkIsNotNull(this.where)) {
+          whereString = `(${this.where}) && (${whereString})`
+        }
+
+        command.field = this._cloudDataPostField()
+        command.where = whereString
+      }
+
+      // 树形
+      if (this.isCloudDataTree) {
+        command.field = this._cloudDataPostField()
+        command.getTreePath = {
+          startWith: `${this.selfField}=='${this.dataValue as string}'`
+        }
+      }
+
+      this._loadCloudData(command, (data : Array<UTSJSONObject>) => {
+        this._extractTreePath(data, this.selectedPaths)
+      })
+    },
+
+    _loadCloudData(options ?: GetCommandOptions, callback ?: ((data : Array<UTSJSONObject>) => void)) {
+      if (this.loading) {
+        return
+      }
+      this.loading = true
+
+      this.error = null
+
+      this._getCommand(options).then((response : UniCloudDBGetResult) => {
+        callback?.(response.data)
+      }).catch((err : any | null) => {
+        this.error = err as UniCloudError
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+
+    _cloudDataPostField() : string {
+      let fields = [this.field];
+      if (this.parentField.length > 0) {
+        fields.push(`${this.parentField} as parent_value`)
+      }
+      return fields.join(',')
+    },
+
+    _cloudDataTreeWhere() : string {
+      let result : Array<string> = []
+      let selectedNodes = this.selectedNodes.length > 0 ? this.selectedNodes : this.selectedPaths
+      let parentField = this.parentField
+      if (parentField.length > 0) {
+        result.push(`${parentField} == null || ${parentField} == ""`)
+      }
+      if (selectedNodes.length > 0) {
+        for (var i = 0; i < selectedNodes.length - 1; i++) {
+          const parentFieldValue = selectedNodes[i].getString('value', '')
+          result.push(`${parentField} == '${parentFieldValue}'`)
+        }
+      }
+
+      let where : Array<string> = []
+      if (this._checkIsNotNull(this.where)) {
+        where.push(`(${this.where as string})`)
+      }
+
+      if (result.length > 0) {
+        where.push(`(${result.join(' || ')})`)
+      }
+
+      return where.join(' && ')
+    },
+
+    _cloudDataNodeWhere() : string {
+      const where : Array<string> = []
+      if (this.selectedNodes.length > 0) {
+        const value = this.selectedNodes[this.selectedNodes.length - 1].getString('value', '')
+        where.push(`${this.parentField} == '${value}'`)
+      }
+
+      let whereString = where.join(' || ')
+      if (this._checkIsNotNull(this.where)) {
+        return `(${this.where as string}) && (${whereString})`
+      }
+
+      return whereString
+    },
+
+    _getWhereByForeignKey() : string {
+      let result : Array<string> = []
+      let whereField = this._getForeignKeyByField();
+      if (whereField.length > 0) {
+        result.push(`${whereField} == '${this.dataValue as string}'`)
+      }
+
+      if (this._checkIsNotNull(this.where)) {
+        return `(${this.where}) && (${result.join(' || ')})`
+      }
+
+      return result.join(' || ')
+    },
+
+    _getForeignKeyByField() : string {
+      const fields = this.field.split(',')
+      let whereField = ''
+      for (let i = 0; i < fields.length; i++) {
+        const items = fields[i].split('as')
+        if (items.length < 2) {
+          continue
+        }
+        if (items[1].trim() === 'value') {
+          whereField = items[0].trim()
+          break
+        }
+      }
+      return whereField
+    },
+
+    _getCommand(options ?: GetCommandOptions) : Promise<UniCloudDBGetResult> {
+      let db = uniCloud.databaseForJQL()
+
+      let collection = Array.isArray(this.collection) ? db.collection(...(this.collection as Array<any>)) : db.collection(this.collection)
+
+      let filter : UniCloudDBFilter | null = null
+      if (this.foreignKey.length > 0) {
+        filter = collection.foreignKey(this.foreignKey)
+      }
+
+      const where : any = options?.where ?? this.where
+      if (typeof where == 'string') {
+        const whereString = where as string
+        if (whereString.length > 0) {
+          filter = (filter != null) ? filter.where(where) : collection.where(where)
+        }
+      } else {
+        filter = (filter != null) ? filter.where(where) : collection.where(where)
+      }
+
+      let query : UniCloudDBQuery | null = null
+      if (this.field.length > 0) {
+        query = (filter != null) ? filter.field(this.field) : collection.field(this.field)
+      }
+      if (this.groupby.length > 0) {
+        if (query != null) {
+          query = query.groupBy(this.groupby)
+        } else if (filter != null) {
+          query = filter.groupBy(this.groupby)
+        }
+      }
+      if (this.groupField.length > 0) {
+        if (query != null) {
+          query = query.groupField(this.groupField)
+        } else if (filter != null) {
+          query = filter.groupField(this.groupField)
+        }
+      }
+      if (this.distinct == true) {
+        if (query != null) {
+          query = query.distinct(this.field)
+        } else if (filter != null) {
+          query = filter.distinct(this.field)
+        }
+      }
+      if (this.orderby.length > 0) {
+        if (query != null) {
+          query = query.orderBy(this.orderby)
+        } else if (filter != null) {
+          query = filter.orderBy(this.orderby)
+        }
+      }
+
+      const size = this.pagination.size
+      const current = this.pagination.current
+      if (query != null) {
+        query = query.skip(size * (current - 1)).limit(size)
+      } else if (filter != null) {
+        query = filter.skip(size * (current - 1)).limit(size)
+      } else {
+        query = collection.skip(size * (current - 1)).limit(size)
+      }
+
+      const getOptions = {}
+      const treeOptions = {
+        limitLevel: this.limitlevel,
+        startWith: this.startwith
+      }
+      if (this.getcount == true) {
+        getOptions['getCount'] = this.getcount
+      }
+
+      const getTree : any = options?.getTree ?? this.gettree
+      if (typeof getTree == 'string') {
+        const getTreeString = getTree as string
+        if (getTreeString.length > 0) {
+          getOptions['getTree'] = treeOptions
+        }
+      } else if (typeof getTree == 'object') {
+        getOptions['getTree'] = treeOptions
+      } else {
+        getOptions['getTree'] = getTree
+      }
+
+      const getTreePath = options?.getTreePath ?? this.gettreepath
+      if (typeof getTreePath == 'string') {
+        const getTreePathString = getTreePath as string
+        if (getTreePathString.length > 0) {
+          getOptions['getTreePath'] = getTreePath
+        }
+      } else {
+        getOptions['getTreePath'] = getTreePath
+      }
+
+      return query.get(getOptions)
+    },
+
+    _checkIsNotNull(value : any) : boolean {
+      if (typeof value == 'string') {
+        const valueString = value as string
+        return (valueString.length > 0)
+      } else if (value instanceof UTSJSONObject) {
+        return true
+      }
+      return false
+    },
+
+    _checkIsLeafNode(nodeData : UTSJSONObject) : boolean {
+      if (this.selectedIndex >= this.limitlevel) {
+        return true
+      }
+
+      if (nodeData.getBoolean('isleaf', false)) {
+        return true
+      }
+
+      return false
+    },
+
+    _checkHasChildren(nodeData : UTSJSONObject) : boolean {
+      const children = nodeData.getArray('children') ?? ([] as Array<any>)
+      return children.length > 0
+    },
+
+    _pushSelectedNodes(nodes : Array<UTSJSONObject>) {
+      this.selectedNodes.push(DefaultSelectedNode)
+      this.selectedPages.push(nodes)
+      this.selectedIndex = this.selectedPages.length - 1
+    },
+
+    _trimSelectedNodes(nodeData : UTSJSONObject) {
+      this.selectedNodes.splice(this.selectedIndex)
+      this.selectedNodes.push(nodeData)
+
+      if (this.selectedPages.length > 0) {
+        this.selectedPages.splice(this.selectedIndex + 1)
+      }
+
+      const children = nodeData.getArray<UTSJSONObject>('children') ?? ([] as Array<UTSJSONObject>)
+      if (children.length > 0) {
+        this.selectedNodes.push(DefaultSelectedNode)
+        this.selectedPages.push(children)
+      }
+
+      this.selectedIndex = this.selectedPages.length - 1
+    },
+
+    _pushSelectedTreeNodes(paths : Array<UTSJSONObject>, nodes : Array<UTSJSONObject>) {
+      let children : Array<UTSJSONObject> = nodes
+      paths.forEach((node : UTSJSONObject) => {
+        const findNode = children.find((item : UTSJSONObject) : boolean => {
+          return (item.getString(this.mappingValueName) == node.getString(this.mappingValueName))
+        })
+        if (findNode != null) {
+          this.selectedPages.push(children)
+          this.selectedNodes.push(node)
+          children = findNode.getArray<UTSJSONObject>('children') ?? ([] as Array<UTSJSONObject>)
+        }
+      })
+      this.selectedIndex = this.selectedPages.length - 1
+    },
+
+    _extractTreePath(nodes : Array<UTSJSONObject>, result : Array<UTSJSONObject>) {
+      if (nodes.length == 0) {
+        return
+      }
+
+      const node = nodes[0]
+      result.push(node)
+
+      const children = node.getArray<UTSJSONObject>('children')
+      if (Array.isArray(children) && children!.length > 0) {
+        this._extractTreePath(children, result)
+      }
+    }
+  }
+})

+ 76 - 0
uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.css

@@ -0,0 +1,76 @@
+.uni-data-pickerview {
+  position: relative;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.loading-cover {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(150, 150, 150, .1);
+}
+
+.error {
+  background-color: #fff;
+  padding: 15px;
+}
+
+.error-text {
+  color: #DD524D;
+}
+
+.selected-node-list {
+  flex-direction: row;
+  flex-wrap: nowrap;
+}
+
+.selected-node-item {
+  margin-left: 10px;
+  margin-right: 10px;
+  padding: 8px 10px 8px 10px;
+  border-bottom: 2px solid transparent;
+}
+
+.selected-node-item-active {
+  color: #007aff;
+  border-bottom-color: #007aff;
+}
+
+.list-view {
+  flex: 1;
+}
+
+.list-item {
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 12px 15px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.item-text {
+  color: #333333;
+}
+
+.item-text-disabled {
+  opacity: .5;
+}
+
+.item-text-overflow {
+  overflow: hidden;
+}
+
+.check {
+  margin-right: 5px;
+  border: 2px solid #007aff;
+  border-left: 0;
+  border-top: 0;
+  height: 12px;
+  width: 6px;
+  transform-origin: center;
+  transform: rotate(45deg);
+}

+ 69 - 0
uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.uvue

@@ -0,0 +1,69 @@
+<template>
+  <view class="uni-data-pickerview">
+    <view v-if="error!=null" class="error">
+      <text class="error-text">{{error!.errMsg}}</text>
+    </view>
+    <scroll-view v-if="!isCloudDataList" :scroll-x="true">
+      <view class="selected-node-list">
+        <template v-for="(item, index) in selectedNodes">
+          <text class="selected-node-item" :class="{'selected-node-item-active':index==selectedIndex}"
+            @click="onTabSelect(index)">
+            {{item[mappingTextName]}}
+          </text>
+        </template>
+      </view>
+    </scroll-view>
+    <list-view class="list-view" :scroll-y="true">
+      <list-item class="list-item" v-for="(item, _) in currentDataList" @click="onNodeClick(item)">
+        <text class="item-text" :class="{'item-text-disabled': item['disable']}">{{item[mappingTextName]}}</text>
+        <text class="check" v-if="item[mappingValueName] == selectedNodes[selectedIndex][mappingValueName]"></text>
+      </list-item>
+    </list-view>
+    <view class="loading-cover" v-if="loading">
+      <slot name="pickerview-loading" :loading="loading"></slot>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { dataPicker } from "./uni-data-picker.uts"
+
+  /**
+   * DataPickerview
+   * @description uni-data-pickerview
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=3796
+   * @property {Array} localdata 本地数据,参考
+   * @property {Boolean} step-searh = [true|false] 是否分布查询
+   * @value true 启用分布查询,仅查询当前选中节点
+   * @value false 关闭分布查询,一次查询出所有数据
+   * @property {String|DBFieldString} self-field 分布查询当前字段名称
+   * @property {String|DBFieldString} parent-field 分布查询父字段名称
+   * @property {String|DBCollectionString} collection 表名
+   * @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
+   * @property {String} orderby 排序字段及正序倒叙设置
+   * @property {String|JQLString} where 查询条件
+   */
+  export default {
+    name: 'UniDataPickerView',
+    emits: ['nodeclick', 'change', 'update:modelValue'],
+    mixins: [dataPicker],
+    props: {
+      ellipsis: {
+        type: Boolean,
+        default: true
+      }
+    },
+    created() {
+      this.loadData()
+    },
+    methods: {
+      onFinish() {
+        this.$emit('change', this.getChangeNodes())
+      }
+    }
+  }
+</script>
+
+<style>
+  @import url("uni-data-pickerview.css");
+</style>

+ 323 - 0
uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue

@@ -0,0 +1,323 @@
+<template>
+  <view class="uni-data-pickerview">
+    <scroll-view v-if="!isCloudDataList" class="selected-area" scroll-x="true">
+      <view class="selected-list">
+          <view 
+            class="selected-item"
+            v-for="(item,index) in selected"
+            :key="index"
+            :class="{
+              'selected-item-active':index == selectedIndex
+            }"
+            @click="handleSelect(index)"
+          >
+            <text>{{item.text || ''}}</text>
+          </view>
+      </view>
+    </scroll-view>
+    <view class="tab-c">
+      <scroll-view class="list" :scroll-y="true">
+        <view class="item" :class="{'is-disabled': !!item.disable}" v-for="(item, j) in dataList[selectedIndex]" :key="j"
+          @click="handleNodeClick(item, selectedIndex, j)">
+          <text class="item-text">{{item[map.text]}}</text>
+          <view class="check" v-if="selected.length > selectedIndex && item[map.value] == selected[selectedIndex].value"></view>
+        </view>
+      </scroll-view>
+
+      <view class="loading-cover" v-if="loading">
+        <uni-load-more class="load-more" :contentText="loadMore" status="loading"></uni-load-more>
+      </view>
+      <view class="error-message" v-if="errorMessage">
+        <text class="error-text">{{errorMessage}}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import dataPicker from "./uni-data-picker.js"
+
+  /**
+   * DataPickerview
+   * @description uni-data-pickerview
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=3796
+   * @property {Array} localdata 本地数据,参考
+   * @property {Boolean} step-searh = [true|false] 是否分布查询
+   * @value true 启用分布查询,仅查询当前选中节点
+   * @value false 关闭分布查询,一次查询出所有数据
+   * @property {String|DBFieldString} self-field 分布查询当前字段名称
+   * @property {String|DBFieldString} parent-field 分布查询父字段名称
+   * @property {String|DBCollectionString} collection 表名
+   * @property {String|DBFieldString} field 查询字段,多个字段用 `,` 分割
+   * @property {String} orderby 排序字段及正序倒叙设置
+   * @property {String|JQLString} where 查询条件
+   */
+  export default {
+    name: 'UniDataPickerView',
+    emits: ['nodeclick', 'change', 'datachange', 'update:modelValue'],
+    mixins: [dataPicker],
+    props: {
+      managedMode: {
+        type: Boolean,
+        default: false
+      },
+      ellipsis: {
+        type: Boolean,
+        default: true
+      }
+    },
+    created() {
+      if (!this.managedMode) {
+        this.$nextTick(() => {
+          this.loadData();
+        })
+      }
+    },
+    methods: {
+      onPropsChange() {
+        this._treeData = [];
+        this.selectedIndex = 0;
+        this.$nextTick(() => {
+          this.loadData();
+        })
+      },
+      handleSelect(index) {
+        this.selectedIndex = index;
+      },
+      handleNodeClick(item, i, j) {
+        if (item.disable) {
+          return;
+        }
+
+        const node = this.dataList[i][j];
+        const text = node[this.map.text];
+        const value = node[this.map.value];
+
+        if (i < this.selected.length - 1) {
+          this.selected.splice(i, this.selected.length - i)
+          this.selected.push({
+            text,
+            value
+          })
+        } else if (i === this.selected.length - 1) {
+          this.selected.splice(i, 1, {
+            text,
+            value
+          })
+        }
+
+        if (node.isleaf) {
+          this.onSelectedChange(node, node.isleaf)
+          return
+        }
+
+        const {
+          isleaf,
+          hasNodes
+        } = this._updateBindData()
+
+        // 本地数据
+        if (this.isLocalData) {
+          this.onSelectedChange(node, (!hasNodes || isleaf))
+        } else if (this.isCloudDataList) { // Cloud 数据 (单列)
+          this.onSelectedChange(node, true)
+        } else if (this.isCloudDataTree) { // Cloud 数据 (树形)
+          if (isleaf) {
+            this.onSelectedChange(node, node.isleaf)
+          } else if (!hasNodes) { // 请求一次服务器以确定是否为叶子节点
+            this.loadCloudDataNode((data) => {
+              if (!data.length) {
+                node.isleaf = true
+              } else {
+                this._treeData.push(...data)
+                this._updateBindData(node)
+              }
+              this.onSelectedChange(node, node.isleaf)
+            })
+          }
+        }
+      },
+      updateData(data) {
+        this._treeData = data.treeData
+        this.selected = data.selected
+        if (!this._treeData.length) {
+          this.loadData()
+        } else {
+          //this.selected = data.selected
+          this._updateBindData()
+        }
+      },
+      onDataChange() {
+        this.$emit('datachange');
+      },
+      onSelectedChange(node, isleaf) {
+        if (isleaf) {
+          this._dispatchEvent()
+        }
+
+        if (node) {
+          this.$emit('nodeclick', node)
+        }
+      },
+      _dispatchEvent() {
+        this.$emit('change', this.selected.slice(0))
+      }
+    }
+  }
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+
+	.uni-data-pickerview {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		overflow: hidden;
+		height: 100%;
+	}
+
+  .error-text {
+    color: #DD524D;
+  }
+
+  .loading-cover {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(255, 255, 255, .5);
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: column;
+    align-items: center;
+    z-index: 1001;
+  }
+
+  .load-more {
+    /* #ifndef APP-NVUE */
+    margin: auto;
+    /* #endif */
+  }
+
+  .error-message {
+    background-color: #fff;
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    padding: 15px;
+    opacity: .9;
+    z-index: 102;
+  }
+
+  /* #ifdef APP-NVUE */
+  .selected-area {
+    width: 750rpx;
+  }
+  /* #endif */
+
+  .selected-list {
+    /* #ifndef APP-NVUE */
+    display: flex;
+    flex-wrap: nowrap;
+    /* #endif */
+    flex-direction: row;
+    padding: 0 5px;
+    border-bottom: 1px solid #f8f8f8;
+  }
+
+  .selected-item {
+    margin-left: 10px;
+    margin-right: 10px;
+    padding: 12px 0;
+    text-align: center;
+    /* #ifndef APP-NVUE */
+    white-space: nowrap;
+    /* #endif */
+  }
+
+  .selected-item-text-overflow {
+    width: 168px;
+    /* fix nvue */
+    overflow: hidden;
+    /* #ifndef APP-NVUE */
+    width: 6em;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    -o-text-overflow: ellipsis;
+    /* #endif */
+  }
+
+	.selected-item-active {
+		border-bottom: 2px solid $uni-primary;
+	}
+
+	.selected-item-text {
+		color: $uni-primary;
+	}
+
+  .tab-c {
+    position: relative;
+    flex: 1;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    overflow: hidden;
+  }
+
+  .list {
+    flex: 1;
+  }
+
+  .item {
+    padding: 12px 15px;
+    /* border-bottom: 1px solid #f0f0f0; */
+    /* #ifndef APP-NVUE */
+    display: flex;
+    /* #endif */
+    flex-direction: row;
+    justify-content: space-between;
+  }
+
+  .is-disabled {
+    opacity: .5;
+  }
+
+  .item-text {
+    /* flex: 1; */
+    color: #333333;
+  }
+
+  .item-text-overflow {
+    width: 280px;
+    /* fix nvue */
+    overflow: hidden;
+    /* #ifndef APP-NVUE */
+    width: 20em;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    -o-text-overflow: ellipsis;
+    /* #endif */
+  }
+
+	.check {
+		margin-right: 5px;
+		border: 2px solid $uni-primary;
+		border-left: 0;
+		border-top: 0;
+		height: 12px;
+		width: 6px;
+		transform-origin: center;
+		/* #ifndef APP-NVUE */
+		transition: all 0.3s;
+		/* #endif */
+		transform: rotate(45deg);
+	}
+</style>

+ 91 - 0
uni_modules/uni-data-picker/package.json

@@ -0,0 +1,91 @@
+{
+  "id": "uni-data-picker",
+  "displayName": "uni-data-picker 数据驱动的picker选择器",
+  "version": "2.0.1",
+  "description": "单列、多列级联选择器,常用于省市区城市选择、公司部门选择、多级分类等场景",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "picker",
+    "级联",
+    "省市区",
+    ""
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [
+      "uni-load-more",
+			"uni-icons",
+			"uni-scss"
+    ],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+        "QQ": "y",
+        "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 22 - 0
uni_modules/uni-data-picker/readme.md

@@ -0,0 +1,22 @@
+## DataPicker 级联选择
+> **组件名:uni-data-picker**
+> 代码块: `uDataPicker`
+> 关联组件:`uni-data-pickerview`、`uni-load-more`。
+
+
+`<uni-data-picker>` 是一个选择类[datacom组件](https://uniapp.dcloud.net.cn/component/datacom)。
+
+支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。
+
+候选数据支持一次性加载完毕,也支持懒加载,比如示例图中,选择了“北京”后,动态加载北京的区县数据。
+
+`<uni-data-picker>` 组件尤其适用于地址选择、分类选择等选择类。
+
+`<uni-data-picker>` 支持本地数据、云端静态数据(json),uniCloud云数据库数据。
+
+`<uni-data-picker>` 可以通过JQL直连uniCloud云数据库,配套[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema),可在schema2code中自动生成前端页面,还支持服务器端校验。
+
+在uniCloud数据表中新建表“uni-id-address”和“opendb-city-china”,这2个表的schema自带foreignKey关联。在“uni-id-address”表的表结构页面使用schema2code生成前端页面,会自动生成地址管理的维护页面,自动从“opendb-city-china”表包含的中国所有省市区信息里选择地址。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-picker)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839