This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
# 1.4.2
新增
1. 新增`filterValue`属性,支持通过此关键词来搜索并筛选树结构的内容
# 1.4.1
修复
1. 修复单选 onlyRadioLeaf 时末级节点无法选中的 bug
# 1.4.0
版本调整
建议更新,但需要注意,异步数据的时候,后台需返回 leaf 字段来判断是否末项数据
1. **调整数据项格式,新增 `leaf` 字段,来判断是否为末节点**
2. **调整数据项格式,新增 `sort` 字段,来排序节点位置**
3. **注意:异步加载数据,当为末项的时候,需要服务端数据返回 `leaf` 字段**
4. 新增 `alwaysFirstLoad` ,即异步数据总会在第一次展开节点时,拉取一次后台数据,来比对是否一致
5. 拆分 `field` 属性,**注意: 1.5.0 版本后将移除 `field` 属性**
6. 新增 `labelField``field.label`,指定节点对象中某个属性为**标签**字段,默认`label`
7. 新增 `valueField``field.key`,指定节点对象中某个属性为**值**字段,默认`value`
8. 新增 `childrenField``field.children`,指定节点对象中某个属性为**子树节点**字段,默认`children`
9. 新增 `disabledField``field.disabled`,指定节点对象中某个属性为**禁用**字段,默认`disabled`
10. 新增 `appendField``field.append`,指定节点对象中某个属性为**副标签**字段,默认`append`
11. 新增 `leafField``field.label`,指定节点对象中某个属性为**末级节点**字段,默认`leaf`
12. 新增 `sortField``field.label`,指定节点对象中某个属性为**排序**字段,默认`sort`
13. 新增 `isLeafFn` ,用来自定义控制数据项的末项
14. 更多的项目示例
15. 支持单选取消选中
16. 修复节点展开时可能存在的 bug
17. 修复节点选择可能存在的 bug
18. 调整为子节点默认继承父节点禁用属性
19. `setExpandedKeys` 添加参数一为 `all` 即可支持一键展开/收起全部节点
20. 其它更多优化
# 1.3.4
优化
1. 优化图标字体命名
# 1.3.3
优化
1. 新增方法调用
> - 新增`getUncheckedKeys`,返回未选的 key
> - 新增`getUncheckedNodes`,返回未选的节点
> - 新增`getUnexpandedKeys`,返回未展开的 key
> - 新增`getUnexpandedNodes`,返回未展开的节点
2. 优化示例项目
# 1.3.2
修复
1. 修复在 APP 真机环境中的报错
# 1.3.1
修复
1. 修复方法`setExpandedKeys`没联动展开上级父子节点
# 1.3.0
优化
1. `field`新增字段 `append` 用于在标签后面显示小提示
2. 新增支持点击标签也能选中节点
3. 方法`setExpandedKeys`支持加载动态数据
4. 修复父节点禁用,则不能展开及图标展开显示
5. 修复动态加载数据时,末级节点的 `children``null` 时仍显示展开图标
# 1.2.6
新增
1. 新增支持主题换色
2. 支持单选的`onlyRadioLeaf``true`时可点父节点展开/收起
3. 优化`expandChecked`调整为不展开无子节点的节点
# 1.2.5
新增
1. 新增 `expandChecked`,控制选择时是否展开当前已选的所有下级节点
# 1.2.4
修复
1. 修复动态数据展开状态异常问题
# 1.2.3
新增
1. 新增 `checkedDisabled`,是否渲染禁用值
2. 新增 `packDisabledkey`,是否返回已禁用并选中的 key
3. 修复选择父级时,子级已禁用但仍被选中的问题
# 1.2.2
优化
1. 调整动态数据载入处理方式
2. 修复节点数据因动态数据引起的状态异常
3. 修复初始节点数据默认选中
# 1.2.1
修复
1. 修复切换`选中状态`被重复选中问题
2. 修复动态数据引起的重复选择问题
# 1.2.0
新增
1. 新增方法调用
> - 新增`setCheckedKeys`,方法设置指定 key 的节点选中状态
> - 新增`setExpandedKeys`,方法设置指定 key 的节点展开状态
2. 修复小程序重复插槽一直刷报错问题
3. 优化展开时,会展开子级所以下级节点
# 1.1.1
新增
1. 新增`data``disabled`,支持节点禁用状态
2. 新增`field``disabled`,可自定`disabled`字段值
# 1.1.0
新增
1. 新增`loadMode``loadApi`,支持展开时加载异步数据
2. 新增方法调用
> - 新增`getCheckedKeys`,方法返回已选的 key
> - 新增`getHalfCheckedKeys`,方法返回半选的 key
> - 新增`getExpandedKeys`,方法返回已展开的 key
> - 新增`getCheckedNodes`,方法返回已选的节点
> - 新增`getHalfCheckedNodes`,方法返回半选的节点
> - 新增`getExpandedNodes`,方法返回已展开的节点
3. 对代码进行重构,更易于后期拓展
4. 此次更新后,页面多个的 DaTee 组件间的数据不再关联
# 1.0.6
新增
1. 新增`checkStrictly`,多选模式下选中时是否父子不关联
# 1.0.5
修复
1. 修复多选时已选数据重复问题
# 1.0.4
修复
1. 修复 `change` 事件回调数据的问题
# 1.0.3
优化
1. 优化文档及示例说明
# 1.0.2
新增
1. 新增 `onlyRadioLeaf` ,单选时只允许选中末级
2. 优化默认展开及默认选择的展开问题
# 1.0.1
新增
1. 支持展开/收起回调事件`@expand`
# 1.0.0
初始版本 1.0.0,基于 Vue3 进行开发,支持单选、多选,兼容各大平台
1. 支持单选
2. 支持多选

1160
components/da-tree/index.vue Normal file

File diff suppressed because it is too large Load Diff

197
components/da-tree/props.ts Normal file
View File

@@ -0,0 +1,197 @@
export default {
/**
* 树的数据
*/
data: {
type: Array,
default: () => [],
},
/**
* 主题色
*/
themeColor: {
type: String,
default: '#007aff',
},
/**
* 是否开启多选,默认单选
*/
showCheckbox: {
type: Boolean,
default: false,
},
/**
* 默认选中的节点注意单选时为单个key多选时为key的数组
*/
defaultCheckedKeys: {
type: [Array, String, Number],
default: null,
},
/**
* 是否默认展开全部
*/
defaultExpandAll: {
type: Boolean,
default: false,
},
/**
* 默认展开的节点
*/
defaultExpandedKeys: {
type: Array,
default: null,
},
/**
* 筛选关键词
*/
filterValue: {
type: String,
default: '',
},
/**
* 是否自动展开到选中的节点,默认不展开
*/
expandChecked: {
type: Boolean,
default: false,
},
/**
* (旧)字段对应内容,默认为 {label: 'label',key: 'key', children: 'children', disabled: 'disabled', append: 'append'}
* 注意1.5.0版本后不再兼容
*/
field: {
type: Object,
default: null,
},
/**
* 标签字段(新,拆分了)
*/
labelField: {
type: String,
default: 'label',
},
/**
* 值字段(新,拆分了)
*/
valueField: {
type: String,
default: 'value',
},
/**
* 下级字段(新,拆分了)
*/
childrenField: {
type: String,
default: 'children',
},
/**
* 禁用字段(新,拆分了)
*/
disabledField: {
type: String,
default: 'disabled',
},
/**
* 末级节点字段(新,拆分了)
*/
leafField: {
type: String,
default: 'leaf',
},
/**
* 副标签字段(新,拆分了)
*/
appendField: {
type: String,
default: 'append',
},
/**
* 排序字段(新,拆分了)
*/
sortField: {
type: String,
default: 'sort',
},
/**
* Api数据返回后的结果路径支持嵌套如`data.list`
*/
resultField: {
type: String,
default: '',
},
isLeafFn: {
type: Function,
default: null,
},
/**
* 是否显示单选图标,默认显示
*/
showRadioIcon: {
type: Boolean,
default: true,
},
/**
* 单选时只允许选中末级,默认可随意选中
*/
onlyRadioLeaf: {
type: Boolean,
default: false,
},
/**
* 多选时,是否执行父子不关联的任意勾选,默认父子关联
*/
checkStrictly: {
type: Boolean,
default: false,
},
/**
* 为 true 时,空的 children 数组会显示展开图标
*/
loadMode: {
type: Boolean,
default: false,
},
/**
* 异步加载接口
*/
loadApi: {
type: Function,
default: null,
},
/**
* 是否总在首次的时候加载一下内容,来比对是否一致
*/
alwaysFirstLoad: {
type: Boolean,
default: false,
},
/**
* 是否渲染(操作)禁用值
*/
checkedDisabled: {
type: Boolean,
default: false,
},
/**
* 是否返回已禁用的但已选中的key
*/
packDisabledkey: {
type: Boolean,
default: true,
},
/**
* 选择框的位置,可选 left/right
*/
checkboxPlacement: {
type: String,
default: 'left',
},
/**
* 子项缩进距离默认40单位rpx
*/
indent: {
type: Number,
default: 40,
},
}

