flat:所属社区

This commit is contained in:
Apcallover
2024-06-05 16:58:38 +08:00
parent 21835e52c1
commit f338226426
30 changed files with 4215 additions and 189 deletions

View File

@@ -135,3 +135,8 @@ export const cancelUserInvite = (params) => request({
method: 'get', method: 'get',
params params
}) })
export const getDeptAllTree = (params) => request({
url: '/api/jobslink-api/system/dept/all-tree',
method: 'get',
params
})

View File

@@ -1,7 +1,9 @@
<template> <template>
<view v-if="showPopup" class="uni-popup"> <view v-if="showPopup" class="uni-popup">
<view :class="[ani, animation ? 'ani' : '', !custom ? 'uni-custom' : '']" class="uni-popup__mask" @click="close(true)" /> <view :class="[ani, animation ? 'ani' : '', !custom ? 'uni-custom' : '']" class="uni-popup__mask"
<view :class="[type, ani, animation ? 'ani' : '', !custom ? 'uni-custom' : '']" class="uni-popup__wrapper" @click="close(true)"> @click="close(true)" />
<view :class="[type, ani, animation ? 'ani' : '', !custom ? 'uni-custom' : '']" class="uni-popup__wrapper"
@click="close(true)">
<view class="uni-popup__wrapper-box" @click.stop="clear"> <view class="uni-popup__wrapper-box" @click.stop="clear">
<slot /> <slot />
</view> </view>
@@ -103,7 +105,7 @@
left: 0; left: 0;
right: 0; right: 0;
z-index: 998; z-index: 998;
/* background: rgba(0, 0, 0, .4); */ background: rgba(0, 0, 0, .4);
opacity: 0 opacity: 0
} }

View File