View File

@@ -0,0 +1,310 @@
# da-tree
一个基于 Vue3 的 tree(树)组件,同时支持主题换色,可能是最适合你的 tree(树)组件
组件一直在更新,遇到问题可在下方讨论。
`同时更新 Vue2 版本,在此查看 ===>` **[Vue2 版](https://ext.dcloud.net.cn/plugin?id=12692)**
### 关于使用
可在右侧的`使用 HBuilderX 导入插件``下载示例项目ZIP`,方便快速上手。
可通过下方的示例及文档说明,进一步了解使用组件相关细节参数。
插件地址https://ext.dcloud.net.cn/plugin?id=12384
### 组件示例
```jsx
<template>
<view>多选</view>
<view><button @click="doCheckedTree(['2'],true)">全选</button></view>
<view><button @click="doCheckedTree(['2'],false)">取消全选</button></view>
<view><button @click="doCheckedTree(['211','222'],true)">选中指定节点</button></view>
<view><button @click="doCheckedTree(['211','222'],false)">取消选中指定节点</button></view>
<view><button @click="doExpandTree('all',true)">展开全部节点</button></view>
<view><button @click="doExpandTree('all',false)">收起全部节点</button></view>
<view><button @click="doExpandTree(['22','23'],true)">展开节点</button></view>
<view><button @click="doExpandTree(['22','23'],false)">收起节点</button></view>
<DaTree
ref="DaTreeRef"
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
showCheckbox
:defaultCheckedKeys="defaultCheckedKeysValue"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>单选</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
:defaultCheckedKeys="defaultCheckedKeysValue2"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>默认展开指定节点</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
:defaultExpandedKeys="defaultExpandKeysValue3"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>异步加载数据</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
loadMode
:loadApi="GetApiData"
defaultExpandAll
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
</template>
```
```js
import { defineComponent, ref } from 'vue'
/**
* 模拟创建一个接口数据
*/
function GetApiData(currentNode) {
const { key } = currentNode
return new Promise((resolve) => {
setTimeout(() => {
// 模拟返回空数据
if (key.indexOf('-') > -1) {
return resolve(null)
// return resolve([])
}
return resolve([
{
id: `${key}-1`,
name: `行政部X${key}-1`,
},
{
id: `${key}-2`,
name: `财务部X${key}-2`,
append: '定义了末项数据',
leaf: true,
},
{
id: `${key}-3`,
name: `资源部X${key}-3`,
},
{
id: `${key}-4`,
name: `资源部X${key}-3`,
append: '被禁用,无展开图标',
disabled: true,
},
])
}, 2000)
})
}
import DaTree from '@/components/da-tree/index.vue'
export default defineComponent({
components: { DaTree },
setup() {
const DaTreeRef = ref()
// key的类型必须对应树数据key的类型
const defaultCheckedKeysValue = ref(['211', '222'])
const defaultCheckedKeysValue2 = ref('222')
const defaultExpandKeysValue3 = ref(['212', '231'])
const roomTreeData = ref([
{
id: '2',
name: '行政中心',
children: [
{
id: '21',
name: '行政部',
children: [
{
id: '211',
name: '行政一部',
children: null,
},
{
id: '212',
name: '行政二部',
children: [],
disabled: true,
},
],
},
{
id: '22',
name: '财务部',
children: [
{
id: '221',
name: '财务一部',
children: [],
disabled: true,
},
{
id: '222',
name: '财务二部',
children: [],
},
],
},
{
id: '23',
name: '人力资源部',
children: [
{
id: '231',
name: '人力一部',
children: [],
},
{
id: '232',
name: '人力二部',
append: '更多示例,请下载示例项目查看',
},
],
},
],
},
])
function doExpandTree(keys, expand) {
DaTreeRef.value?.setExpandedKeys(keys, expand)
const gek = DaTreeRef.value?.getExpandedKeys()
console.log('当前已展开的KEY ==>', gek)
}
function doCheckedTree(keys, checked) {
DaTreeRef.value?.setCheckedKeys(keys, checked)
const gek = DaTreeRef.value?.getCheckedKeys()
console.log('当前已选中的KEY ==>', gek)
}
function handleTreeChange(allSelectedKeys, currentItem) {
console.log('handleTreeChange ==>', allSelectedKeys, currentItem)
}
function handleExpandChange(expand, currentItem) {
console.log('handleExpandChange ==>', expand, currentItem)
}
return {
DaTreeRef,
roomTreeData,
defaultCheckedKeysValue,
defaultCheckedKeysValue2,
defaultExpandKeysValue3,
handleTreeChange,
handleExpandChange,
GetApiData,
doExpandTree,
doCheckedTree,
}
},
})
```
** 更多示例请下载/导入示例项目 ZIP 查看 **
### 组件参数
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| :------------------ | :------------------------------ | :--------- | :--- | :--------------------------------------------------------------------------- |
| data | `Array` | - | 是 | 树的数据 |
| themeColor | `String` | `#007aff` | 否 | 主题色,十六进制 |
| defaultCheckedKeys | `Array` \| `Number` \| `String` | - | 否 | 默认选中的节点,单选为单个 key多选为 key 的数组 |
| showCheckbox | `Boolean` | `false` | 否 | 是否开启多选,默认单选 |
| checkStrictly | `Boolean` | `false` | 否 | 多选时,是否执行父子不关联的任意勾选,默认父子关联 |
| showRadioIcon | `Boolean` | `true` | 否 | 是否显示单选图标,默认显示 |
| onlyRadioLeaf | `Boolean` | `true` | 否 | 单选时只允许选中末级,默认可随意选中 |
| defaultExpandAll | `Boolean` | `false` | 否 | 是否默认展开全部 |
| defaultExpandedKeys | `Array` | - | 否 | 默认展开的节点 |
| indent | `Number` | `40` | 否 | 子项缩进距离,单位 rpx |
| checkboxPlacement | `String` | `left` | 否 | 选择框的位置,可选 left/right |
| loadMode | `Boolean` | `false` | 否 | 为 true 时,空的 children 数组会显示展开图标 |
| loadApi | `Function` | - | 否 | 选择框的位置,可选 left/right |
| checkedDisabled | `Boolean` | `false` | 否 | 是否渲染禁用值,默认不渲染 |
| packDisabledkey | `Boolean` | `true` | 否 | 是否返回已禁用的但已选中的 key默认返回禁用已选值 |
| expandChecked | `Boolean` | `false` | 否 | 是否自动展开到选中的节点,默认不展开 |
| alwaysFirstLoad | `Boolean` | `false` | 否 | 是否总在首次的时候加载一下内容,默认不加载,否则只有展开末级节点才会加载数据 |
| isLeafFn | `Function` | - | 否 | 自定义函数返回来控制数据项的末项 |
| field | `Object` | - | 否 | 字段对应内容,格式参考下方(1.5.0 后移除,请用单独的字段匹配) |
| labelField | `String` | `label` | 否 | 指定节点对象中某个属性为标签字段,默认`label` |
| valueField | `String` | `value` | 否 | 指定节点对象中某个属性为值字段,默认`value` |
| childrenField | `String` | `children` | 否 | 指定节点对象中某个属性为子树节点字段,默认`children` |
| disabledField | `String` | `disabled` | 否 | 指定节点对象中某个属性为禁用字段,默认`disabled` |
| appendField | `String` | `append` | 否 | 指定节点对象中某个属性为副标签字段,默认`append` |
| leafField | `String` | `leaf` | 否 | 指定节点对象中某个属性为末级节点字段,默认`leaf` |
| sortField | `String` | `sort` | 否 | 指定节点对象中某个属性为排序字段,默认`sort` |
| filterValue | `String` | - | 否 | 搜索筛选的关键词,通过输入关键词筛选内容 |
**field 格式(1.5.0 后移除,请用单独的字段匹配)**
```js
{
label: 'label',
key: 'key',
children: 'children',
disabled: 'disabled',
append: 'append'
}
```
### 组件事件
| 事件名称 | 回调参数 | 说明 |
| :------- | :-------------------------------------- | :-------------- |
| change | `(allCheckedKeys, currentItem) => void` | 选中时回调 |
| expand | `(expandState, currentItem) => void` | 展开/收起时回调 |
### 组件方法
| 方法名称 | 参数 | 说明 |
| :------------------ | :--------------- | :------------------------------------------------------------------------------------------------ |
| setCheckedKeys | `(keys,checked)` | 设置指定 key 的节点选中/取消选中的状态。注: keys 单选时为 key多选时为 key 的数组 |
| setExpandedKeys | `(keys,expand)` | 设置指定 key 的节点展开/收起的状态,当 keys 为 all 时即代表展开/收起全部。注keys 为数组或 `all` |
| getCheckedKeys | - | 返回已选的 key |
| getHalfCheckedKeys | - | 返回半选的 key |
| getUncheckedKeys | - | 返回未选的 key |
| getCheckedNodes | - | 返回已选的节点 |
| getUncheckedNodes | - | 返回未选的节点 |
| getHalfCheckedNodes | - | 返回半选的节点 |
| getExpandedKeys | - | 返回已展开的 key |
| getUnexpandedKeys | - | 返回未展开的 key |
| getExpandedNodes | - | 返回已展开的节点 |
| getUnexpandedNodes | - | 返回未展开的节点 |
### 组件版本
v1.4.2
### 差异化
已通过测试
> - H5 页面
> - 微信小程序
> - 支付宝、钉钉小程序
> - 字节跳动、抖音、今日头条小程序
> - 百度小程序
> - 飞书小程序
> - QQ 小程序
> - 京东小程序
未测试
> - 快手小程序由于非企业用户暂无演示
> - 快应用、360 小程序因 Vue3 支持的原因暂无演示
### 开发组
[@CRLANG](https://crlang.com)

150
components/da-tree/utils.ts Normal file
View File

@@ -0,0 +1,150 @@
/** 未选 */
export const unCheckedStatus = 0
/** 半选 */
export const halfCheckedStatus = 1
/** 选中 */
export const isCheckedStatus = 2
/**
* 深拷贝内容
* @param originData 拷贝对象
* @author crlang(https://crlang.com)
*/
export function deepClone(originData) {
const type = Object.prototype.toString.call(originData)
let data
if (type === '[object Array]') {
data = []
for (let i = 0; i < originData.length; i++) {
data.push(deepClone(originData[i]))
}
} else if (type === '[object Object]') {
data = {}
for (const prop in originData) {
// eslint-disable-next-line no-prototype-builtins
if (originData.hasOwnProperty(prop)) { // 非继承属性
data[prop] = deepClone(originData[prop])
}
}
} else {
data = originData
}
return data
}
/**
* 获取所有指定的节点
* @param type
* @param value
* @author crlang(https://crlang.com)
*/
export function getAllNodes(list, type, value, packDisabledkey = true) {
if (!list || list.length === 0) {
return []
}
const res = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item)
}
}
}
return res
}
/**
* 获取所有指定的key值
* @param type
* @param value
* @author crlang(https://crlang.com)
*/
export function getAllNodeKeys(list, type, value, packDisabledkey = true) {
if (!list || list.length === 0) {
return null
}
const res = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item.key)
}
}
}
return res.length ? res : null
}
/**
* 错误输出
*
* @param msg
*/
export function logError(msg, ...args) {
console.error(`DaTree: ${msg}`, ...args)
}
const toString = Object.prototype.toString
export function is(val, type) {
return toString.call(val) === `[object ${type}]`
}
/**
* 是否对象(Object)
* @param val
*/
export function isObject(val) {
return val !== null && is(val, 'Object')
}
/**
* 是否数字(Number)
* @param val
*/
export function isNumber(val) {
return is(val, 'Number')
}
/**
* 是否字符串(String)
* @param val
*/
export function isString(val) {
return is(val, 'String')
}
/**
* 是否函数方法(Function)
* @param val
*/
export function isFunction(val) {
return typeof val === 'function'
}
/**
* 是否布尔(Boolean)
* @param val
*/
export function isBoolean(val) {
return is(val, 'Boolean')
}
/**
* 是否数组(Array)
* @param val
*/
export function isArray(val) {
return val && Array.isArray(val)
}

View File

@@ -0,0 +1,139 @@
<template>
<view class="navbar">
<view class="navbar-fixed">
<!-- 状态栏占位 -->
<view :style="{height: props.statusBarHeight+'px'}"></view>
<!-- 导航栏内容 -->
<view class="navbar-content" :style="{height:props.navbarHeight+'px',width:props.windowWidth+'px'}">
<view class="header">
<view class="company-name">{{title}}</view>
<nut-animate type="flicker" :loop="true">
<view class="description">{{desc}}</view>
</nut-animate>
</view>
</view>
<!-- <nut-searchbar clearable v-model="search_val" background="transparent" input-background="#fff"
@search="onSearch" @clear="onClear">
<template #rightout>
<nut-icon @click="onScan" name="scan2" custom-color="#ffffff" />
</template>
</nut-searchbar> -->
</view>
<!-- 占位状态栏+导航栏的高度,使下面的内容不会被遮挡 -->
<view :style="{height:(statusBarHeight + navbarHeight+ navbarSearchBoxHeight)+'px'}"></view>
</view>
</template>
<script setup>
import {
ref,
defineProps
} from 'vue';
// import {
// fetchGoodsList
// } from '../../api/goods';
import {
navigateTo
} from '@/utils/helper';
// const navbarSearchBoxHeight = 50
const navbarSearchBoxHeight = 0
const props = defineProps({
statusBarHeight: Number,
windowWidth: Number,
navbarHeight: Number,
title:String,
desc:String,
// onIndexPageSearch:Function,
// onIndexPageClear:Function
})
// const search_val = ref('')
// const onSearch = () => {
// if(props.onIndexPageSearch) {
// props.onIndexPageSearch(search_val.value)
// }
// }
// const onClear = () => {
// if(props.onIndexPageClear) {
// props.onIndexPageClear()
// }
// }
// const onScan =() => {
// uni.scanCode({
// onlyFromCamera:true,
// success: (res) => {
// fetchGoodsList({
// search_params: {goods_no:res.result}
// }).then(res => {
// if (res.list.total >= 1) {
// navigateTo("/pages/mall/item/index?id=" + res.list?.data[0]?.goods_id)
// } else {
// uni.showToast({
// icon:'none',
// title:'暂无该商品'
// })
// }
// })
// },
// fail: () => {
// uni.showToast({
// icon:'none',
// title:'扫码失败'
// })
// }
// })
// }
</script>
<style scoped lang="scss">
.navbar {}
.navbar-fixed {
/* // 固定定位 */
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
/* background: linear-gradient(90deg, #b79dff, #f2a4ff); */
/* 渐变背景 */
background-image: -webkit-linear-gradient(60deg, rgba(233, 100, 67, 1), rgba(198, 77, 255, 1));
background-image: linear-gradient(30deg, rgba(233, 100, 67, 1), rgba(198, 77, 255, 1));
background-position: 0 0;
background-repeat: no-repeat;
background-size: 100% auto;
opacity: 1;
/* 确保背景不透明 */
}
.navbar-content {
display: flex;
flex-flow: column;
/* // 水平居中 */
justify-content: center;
/* // 垂直居中 */
/* // align-items: center; */
padding: 0 30rpx;
/* // 盒内显示 (父元素和子元素宽度都是100%,但是父元素加了左右内填充,会导致子元素不在父元素内) */
box-sizing: border-box;
}
.company-name {
font-family: Akrobat ExtraBold !important;
color: #fff;
font-size: 24rpx;
font-weight: 700
}
.description {
font-family: Akrobat ExtraBold !important;
color: #fff;
font-size: 16rpx
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<view class="navbar">
<view class="navbar-fixed">
<!-- 状态栏占位 -->
<view :style="{height: props.statusBarHeight+'px'}"></view>
<!-- 导航栏内容 -->
<view class="navbar-content" :style="{height:props.navbarHeight+'px',width:props.windowWidth+'px'}">
<view class="header">
<view class="company-name">济南远阳数码</view>
<view class="description">专注于电子组件研发20年提供优质产品与服务</view>
</view>
</view>
<!-- <nut-searchbar clearable v-model="search_val" background="transparent" input-background="#fff"
@search="onSearch" @clear="onClear">
<template #rightout>
<nut-icon @click="onScan" name="scan2" custom-color="#ffffff" />
</template>
</nut-searchbar> -->
</view>
<!-- 占位状态栏+导航栏的高度,使下面的内容不会被遮挡 -->
<view :style="{height:(statusBarHeight + navbarHeight+ navbarSearchBoxHeight)+'px'}"></view>
</view>
</template>
<script setup>
import {
ref,
defineProps
} from 'vue';
import {
fetchGoodsList
} from '../../api/goods';
import {
navigateTo
} from '../../utils/helper';
// const navbarSearchBoxHeight = 50
const navbarSearchBoxHeight = 0
const props = defineProps({
statusBarHeight: Number,
windowWidth: Number,
navbarHeight: Number,
// onIndexPageSearch:Function,
// onIndexPageClear:Function
})
// const search_val = ref('')
// const onSearch = () => {
// if(props.onIndexPageSearch) {
// props.onIndexPageSearch(search_val.value)
// }
// }
// const onClear = () => {
// if(props.onIndexPageClear) {
// props.onIndexPageClear()
// }
// }
// const onScan =() => {
// uni.scanCode({
// onlyFromCamera:true,
// success: (res) => {
// fetchGoodsList({
// search_params: {goods_no:res.result}
// }).then(res => {
// if (res.list.total >= 1) {
// navigateTo("/pages/mall/item/index?id=" + res.list?.data[0]?.goods_id)
// } else {
// uni.showToast({
// icon:'none',
// title:'暂无该商品'
// })
// }
// })
// },
// fail: () => {
// uni.showToast({
// icon:'none',
// title:'扫码失败'
// })
// }
// })
// }
</script>
<style scoped lang="scss">
.navbar {}
.navbar-fixed {
/* // 固定定位 */
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
/* background: linear-gradient(90deg, #b79dff, #f2a4ff); */
/* 渐变背景 */
background-image: -webkit-linear-gradient(60deg, rgba(233, 100, 67, 1), rgba(198, 77, 255, 1));
background-image: linear-gradient(30deg, rgba(233, 100, 67, 1), rgba(198, 77, 255, 1));
background-position: 0 0;
background-repeat: no-repeat;
background-size: 100% auto;
opacity: 1;
/* 确保背景不透明 */
}
.navbar-content {
display: flex;
flex-flow: column;
/* // 水平居中 */
justify-content: center;
/* // 垂直居中 */
/* // align-items: center; */
padding: 0 30rpx;
/* // 盒内显示 (父元素和子元素宽度都是100%,但是父元素加了左右内填充,会导致子元素不在父元素内) */
box-sizing: border-box;
}
.company-name {
font-family: Akrobat ExtraBold !important;
color: #fff;
font-size: 24rpx;
font-weight: 700
}
.description {
font-family: Akrobat ExtraBold !important;
color: #fff;
font-size: 16rpx
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view
class="inline-select"
:class="{ 'is-disabled': disabled, 'is-error': hasError }"
:tabindex="disabled ? -1 : 0"
@keydown.enter="handleTriggerClick"
@keydown.space.prevent="handleTriggerClick"
@keydown.esc="onCancel"
>
<view class="inline-select__trigger" :style="customStyle" @click="handleTriggerClick">
<view class="inline-select__content">
<view class="inline-select__value" :class="{ 'is-placeholder': !hasValue }">{{ displayValue }}</view>
<slot name="suffix">
<view class="inline-select__indicator">
<text v-if="loading" class="inline-select__loading"></text>
<text v-else class="inline-select__arrow" :class="{ 'is-active': show_popup }"></text>
</view>
</slot>
</view>
</view>
</view>
<nut-popup v-model:visible="show_popup" position="bottom" safe-area-inset-bottom :close-on-click-overlay="closeOnClickOverlay">
<nut-picker
:columns="columns"
:field-names="fieldNames"
v-bind="$attrs"
v-model="currentPickerValue"
@confirm="onConfirm"
@change="onChange"
@cancel="onCancel"
>
<view v-if="columns.length === 0" class="inline-select__empty">{{ emptyText }}</view>
<template v-else-if="$slots.option" #default="{ option }">
<slot name="option" :option="option"></slot>
</template>
</nut-picker>
</nut-popup>
</template>
<script setup>
/**
* @component InlineSelect
* @description 行内选择组件 - 基于 NutUI Picker 的移动端下拉选择器,提供了丰富的自定义选项和事件处理能力。
* 该组件适用于在表单中进行单项选择的场景,支持自定义样式、异步加载、键盘操作等特性。
*
* @property {String|Number} modelValue - 选择器的当前值支持v-model双向绑定
* @property {String} [title='请选择'] - 选择器标题,显示在弹出层顶部
* @property {String} [placeholder='请选择'] - 选择框占位文本,未选择时显示
* @property {Object} [fieldNames={ text: 'name', value: 'value' }] - 自定义字段名映射,用于适配不同数据结构
* @property {String} [fieldNames.text='name'] - 选项显示文本的字段名
* @property {String} [fieldNames.value='value'] - 选项值的字段名
* @property {Array<{[key: string]: any}>} columns - 选项数据列表必填每个选项对象必须包含text和value字段支持自定义字段名
* @property {String|Number} [defaultValue=''] - 默认选中值当modelValue未设置时生效
* @property {Boolean} [disabled=false] - 是否禁用选择器,禁用时无法打开选择弹窗
* @property {Boolean} [closeOnClickOverlay=true] - 是否在点击遮罩层时关闭弹窗
* @property {Object} [customStyle={}] - 自定义样式对象,用于定制选择器外观
* @property {Boolean} [loading=false] - 是否显示加载状态加载时显示loading图标
* @property {String} [emptyText='暂无可选项'] - 无选项时显示的文本
* @property {String} [theme='default'] - 主题样式,可选值:'default'|'primary'|'success'|'warning'|'danger'
*
* @event {Function} update:modelValue - 值更新时触发,参数: (value: string|number)
* @event {Function} change - 选项改变时触发,参数: { columnIndex: number, selectedValue: Array, selectedOptions: Array }
* @event {Function} confirm - 点击确定按钮时触发,参数: { value: string|number, option: Object }
* @event {Function} cancel - 点击取消按钮时触发
* @event {Function} error - 选中值无效时触发,参数: { value: string|number, message: string }
*
* @slot suffix - 选择框后置内容,默认显示箭头图标
* @slot option - 自定义选项渲染,参数: { option: Object }
*
* @see 更多信息请参考 NutUI Picker 组件文档
*/
import { ref, computed, watch, shallowRef, onMounted } from 'vue';
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '未选择',
},
fieldNames: {
type: Object,
default() {
return {
text: 'name',
value: 'value',
};
},
},
columns: {
type: Array,
default() {
return [];
},
required: true,
},
defaultValue: {
type: [String, Number],
default: '',
},
disabled: {
type: Boolean,
default: false,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
customStyle: {
type: Object,
default() {
return {};
},
},
loading: {
type: Boolean,
default: false,
},
emptyText: {
type: String,
default: '暂无可选项',
},
});
const emit = defineEmits(['update:modelValue', 'change', 'confirm', 'cancel', 'error']);
// 状态管理
const show_popup = ref(false);
const currentValue = shallowRef('');
const hasError = ref(false);
const optionsCache = new Map();
// 初始化currentValue
const initCurrentValue = () => {
if (props.modelValue !== undefined && props.modelValue !== null) {
currentValue.value = props.modelValue;
} else if (props.defaultValue !== undefined && props.defaultValue !== null) {
currentValue.value = props.defaultValue;
emit('update:modelValue', props.defaultValue);
}
};
onMounted(initCurrentValue);
// 监听columns变化重新初始化currentValue
watch(
() => props.columns,
() => {
initCurrentValue();
},
{ immediate: true }
);
const currentPickerValue = ref([]);
// 监听columns变化重新初始化currentValue
watch(
() => currentValue.value,
() => {
currentPickerValue.value = [currentValue.value];
},
{ immediate: true }
);
// 计算属性
const hasValue = computed(() => currentValue.value !== '' && currentValue.value !== undefined && currentValue.value !== null);
const displayValue = computed(() => {
if (!hasValue.value || !props.columns.length) return props.placeholder;
const cachedOption = optionsCache.get(currentValue.value);
if (cachedOption) {
hasError.value = false;
return cachedOption[props.fieldNames.text];
}
const selectedItem = props.columns.find(item => item[props.fieldNames.value] === currentValue.value);
if (selectedItem) {
optionsCache.set(currentValue.value, selectedItem);
hasError.value = false;
return selectedItem[props.fieldNames.text];
}
hasError.value = true;
emit('error', { value: currentValue.value, message: '选项不存在' });
return props.placeholder;
});
// 事件处理
const handleTriggerClick = () => {
if (props.disabled) return;
show_popup.value = true;
};
const onChange = ({ columnIndex, selectedValue, selectedOptions }) => {
emit('change', { columnIndex, selectedValue, selectedOptions });
};
const onConfirm = ({ selectedValue, selectedOptions }) => {
if (!Array.isArray(selectedValue) || !Array.isArray(selectedOptions) || !selectedValue.length || !selectedOptions.length) return;
const value = selectedValue[0];
const option = selectedOptions[0];
if (value === undefined || option === undefined) return;
currentValue.value = value;
emit('update:modelValue', value);
emit('confirm', { value, option });
show_popup.value = false;
};
const onCancel = () => {
show_popup.value = false;
emit('cancel');
};
// 监听modelValue的变化
watch(
() => props.modelValue,
newValue => {
if (newValue !== undefined && newValue !== null && newValue !== currentValue.value) {
currentValue.value = newValue;
}
}
);
</script>
<style lang="scss">
.inline-select {
width: 100%;
outline: none;
&:focus:not(.is-disabled) .inline-select__trigger {
outline: none;
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
.inline-select__trigger {
pointer-events: none;
}
}
&.is-error .inline-select__trigger {
border-color: var(--theme-error-color, #f56c6c);
}
&__trigger {
position: relative;
display: flex;
align-items: center;
background-color: var(--theme-bg-color, #fff);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active:not(.is-disabled) {
background-color: var(--theme-hover-bg-color, #f5f5f5);
}
}
&__content {
display: flex;
align-items: center;
width: 100%;
gap: 16rpx;
}
&__value {
flex: 1;
line-height: 1.5;
color: var(--theme-text-color, #333);
&.is-placeholder {
color: var(--theme-placeholder-color, #999);
}
}
&__indicator {
display: flex;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
}
&__arrow {
width: 0;
height: 0;
border: 12rpx solid transparent;
border-top-color: var(--theme-arrow-color, #999);
transform: translateY(25%) rotate(0deg);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.is-active {
transform: translateY(25%) rotate(180deg);
}
}
&__loading {
display: inline-block;
width: 24rpx;
height: 24rpx;
border: 3rpx solid var(--theme-loading-color, #2d8cf0);
border-radius: 50%;
border-top-color: transparent;
animation: inline-select-loading 0.8s infinite linear;
}
&__empty {
padding: 64rpx;
text-align: center;
color: var(--theme-empty-color, #999);
}
}
@keyframes inline-select-loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
:root {
--theme-color-rgb: 45, 140, 240;
--theme-bg-color: #fff;
--theme-text-color: #333;
--theme-border-color: #dcdfe6;
--theme-placeholder-color: #999;
--theme-hover-bg-color: #f5f5f5;
--theme-arrow-color: #999;
--theme-loading-color: #2d8cf0;
--theme-empty-color: #999;
--theme-error-color: #f56c6c;
}
</style>

View File

@@ -0,0 +1,7 @@
## 1.1.12025-03-06
1.1.1
## 1.1.02025-03-06
1.1.0
## 1.0.02025-03-05
1.0.0
1.0.0

View File

@@ -0,0 +1,244 @@
<template>
<view class="op-cascader" :style="{maxHeight: maxHeight}">
<template v-for="(opt, optInx) in newOption" :key="optInx">
<view class="cascader-content">
<scroll-view :scroll-y="true" style="height: 100%;">
<template v-for="(item, index) in opt" :key="index">
<view v-if="item._parentId==0 || (optInx >0 && modelValue[optInx-1] == item._parentId)"
class="cascader-item" :class="{active:
isActive(item,optInx,valueKey)
}" @click="select(item[valueKey], optInx, index, item._level)">
<view class="cascader-item-label">{{ item[labelKey] }}</view>
<template v-if="iconShow">
<image v-if="isActive(item,optInx,valueKey)" class="icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAOHSURBVHgB7ZldbtpAEMdnFgeBaCV6gnKDkhMUpCbKW9oTRDlB04cS5SnwUFWiL+EEbW+QxzSJFHqCcIOSGyD1I4Hgne44obWNbXb9BQ/8HhJsL+Y/u7O7szMAa9YkAiEFGm2qliq/6rZdeI1CPAegOiFVkbDKz9XnEZIYEqn/IL8LIftn75/2IQUSGbDz6WeDRUMB9mZidWGj1J9TALt30XoygJjEMsARDtYxEjQgBYigX5Cwf3ZUHoIhRgawqxQr42P1iweQBUgnk9/lTr+NI+2v6Dbc+XhbkwW4Uh9rkC1DYUNTdzSETqOtrpqgFl1D9uIZp6O2upO6TuOFI8DiAcWV6SRNDo4IRPOiVYyc4JEG5Og2IeBI2LQZ5U6hLsQTdrnimQcNrCWsRagBzmqzVPH/qBUrt8dhDwNd6NF1fsAKIXHavAzYvQNHwLbgM6wYKK3AUZgbga0Pkzpu2NewggSNwtwIkHWfzS4bAocRum2R4y4fcwagwF3IC5Kdi8NyEwg6Os1VpLvnX5E8BqjJ28htw1Lizw8rbf54flhuE+qMBFUtFba773gMkEI2IA9c4pnt7lg7svW7kccAEuIlZE2AeNV1bd2vo29v8s4BomzdJ6F4B4EvPJeeh2i285qsIKmId8DwSWw0gU1WkNTEOy8LN0D7Farn3StIpBGpip/HO4n5oK0BIjS2u/8DrFAjMhHvPW76XUj7LKpoRxqRWc/LofvKuw8QmqY3go3I0G3I18mW+wJR3sTItLARcN4qO73vGOEibZ/nxJj72jsHbE40xcIzEjPSFs+oaKHvvvYYMC3dDXQncgAeI7IQzxPYH07Pnwe6f04Q8C3Ep/3QL2mLd8R++dYq77vvWf5G7EZYgIQGSMgCCYWe/97cRnZ5VO7rhbb5wptnUI4ocCemqd4BI084+Rt0P9AAZxSAerAikKReWHIrNBa63xjzpjSEZaM03N959xY3oQb03z0bCQnNBMtqcpR41hCVbo+MRp1hI7kUI5QLjwjtN4vS7AvDaaf8o4zI051YvFo0mzqlJ7MCh1DJXsNTmzGPbpNqgYPhF06Kd5tZrk682kxuS5smtbJ4RT41Gpw/TbPIRxI6vHyDIYnKrFy9UYegA0LcRTAss7KfS/iq4vvTOMJnpFLoZl6prJ4qYDec3NJDeqY2M4rFonMUxAFJecOip+PSwKQauWZNRvwFjird3f4t0GAAAAAASUVORK5CYII="
mode="aspectFit"></image>
<image v-else class="icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQdSURBVHgB7VlbTiJBFC3QGOOHz1/RxOjPGP8HF+C4AMUFDC5AWYfMAtQFyMwC0AXIAgwmRuMP86uJfBhjVOYcUtW5XV0N3dA0TMJJSFc/qLqn7q2q+1BqjDH6QkYlgPv7+1lcNrPZ7I+vr68VXL/hOpfJZPhctVqtJp418IzXa1xrGxsbNZUA+iIAwfNa6AMjbFSQFC5V/M5Apq56RE8EKDguJQidVwkAZGpvb29HW1tbDRUTsQhoU6HgxUBHmUwD2qh+fHzcol1bWFh4WVpa4iyrp6en2efn5xyay5OTk3lobBdC51SQyBkuJ9BIU0VEZAI3Nze5mZmZ3/bAELYCgSpxbRqTwTVTRH8Fq7/G6+vrflRtRCJQr9c3p6am/kg7R/sagx+vr6/HVrvEw8NDDn0dSyIkgUn5GWVtdCVgC8/FNzExcbK2tnamEsTj42Px8/OzZMbBtQkSe91IdCRgm02cmekFWhtyvCbMaaeTOYUS4IKFjV5J4dHe79dkusFBgpO2E7awsx36KplOaDZpCE9wDGpZnxNKy1AK+95JgKYjt0rafBrCG9BEOaa5pyz67AnASWB6evqX+HMl6QUbBRyTB5x45NRCgAD3Z3nCopOyGhIgx5Fo511acGmgKP5USdN0bHBsymDu6XfZ37gI7JoGjv3UTccGT3lxe6DdGQ8+AlSROEgaq6urA9nv44AuCmVhG+bcdtvlex8BqEjaWFWNDjxZbDPyEYC6tkU7kYAjCUCWW9Feke/sNeDZF/yfv2pEQPfctBntyXc2Ac9Vfn9/H9ruYwO2/yJu5+Q7HwHpLscJKgYNKYteyB6y6j+Hj4BxoAh7vx0mpCx0seU7ext9ER/OqdGBtzaxC/nWpm1C3sEFMt/V6GDZNOQkt++tDz12MKdNNSKwDljf+WQfZJfidleNDjxZ7AM2YEIyEgoLItIE3XsZI9vpGx8B7rdQ14V4VFDDh0yiBfyzwDkgzQiMCwwv1ZCgQ1tvEl3ufYAAVSRDORlepg2MfWzaTKS53Puwk1gG1HmkOg5Vyri7uzuUs88soOs7JwEdRJyae6b+uJhUSqDpYC3K2T8NC21DfSGshbKMhNDheRrrwWQDZWQ4Pz8fmlgIJcAdicksua2y40GSYB7WTmVSBpOmd6GjN6rVtmeRuBrEmqDNM4kss4HM0HXLikRKr+tc/rmsDaBdQVWl3EtVRYIa5U5n5aI4YXuJpNcN7KSrGIxpj9h1Ln3KF+RO0xYoZhI5domJuwMGCJiQXvBVvK+hzMR4umEiKf6P7jn+l9cxrbPExN2GC7aTzfdFwEBrowwhtlUC4CHFXa+X0mu/ZdZ2nYtFu17KrHANLqCty35qxokUugldM6ZdswrJaC5nFboZiNRZ8KbQi4uL9TimMsYYA8I/5WtETy5O2jYAAAAASUVORK5CYII="
mode="aspectFit"></image>
</template>
</view>
</template>
</scroll-view>
</view>
</template>
</view>
</template>
<script setup>
import {
ref,
defineModel,
defineProps,
defineEmits,
watch
} from 'vue';
const modelValue = defineModel({
type: Array,
default: []
});
const DefProps = defineProps({
option: { // tree数据
type: Array,
default: () => []
},
props: { // 属性值
type: Object,
default: {
label: 'label',
value: 'value',
children: 'children',
disabled: 'disabled',
}
},
maxHeight: { //最大下拉高度
type: String,
default: "400rpx"
},
iconShow: { // 是由否显示左侧按钮
type: Boolean,
default: false,
}
})
const Emits = defineEmits(['change'])
const newOption = ref([]); // 选项tree
const labelKey = ref('label'); // 默认属性key
const valueKey = ref('value');
const childrenKey = ref('children');
const disabled = ref('disabledKey');
// 配置属性key
watch(() => DefProps.props, (newVal, oldVal) => {
labelKey.value = newVal.label ?? 'label'
valueKey.value = newVal.value ?? 'value'
childrenKey.value = newVal.children ?? 'children'
disabled.value = newVal.disabledKey ?? 'disabledKey'
}, {
deep: true,
immediate: true
})
// 整理tree数据根据层级分类数组
watch(() => DefProps.option, (newVal, oldVal) => {
if (newVal) {
let tree = treeConfig(newVal);
newOption.value = setData(tree);
}
}, {
deep: true,
immediate: true
});
const isActive = (item, optInx, valueKey) => {
if (item._level !== newOption.value.length) {
return modelValue.value[optInx] == item[valueKey]
} else {
if (modelValue.value[item._level - 1] === undefined) {
return false
} else {
return modelValue.value[item._level - 1].includes(item[valueKey])
}
}
}
// 默认tree列表配置设置父子级关系和层级
function treeConfig(arr, parentId = 0, level = 0) {
let list = JSON.parse(JSON.stringify(arr))
list.forEach(item => {
item._parentId = parentId;
item._level = level + 1;
if (item[childrenKey.value] && item[childrenKey.value].length > 0) {
item[childrenKey.value] = treeConfig(item[childrenKey.value], item[valueKey.value], item._level);
}
})
return list
}
// 将tree数据根据层级分类
function setData(tree, list = []) {
tree.forEach(item => {
let obj = {}
for (let k in item) {
if (k != childrenKey.value) {
obj[k] = item[k];
}
}
if (!list[obj._level - 1]) {
list[obj._level - 1] = [];
}
list[obj._level - 1].push(obj);
if (item[childrenKey.value] && item[childrenKey.value].length > 0) {
list = setData(item[childrenKey.value], list)
}
})
return list
}
// 选择
function select(value, parentIndex, index, _level) {
let arr = JSON.parse(JSON.stringify(modelValue.value))
arr[parentIndex] = value;
let list = []
for (let i = 0; i < arr.length; i++) {
if (i <= parentIndex) {
list.push(arr[i]);
}
}
let origin_list = modelValue.value
let current_list = list
if (JSON.stringify(origin_list.slice(0, newOption.value.length - 1)) === JSON.stringify(current_list.slice(0,
newOption.value.length - 1))) {
// 将最后一个层级设置为多选
if (_level === newOption.value.length) {
if (modelValue.value[_level - 1] === undefined) {
modelValue.value[_level - 1] = []
}
if (modelValue.value[_level - 1].includes(value)) {
modelValue.value[_level - 1] = modelValue.value[_level - 1].filter(e => e != value)
} else {
modelValue.value[_level - 1].push(value)
}
}
} else {
modelValue.value = list;
}
Emits('change', modelValue.value, value)
}
</script>
<style lang="scss" scoped>
page,
view,
text,
swiper,
swiper-item,
image,
navigator,
icon,
form,
input,
button {
box-sizing: border-box;
}
.op-cascader {
width: 100%;
height: 100%;
display: flex;
box-sizing: border-box;
.cascader-content {
flex: 1;
// overflow: hidden;
width: 100%;
background-color: #fff;
.cascader-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 20rpx;
padding-right: 8rpx;
&.active {
.cascader-item-label {
color: #468AF7;
}
}
.cascader-item-label {
flex: 1;
text-align: left;
color: #364462;
font-size: 25rpx;
line-height: 92rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
&.active {
color: #468AF7;
}
}
>.icon {
width: 40rpx;
height: 40rpx;
}
}
}
.cascader-content:first-of-type {
background-color: #f9f9f9;
}
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<view class="op-dateTime-picker">
<view class="dateTime">
<!-- 日期时间 -->
<view class="dateTime-input" v-if="type.indexOf('range') == -1">
<view class="dateTime-label">{{ label }}</view>
<input v-model="timeVal" type="date" :placeholder="placeholder" disabled @click.stop="openTimeSelect()"></input>
</view>
<!-- 日期时间范围 -->
<template v-if="type.indexOf('range') > -1">
<view class="dateTime-input">
<view class="dateTime-label">开始{{ label }}</view>
<input v-model="timeVals[0]" type="date" :placeholder="startPlaceholder" disabled @click.stop="openTimeSelect(0)"></input>
</view>
<view class="dateTime-input">
<view class="dateTime-label">结束{{ label }}</view>
<input v-model="timeVals[1]" type="date" :placeholder="endPlaceholder" disabled @click.stop="openTimeSelect(1)"></input>
</view>
</template>
</view>
<!-- 下拉选择 -->
<view v-if="dateTimeVisible">
<picker-view :value="defaultVal" class="pickerView" indicator-class="pickerViewColumn" @change="pickerChange">
<picker-view-column :class="{show: type.indexOf('date') > -1 || type.indexOf('year') > -1}">
<view class="item" v-for="(item,index) in years" :key="index">{{item}}</view>
</picker-view-column>
<picker-view-column :class="{show: type.indexOf('date') > -1 || type.indexOf('month') > -1}">
<view class="item" v-for="(item,index) in months" :key="index">{{item}}</view>
</picker-view-column>
<picker-view-column :class="{show: type.indexOf('date') > -1}">
<view class="item" v-for="(item,index) in days" :key="index">{{item}}</view>
</picker-view-column>
<picker-view-column :class="{show: type.indexOf('time') > -1}">
<view class="item" v-for="(item,index) in hours" :key="index">{{item}}</view>
</picker-view-column>
<picker-view-column :class="{show: type.indexOf('time') > -1}">
<view class="item" v-for="(item,index) in minutes" :key="index">{{item}}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script setup>
import { ref, defineModel, defineProps, defineEmits, watch } from 'vue';
const modelVal= defineModel();
const Props = defineProps({
type: {
type: String,
default: 'datetime', // 'date、datetime、time、yearmonth、year、daterange、datetimerange、timerange、yearmonthrange、yearrange'
},
placeholder: {
type: String,
default: '请选择'
},
startPlaceholder: {
type: String,
default: '请选择'
},
endPlaceholder: {
type: String,
default: '请选择'
}
})
const Emits = defineEmits(["change", "update:modelVal"]);
const label = ref("");
const timeVal = ref(""); // 日期时间
const timeVals = ref([]); // 日期时间范围
const dateTimeVisible = ref(false); // 日期时间选择显示隐藏
let rangeInx = 0; // 范围值的索引
watch(() => modelVal.value, (newVal, oldVal) => {
if( Props.type.indexOf("range") > -1 ) {
timeVals.value = newVal??[];
} else {
timeVal.value = newVal
}
}, { immediate: true, deep: true})
watch(() => Props.type, (newVal, oldVal) => {
switch(newVal) {
case 'date':
label.value = '日期';
break
case 'daterange':
label.value = '日期';
break
case 'datetimerange':
label.value = '日期时间';
break
case 'datetime':
label.value = '日期时间';
break
case 'time':
label.value = '时间';
break
case 'timerange':
label.value = '时间';
break
case 'year':
label.value = '年份';
break
case 'yearrange':
label.value = '年份';
break
case 'yearmonth':
label.value = '年月';
break
case 'yearmonthrange':
label.value = '年月';
break
}
}, { immediate: true})
const defaultVal = ref([]); // 下拉时间默认值
const years = ref([]); //年
const nowYear = new Date().getFullYear();
// 设置年份数组
for (let i = nowYear - 100; i <= nowYear+100; i++) {
if(i < 1970) continue;
years.value.push(i.toString());
}
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; //月
const days = ref([]); // 日
const hours = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
const minutes = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59']
/** 打开日期时间选择
* @param { String } key form属性键
* @param { String } datetype 时间类型
* @param { Number } inx 值在时间范围的索引
*/
function openTimeSelect(inx=0) {
// 设置下拉默认值
const data = (Props.type.indexOf("range") > -1)? timeVals.value[inx] :timeVal.value;
let now;
// 获取当前时间
if (Props.type == 'time' || Props.type == 'timerange') {
now = data?new Date('2025-01-01 '+data):new Date();
} else {
now = data?new Date(data):new Date();
}
// 获取当前年份
const currentYear = now.getFullYear();
const currentMonth = now.getMonth()+1;
const currentDay = now.getDate();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// 设置天数数组
const dayCount = new Date(currentYear, currentMonth, 0).getDate(); //此月天数
days.value = []
for (let i = 1; i <= dayCount; i++) {
days.value.push(i>9?i.toString():'0'+i);
}
// 设置默认年月日时分
setIndex(currentYear, 0, years.value);
setIndex(currentMonth, 1, months);
setIndex(currentDay, 2, days.value);
setIndex(currentHour, 3, hours);
setIndex(currentMinute, 4, minutes);
// 下拉选择显示
if(dateTimeVisible.value && rangeInx == inx){
dateTimeVisible.value = false;
rangeInx = 0;
} else {
dateTimeVisible.value = false;
rangeInx = inx;
setTimeout(() => {
dateTimeVisible.value = true;
}, 60)
}
}
// 根据值设置各列索引
function setIndex(val, inx, list) {
defaultVal.value[inx] = list.findIndex(item => item == val)
}
// 日期时间改变
function pickerChange(val) {
const Y = years.value[ val.detail.value[0] ];
const M = months[ val.detail.value[1] ];
const D = days.value[ val.detail.value[2] ];
const h = hours[ val.detail.value[3] ];
const m = minutes[ val.detail.value[4] ];
// 更新天数
const dayCount = new Date(Y, M, 0).getDate(); //此月天数
if(days.value.length != dayCount){
days.value = [];
for (let i = 1; i <= dayCount; i++) {
days.value.push(i>9?i.toString():'0'+i);
}
}
switch(Props.type) {
case 'datetime':
timeVal.value = `${Y}-${M}-${D} ${h}:${m}:00`
break;
case 'datetimerange':
timeVals.value[rangeInx] = `${Y}-${M}-${D} ${h}:${m}:00`
break;
case 'date':
timeVal.value = `${Y}-${M}-${D}`
break;
case 'daterange':
timeVals.value[rangeInx] = `${Y}-${M}-${D}`
break;
case 'time':
timeVal.value = `${h}:${m}:00`
break;
case 'timerange':
timeVals.value[rangeInx] = `${h}:${m}:00`
break;
case 'yearmonth':
timeVal.value = `${Y}-${M}`
break;
case 'yearmonthrange':
timeVals.value[rangeInx] = `${Y}-${M}`
break;
case 'year':
timeVal.value = `${Y}`
break;
case 'yearrange':
timeVals.value[rangeInx] = `${Y}`
break;
}
if(Props.type.indexOf('range') > -1){
// 根据大小重新排序
Emits('change', timeVals.value);
Emits("update:modelVal", timeVals.value)
} else {
Emits('change', timeVal.value);
modelVal.value = timeVal.value;
}
}
</script>
<style lang="scss" scoped>
page, view, text, swiper, swiper-item, image, navigator, icon, form, input, button{
box-sizing: border-box;
}
.op-dateTime-picker{
.dateTime{
width: 100%;
padding: 20rpx;
.dateTime-input{
padding: 20rpx 32rpx;
display: flex;
.dateTime-label{
font-size: 28rpx;
line-height: 72rpx;
margin-right: 10rpx;
color: #364462;
}
input{
flex: 1;
height: 72rpx;
background-color: rgb(249, 249, 249);
border-radius: 8rpx;
padding: 0 10px;
color: rgb(96, 98, 102);
font-size: 14px;
position: relative;
&::after{
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
}
}
.pickerView{
width: 100%;
height: 350rpx;
background-color: #f2f2f2;
:deep(.pickerViewColumn){
height: 70rpx;
}
picker-view-column{
flex: 0;
.item{
line-height: 70rpx;
text-align: center;
}
}
.show{
flex: 1;
}
}
}
</style>

View File

@@ -0,0 +1,304 @@
<template>
<view class="drop-down-container">
<view class="drop-down-select">
<!-- 下拉按钮 -->
<view class="select-up">
<template v-for="(item, index) in newOption" :key="index">
<view class="select-button" @click="openMenu(index)">
<text :class="{active: item.menuVisible}">{{item.newTitle??item.title}}</text>
<image v-if="item.menuVisible" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAOCAYAAAAi2ky3AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAC3SURBVHgBpc9NDoIwEAXgmWI07jhCPYzGJZ5Aj4Iro5fgKHokFv5EQlooIW3aYVoS3gbSTifvA0hk/3hfDvdPkZrD2OXx9pMqg2c/ljffze5VYs3NitiiVqiy/0gAna+3/yo2yy4yJIHibA9QFzHiJM2RTBtvvOaIk40cKQxPJIsIKQxD9Gg8iTwjRK8RTwpDiXZRkkRK+cSBNp9Etlni0Gg+KYwjoiEhZBUsiVan1fhzhQVBQNkBj+1WDGeu7GQAAAAASUVORK5CYII=" ></image>
<image v-else src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAOCAYAAAAi2ky3AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACtSURBVHgBpZNNDsIgEEaBGNYugVN5A+0NPIKeRD1BeyNnB8ueAJxprMEE6LR9yYTh7wtvgbTWXqWUR7GDGOP7gCNg9WIfF+W9H7AZxHaeIYSXok5r3eEwipWklAC17tRPQQAw4mInVoJ3bvga+AURGxQnpXmi8h2uYq5UDOIq5krFIIKh+KdUDSJqiiWlZlBNsaTEwjnXY6VvPVpnVWtzVmwpscFPfTLGnJfOfQDwZHGVR0KC2QAAAABJRU5ErkJggg==" ></image>
</view>
</template>
</view>
<!-- 下拉选项 -->
<view class="select-down">
<template v-for="(item, index) in newOption" :key="index">
<view v-if="item.menuVisible" class="select-menu">
<!-- 联级 -->
<Op-Cascader v-if="item.type=='cascader'" v-model="form[newOption[index].key]" :option="item.option" :props="item.props" :iconShow="true" maxHeight="600rpx"></Op-Cascader>
<!-- 下拉选择 -->
<view class="select" v-if="item.type == 'select'">
<view class="label"
v-for="(itm, inx) in item.option" :key="inx"
:class="{ active: itm.value == form[item.key]}"
@click="select(itm.value, itm, inx, index)"
>
{{itm.label}}
</view>
</view>
<!-- 日期时间选择 -->
<Op-DateTime-Picker v-if="['date','time','yearmonth','year','datetime','daterange','timerange','yearmonthrange','yearrange','datetimerange'].includes(item.type)" v-model="form[newOption[index].key]" :type="item.type" :placeholder="item.placeholder" :startPlaceholder="item.startPlaceholder" :endPlaceholder="item.endPlaceholder"></Op-DateTime-Picker>
<!-- #ifdef H5 || APP-PLUS -->
<slot v-if="item.type == 'custom'" :name="newOption[index].key"></slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<slot v-if="item.type == 'custom'" name="custom-{{index}}"></slot>
<!-- #endif -->
<!-- 底部确认按钮 -->
<view class="drop-down-button">
<view @click="reset(index)">重置</view>
<view @click="submit(index)">确定</view>
</view>
</view>
</template>
</view>
</view>
<view class="drop-down-modal" v-if="modalShow" @click="closeModal"></view>
</view>
</template>
<script setup>
import { reactive, ref, defineProps, watch, defineEmits } from 'vue';
import OpCascader from '@/components/op-drop-down/components/op-cascader/op-cascader.vue';
import OpDateTimePicker from '@/components/op-drop-down/components/op-dateTime-picker/op-dateTime-picker.vue';
const form = reactive({});
const Props = defineProps({
defaultValue: {
type: Object,
default: () => {}
},
option: {
type: Array,
default: () => []
},
closeOnClickModal:{ // 是否点击模态窗关闭
type: Boolean,
default: false
}
})
const Emits = defineEmits(['update:modelValue', "submit", "reset", "change" ]);
const newOption = ref([]); // 所有选项
const modalShow = ref(false); // 模态框
watch(() => Props.defaultValue, (newVal, oldVal) => {
if(newVal){
for(let k in newVal){
form[k] = newVal[k];
}
}
}, { deep: true, immediate: true })
watch(() => Props.option, (newVal, oldVal) => {
if(newVal){
newOption.value = JSON.parse(JSON.stringify(newVal));
}
}, { deep: true, immediate: true })
/** 展开菜单 */
function openMenu(inx) {
if(newOption.value[inx].menuVisible){
newOption.value[inx].menuVisible = false;
modalShow.value = false;
} else {
newOption.value.forEach((item, index) => {
item.menuVisible = index == inx
})
modalShow.value = true;
}
}
/** select菜单选择
* @param { String | Number } val 选中值
* @param { Object } row 选中行
* @param { Number } inx 选中行索引
* @param { Number } parentInx 选中行父级索引
*/
function select(val, row, inx, parentInx) {
form[newOption.value[parentInx].key] = val;
}
// 重置
function reset(inx) {
// 初始话默认值,联级选择需要设置为[],防止双向数据绑定失效
if(newOption.value[inx].type == 'cascader' || newOption.value[inx].type.indexOf('range') > -1){
form[newOption.value[inx].key] = [];
} else {
form[newOption.value[inx].key] = undefined;
}
newOption.value[inx].menuVisible = false;
modalShow.value = false;
const data = JSON.parse(JSON.stringify(form));
Emits("reset", data);
Emits("change", data);
}
// 确认提交
function submit(inx) {
newOption.value[inx].menuVisible = false;
modalShow.value = false;
let data = JSON.parse(JSON.stringify(form));
if(newOption.value[inx].type.indexOf('range') > -1 ){
const key = newOption.value[inx].key;
if(data[key] && data[key][0] && data[key][1] ){
data[key] = againSort(data[key], newOption.value[inx].type)
}
}
Emits("submit", data);
if(JSON.stringify(Props.value) != JSON.stringify(form)){
Emits("change", data);
}
}
// 重新排序
function againSort(arr, type) {
if(!arr[0] || !arr[1]) return arr;
let start = 0;
let end = 0;
if(type.indexOf('date') > -1 && type.indexOf('year') > -1){
start = new Date(arr[0]).getTime();
end = new Date(arr[1]).getTime();
} else if (type == 'timerange') {
start = arr[0].split(":").join("");
end = arr[1].split(":").join("");
}
return end > start? arr: [ arr[1], arr[0] ]
}
// 关闭模态窗
function closeModal() {
if(!Props.closeOnClickModal) return
newOption.value.forEach(item => {
item.menuVisible = false;
})
modalShow.value = false;
}
</script>
<style lang="scss" scoped>
page, view, text, swiper, swiper-item, image, navigator, icon, form, input, button{
box-sizing: border-box;
}
.drop-down-container{
flex-shrink: 0;
background-color: #fff;
position: relative;
.drop-down-select{
width: 100%;
position: relative;
z-index: 2;
.select-up{
width: 100%;
height: 92rpx;
display: flex;
.select-button{
flex: 1;
padding: 24rpx 10rpx;
display: flex;
align-items: center;
justify-content: center;
display: flex;
overflow: hidden;
text{
font-size: 30rpx;
line-height: 44rpx;
color: #1A1A1A;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
&.active{
color: #468AF7;
}
}
image{
flex-shrink: 0;
width: 18rpx;
height: 14rpx;
margin-left: 22rpx;
}
&.active{
text{
color: #468AF7;
}
}
}
}
.select-down{
width: 100%;
border-top: 1rpx solid rgba(0,0,0,0.1);
background-color: #fff;
position: absolute;
left: 0;
top: 92rpx;
.select{
width: 100%;
.label{
text-align: center;
color: #364462;
font-size: 32rpx;
line-height: 92rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 10rpx;
&.active{
color: #468AF7;
}
}
}
.dateTime{
width: 100%;
padding: 20rpx;
.dateTime-input{
padding: 20rpx 32rpx;
display: flex;
.dateTime-label{
font-size: 28rpx;
line-height: 72rpx;
margin-right: 10rpx;
color: #364462;
}
input{
flex: 1;
height: 72rpx;
background-color: rgb(249, 249, 249);
border-radius: 8rpx;
padding: 0 10px;
color: rgb(96, 98, 102);
font-size: 14px;
position: relative;
&::after{
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
}
}
.drop-down-button{
width: 100%;
display: flex;
view{
flex: 1;
height: 88rpx;
font-size: 32rpx;
line-height: 88rpx;
text-align: center;
font-size: rgba(0,0,0,0.9);
&:nth-of-type(1){
border-top: 1rpx solid rgba(0,0,0,0.1);
}
&:nth-of-type(2){
background-color: #468AF7;
color: #fff;
}
}
}
}
}
.drop-down-modal{
width: 100%;
height: 100vh;
position: absolute;
z-index: 1;
background-color: rgba(0,0,0,0.5);
}
}
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "op-drop-down",
"displayName": "op-drop-down",
"version": "1.1.1",
"description": "多功能下拉筛选支持tree联机筛选、时间范围筛选、数组筛选、自定义筛选菜单",
"keywords": [
"drop-down",
"datetimerange",
"cascader",
"自定义",
"下拉筛选"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "u",
"app-uvue": "u",
"app-harmony": "u"
},
"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",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,167 @@
# op-drop-down
# 前端UI下拉选择组件
### 使用方式
```html
<Op-Drop-Down :defaultValue="form" :option="paramOptions" :closeOnClickModal="true" @change="onChange" @reset="reset">
<template #customKey>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
</template>
</Op-Drop-Down>
```
```javascript
import OpDropDown from '@/uni_modules/op-drop-down/components/op-drop-down/op-drop-down.vue';
export default {
components: { OpDropDown },
data() {
return {
form: {
type: [],
status: '',
time: '',
dateTimeRange: [],
customKey: ''
},
paramOptions: [
{
title: '分类',
key: 'type',
type: 'cascader',
props: { label: 'label', value: 'value', children: 'children', disabled: 'disabled'},
option: [
{
label: '标题1',
value: '1',
children: [
{
label: '标题1-1',
value: '1-1',
children: [
{ label: '标题1-1-1', value: '1-1-1' },
{ label: '标题1-1-2', value: '1-1-2' },
{ label: '标题1-1-3', value: '1-1-3', disabled: true },
{ label: '标题1-1-4', value: '1-1-4', disabled: true },
{ label: '标题1-1-5', value: '1-1-5' },
{ label: '标题1-1-6', value: '1-1-6' }
]
},
{
label: '标题1-2',
value: '1-2',
children: [
{ label: '标题1-2-1', value: '1-2-1' },
{ label: '标题1-2-2', value: '1-2-2' }
]
}
]
},
{
label: '标题2',
value: '2',
children: [
{
label: '标题2-1',
value: '2-1',
children: [
{ label: '标题2-1-1', value: '2-1-1' },
{ label: '标题2-1-2', value: '2-1-2' }
]
},
{
label: '标题2-2',
value: '2-2',
children: [
{ label: '标题2-2-1', value: '2-2-1' },
{ label: '标题2-2-2', value: '2-2-2' }
]
}
]
},
{
label: '标题3',
value: '3',
},
{
label: '标题4',
value: '4',
},
{
label: '标题5',
value: '5',
}
]
},
{
title: '状态',
key: 'status',
type: 'select',
option: [
{ label: '在线', value: 'online'},
{ label: '离线', value: 'offline'},
]
},
{
title: '时间',
key: 'time',
type: 'time',
placeholder: '请选择',
},
{
title: '日期范围',
key: 'dateTimeRange',
type: 'datetimerange',
startPlaceholder: '请选择',
endPlaceholder: '请选择',
},
{
title: '自定义',
key: 'customKey',
type: 'custom',
}
]
}
},
onLoad() {
},
methods: {
onChange(val) {
this.form = val
console.log(val)
},
reset(val) {
this.form = val
console.log(val)
}
}
}
```
###组件的属性说明如下:
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| ---------------- | ------- | ------- | ---- | ------------------------------ |
| defaultValue | Object | {} | 是 | 设置整个下拉的默认键值对 |
| option | Array | [] | 是 | 下拉菜单配置的数据 |
| closeOnClickModal | Boolean | true | 否 | 点击模态窗是否关闭下拉 |
####option参数说明
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| ---------------- | ------- | ------- | ---- | ------------------------------ |
| title | String | | 是 | 属性名,显示在页面上 |
| key | String | | 是 | 与defaultValue对应的键 |
| type | String | 可选cascader、select、date、time、yearmonth、year、datetime、daterange、timerange、yearmonthrange、yearrange、datetimerange、custom | 是 | 下拉类型 |
| props | Object | { label: label, value: value, children: children } | 否 | 只针对联级下拉选择项cascader自定义节点 label、value、options 的字段 |
| placeholder | String | | 否 | 时间选择占位文本 |
| startPlaceholder | String | | 否 | 时间范围选择占位文本 |
| endPlaceholder | String | | 否 | 时间范围选择占位文本 |
###事件
| 事件名称 | 回调参数 | 说明 |
| --------- | -------------------- | ------------------------------------------------------------ |
| change | (data) => void | 改变事件data为当前操作后的数据 |
| reset | (data) => void | 重置其中一个选中数据data为当前操作后的数据 |