@@ -101,6 +101,11 @@
<u--input v-model="info.jobCompanyIndustry" border="none" :disabled="true" disabledColor="#ffffff" <u--input v-model="info.jobCompanyIndustry" border="none" :disabled="true" disabledColor="#ffffff"
placeholder="请输入所属行业"></u--input> placeholder="请输入所属行业"></u--input>
</u-form-item> </u-form-item>
<u-form-item label="所属社区" prop="createDept" borderBottom labelWidth="80" ref="item1">
<custom-tree-select style="width: 100%;" :listData="deptTreeData" v-model="info.createDept"
data-label="title" data-value="value" boxStyle="border: 0; paddingLeft: 0;" search border
placeholder="请选择所属社区" />
</u-form-item>
<u-form-item label="企业性质" prop="jobCompanyNature" borderBottom labelWidth="80" ref="item1"> <u-form-item label="企业性质" prop="jobCompanyNature" borderBottom labelWidth="80" ref="item1">
<u--input v-model="info.jobCompanyNature" border="none" :disabled="true" disabledColor="#ffffff" <u--input v-model="info.jobCompanyNature" border="none" :disabled="true" disabledColor="#ffffff"
placeholder="请输入企业性质"></u--input> placeholder="请输入企业性质"></u--input>
@@ -176,7 +181,8 @@
submitInfo, submitInfo,
getWorktypesBaseList, getWorktypesBaseList,
findTradeList, findTradeList,
getDictionary getDictionary,
getDeptAllTree
} from '@/api/userrecruit.js' } from '@/api/userrecruit.js'
import PickerTree from './enterpriceCertification/pickerTree.vue' import PickerTree from './enterpriceCertification/pickerTree.vue'
import { import {
@@ -191,6 +197,7 @@
export default { export default {
data() { data() {
return { return {
deptTreeData: [],
addressOptions: [], addressOptions: [],
loading: false, loading: false,
latitude: 31.05, //中心点 latitude: 31.05, //中心点
@@ -242,6 +249,7 @@
callTel: '', // 手机号 callTel: '', // 手机号
callNumber: '', // 座机 callNumber: '', // 座机
address: '', // 公司详细地址 address: '', // 公司详细地址
createDept: []
// userInfo: { // userInfo: {
// name: '楼兰', // name: '楼兰',
// sex: '', // sex: '',
@@ -391,6 +399,12 @@
message: '请输入详细地址', message: '请输入详细地址',
trigger: ['change'] trigger: ['change']
}, },
createDept: {
type: 'array',
required: true,
message: '请选择所属社区',
trigger: ['change']
}
}, },
} }
}, },
@@ -412,6 +426,7 @@
} = dic.wageUnitCategoryState[0].filter(item => item.id == 3)[0] } = dic.wageUnitCategoryState[0].filter(item => item.id == 3)[0]
this.info.wageUnitCategory = staId this.info.wageUnitCategory = staId
this.info.wageUnitCategoryName = staLabel this.info.wageUnitCategoryName = staLabel
this.getTreeDept()
if (this.company) { if (this.company) {
this.backfill(this.company) this.backfill(this.company)
} }
@@ -426,6 +441,13 @@
}, },
methods: { methods: {
async getTreeDept() {
let resData = await getDeptAllTree()
if (resData.data.code === 200) {
this.deptTreeData = resData.data.data
console.log('dept', resData.data.data)
}
},
selectInput: debounce(function(val) { selectInput: debounce(function(val) {
// querySearch(val, '3CXBZ-SKHCL-QC6PH-MLJAE-ZYCFK-6MBR5').then(res => { // querySearch(val, '3CXBZ-SKHCL-QC6PH-MLJAE-ZYCFK-6MBR5').then(res => {
// console.log(res) // console.log(res)
@@ -484,6 +506,7 @@
this.info.jobCompanyIndustry = info.jobCompanyIndustry this.info.jobCompanyIndustry = info.jobCompanyIndustry
this.info.jobCompanyNature = info.jobCompanyNature this.info.jobCompanyNature = info.jobCompanyNature
this.info.jobCompanyDescription = info.jobCompanyDescription this.info.jobCompanyDescription = info.jobCompanyDescription
this.info.createDept = [String(info.createDept)]
this.info.callName = info.callName this.info.callName = info.callName
this.info.callTel = info.callTel this.info.callTel = info.callTel
this.info.callNumber = info.callNumber this.info.callNumber = info.callNumber
@@ -658,6 +681,7 @@
} }
params.lon = that.longitude params.lon = that.longitude
params.lat = that.latitude params.lat = that.latitude
params.createDept = params.createDept[0]
submitInfo(params).then(res => { submitInfo(params).then(res => {
if (res.data.code == 200) { if (res.data.code == 200) {
that.reset() that.reset()
@@ -700,6 +724,7 @@
'jobCompanyIndustry', 'jobCompanyIndustry',
'jobCompanyNature', 'jobCompanyNature',
'jobCompanyDescription', 'jobCompanyDescription',
"createDept",
'callName', 'callName',
'callTel', 'callTel',
'callNumber', 'callNumber',

View File

@@ -105,6 +105,11 @@
<u--input v-model="info.jobCompanyIndustry" border="none" placeholder="请输入所属行业" :disabled="true" <u--input v-model="info.jobCompanyIndustry" border="none" placeholder="请输入所属行业" :disabled="true"
disabledColor="#ffffff"></u--input> disabledColor="#ffffff"></u--input>
</u-form-item> </u-form-item>
<u-form-item label="所属社区" prop="createDept" borderBottom labelWidth="80" ref="item1">
<custom-tree-select style="width: 100%;" ceilStyle="border: 0;" :listData="deptTreeData"
v-model="info.createDept" data-label="title" data-value="id"
boxStyle="border: 0; paddingLeft: 0;" search border placeholder="请选择所属社区" />
</u-form-item>
<u-form-item label="企业性质" prop="jobCompanyNature" borderBottom labelWidth="80" ref="item1"> <u-form-item label="企业性质" prop="jobCompanyNature" borderBottom labelWidth="80" ref="item1">
<u--input v-model="info.jobCompanyNature" border="none" placeholder="请输入企业性质" :disabled="true" <u--input v-model="info.jobCompanyNature" border="none" placeholder="请输入企业性质" :disabled="true"
disabledColor="#ffffff"></u--input> disabledColor="#ffffff"></u--input>
@@ -177,7 +182,8 @@
submitInfo, submitInfo,
getWorktypesBaseList, getWorktypesBaseList,
findTradeList, findTradeList,
getDictionary getDictionary,
getDeptAllTree
} from '@/api/userrecruit.js' } from '@/api/userrecruit.js'
import PickerTree from './enterpriceCertification/pickerTree.vue' import PickerTree from './enterpriceCertification/pickerTree.vue'
import { import {
@@ -186,6 +192,7 @@
export default { export default {
data() { data() {
return { return {
deptTreeData: [],
latitude: 31.05, //中心点 latitude: 31.05, //中心点
longitude: 104.20, longitude: 104.20,
covers: [{ //marker标记位置 covers: [{ //marker标记位置
@@ -384,6 +391,12 @@
message: '请输入详细地址', message: '请输入详细地址',
trigger: ['change'] trigger: ['change']
}, },
createDept: {
type: 'array',
required: true,
message: '请选择所属社区',
trigger: ['change']
}
}, },
} }
}, },
@@ -412,6 +425,7 @@
this.info.wageUnitCategory = staId this.info.wageUnitCategory = staId
this.info.wageUnitCategoryName = staLabel this.info.wageUnitCategoryName = staLabel
console.log(this.area) console.log(this.area)
this.getTreeDept()
this.getWorkTypes() this.getWorkTypes()
this.dictionary() this.dictionary()
if (this.company) { if (this.company) {
@@ -419,6 +433,12 @@
} }
}, },
methods: { methods: {
async getTreeDept() {
let resData = await getDeptAllTree()
if (resData.data.code === 200) {
this.deptTreeData = resData.data.data
}
},
onBlurWage(value) { onBlurWage(value) {
const val = this.wallMaxAndMin const val = this.wallMaxAndMin
if (!val.length) { if (!val.length) {
@@ -468,6 +488,7 @@
this.info.jobCompanyNature = info.jobCompanyNature this.info.jobCompanyNature = info.jobCompanyNature
this.info.jobCompanyDescription = info.jobCompanyDescription this.info.jobCompanyDescription = info.jobCompanyDescription
this.info.callName = info.callName this.info.callName = info.callName
this.info.createDept = [String(info.createDept)]
this.info.callTel = info.callTel this.info.callTel = info.callTel
this.info.callNumber = info.callNumber this.info.callNumber = info.callNumber
this.info.address = info.address this.info.address = info.address
@@ -648,6 +669,7 @@
} }
params.lon = that.longitude params.lon = that.longitude
params.lat = that.latitude params.lat = that.latitude
params.createDept = params.createDept[0]
submitInfo(params).then(res => { submitInfo(params).then(res => {
if (res.data.code == 200) { if (res.data.code == 200) {
that.reset() that.reset()
@@ -701,6 +723,7 @@
'jobCompanyNature', 'jobCompanyNature',
'jobCompanyDescription', 'jobCompanyDescription',
'callName', 'callName',
'createDept',
'callTel', 'callTel',
'callNumber', 'callNumber',
'address' 'address'

View File

@@ -0,0 +1,187 @@
## 4.1.02023-10-22
- 修复小程序端无法移除已选项问题
- 修复小程序端异步加载节点不可用问题
- 增加点击左侧箭头同样可以展开元素
## 4.0.02023-09-17
- 调整 v-model 传参类型,为适应数值和字符串同时存在的树形数据
## 3.8.42023-09-17
- 支持动态加载节点
## 3.8.32023-09-17
- 文档更新
## 3.8.22023-09-17
- 新增路径模式吗,解决重名选项无法辨认问题
## 3.8.12023-08-06
- 修复加载选择器数据后,选择框没有数据问题
## 3.8.02023-07-29
- 修复父子联动状态下,数据回显父级半选状态不展示问题
- selectChange 返回数据调整
- 新增 contentHeight 、disabledList 属性
- 文档更新
## 3.7.92023-07-11
- 文档更新
## 3.7.82023-07-10
- 修复父元素未完全选中状态显示异常问题
## 3.7.72023-07-07
- 文档更新
## 3.7.62023-07-07
- 重写checkbox解决与colorui样式冲突问题
- 父级添加子级未全选状态,更直观了解选中数据情况
- 样式优化,提取文件内样式变量
- 代码体积优化
## 3.7.52023-05-15
- 修复visible在数据初始化时配置有时失效问题
## 3.7.42023-05-11
- 修复父元素下某个子元素设置隐藏时,父子联动状态异常问题
- 对性能以及代码体积做了进一步优化
## 3.7.32023-05-08
- 修复子元素展开懒加载功能被覆盖问题
- 优化数据初始化速度
## 3.7.22023-05-04
- uni.$emit 替换 bus事件
## 3.7.12023-04-10
- 解决小程序无法折叠或展开问题
## 3.7.02023-04-09
- 全选默认不开启,并且只在多选模式下生效
## 3.6.912023-04-09
- 文档更新
## 3.6.92023-04-09
- 代码整体优化
- 新增弹窗安全区配置选项
- 新增全选功能配置选项
- 新增搜索框清除按钮是否直接重置搜索结果选项
- 遗留问题:小程序端搜索后无法折叠或打开
## 3.6.82023-04-07
- 优化deepClone
## 3.6.72023-04-03
- 取消对微信小程序的支持
## 3.6.62023-04-03
- 修复listData刷新后不能正确渲染勾选状态问题
## 3.6.52023-03-30
- 修复小程序节点展开或折叠时指示箭头不变换状态
- 修复回显数据有时不展示问题
## 3.6.32023-03-27
- [紧急] 修复单选状态仍可以多选bug
## 3.6.22023-03-27
- 修复偶尔出现节点无法根据绑定数据改变选中状态问题
## 3.6.12023-03-24
- 修复引入问题
## 3.6.02023-03-24
- 重构去除冗余代码,功能不变
## 3.5.02023-03-23
- 修复没有子元素时点击报错问题
## 3.3.92023-03-22
- 解决搜索时 toLowerCase 报错问题
## 3.3.82023-03-22
- 懒加载优化
## 3.3.72023-03-20
- 样式更新
## 3.3.62023-03-20
- 修复子元素折叠或展开不触发问题
## 3.3.52023-03-15
- isArray 替换 instanceof
## 3.3.42023-03-11
- 修复搜索后无法展开或折叠问题
- 搜索后直接展开搜索项
## 3.3.32023-03-06
- 优化展示效果
- 添加引导线选项
## 3.3.22023-02-22
- 修复搜索内容包含隐藏数据的问题
- 优化搜索触发方式
## 3.3.12023-02-16
- 修复异常
## 3.2.92023-02-16
- 修复编译到小程序报错问题
## 3.2.82023-02-16
- 修复多个组件key重复问题
## 3.2.72023-02-16
- 全新底层架构设计
- 修改默认配色,更显成熟稳重
## 3.2.62023-02-15
- 修改文档
## 3.2.52023-02-15
- 文档修改
## 3.2.32023-02-15
- 修复小程序组件无法折叠问题
## 3.2.22023-02-10
- 文档更新
## 3.2.12023-02-09
- 增加节点展开和关闭全局配置
- 修复搜索后数据无法展开和关闭问题
- 修复搜索后不能自动滚动到顶部问题
## 3.2.02023-01-16
- 修复事件冲突
## 3.1.52023-01-13
- 样式调整
## 3.1.42023-01-13
- 样式修复
## 3.1.32023-01-13
- 文档更新
## 3.1.22023-01-13
- 修复一些已知问题
## 3.1.12023-01-12
- 解决微信小程序bus事件冲突问题
## 3.1.02023-01-11
- 支持微信小程序
## 3.0.32023-01-10
- 更新文档
## 3.0.22023-01-09
- 修复了一些已知问题
## 3.0.12023-01-09
- 修复了一些已知问题
## 3.0.02023-01-09
- 全面支持懒加载,大量数据也能快速打开
## 2.0.012022-12-29
- 文档修改
## 2.0.02022-12-28
- 添加搜索功能
- 完善文档
## 1.9.22022-12-14
- 修复了一些已知问题
## 1.9.12022-12-14
- 修复了一些已知问题
## 1.9.02022-12-14
- 增加选项禁用、隐藏功能
- 文档添加示例
## 1.8.42022-12-13
- 文档修复
## 1.8.32022-12-09
- 样式调整
## 1.8.22022-12-07
- 修复弹窗不同设备显示高度问题
## 1.8.12022-12-06
- 修复了一些已知问题
## 1.8.02022-12-05
- 修复了一些已知问题
- 图标替换图片
## 1.7.02022-12-05
- 增加 disabled 属性,适用于查看页面
- 修复了一些已知问题
## 1.6.02022-12-05
- 修复了一些已知问题
## 1.5.02022-12-05
- 根据绑定数据类型自动判断返回值类型
- 完善文档
## 1.4.02022-12-02
- 修复了一些已知问题
## 1.3.12022-12-02
- 修复了一些已知问题
## 1.3.02022-12-02
- 修复了一些已知问题
## 1.2.012022-12-02
- 文档更新
## 1.2.02022-12-02
- 添加一键清除功能
- 添加多选、单选功能
## 1.1.02022-12-02
- 支持数据回显、已选数据移除
- 对标 uni-easyinput 默认样式
- 支持更多属性和事件
## 1.0.32022-12-01
- 修改信息
## 1.0.22022-12-01
- 更新文档
## 1.0.12022-12-01
- 更新预览截图
## 1.0.02022-12-01
- 插件更新

View File

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

View File

@@ -0,0 +1,363 @@
<template>
<view
class="custom-tree-select-content"
:class="{
border:
border &&
node[dataChildren] &&
node[dataChildren].length &&
node.showChildren
}"
:style="{ marginLeft: `${level ? 14 : 0}px` }"
>
<view v-if="node.visible" class="custom-tree-select-item">
<view class="item-content">
<view class="left">
<view
v-if="node[dataChildren] && node[dataChildren].length"
:class="['right-icon', { active: node.showChildren }]"
@click.stop="nameClick(node)"
>
<uni-icons type="forward" size="14" color="#333"></uni-icons>
</view>
<view v-else class="smallcircle-filled">
<uni-icons
class="smallcircle-filled-icon"
type="smallcircle-filled"
size="10"
color="#333"
></uni-icons>
</view>
<view
v-if="loadingArr.includes(node[dataValue])"
class="loading-icon-box"
>
<uni-icons
class="loading-icon"
type="spinner-cycle"
size="14"
color="#333"
></uni-icons>
</view>
<view
class="name"
:style="node.disabled ? 'color: #999' : ''"
@click.stop="nameClick(node)"
>
<text>{{ node[dataLabel] }}</text>
</view>
</view>
<view
v-if="
choseParent ||
(!choseParent && !node[dataChildren]) ||
(!choseParent && node[dataChildren] && !node[dataChildren].length)
"
:class="['check-box', { disabled: node.disabled }]"
@click.stop="nodeClick(node)"
>
<view
v-if="!node.checked && node.partChecked && linkage"
class="part-checked"
></view>
<uni-icons
v-if="node.checked"
type="checkmarkempty"
size="18"
:color="node.disabled ? '#333' : '#007aff'"
></uni-icons>
</view>
</view>
</view>
<view
v-if="
node.showChildren && node[dataChildren] && node[dataChildren].length
"
>
<data-select-item
v-for="item in listData"
:key="item[dataValue]"
:node="item"
:dataLabel="dataLabel"
:dataValue="dataValue"
:dataChildren="dataChildren"
:choseParent="choseParent"
:border="border"
:linkage="linkage"
:level="level + 1"
:load="load"
:lazyLoadChildren="lazyLoadChildren"
></data-select-item>
</view>
</view>
</template>
<script>
import dataSelectItem from './data-select-item.vue'
import { paging } from './utils'
export default {
name: 'data-select-item',
components: {
'data-select-item': dataSelectItem
},
props: {
node: {
type: Object,
default: () => ({})
},
choseParent: {
type: Boolean,
default: true
},
dataLabel: {
type: String,
default: 'name'
},
dataValue: {
type: String,
default: 'value'
},
dataChildren: {
type: String,
default: 'children'
},
border: {
type: Boolean,
default: false
},
linkage: {
type: Boolean,
default: false
},
level: {
type: Number,
default: 0
},
load: {
type: Function,
default: function () {}
},
lazyLoadChildren: {
type: Boolean,
default: false
}
},
data() {
return {
listData: [],
clearTimerList: [],
loadingArr: []
}
},
computed: {
watchData() {
const { node, dataChildren } = this
return {
node,
dataChildren
}
}
},
watch: {
watchData: {
immediate: true,
handler(newVal) {
const { node, dataChildren } = newVal
if (
node.showChildren &&
node[dataChildren] &&
node[dataChildren].length
) {
this.resetClearTimerList()
this.renderTree(node[dataChildren])
}
}
}
},
methods: {
// 懒加载
renderTree(arr) {
const pagingArr = paging(arr)
this.listData.splice(0, this.listData.length, ...(pagingArr?.[0] || []))
this.lazyRenderList(pagingArr, 1)
},
// 懒加载具体逻辑
lazyRenderList(arr, startIndex) {
for (let i = startIndex; i < arr.length; i++) {
let timer = null
timer = setTimeout(() => {
this.listData.push(...arr[i])
}, i * 500)
this.clearTimerList.push(() => clearTimeout(timer))
}
},
// 中断懒加载
resetClearTimerList() {
const list = [...this.clearTimerList]
this.clearTimerList.splice(0, this.clearTimerList.length)
list.forEach((item) => item())
},
async nameClick(node) {
if (!node[this.dataChildren]?.length && this.lazyLoadChildren) {
this.loadingArr.push(node[this.dataValue])
try {
const res = await this.load(node)
if (Array.isArray(res)) {
uni.$emit('custom-tree-select-load', {
source: node,
target: res
})
}
} finally {
this.loadingArr = []
}
} else {
if (
!node.showChildren &&
node[this.dataChildren] &&
node[this.dataChildren].length
) {
// 打开
this.renderTree(node[this.dataChildren])
} else {
// 关闭
this.resetClearTimerList()
this.listData.splice(0, this.listData.length)
}
uni.$emit('custom-tree-select-name-click', node)
}
},
nodeClick(node) {
if (!node.disabled) {
uni.$emit('custom-tree-select-node-click', node)
}
}
},
options: {
styleIsolation: 'shared'
}
}
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
$primary-color: #007aff;
$col-sm: 4px;
$col-base: 8px;
$col-lg: 12px;
$row-sm: 5px;
$row-base: 10px;
$row-lg: 15px;
$radius-sm: 3px;
$radius-base: 6px;
$border-color: #c8c7cc;
.custom-tree-select-content {
&.border {
border-left: 1px solid $border-color;
}
/deep/ .uni-checkbox-input {
margin: 0 !important;
}
.item-content {
margin: 0 0 $col-lg;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 3px;
background-color: #fff;
transform: translateX(-2px);
z-index: 1;
}
.left {
flex: 1;
display: flex;
align-items: center;
.right-icon {
transition: 0.15s ease;
&.active {
transform: rotate(90deg);
}
}
.smallcircle-filled {
width: 14px;
height: 13.6px;
display: flex;
align-items: center;
.smallcircle-filled-icon {
transform-origin: center;
transform: scale(0.55);
}
}
.loading-icon-box {
margin-right: $row-sm;
width: 14px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.loading-icon {
transform-origin: center;
animation: rotating infinite 0.2s ease;
}
}
.name {
flex: 1;
}
}
}
}
.check-box {
width: 23.6px;
height: 23.6px;
border: 1px solid $border-color;
border-radius: $radius-sm;
display: flex;
justify-content: center;
align-items: center;
&.disabled {
background-color: rgb(225, 225, 225);
}
.part-checked {
width: 60%;
height: 2px;
background-color: $primary-color;
}
}
@keyframes rotating {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,17 @@
export function isString(data) {
return typeof data === 'string'
}
// 分页
export function paging(data, PAGENUM = 50) {
if (!Array.isArray(data) || !data.length) return data
const pages = []
data.forEach((item, index) => {
const i = Math.floor(index / PAGENUM)
if (!pages[i]) {
pages[i] = []
}
pages[i].push(item)
})
return pages
}

View File

@@ -0,0 +1,83 @@
{
"id": "custom-tree-select",
"displayName": "custom-tree-select树形选择器支持v-model",
"version": "4.1.0",
"description": "树形选择器基于uni-uiv-model绑定数据父级可选、数据回显、移除选项",
"keywords": [
"custom",
"tree-select",
"选择器",
"树形选择器"
],
"repository": "https://github.com/qzlthxp/custom-tree-select",
"engines": {
},
"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"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "u"
},
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,228 @@
# custom-tree-select 使用指南
找工作中…,大佬们有内推加群联系我
**提示:** 使用该插件前确保你已经导入 `uni-popup` `uni-icons` `uni-easyinput` 插件。
当前插件主要作为表单中选择器来使用,如果只是作为弹窗会出现数据状态无法重置,此时推荐使用 [`custom-tree-popup`](https://ext.dcloud.net.cn/plugin?name=custom-tree-popup) 组件。
**如果在微信小程序中使用,在 `main.js` 文件中添加以下代码,2023/05/04 之前安装插件或者本地插件版本 `<=3.7.1` 需要添加**
```js
// #ifdef MP-WEIXIN
Vue.prototype.$bus = new Vue()
// #endif
```
**有问题可以加 QQ 群297080738**
## 优势
💪:基于 `uni-popup``uni-icons``uni-easyinput` 插件进行开发,默认样式与 `uni-easyinput` 样式对标。
⚡:全面支持懒加载应对大量数据。
🚀v-model 绑定数据、数据回显、移除选项。
⚙ :提供更多配置项。
📦:开箱即用。
## Props
| 属性名 | 类型 | 默认值 | 说明 | 版本要求 |
| :-------------------: | :---------: | :-----------------------------: | :----------------------------------------------------------: | --------- |
| canSelectAll | Boolean | false | 开启一键全选功能 | |
| contentHeight | String | '500px' \| 视窗高度的 75%的像素 | 弹出内选择器容器高度,为解决搜索时搜索框被顶出屏幕,监听输入框 focus 事件 动态修改弹窗内选择器容器高度 | |
| clearResetSearch | Boolean | false | 设置为 `true` 并且搜索之后,点击输入框清除按钮,会清空搜索内容并且会直接重置整个弹窗内树形选择器内容,默认情况下只有清除之后再次进行查询才会重置选择器 | |
| animation | Boolean | ture | 是否开启弹窗动画 | |
| is-mask-click | Boolean | true | 点击遮罩关闭弹窗 | |
| mask-background-color | String | rgba(0,0,0,0.4) | 蒙版颜色,建议使用 rgba 颜色值 | |
| background-color | String | none | 主窗口背景色 | |
| safe-area | Boolean | true | 是否适配底部安全区 | |
| **choseParent** | **Boolean** | **true** | **父节点是否可选** | |
| **linkage** | **Boolean** | **false** | **父子节点是否联动** | |
| placeholder | String | 请选择 | 空状态信息提示、弹窗标题 | |
| confirmText | String | 完成 | 确定按钮文字 | |
| confirmTextColor | String | #007aff | 确定按钮文字颜色 | |
| listData | Array | - | 展示的数据 | |
| **dataLabel** | **String** | **name** | **listData 中对应数据的 label** | |
| **dataValue** | **String** | **id** | **listData 中对应数据的 value** | |
| **dataChildren** | **String** | **children** | **listData 中对应数据的 children** | |
| clearable | Boolean | false | 是否显示清除按钮,点击清除所有已选项 | |
| **mutiple** | **Boolean** | **false** | **是否可以多选** | |
| **disabled** | **Boolean** | **false** | **是否允许修改** | |
| disabledList | Array | [] | 设置某些选项为不可选,数组元素为数据 dataValue 对应的值,也可以操作数据 disabled 属性实现 | |
| search | Boolean | false | 是否可以搜索(常用于数据较多的情况) | |
| showChildren | Boolean | false | 默认展开(数据内部 showChildren 属性优先级更高,可以设置全局收起,单独展开某一条数据) | |
| border | Boolean | false | 显示引导线 | |
| pathMode | Boolean | false | 路径模式,开启后选择框内展示选项所在层级的完整信息如:`城市/街道/小区` | >= 3.8.2 |
| pathHyphen | String | / | 路径模式下连字符 | >= 3.8.2 |
| lazyLoadChildren | Boolean | false | 是否允许动态加载获取子节点 | >= 3.8.4 |
| load | Function | (node) => Promise<void> | 动态加载函数,具体用法见下方示例 | >= 3.8.4 |
| **v-model/value** | **Array** | **[ ]** | **已选择的值,通过 v-model 进行绑定例如v-model="formData.selectedList"** (根据你绑定数据的类型自动返回相同类型的数据String 类型通过 `,` 进行分隔。>=4.0.0 版本不在支持字符串类型传参,修改为数组类型) | >= 4.0.0 |
## listdata 特有属性
| 名称 | 类型 | 默认值 | 说明 |
| ------------ | ------- | ------ | ------------------ |
| disabled | Boolean | false | 选项是否可选 |
| visible | Boolean | true | 选项是否展示 |
| showChildren | Boolean | true | 选项是否展示子节点 |
## Events
| 事件名称 | 说明 | 返回值 |
| ------------ | ------------------------ | -------------------------------------- |
| change | 弹窗组件状态发生变化触发 | e={show: true false, type:当前模式} |
| maskClick | 点击遮罩层触发 | |
| input | 选中数据或取消选中时触发 | 以数组形式返回已选择数据 |
| selectChange | 选中数据或取消选中时触发 | 以数组形式返回已选择数据完整信息 |
| removeSelect | 选择框移除选项时触发 | 返回对应数据的完整信息 |
## 基础使用示例
```vue
<template>
<!--/pages/index/index-->
<custom-tree-select :listData="listData" v-model="formData.selectedArr" />
</template>
<script>
export default {
data() {
return {
formData: {
selectedArr: [],
},
listData: [
{
id: 1,
name: '城市1',
children: [
{
id: 3,
name: '街道1',
children: [
{
id: 4,
name: '小区1'
}
]
}
]
},
{
id: 2,
name: '城市2',
children: [
{
id: 6,
name: '街道2'
}
]
}
]
}
}
}
</script>
```
## 禁用某些选项,或隐藏某些选项
```vue
<template>
<!--/pages/index/index-->
<custom-tree-select
mutiple
linkage
clearable
search
dataLabel="text"
dataValue="value"
:listData="listData"
:disabledList="[6]"
v-model="formData.selected"
></custom-tree-select>
</template>
<script>
export default {
data() {
return {
formData: {
selected: []
},
listData: [
{
value: 1,
text: '城市1',
children: [
{
value: 3,
text: '街道1',
disabled: true
}
]
},
{
value: 2,
text: '城市2',
children: [
{
value: 6,
text: '街道2'
}
]
},
{
value: 7,
text: '城市3',
visible: false
}
]
}
}
}
</script>
```
## 动态加载节点
```vue
<custom-tree-select
:listData="listData"
:load="load"
lazyLoadChildren
pathMode
v-model="selectedArr"
/>
<script>
export default {
data() {
return {
selectedArr: []
}
},
methods: {
load(node) {
return new Promise((resolve, reject) => {
if (node) {
setTimeout(() => {
resolve([
{
value: '128047129041',
text: '测试异步加载'
}
])
}, 2000)
}
})
}
}
}
</script>
```

View File

@@ -0,0 +1,84 @@
## 1.9.12024-04-02
- 修复 uni-popup-dialog vue3下使用value无法进行绑定的bug(双向绑定兼容旧写法)
## 1.9.02024-03-28
- 修复 uni-popup-dialog 双向绑定时初始化逻辑修正
## 1.8.92024-03-20
- 修复 uni-popup-dialog 数据输入时修正为双向绑定
## 1.8.82024-02-20
- 修复 uni-popup 在微信小程序下出现文字向上闪动的bug
## 1.8.72024-02-02
- 新增 uni-popup-dialog 新增属性focusinput模式下是否自动自动聚焦
## 1.8.62024-01-30
- 新增 uni-popup-dialog 新增属性maxLength:限制输入框字数
## 1.8.52024-01-26
- 新增 uni-popup-dialog 新增属性showClose:控制关闭按钮的显示
## 1.8.42023-11-15
- 新增 uni-popup 支持uni-app-x 注意暂时仅支持 `maskClick` `@open` `@close`
## 1.8.32023-04-17
- 修复 uni-popup 重复打开时的 bug
## 1.8.22023-02-02
- uni-popup-dialog 组件新增 inputType 属性
## 1.8.12022-12-01
- 修复 nvue 下 v-show 报错
## 1.8.02022-11-29
- 优化 主题样式
## 1.7.92022-04-02
- 修复 弹出层内部无法滚动的bug
## 1.7.82022-03-28
- 修复 小程序中高度错误的bug
## 1.7.72022-03-17
- 修复 快速调用open出现问题的Bug
## 1.7.62022-02-14
- 修复 safeArea 属性不能设置为false的bug
## 1.7.52022-01-19
- 修复 isMaskClick 失效的bug
## 1.7.42022-01-19
- 新增 cancelText \ confirmText 属性 ,可自定义文本
- 新增 maskBackgroundColor 属性 ,可以修改蒙版颜色
- 优化 maskClick属性 更新为 isMaskClick ,解决微信小程序警告的问题
## 1.7.32022-01-13
- 修复 设置 safeArea 属性不生效的bug
## 1.7.22021-11-26
- 优化 组件示例
## 1.7.12021-11-26
- 修复 vuedoc 文字错误
## 1.7.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-popup](https://uniapp.dcloud.io/component/uniui/uni-popup)
## 1.6.22021-08-24
- 新增 支持国际化
## 1.6.12021-07-30
- 优化 vue3下事件警告的问题
## 1.6.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.5.02021-06-23
- 新增 mask-click 遮罩层点击事件
## 1.4.52021-06-22
- 修复 nvue 平台中间弹出后点击内容再点击遮罩无法关闭的Bug
## 1.4.42021-06-18
- 修复 H5平台中间弹出后点击内容再点击遮罩无法关闭的Bug
## 1.4.32021-06-08
- 修复 错误的 watch 字段
- 修复 safeArea 属性不生效的问题
- 修复 点击内容再点击遮罩无法关闭的Bug
## 1.4.22021-05-12
- 新增 组件示例地址
## 1.4.12021-04-29
- 修复 组件内放置 input 、textarea 组件,无法聚焦的问题
## 1.4.0 2021-04-29
- 新增 type 属性的 left\right 值,支持左右弹出
- 新增 open(String:type) 方法参数 ,可以省略 type 属性 ,直接传入类型打开指定弹窗
- 新增 backgroundColor 属性,可定义主窗口背景色,默认不显示背景色
- 新增 safeArea 属性,是否适配底部安全区
- 修复 App\h5\微信小程序底部安全区占位不对的Bug
- 修复 App 端弹出等待的Bug
- 优化 提升低配设备性能,优化动画卡顿问题
- 优化 更简单的组件自定义方式
## 1.2.92021-02-05
- 优化 组件引用关系通过uni_modules引用组件
## 1.2.82021-02-05
- 调整为uni_modules目录规范
## 1.2.72021-02-05
- 调整为uni_modules目录规范
- 新增 支持 PC 端
- 新增 uni-popup-message 、uni-popup-dialog扩展组件支持 PC 端

View File

@@ -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

View File

@@ -0,0 +1,316 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{titleText}}</text>
</view>
<view v-if="mode === 'base'" class="uni-dialog-content">
<slot>
<text class="uni-dialog-content-text">{{content}}</text>
</slot>
</view>
<view v-else class="uni-dialog-content">
<slot>
<input class="uni-dialog-input" :maxlength="maxlength" v-model="val" :type="inputType"
:placeholder="placeholderText" :focus="focus">
</slot>
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" v-if="showClose" @click="closeDialog">
<text class="uni-dialog-button-text">{{closeText}}</text>
</view>
<view class="uni-dialog-button" :class="showClose?'uni-border-left':''" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">{{okText}}</text>
</view>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const {
t
} = initVueI18n(messages)
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {Boolean} focus input模式下是否自动聚焦默认为true
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式、
* @value base 基础对话框
* @value input 可输入对话框
* @showClose {Boolean} 是否显示关闭按钮
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @property {Number} maxlength 输入
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: "uniPopupDialog",
mixins: [popup],
emits: ['confirm', 'close', 'update:modelValue', 'input'],
props: {
inputType: {
type: String,
default: 'text'
},
showClose: {
type: Boolean,
default: true
},
// #ifdef VUE2
value: {
type: [String, Number],
default: ''
},
// #endif
// #ifdef VUE3
modelValue: {
type: [Number, String],
default: ''
},
// #endif
placeholder: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'error'
},
mode: {
type: String,
default: 'base'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
},
cancelText: {
type: String,
default: ''
},
confirmText: {
type: String,
default: ''
},
maxlength: {
type: Number,
default: -1,
},
focus: {
type: Boolean,
default: true,
}
},
data() {
return {
dialogType: 'error',
val: ""
}
},
computed: {
okText() {
return this.confirmText || t("uni-popup.ok")
},
closeText() {
return this.cancelText || t("uni-popup.cancel")
},
placeholderText() {
return this.placeholder || t("uni-popup.placeholder")
},
titleText() {
return this.title || t("uni-popup.title")
}
},
watch: {
type(val) {
this.dialogType = val
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info'
}
},
value(val) {
if (this.maxlength != -1 && this.mode === 'input') {
this.val = val.slice(0, this.maxlength);
} else {
this.val = val
}
},
val(val) {
// #ifdef VUE2
// TODO 兼容 vue2
this.$emit('input', val);
// #endif
// #ifdef VUE3
// TODO 兼容 vue3
this.$emit('update:modelValue', val);
// #endif
}
},
created() {
// 对话框遮罩不可点击
this.popup.disableMask()
// this.popup.closeMask()
if (this.mode === 'input') {
this.dialogType = 'info'
this.val = this.value;
// #ifdef VUE3
this.val = this.modelValue;
// #endif
} else {
this.dialogType = this.type
}
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
if (this.mode === 'input') {
this.$emit('confirm', this.val)
} else {
this.$emit('confirm')
}
if (this.beforeClose) return
this.popup.close()
},
/**
* 点击取消按钮
*/
closeDialog() {
this.$emit('close')
if (this.beforeClose) return
this.popup.close()
},
close() {
this.popup.close()
}
}
}
</script>
<style lang="scss">
.uni-popup-dialog {
width: 300px;
border-radius: 11px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 25px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 20px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6C6C6C;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 16px;
color: #333;
}
.uni-button-color {
color: #007aff;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
border: 1px #eee solid;
height: 40px;
padding: 0 10px;
border-radius: 5px;
color: #555;
}
.uni-popup__success {
color: #4cd964;
}
.uni-popup__warn {
color: #f0ad4e;
}
.uni-popup__error {
color: #dd524d;
}
.uni-popup__info {
color: #909399;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<view class="uni-popup-message">
<view class="uni-popup-message__box fixforpc-width" :class="'uni-popup__'+type">
<slot>
<text class="uni-popup-message-text" :class="'uni-popup__'+type+'-text'">{{message}}</text>
</slot>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
/**
* PopUp 弹出层-消息提示
* @description 弹出层-消息提示
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} message 消息提示文字
* @property {String} duration 显示时间,设置为 0 则不会自动关闭
*/
export default {
name: 'uniPopupMessage',
mixins:[popup],
props: {
/**
* 主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'success'
},
/**
* 消息文字
*/
message: {
type: String,
default: ''
},
/**
* 显示时间,设置为 0 则不会自动关闭
*/
duration: {
type: Number,
default: 3000
},
maskShow:{
type:Boolean,
default:false
}
},
data() {
return {}
},
created() {
this.popup.maskShow = this.maskShow
this.popup.messageChild = this
},
methods: {
timerClose(){
if(this.duration === 0) return
clearTimeout(this.timer)
this.timer = setTimeout(()=>{
this.popup.close()
},this.duration)
}
}
}
</script>
<style lang="scss" >
.uni-popup-message {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
}
.uni-popup-message__box {
background-color: #e1f3d8;
padding: 10px 15px;
border-color: #eee;
border-style: solid;
border-width: 1px;
flex: 1;
}
@media screen and (min-width: 500px) {
.fixforpc-width {
margin-top: 20px;
border-radius: 4px;
flex: none;
min-width: 380px;
/* #ifndef APP-NVUE */
max-width: 50%;
/* #endif */
/* #ifdef APP-NVUE */
max-width: 500px;
/* #endif */
}
}
.uni-popup-message-text {
font-size: 14px;
padding: 0;
}
.uni-popup__success {
background-color: #e1f3d8;
}
.uni-popup__success-text {
color: #67C23A;
}
.uni-popup__warn {
background-color: #faecd8;
}
.uni-popup__warn-text {
color: #E6A23C;
}
.uni-popup__error {
background-color: #fde2e2;
}
.uni-popup__error-text {
color: #F56C6C;
}
.uni-popup__info {
background-color: #F2F6FC;
}
.uni-popup__info-text {
color: #909399;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<view class="uni-popup-share">
<view class="uni-share-title"><text class="uni-share-title-text">{{shareTitleText}}</text></view>
<view class="uni-share-content">
<view class="uni-share-content-box">
<view class="uni-share-content-item" v-for="(item,index) in bottomData" :key="index" @click.stop="select(item,index)">
<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
<text class="uni-share-text">{{item.text}}</text>
</view>
</view>
</view>
<view class="uni-share-button-box">
<button class="uni-share-button" @click="close">{{cancelText}}</button>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
export default {
name: 'UniPopupShare',
mixins:[popup],
emits:['select'],
props: {
title: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
}
},
data() {
return {
bottomData: [{
text: '微信',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/c2b17470-50be-11eb-b680-7980c8a877b8.png',
name: 'wx'
},
{
text: '支付宝',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/d684ae40-50be-11eb-8ff1-d5dcf8779628.png',
name: 'ali'
},
{
text: 'QQ',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/e7a79520-50be-11eb-b997-9918a5dda011.png',
name: 'qq'
},
{
text: '新浪',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/0dacdbe0-50bf-11eb-8ff1-d5dcf8779628.png',
name: 'sina'
},
// {
// text: '百度',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/1ec6e920-50bf-11eb-8a36-ebb87efcf8c0.png',
// name: 'copy'
// },
// {
// text: '其他',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/2e0fdfe0-50bf-11eb-b997-9918a5dda011.png',
// name: 'more'
// }
]
}
},
created() {},
computed: {
cancelText() {
return t("uni-popup.cancel")
},
shareTitleText() {
return this.title || t("uni-popup.shareTitle")
}
},
methods: {
/**
* 选择内容
*/
select(item, index) {
this.$emit('select', {
item,
index
})
this.close()
},
/**
* 关闭窗口
*/
close() {
if(this.beforeClose) return
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-share {
background-color: #fff;
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}
.uni-share-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
}
.uni-share-title-text {
font-size: 14px;
color: #666;
}
.uni-share-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 10px;
}
.uni-share-content-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: wrap;
width: 360px;
}
.uni-share-content-item {
width: 90px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
padding: 10px 0;
align-items: center;
}
.uni-share-content-item:active {
background-color: #f5f5f5;
}
.uni-share-image {
width: 30px;
height: 30px;
}
.uni-share-text {
margin-top: 10px;
font-size: 14px;
color: #3B4144;
}
.uni-share-button-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 10px 15px;
}
.uni-share-button {
flex: 1;
border-radius: 50px;
color: #666;
font-size: 16px;
}
.uni-share-button::after {
border-radius: 50px;
}
</style>

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "cancel",
"uni-popup.ok": "ok",
"uni-popup.placeholder": "pleace enter",
"uni-popup.title": "Hint",
"uni-popup.shareTitle": "Share to"
}

View File

@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "确定",
"uni-popup.placeholder": "请输入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "確定",
"uni-popup.placeholder": "請輸入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@@ -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

View File

@@ -0,0 +1,26 @@
export default {
data() {
return {
}
},
created(){
this.popup = this.getParent()
},
methods:{
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
}
}

View File

@@ -0,0 +1,90 @@
<template>
<view class="popup-root" v-if="isOpen" v-show="isShow" @click="clickMask">
<view @click.stop>
<slot></slot>
</view>
</view>
</template>
<script>
type CloseCallBack = ()=> void;
let closeCallBack:CloseCallBack = () :void => {};
export default {
emits:["close","clickMask"],
data() {
return {
isShow:false,
isOpen:false
}
},
props: {
maskClick: {
type: Boolean,
default: true
},
},
watch: {
// 设置show = true 时,如果没有 open 需要设置为 open
isShow:{
handler(isShow) {
// console.log("isShow",isShow)
if(isShow && this.isOpen == false){
this.isOpen = true
}
},
immediate:true
},
// 设置isOpen = true 时,如果没有 isShow 需要设置为 isShow
isOpen:{
handler(isOpen) {
// console.log("isOpen",isOpen)
if(isOpen && this.isShow == false){
this.isShow = true
}
},
immediate:true
}
},
methods:{
open(){
// ...funs : CloseCallBack[]
// if(funs.length > 0){
// closeCallBack = funs[0]
// }
this.isOpen = true;
},
clickMask(){
if(this.maskClick == true){
this.$emit('clickMask')
this.close()
}
},
close(): void{
this.isOpen = false;
this.$emit('close')
closeCallBack()
},
hiden(){
this.isShow = false
},
show(){
this.isShow = true
}
}
}
</script>
<style>
.popup-root {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 100%;
flex: 1;
background-color: rgba(0, 0, 0, 0.3);
justify-content: center;
align-items: center;
z-index: 99;
}
</style>

View File

@@ -0,0 +1,503 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']">
<view @touchstart="touchstart">
<uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass"
:duration="duration" :show="showTrans" @click="onTap" />
<uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration"
:show="showTrans" @click="onTap">
<view class="uni-popup__wrapper" :style="getStyles" :class="[popupstyle]" @click="clear">
<slot />
</view>
</uni-transition>
</view>
<!-- #ifdef H5 -->
<keypress v-if="maskShow" @esc="onTap" />
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [true|false] 是否开启动画
* @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
* @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {String} maskBackgroundColor 蒙版颜色
* @property {String} borderRadius 设置圆角(左上、右上、右下和左下) 示例:"10px 10px 10px 10px"
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
export default {
name: 'uniPopup',
components: {
// #ifdef H5
keypress
// #endif
},
emits: ['change', 'maskClick'],
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
isMaskClick: {
type: Boolean,
default: null
},
// TODO 2 个版本后废弃属性 ,使用 isMaskClick
maskClick: {
type: Boolean,
default: null
},
backgroundColor: {
type: String,
default: 'none'
},
safeArea: {
type: Boolean,
default: true
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)'
},
borderRadius:{
type: String,
}
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true
},
isDesktop: {
handler: function(newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
isMaskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
// H5 下禁止底部滚动
showPopup(show) {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
// #endif
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom'
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
transClass: {
backgroundColor: 'transparent',
borderRadius: this.borderRadius || "0",
position: 'fixed',
left: 0,
right: 0
},
maskShow: true,
mkclick: true,
popupstyle: 'top'
}
},
computed: {
getStyles() {
let res = { backgroundColor: this.bg };
if (this.borderRadius || "0") {
res = Object.assign(res, { borderRadius: this.borderRadius })
}
return res;
},
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
}
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeArea,
screenHeight,
safeAreaInsets
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + (windowTop || 0)
// TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
if (safeArea && this.safeArea) {
// #ifdef MP-WEIXIN
this.safeAreaInsets = screenHeight - safeArea.bottom
// #endif
// #ifndef MP-WEIXIN
this.safeAreaInsets = safeAreaInsets.bottom
// #endif
} else {
this.safeAreaInsets = 0
}
}
fixSize()
// #ifdef H5
// window.addEventListener('resize', fixSize)
// this.$once('hook:beforeDestroy', () => {
// window.removeEventListener('resize', fixSize)
// })
// #endif
},
// #ifndef VUE3
// TODO vue2
destroyed() {
this.setH5Visible()
},
// #endif
// #ifdef VUE3
// TODO vue3
unmounted() {
this.setH5Visible()
},
// #endif
activated() {
this.setH5Visible(!this.showPopup);
},
deactivated() {
this.setH5Visible(true);
},
created() {
// this.mkclick = this.isMaskClick || this.maskClick
if (this.isMaskClick === null && this.maskClick === null) {
this.mkclick = true
} else {
this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
}
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO 处理 message 组件生命周期异常的问题
this.messageChild = null
// TODO 解决头条冒泡的问题
this.clearPropagation = false
this.maskClass.backgroundColor = this.maskBackgroundColor
},
methods: {
setH5Visible(visible = true) {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = visible ? "visible" : "hidden";
// #endif
},
/**
* 公用方法,不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法,遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue 取消冒泡
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction) {
// fix by mehaotian 处理快速打开关闭的情况
if (this.showPopup) {
return
}
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
if (!(direction && innerType.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// // 自定义关闭事件
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
},
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian 兼容 nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg,
borderRadius:this.borderRadius || "0"
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: this.safeAreaInsets + 'px',
backgroundColor: this.bg,
borderRadius:this.borderRadius || "0",
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
//微信小程序下,组合动画会出现文字向上闪动问题,再此做特殊处理
// #ifdef MP-WEIXIN
this.ani = ['fade']
// #endif
// #ifndef MP-WEIXIN
this.ani = ['zoom-out', 'fade']
// #endif
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center',
borderRadius:this.borderRadius || "0"
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
borderRadius:this.borderRadius || "0",
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
borderRadius:this.borderRadius || "0",
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
}
}
}
</script>
<style lang="scss">
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
&.top,
&.left,
&.right {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@@ -0,0 +1,88 @@
{
"id": "uni-popup",
"displayName": "uni-popup 弹出层",
"version": "1.9.1",
"description": " Popup 组件,提供常用的弹层",
"keywords": [
"uni-ui",
"弹出层",
"弹窗",
"popup",
"弹框"
],
"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-scss",
"uni-transition"
],
"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"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
## Popup 弹出层
> **组件名uni-popup**
> 代码块: `uPopup`
> 关联组件:`uni-transition`
弹出层组件,在应用中弹出一个消息提示窗口、提示框等
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-popup)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,24 @@
## 1.3.32024-04-23
- 修复 当元素会受变量影响自动隐藏的bug
## 1.3.22023-05-04
- 修复 NVUE 平台报错的问题
## 1.3.12021-11-23
- 修复 init 方法初始化问题
## 1.3.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-transition](https://uniapp.dcloud.io/component/uniui/uni-transition)
## 1.2.12021-09-27
- 修复 init 方法不生效的 Bug
## 1.2.02021-07-30
- 组件兼容 vue3如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.12021-05-12
- 新增 示例地址
- 修复 示例项目缺少组件的 Bug
## 1.1.02021-04-22
- 新增 通过方法自定义动画
- 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式
- 优化 动画触发逻辑,使动画更流畅
- 优化 支持单独的动画类型
- 优化 文档示例
## 1.0.22021-02-05
- 调整为 uni_modules 目录规范

View File

@@ -0,0 +1,131 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
// 在iOS10+QQ小程序平台下传给原生的对象一定是个普通对象而不是Proxy对象否则会报parameter should be Object instead of ProxyObject的错误
this.animation = uni.createAnimation({
...options
})
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@@ -0,0 +1,286 @@
<template>
<!-- #ifndef APP-NVUE -->
<view v-show="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
},
onceRender:{
type:Boolean,
default:false
},
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// 避免上来就执行 close,导致动画错乱
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
// 生成样式数据
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
// 初始化动画条件
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
// 动画默认配置
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
// 开始过度动画
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
this.$nextTick(() => {
// TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
// 关闭过度动画
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
// 处理动画开始前的默认样式
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
// 处理内置组合动画
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 0 : 1,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
// 内置动画类型与实际动画对应字典
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 驼峰转中横线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@@ -0,0 +1,85 @@
{
"id": "uni-transition",
"displayName": "uni-transition 过渡动画",
"version": "1.3.3",
"description": "元素的简单过渡动画",
"keywords": [
"uni-ui",
"uniui",
"动画",
"过渡",
"过渡动画"
],
"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-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"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
## Transition 过渡动画
> **组件名uni-transition**
> 代码块: `uTransition`
元素过渡动画
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-transition)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839