diff --git a/components/animation-group/index.js b/components/animation-group/index.js
new file mode 100644
index 0000000..bbfdc01
--- /dev/null
+++ b/components/animation-group/index.js
@@ -0,0 +1,383 @@
+import baseComponent from '../helpers/baseComponent'
+import styleToCssString from '../helpers/styleToCssString'
+
+const ENTER = 'enter'
+const ENTERING = 'entering'
+const ENTERED = 'entered'
+const EXIT = 'exit'
+const EXITING = 'exiting'
+const EXITED = 'exited'
+const UNMOUNTED = 'unmounted'
+
+const TRANSITION = 'transition'
+const ANIMATION = 'animation'
+
+const TIMEOUT = 1000 / 60
+
+const defaultClassNames = {
+ enter: '', // 进入过渡的开始状态,在过渡过程完成之后移除
+ enterActive: '', // 进入过渡的结束状态,在过渡过程完成之后移除
+ enterDone: '', // 进入过渡的完成状态
+ exit: '', // 离开过渡的开始状态,在过渡过程完成之后移除
+ exitActive: '', // 离开过渡的结束状态,在过渡过程完成之后移除
+ exitDone: '', // 离开过渡的完成状态
+}
+
+baseComponent({
+ properties: {
+ // 触发组件进入或离开过渡的状态
+ in: {
+ type: Boolean,
+ value: false,
+ observer(newVal) {
+ if (this.data.isMounting) {
+ this.updated(newVal)
+ }
+ },
+ },
+ // 过渡的类名
+ classNames: {
+ type: null,
+ value: defaultClassNames,
+ },
+ // 过渡持续时间
+ duration: {
+ type: null,
+ value: null,
+ },
+ // 过渡动效的类型
+ type: {
+ type: String,
+ value: TRANSITION,
+ },
+ // 首次挂载时是否触发进入过渡
+ appear: {
+ type: Boolean,
+ value: false,
+ },
+ // 是否启用进入过渡
+ enter: {
+ type: Boolean,
+ value: true,
+ },
+ // 是否启用离开过渡
+ exit: {
+ type: Boolean,
+ value: true,
+ },
+ // 首次进入过渡时是否懒挂载组件
+ mountOnEnter: {
+ type: Boolean,
+ value: true,
+ },
+ // 离开过渡完成时是否卸载组件
+ unmountOnExit: {
+ type: Boolean,
+ value: true,
+ },
+ // 自定义类名
+ wrapCls: {
+ type: String,
+ value: '',
+ },
+ // 自定义样式
+ wrapStyle: {
+ type: [String, Object],
+ value: '',
+ observer(newVal) {
+ this.setData({
+ extStyle: styleToCssString(newVal),
+ })
+ },
+ },
+ disableScroll: {
+ type: Boolean,
+ value: false,
+ },
+ },
+ data: {
+ animateCss: '', // 动画样式
+ animateStatus: EXITED, // 动画状态,可选值 entering、entered、exiting、exited
+ isMounting: false, // 是否首次挂载
+ extStyle: '', // 组件样式
+ },
+ methods: {
+ /**
+ * 监听过渡或动画的回调函数
+ */
+ addEventListener() {
+ const { animateStatus } = this.data
+ const { enter, exit } = this.getTimeouts()
+
+ if (animateStatus === ENTERING && !enter && this.data.enter) {
+ this.performEntered()
+ }
+
+ if (animateStatus === EXITING && !exit && this.data.exit) {
+ this.performExited()
+ }
+ },
+ /**
+ * 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
+ */
+ onTransitionEnd() {
+ if (this.data.type === TRANSITION) {
+ this.addEventListener()
+ }
+ },
+ /**
+ * 会在一个 WXSS animation 动画完成时触发
+ */
+ onAnimationEnd() {
+ if (this.data.type === ANIMATION) {
+ this.addEventListener()
+ }
+ },
+ /**
+ * 更新组件状态
+ * @param {String} nextStatus 下一状态,ENTERING 或 EXITING
+ * @param {Boolean} mounting 是否首次挂载
+ */
+ updateStatus(nextStatus, mounting = false) {
+ if (nextStatus !== null) {
+ this.cancelNextCallback()
+ this.isAppearing = mounting
+
+ if (nextStatus === ENTERING) {
+ this.performEnter()
+ } else {
+ this.performExit()
+ }
+ }
+ },
+ /**
+ * 进入过渡
+ */
+ performEnter() {
+ const { className, activeClassName } = this.getClassNames(ENTER)
+ const { enter } = this.getTimeouts()
+ const enterParams = {
+ animateStatus: ENTER,
+ animateCss: className,
+ }
+ const enteringParams = {
+ animateStatus: ENTERING,
+ animateCss: `${className} ${activeClassName}`,
+ }
+
+ // 若已禁用进入过渡,则更新状态至 ENTERED
+ if (!this.isAppearing && !this.data.enter) {
+ return this.performEntered()
+ }
+
+ // 第一阶段:设置进入过渡的开始状态,并触发 ENTER 事件
+ // 第二阶段:延迟一帧后,设置进入过渡的结束状态,并触发 ENTERING 事件
+ // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发进入过渡完成 performEntered,否则等待触发 onTransitionEnd 或 onAnimationEnd
+ this.safeSetData(enterParams, () => {
+ this.triggerEvent('change', { animateStatus: ENTER })
+ this.triggerEvent(ENTER, { isAppearing: this.isAppearing })
+
+ // 由于有些时候不能正确的触发动画完成的回调,具体原因未知
+ // 所以采用延迟一帧的方式来确保可以触发回调
+ this.delayHandler(TIMEOUT, () => {
+ this.safeSetData(enteringParams, () => {
+ this.triggerEvent('change', { animateStatus: ENTERING })
+ this.triggerEvent(ENTERING, { isAppearing: this.isAppearing })
+
+ if (enter) {
+ this.delayHandler(enter, this.performEntered)
+ }
+ })
+ })
+ })
+ },
+ /**
+ * 进入过渡完成
+ */
+ performEntered() {
+ const { doneClassName } = this.getClassNames(ENTER)
+ const enteredParams = {
+ animateStatus: ENTERED,
+ animateCss: doneClassName,
+ }
+
+ // 第三阶段:设置进入过渡的完成状态,并触发 ENTERED 事件
+ this.safeSetData(enteredParams, () => {
+ this.triggerEvent('change', { animateStatus: ENTERED })
+ this.triggerEvent(ENTERED, { isAppearing: this.isAppearing })
+ })
+ },
+ /**
+ * 离开过渡
+ */
+ performExit() {
+ const { className, activeClassName } = this.getClassNames(EXIT)
+ const { exit } = this.getTimeouts()
+ const exitParams = {
+ animateStatus: EXIT,
+ animateCss: className,
+ }
+ const exitingParams = {
+ animateStatus: EXITING,
+ animateCss: `${className} ${activeClassName}`,
+ }
+
+ // 若已禁用离开过渡,则更新状态至 EXITED
+ if (!this.data.exit) {
+ return this.performExited()
+ }
+
+ // 第一阶段:设置离开过渡的开始状态,并触发 EXIT 事件
+ // 第二阶段:延迟一帧后,设置离开过渡的结束状态,并触发 EXITING 事件
+ // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发离开过渡完成 performExited,否则等待触发 onTransitionEnd 或 onAnimationEnd
+ this.safeSetData(exitParams, () => {
+ this.triggerEvent('change', { animateStatus: EXIT })
+ this.triggerEvent(EXIT)
+
+ this.delayHandler(TIMEOUT, () => {
+ this.safeSetData(exitingParams, () => {
+ this.triggerEvent('change', { animateStatus: EXITING })
+ this.triggerEvent(EXITING)
+
+ if (exit) {
+ this.delayHandler(exit, this.performExited)
+ }
+ })
+ })
+ })
+ },
+ /**
+ * 离开过渡完成
+ */
+ performExited() {
+ const { doneClassName } = this.getClassNames(EXIT)
+ const exitedParams = {
+ animateStatus: EXITED,
+ animateCss: doneClassName,
+ }
+
+ // 第三阶段:设置离开过渡的完成状态,并触发 EXITED 事件
+ this.safeSetData(exitedParams, () => {
+ this.triggerEvent('change', { animateStatus: EXITED })
+ this.triggerEvent(EXITED)
+
+ // 判断离开过渡完成时是否卸载组件
+ if (this.data.unmountOnExit) {
+ this.setData({ animateStatus: UNMOUNTED }, () => {
+ this.triggerEvent('change', { animateStatus: UNMOUNTED })
+ })
+ }
+ })
+ },
+ /**
+ * 获取指定状态下的类名
+ * @param {String} type 过渡类型,enter 或 exit
+ */
+ getClassNames(type) {
+ const { classNames } = this.data
+ const className = typeof classNames !== 'string' ? classNames[type] : `${classNames}-${type}`
+ const activeClassName = typeof classNames !== 'string' ? classNames[`${type}Active`] : `${classNames}-${type}-active`
+ const doneClassName = typeof classNames !== 'string' ? classNames[`${type}Done`] : `${classNames}-${type}-done`
+
+ return {
+ className,
+ activeClassName,
+ doneClassName,
+ }
+ },
+ /**
+ * 获取过渡持续时间
+ */
+ getTimeouts() {
+ const { duration } = this.data
+
+ if (duration !== null && typeof duration === 'object') {
+ return {
+ enter: duration.enter,
+ exit: duration.exit,
+ }
+ } else if (typeof duration === 'number') {
+ return {
+ enter: duration,
+ exit: duration,
+ }
+ }
+
+ return {}
+ },
+ /**
+ * 属性值 in 被更改时的响应函数
+ * @param {Boolean} newVal 触发组件进入或离开过渡的状态
+ */
+ updated(newVal) {
+ let { animateStatus } = this.pendingData || this.data
+ let nextStatus = null
+
+ if (newVal) {
+ if (animateStatus === UNMOUNTED) {
+ animateStatus = EXITED
+ this.setData({ animateStatus: EXITED }, () => {
+ this.triggerEvent('change', { animateStatus: EXITED })
+ })
+ }
+ if (animateStatus !== ENTER && animateStatus !== ENTERING && animateStatus !== ENTERED) {
+ nextStatus = ENTERING
+ }
+ } else {
+ if (animateStatus === ENTER || animateStatus === ENTERING || animateStatus === ENTERED) {
+ nextStatus = EXITING
+ }
+ }
+
+ this.updateStatus(nextStatus)
+ },
+ /**
+ * 延迟一段时间触发回调
+ * @param {Number} timeout 延迟时间
+ * @param {Function} handler 回调函数
+ */
+ delayHandler(timeout, handler) {
+ if (timeout) {
+ this.setNextCallback(handler)
+ setTimeout(this.nextCallback, timeout)
+ }
+ },
+ /**
+ * 点击事件
+ */
+ onTap() {
+ this.triggerEvent('click')
+ },
+ /**
+ * 阻止移动触摸
+ */
+ noop() {
+ },
+ },
+ attached() {
+ let animateStatus = null
+ let appearStatus = null
+
+ if (this.data.in) {
+ if (this.data.appear) {
+ animateStatus = EXITED
+ appearStatus = ENTERING
+ } else {
+ animateStatus = ENTERED
+ }
+ } else {
+ if (this.data.unmountOnExit || this.data.mountOnEnter) {
+ animateStatus = UNMOUNTED
+ } else {
+ animateStatus = EXITED
+ }
+ }
+
+ // 由于小程序组件首次挂载时 observer 事件总是优先于 attached 事件
+ // 所以使用 isMounting 来强制优先触发 attached 事件
+ this.safeSetData({ animateStatus, isMounting: true }, () => {
+ this.triggerEvent('change', { animateStatus })
+ this.updateStatus(appearStatus, true)
+ })
+ },
+})
diff --git a/components/animation-group/index.json b/components/animation-group/index.json
new file mode 100644
index 0000000..fba482a
--- /dev/null
+++ b/components/animation-group/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
\ No newline at end of file
diff --git a/components/animation-group/index.wxml b/components/animation-group/index.wxml
new file mode 100644
index 0000000..7a68452
--- /dev/null
+++ b/components/animation-group/index.wxml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/components/animation-group/index.wxss b/components/animation-group/index.wxss
new file mode 100644
index 0000000..ac89862
--- /dev/null
+++ b/components/animation-group/index.wxss
@@ -0,0 +1,206 @@
+.wux-animate--fadeIn-enter {
+ transition: opacity .3s;
+ opacity: 0
+}
+.wux-animate--fadeIn-enter-active,
+.wux-animate--fadeIn-enter-done {
+ opacity: 1
+}
+.wux-animate--fadeIn-exit {
+ transition: opacity .3s;
+ opacity: 1
+}
+.wux-animate--fadeIn-exit-active,
+.wux-animate--fadeIn-exit-done {
+ opacity: 0
+}
+.wux-animate--fadeInDown-enter {
+ transition: opacity .3s,transform .3s;
+ opacity: 0;
+ transform: translate3d(0,-100%,0)
+}
+.wux-animate--fadeInDown-enter-active,
+.wux-animate--fadeInDown-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInDown-exit {
+ transition: opacity .3s,transform .3s;
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInDown-exit-active,
+.wux-animate--fadeInDown-exit-done {
+ opacity: 0;
+ transform: translate3d(0,-100%,0)
+}
+.wux-animate--fadeInLeft-enter {
+ transition: opacity .3s,transform .3s;
+ opacity: 0;
+ transform: translate3d(-100%,0,0)
+}
+.wux-animate--fadeInLeft-enter-active,
+.wux-animate--fadeInLeft-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInLeft-exit {
+ transition: opacity .3s,transform .3s;
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInLeft-exit-active,
+.wux-animate--fadeInLeft-exit-done {
+ opacity: 0;
+ transform: translate3d(-100%,0,0)
+}
+.wux-animate--fadeInRight-enter {
+ transition: opacity .3s,transform .3s;
+ opacity: 0;
+ transform: translate3d(100%,0,0)
+}
+.wux-animate--fadeInRight-enter-active,
+.wux-animate--fadeInRight-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInRight-exit {
+ transition: opacity .3s,transform .3s;
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInRight-exit-active,
+.wux-animate--fadeInRight-exit-done {
+ opacity: 0;
+ transform: translate3d(100%,0,0)
+}
+.wux-animate--fadeInUp-enter {
+ transition: opacity .3s,transform .3s;
+ opacity: 0;
+ transform: translate3d(0,100%,0)
+}
+.wux-animate--fadeInUp-enter-active,
+.wux-animate--fadeInUp-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInUp-exit {
+ transition: opacity .3s,transform .3s;
+ opacity: 1;
+ transform: none
+}
+.wux-animate--fadeInUp-exit-active,
+.wux-animate--fadeInUp-exit-done {
+ opacity: 0;
+ transform: translate3d(0,100%,0)
+}
+.wux-animate--slideInUp-enter {
+ transition: transform .3s;
+ transform: translate3d(0,100%,0);
+ visibility: visible
+}
+.wux-animate--slideInUp-enter-active,
+.wux-animate--slideInUp-enter-done {
+ transform: translateZ(0)
+}
+.wux-animate--slideInUp-exit {
+ transition: transform .3s;
+ transform: translateZ(0)
+}
+.wux-animate--slideInUp-exit-active,
+.wux-animate--slideInUp-exit-done {
+ transform: translate3d(0,100%,0);
+ visibility: visible
+}
+.wux-animate--slideInDown-enter {
+ transition: transform .3s;
+ transform: translate3d(0,-100%,0);
+ visibility: visible
+}
+.wux-animate--slideInDown-enter-active,
+.wux-animate--slideInDown-enter-done {
+ transform: translateZ(0)
+}
+.wux-animate--slideInDown-exit {
+ transition: transform .3s;
+ transform: translateZ(0)
+}
+.wux-animate--slideInDown-exit-active,
+.wux-animate--slideInDown-exit-done {
+ transform: translate3d(0,-100%,0);
+ visibility: visible
+}
+.wux-animate--slideInLeft-enter {
+ transition: transform .3s;
+ transform: translate3d(-100%,0,0);
+ visibility: visible
+}
+.wux-animate--slideInLeft-enter-active,
+.wux-animate--slideInLeft-enter-done {
+ transform: translateZ(0)
+}
+.wux-animate--slideInLeft-exit {
+ transition: transform .3s;
+ transform: translateZ(0)
+}
+.wux-animate--slideInLeft-exit-active,
+.wux-animate--slideInLeft-exit-done {
+ transform: translate3d(-100%,0,0);
+ visibility: visible
+}
+.wux-animate--slideInRight-enter {
+ transition: transform .3s;
+ transform: translate3d(100%,0,0);
+ visibility: visible
+}
+.wux-animate--slideInRight-enter-active,
+.wux-animate--slideInRight-enter-done {
+ transform: none
+}
+.wux-animate--slideInRight-exit {
+ transition: transform .3s;
+ transform: none
+}
+.wux-animate--slideInRight-exit-active,
+.wux-animate--slideInRight-exit-done {
+ transform: translate3d(100%,0,0);
+ visibility: visible
+}
+.wux-animate--zoom-enter {
+ transition: all .3s cubic-bezier(.215,.61,.355,1);
+ opacity: .01;
+ transform: scale(.75)
+}
+.wux-animate--zoom-enter-active,
+.wux-animate--zoom-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--zoom-exit {
+ transition: all .25s linear;
+ transform: none
+}
+.wux-animate--zoom-exit-active,
+.wux-animate--zoom-exit-done {
+ opacity: .01;
+ transform: scale(.75)
+}
+.wux-animate--punch-enter {
+ transition: all .3s cubic-bezier(.215,.61,.355,1);
+ opacity: .01;
+ transform: scale(1.35)
+}
+.wux-animate--punch-enter-active,
+.wux-animate--punch-enter-done {
+ opacity: 1;
+ transform: none
+}
+.wux-animate--punch-exit {
+ transition: all .25s linear;
+ transform: none
+}
+.wux-animate--punch-exit-active,
+.wux-animate--punch-exit-done {
+ opacity: .01;
+ transform: scale(1.35)
+}
\ No newline at end of file
diff --git a/components/backdrop/index.js b/components/backdrop/index.js
new file mode 100644
index 0000000..cf43b99
--- /dev/null
+++ b/components/backdrop/index.js
@@ -0,0 +1,62 @@
+import baseComponent from '../helpers/baseComponent'
+
+baseComponent({
+ properties: {
+ prefixCls: {
+ type: String,
+ value: 'wux-backdrop',
+ },
+ transparent: {
+ type: Boolean,
+ value: false,
+ },
+ zIndex: {
+ type: Number,
+ value: 1000,
+ },
+ classNames: {
+ type: null,
+ value: 'wux-animate--fadeIn',
+ },
+ },
+ computed: {
+ classes: ['prefixCls, transparent', function(prefixCls, transparent) {
+ const wrap = transparent ? `${prefixCls}--transparent` : prefixCls
+
+ return {
+ wrap,
+ }
+ }],
+ },
+ methods: {
+ /**
+ * 保持锁定
+ */
+ retain() {
+ if (typeof this.backdropHolds !== 'number' || !this.backdropHolds) {
+ this.backdropHolds = 0
+ }
+
+ this.backdropHolds = this.backdropHolds + 1
+
+ if (this.backdropHolds === 1) {
+ this.setData({ in: true })
+ }
+ },
+ /**
+ * 释放锁定
+ */
+ release() {
+ if (this.backdropHolds === 1) {
+ this.setData({ in: false })
+ }
+ this.backdropHolds = Math.max(0, this.backdropHolds - 1)
+ },
+ /**
+ * 点击事件
+ */
+ onClick() {
+ this.triggerEvent('click')
+ },
+ },
+})
diff --git a/components/backdrop/index.json b/components/backdrop/index.json
new file mode 100644
index 0000000..9b9e7ab
--- /dev/null
+++ b/components/backdrop/index.json
@@ -0,0 +1,6 @@
+{
+ "component": true,
+ "usingComponents": {
+ "wux-animation-group": "../animation-group/index"
+ }
+}
\ No newline at end of file
diff --git a/components/backdrop/index.wxml b/components/backdrop/index.wxml
new file mode 100644
index 0000000..d6ee398
--- /dev/null
+++ b/components/backdrop/index.wxml
@@ -0,0 +1 @@
+
diff --git a/components/backdrop/index.wxss b/components/backdrop/index.wxss
new file mode 100644
index 0000000..be44d1b
--- /dev/null
+++ b/components/backdrop/index.wxss
@@ -0,0 +1,15 @@
+.wux-backdrop {
+ background: rgba(0,0,0,.4)
+}
+.wux-backdrop,
+.wux-backdrop--transparent {
+ position: fixed;
+ z-index: 1000;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0
+}
+.wux-backdrop--transparent {
+ background: 0 0
+}
\ No newline at end of file
diff --git a/components/helpers/arrayTreeFilter.js b/components/helpers/arrayTreeFilter.js
new file mode 100644
index 0000000..94736eb
--- /dev/null
+++ b/components/helpers/arrayTreeFilter.js
@@ -0,0 +1,24 @@
+/**
+ * https://github.com/afc163/array-tree-filter
+ */
+function arrayTreeFilter(data, filterFn, options) {
+ options = options || {}
+ options.childrenKeyName = options.childrenKeyName || 'children'
+ let children = data || []
+ const result = []
+ let level = 0
+ do {
+ const foundItem = children.filter(function(item) {
+ return filterFn(item, level)
+ })[0]
+ if (!foundItem) {
+ break
+ }
+ result.push(foundItem)
+ children = foundItem[options.childrenKeyName] || []
+ level += 1
+ } while (children.length > 0)
+ return result
+}
+
+export default arrayTreeFilter
\ No newline at end of file
diff --git a/components/helpers/baseComponent.js b/components/helpers/baseComponent.js
new file mode 100644
index 0000000..7c5d66d
--- /dev/null
+++ b/components/helpers/baseComponent.js
@@ -0,0 +1,77 @@
+import computedBehavior from './computedBehavior'
+import relationsBehavior from './relationsBehavior'
+import safeAreaBehavior from './safeAreaBehavior'
+import safeSetDataBehavior from './safeSetDataBehavior'
+import funcBehavior from './funcBehavior'
+import compareVersion from './compareVersion'
+
+const { platform, SDKVersion } = wx.getSystemInfoSync()
+const libVersion = '2.6.6'
+
+// check SDKVersion
+if (platform === 'devtools' && compareVersion(SDKVersion, libVersion) < 0) {
+ if (wx && wx.showModal) {
+ wx.showModal({
+ title: '提示',
+ content: `当前基础库版本(${SDKVersion})过低,无法使用 Wux Weapp 组件库,请更新基础库版本 >=${libVersion} 后重试。`,
+ })
+ }
+}
+
+const baseComponent = (options = {}) => {
+ // add default externalClasses
+ options.externalClasses = [
+ 'wux-class',
+ 'wux-hover-class',
+ ...(options.externalClasses = options.externalClasses || []),
+ ]
+
+ // add default behaviors
+ options.behaviors = [
+ relationsBehavior,
+ safeSetDataBehavior,
+ ...(options.behaviors = options.behaviors || []),
+ computedBehavior, // make sure it's triggered
+ ]
+
+ // use safeArea
+ if (options.useSafeArea) {
+ options.behaviors = [...options.behaviors, safeAreaBehavior]
+ delete options.useSafeArea
+ }
+
+ // use func
+ if (options.useFunc) {
+ options.behaviors = [...options.behaviors, funcBehavior]
+ delete options.useFunc
+ }
+
+ // use field
+ if (options.useField) {
+ options.behaviors = [...options.behaviors, 'wx://form-field']
+ delete options.useField
+ }
+
+ // use export
+ if (options.useExport) {
+ options.behaviors = [...options.behaviors, 'wx://component-export']
+ options.methods = {
+ export () {
+ return this
+ },
+ ...options.methods,
+ }
+ delete options.useExport
+ }
+
+ // add default options
+ options.options = {
+ multipleSlots: true,
+ addGlobalClass: true,
+ ...options.options,
+ }
+
+ return Component(options)
+}
+
+export default baseComponent
diff --git a/components/helpers/checkIPhoneX.js b/components/helpers/checkIPhoneX.js
new file mode 100644
index 0000000..5d65c52
--- /dev/null
+++ b/components/helpers/checkIPhoneX.js
@@ -0,0 +1,29 @@
+/**
+ * 获取系统信息
+ */
+
+let systemInfo = null
+
+export const getSystemInfo = (isForce) => {
+ if (!systemInfo || isForce) {
+ try {
+ systemInfo = wx.getSystemInfoSync()
+ } catch(e) { /* Ignore */ }
+ }
+
+ return systemInfo
+}
+
+// iPhoneX 竖屏安全区域
+export const safeAreaInset = {
+ top: 88, // StatusBar & NavBar
+ left: 0,
+ right: 0,
+ bottom: 34, // Home Indicator
+}
+
+const isIPhoneX = ({ model, platform }) => {
+ return /iPhone X/.test(model) && platform === 'ios'
+}
+
+export const checkIPhoneX = (isForce) => isIPhoneX(getSystemInfo(isForce))
diff --git a/components/helpers/classNames.js b/components/helpers/classNames.js
new file mode 100644
index 0000000..e2eb2d4
--- /dev/null
+++ b/components/helpers/classNames.js
@@ -0,0 +1,39 @@
+/*!
+ Copyright (c) 2018 Jed Watson.
+ Licensed under the MIT License (MIT), see
+ http://jedwatson.github.io/classnames
+*/
+/* global define */
+'use strict';
+
+var hasOwn = {}.hasOwnProperty;
+
+function classNames() {
+ var classes = [];
+
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (!arg) continue;
+
+ var argType = typeof arg;
+
+ if (argType === 'string' || argType === 'number') {
+ classes.push(arg);
+ } else if (Array.isArray(arg) && arg.length) {
+ var inner = classNames.apply(null, arg);
+ if (inner) {
+ classes.push(inner);
+ }
+ } else if (argType === 'object') {
+ for (var key in arg) {
+ if (hasOwn.call(arg, key) && arg[key]) {
+ classes.push(key);
+ }
+ }
+ }
+ }
+
+ return classes.join(' ');
+}
+
+export default classNames
\ No newline at end of file
diff --git a/components/helpers/colors.js b/components/helpers/colors.js
new file mode 100644
index 0000000..fc4b9d8
--- /dev/null
+++ b/components/helpers/colors.js
@@ -0,0 +1,27 @@
+/**
+ * Don't modify this file!
+ * Colors generated by themes!
+ */
+
+/* eslint-disable */
+
+export const colors = {
+ 'light': '#ddd',
+ 'stable': '#b2b2b2',
+ 'positive': '#387ef5',
+ 'calm': '#11c1f3',
+ 'balanced': '#33cd5f',
+ 'energized': '#ffc900',
+ 'assertive': '#ef473a',
+ 'royal': '#886aea',
+ 'dark': '#444',
+}
+
+export const isPresetColor = (color) => {
+ if (!color) {
+ return false
+ }
+ return colors[color] ? colors[color] : color
+}
+
+/* eslint-enable */
diff --git a/components/helpers/compareVersion.js b/components/helpers/compareVersion.js
new file mode 100644
index 0000000..47a2d59
--- /dev/null
+++ b/components/helpers/compareVersion.js
@@ -0,0 +1,27 @@
+function compareVersion(v1, v2) {
+ const $v1 = v1.split('.')
+ const $v2 = v2.split('.')
+ const len = Math.max($v1.length, $v2.length)
+
+ while ($v1.length < len) {
+ $v1.push('0')
+ }
+ while ($v2.length < len) {
+ $v2.push('0')
+ }
+
+ for (let i = 0; i < len; i++) {
+ const num1 = parseInt($v1[i])
+ const num2 = parseInt($v2[i])
+
+ if (num1 > num2) {
+ return 1
+ } else if (num1 < num2) {
+ return -1
+ }
+ }
+
+ return 0
+}
+
+export default compareVersion
diff --git a/components/helpers/computedBehavior.js b/components/helpers/computedBehavior.js
new file mode 100644
index 0000000..8650193
--- /dev/null
+++ b/components/helpers/computedBehavior.js
@@ -0,0 +1,48 @@
+import isEmpty from './isEmpty'
+import shallowEqual from './shallowEqual'
+
+const ALL_DATA_KEY = '**'
+
+const trim = (str = '') => str.replace(/\s/g, '')
+
+export default Behavior({
+ lifetimes: {
+ attached() {
+ this.initComputed()
+ },
+ },
+ definitionFilter(defFields) {
+ const { computed = {} } = defFields
+ const observers = Object.keys(computed).reduce((acc, name) => {
+ const [field, getter] = Array.isArray(computed[name]) ? computed[name] : [ALL_DATA_KEY, computed[name]]
+ return {
+ ...acc,
+ [field]: function(...args) {
+ if (typeof getter === 'function') {
+ const newValue = getter.apply(this, args)
+ const oldValue = this.data[name]
+ if (!isEmpty(newValue) && !shallowEqual(newValue, oldValue)) {
+ this.setData({ [name]: newValue })
+ }
+ }
+ },
+ }
+ }, {})
+
+ Object.assign(defFields.observers = (defFields.observers || {}), observers)
+ Object.assign(defFields.methods = (defFields.methods || {}), {
+ initComputed: function(data = {}, isForce = false) {
+ if (!this.runInitComputed || isForce) {
+ this.runInitComputed = false
+ const context = this
+ const result = { ...this.data, ...data }
+ Object.keys(observers).forEach((key) => {
+ const values = trim(key).split(',').reduce((acc, name) => ([...acc, result[name]]), [])
+ observers[key].apply(context, values)
+ })
+ this.runInitComputed = true
+ }
+ },
+ })
+ },
+})
diff --git a/components/helpers/createFieldsStore.js b/components/helpers/createFieldsStore.js
new file mode 100644
index 0000000..9dbb3ee
--- /dev/null
+++ b/components/helpers/createFieldsStore.js
@@ -0,0 +1,75 @@
+class FieldsStore {
+ constructor(fields = {}) {
+ this.fields = fields
+ }
+
+ setFields(fields) {
+ Object.assign(this.fields, fields)
+ }
+
+ updateFields(fields) {
+ this.fields = fields
+ }
+
+ clearField(name) {
+ delete this.fields[name]
+ }
+
+ getValueFromFields(name, fields) {
+ const field = fields[name]
+ if (field && 'value' in field) {
+ return field.value
+ }
+ return field.initialValue
+ }
+
+ getAllFieldsName() {
+ const { fields } = this
+ return fields ? Object.keys(fields) : []
+ }
+
+ getField(name) {
+ return {
+ ...this.fields[name],
+ name,
+ }
+ }
+
+ getFieldValuePropValue(fieldOption) {
+ const { name, valuePropName } = fieldOption
+ const field = this.getField(name)
+ const fieldValue = 'value' in field ? field.value : field.initialValue
+
+ return {
+ [valuePropName]: fieldValue,
+ }
+ }
+
+ getFieldValue(name) {
+ return this.getValueFromFields(name, this.fields)
+ }
+
+ getFieldsValue(names) {
+ const fields = names || this.getAllFieldsName()
+ return fields.reduce((acc, name) => {
+ acc[name] = this.getFieldValue(name)
+ return acc
+ }, {})
+ }
+
+ resetFields(ns) {
+ const { fields } = this
+ const names = ns || this.getAllFieldsName()
+ return names.reduce((acc, name) => {
+ const field = fields[name]
+ if (field) {
+ acc[name] = field.initialValue
+ }
+ return acc
+ }, {})
+ }
+}
+
+export default function createFieldsStore(fields) {
+ return new FieldsStore(fields)
+}
diff --git a/components/helpers/debounce.js b/components/helpers/debounce.js
new file mode 100644
index 0000000..207e486
--- /dev/null
+++ b/components/helpers/debounce.js
@@ -0,0 +1,56 @@
+export default function debounce(func, wait, immediate) {
+ let timeout,
+ args,
+ context,
+ timestamp,
+ result
+
+ function later() {
+ const last = +(new Date()) - timestamp
+ if (last < wait && last >= 0) {
+ timeout = setTimeout(later, wait - last)
+ } else {
+ timeout = undefined
+ if (!immediate) {
+ result = func.apply(context, args)
+ if (!timeout) {
+ context = undefined
+ args = undefined
+ }
+ }
+ }
+ }
+
+ function debounced() {
+ context = this
+ args = arguments
+ timestamp = +(new Date())
+
+ const callNow = immediate && !timeout
+ if (!timeout) {
+ timeout = setTimeout(later, wait)
+ }
+
+ if (callNow) {
+ result = func.apply(context, args)
+ context = undefined
+ args = undefined
+ }
+
+ return result
+ }
+
+ function cancel() {
+ if (timeout !== undefined) {
+ clearTimeout(timeout)
+ timeout = undefined
+ }
+
+ context = undefined
+ args = undefined
+ }
+
+ debounced.cancel = cancel
+
+ return debounced
+}
\ No newline at end of file
diff --git a/components/helpers/eventsMixin.js b/components/helpers/eventsMixin.js
new file mode 100644
index 0000000..f690335
--- /dev/null
+++ b/components/helpers/eventsMixin.js
@@ -0,0 +1,53 @@
+const defaultEvents = {
+ onChange() {},
+}
+
+export default function eventsMixin(params = { defaultEvents }) {
+ return Behavior({
+ lifetimes: {
+ created () {
+ this._oriTriggerEvent = this.triggerEvent
+ this.triggerEvent = this._triggerEvent
+ },
+ },
+ properties: {
+ events: {
+ type: Object,
+ value: defaultEvents,
+ },
+ },
+ data: {
+ inputEvents: defaultEvents,
+ },
+ definitionFilter(defFields) {
+ // set default data
+ Object.assign(defFields.data = (defFields.data || {}), {
+ inputEvents: Object.assign({}, defaultEvents, defFields.inputEvents),
+ })
+
+ // set default methods
+ Object.assign(defFields.methods = (defFields.methods || {}), {
+ _triggerEvent(name, params, runCallbacks = true, option) {
+ const { inputEvents } = this.data
+ const method = `on${name[0].toUpperCase()}${name.slice(1)}`
+ const func = inputEvents[method]
+
+ if (runCallbacks && typeof func === 'function') {
+ func.call(this, params)
+ }
+
+ this._oriTriggerEvent(name, params, option)
+ },
+ })
+
+ // set default observers
+ Object.assign(defFields.observers = (defFields.observers || {}), {
+ events(newVal) {
+ this.setData({
+ inputEvents: Object.assign({}, defaultEvents, this.data.inputEvents, newVal),
+ })
+ },
+ })
+ },
+ })
+}
diff --git a/components/helpers/funcBehavior.js b/components/helpers/funcBehavior.js
new file mode 100644
index 0000000..948075c
--- /dev/null
+++ b/components/helpers/funcBehavior.js
@@ -0,0 +1,97 @@
+/**
+ * 过滤对象的函数属性
+ * @param {Object} opts
+ */
+const mergeOptionsToData = (opts = {}) => {
+ const options = Object.assign({}, opts)
+
+ for (const key in options) {
+ if (options.hasOwnProperty(key) && typeof options[key] === 'function') {
+ delete options[key]
+ }
+ }
+
+ return options
+}
+
+/**
+ * Simple bind, faster than native
+ *
+ * @param {Function} fn
+ * @param {Object} ctx
+ * @return {Function}
+ */
+const bind = (fn, ctx) => {
+ return (...args) => {
+ return args.length ? fn.apply(ctx, args) : fn.call(ctx)
+ }
+}
+
+/**
+ * Object assign
+ */
+const assign = (...args) => Object.assign({}, ...args)
+
+export default Behavior({
+ definitionFilter(defFields) {
+ defFields.data = mergeOptionsToData(defFields.data)
+ defFields.data.in = false
+ defFields.data.visible = false
+ },
+ methods: {
+ /**
+ * 过滤对象的函数属性
+ * @param {Object} opts
+ */
+ $$mergeOptionsToData: mergeOptionsToData,
+ /**
+ * 合并参数并绑定方法
+ *
+ * @param {Object} opts 参数对象
+ * @param {Object} fns 方法挂载的属性
+ */
+ $$mergeOptionsAndBindMethods (opts = {}, fns = this.fns) {
+ const options = Object.assign({}, opts)
+
+ for (const key in options) {
+ if (options.hasOwnProperty(key) && typeof options[key] === 'function') {
+ fns[key] = bind(options[key], this)
+ delete options[key]
+ }
+ }
+
+ return options
+ },
+ /**
+ * Promise setData
+ * @param {Array} args 参数对象
+ */
+ $$setData (...args) {
+ const params = assign({}, ...args)
+
+ return new Promise((resolve) => {
+ this.setData(params, resolve)
+ })
+ },
+ /**
+ * 延迟指定时间执行回调函数
+ * @param {Function} callback 回调函数
+ * @param {Number} timeout 延迟时间
+ */
+ $$requestAnimationFrame (callback = () => {}, timeout = 1000 / 60) {
+ return new Promise((resolve) => setTimeout(resolve, timeout)).then(callback)
+ },
+ },
+ /**
+ * 组件生命周期函数,在组件实例进入页面节点树时执行
+ */
+ created () {
+ this.fns = {}
+ },
+ /**
+ * 组件生命周期函数,在组件实例被从页面节点树移除时执行
+ */
+ detached () {
+ this.fns = {}
+ },
+})
diff --git a/components/helpers/gestures.js b/components/helpers/gestures.js
new file mode 100644
index 0000000..99a4a40
--- /dev/null
+++ b/components/helpers/gestures.js
@@ -0,0 +1,50 @@
+/**
+ * 获取触摸点位置信息
+ */
+export const getTouchPoints = (nativeEvent, index = 0) => {
+ const touches = nativeEvent.touches
+ const changedTouches = nativeEvent.changedTouches
+ const hasTouches = touches && touches.length > 0
+ const hasChangedTouches = changedTouches && changedTouches.length > 0
+ const points = !hasTouches && hasChangedTouches ? changedTouches[index] : hasTouches ? touches[index] : nativeEvent
+
+ return {
+ x: points.pageX,
+ y: points.pageY,
+ }
+}
+
+/**
+ * 获取触摸点个数
+ */
+export const getPointsNumber = (e) => e.touches && e.touches.length || e.changedTouches && e.changedTouches.length
+
+/**
+ * 判断是否为同一点
+ */
+export const isEqualPoints = (p1, p2) => p1.x === p2.x && p1.y === p2.y
+
+/**
+ * 判断是否为相近的两点
+ */
+export const isNearbyPoints = (p1, p2, DOUBLE_TAP_RADIUS = 25) => {
+ const xMove = Math.abs(p1.x - p2.x)
+ const yMove = Math.abs(p1.y - p2.y)
+ return xMove < DOUBLE_TAP_RADIUS & yMove < DOUBLE_TAP_RADIUS
+}
+
+/**
+ * 获取两点之间的距离
+ */
+export const getPointsDistance = (p1, p2) => {
+ const xMove = Math.abs(p1.x - p2.x)
+ const yMove = Math.abs(p1.y - p2.y)
+ return Math.sqrt(xMove * xMove + yMove * yMove)
+}
+
+/**
+ * 获取触摸移动方向
+ */
+export const getSwipeDirection = (x1, x2, y1, y2) => {
+ return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
+}
diff --git a/components/helpers/isEmpty.js b/components/helpers/isEmpty.js
new file mode 100644
index 0000000..56fc4f8
--- /dev/null
+++ b/components/helpers/isEmpty.js
@@ -0,0 +1,19 @@
+/**
+ * Checks if a value is empty.
+ */
+function isEmpty(value) {
+ if (Array.isArray(value)) {
+ return value.length === 0
+ } else if (typeof value === 'object') {
+ if (value) {
+ for (const _ in value) {
+ return false
+ }
+ }
+ return true
+ } else {
+ return !value
+ }
+}
+
+export default isEmpty
\ No newline at end of file
diff --git a/components/helpers/mergeOptionsToData.js b/components/helpers/mergeOptionsToData.js
new file mode 100644
index 0000000..47f5acc
--- /dev/null
+++ b/components/helpers/mergeOptionsToData.js
@@ -0,0 +1,17 @@
+/**
+ * 过滤对象的函数属性
+ * @param {Object} opts
+ */
+const mergeOptionsToData = (opts = {}) => {
+ const options = Object.assign({}, opts)
+
+ for (const key in options) {
+ if (options.hasOwnProperty(key) && typeof options[key] === 'function') {
+ delete options[key]
+ }
+ }
+
+ return options
+}
+
+export default mergeOptionsToData
diff --git a/components/helpers/relationsBehavior.js b/components/helpers/relationsBehavior.js
new file mode 100644
index 0000000..2f05c35
--- /dev/null
+++ b/components/helpers/relationsBehavior.js
@@ -0,0 +1,67 @@
+import isEmpty from './isEmpty'
+import debounce from './debounce'
+
+/**
+ * bind func to obj
+ */
+function bindFunc(obj, method, observer) {
+ const oldFn = obj[method]
+ obj[method] = function(target) {
+ if (observer) {
+ observer.call(this, target, {
+ [method]: true,
+ })
+ }
+ if (oldFn) {
+ oldFn.call(this, target)
+ }
+ }
+}
+
+// default methods
+const methods = ['linked', 'linkChanged', 'unlinked']
+
+// extra props
+const extProps = ['observer']
+
+export default Behavior({
+ lifetimes: {
+ created() {
+ this._debounce = null
+ },
+ detached() {
+ if (this._debounce && this._debounce.cancel) {
+ this._debounce.cancel()
+ }
+ },
+ },
+ definitionFilter(defFields) {
+ const { relations } = defFields
+
+ if (!isEmpty(relations)) {
+ for (const key in relations) {
+ const relation = relations[key]
+
+ // bind func
+ methods.forEach((method) => bindFunc(relation, method, relation.observer))
+
+ // delete extProps
+ extProps.forEach((prop) => delete relation[prop])
+ }
+ }
+
+ Object.assign(defFields.methods = (defFields.methods || {}), {
+ getRelationsName: function(types = ['parent', 'child', 'ancestor', 'descendant']) {
+ return Object.keys(relations || {}).map((key) => {
+ if (relations[key] && types.includes(relations[key].type)) {
+ return key
+ }
+ return null
+ }).filter((v) => !!v)
+ },
+ debounce: function(func, wait = 0, immediate = false) {
+ return (this._debounce = this._debounce || debounce(func.bind(this), wait, immediate)).call(this)
+ },
+ })
+ },
+})
diff --git a/components/helpers/safeAreaBehavior.js b/components/helpers/safeAreaBehavior.js
new file mode 100644
index 0000000..c81fad0
--- /dev/null
+++ b/components/helpers/safeAreaBehavior.js
@@ -0,0 +1,46 @@
+import { getSystemInfo, checkIPhoneX } from './checkIPhoneX'
+
+const defaultSafeArea = {
+ top: false,
+ bottom: false,
+}
+
+const setSafeArea = (params) => {
+ if (typeof params === 'boolean') {
+ return Object.assign({}, defaultSafeArea, {
+ top: params,
+ bottom: params,
+ })
+ } else if (params !== null && typeof params === 'object') {
+ return Object.assign({}, defaultSafeArea)
+ } else if (typeof params === 'string') {
+ return Object.assign({}, defaultSafeArea, {
+ [params]: true,
+ })
+ }
+ return defaultSafeArea
+}
+
+export default Behavior({
+ properties: {
+ safeArea: {
+ type: [Boolean, String, Object],
+ value: false,
+ },
+ },
+ observers: {
+ safeArea(newVal) {
+ this.setData({ safeAreaConfig: setSafeArea(newVal) })
+ },
+ },
+ definitionFilter(defFields) {
+ const { statusBarHeight } = getSystemInfo() || {}
+ const isIPhoneX = checkIPhoneX()
+
+ Object.assign(defFields.data = (defFields.data || {}), {
+ safeAreaConfig: defaultSafeArea,
+ statusBarHeight,
+ isIPhoneX,
+ })
+ },
+})
diff --git a/components/helpers/safeSetDataBehavior.js b/components/helpers/safeSetDataBehavior.js
new file mode 100644
index 0000000..f00f0ae
--- /dev/null
+++ b/components/helpers/safeSetDataBehavior.js
@@ -0,0 +1,57 @@
+export default Behavior({
+ lifetimes: {
+ created() {
+ this.nextCallback = null
+ },
+ detached() {
+ this.cancelNextCallback()
+ },
+ },
+ methods: {
+ /**
+ * safeSetData
+ * @param {Object} nextData 数据对象
+ * @param {Function} callback 回调函数
+ */
+ safeSetData(nextData, callback) {
+ this.pendingData = Object.assign({}, this.data, nextData)
+ callback = this.setNextCallback(callback)
+
+ this.setData(nextData, () => {
+ this.pendingData = null
+ callback()
+ })
+ },
+ /**
+ * 设置下一回调函数
+ * @param {Function} callback 回调函数
+ */
+ setNextCallback(callback) {
+ let active = true
+
+ this.nextCallback = (event) => {
+ if (active) {
+ active = false
+ this.nextCallback = null
+
+ callback.call(this, event)
+ }
+ }
+
+ this.nextCallback.cancel = () => {
+ active = false
+ }
+
+ return this.nextCallback
+ },
+ /**
+ * 取消下一回调函数
+ */
+ cancelNextCallback() {
+ if (this.nextCallback !== null) {
+ this.nextCallback.cancel()
+ this.nextCallback = null
+ }
+ },
+ },
+})
\ No newline at end of file
diff --git a/components/helpers/shallowEqual.js b/components/helpers/shallowEqual.js
new file mode 100644
index 0000000..8aece72
--- /dev/null
+++ b/components/helpers/shallowEqual.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @typechecks
+ *
+ */
+
+/*eslint-disable no-self-compare */
+
+'use strict';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * inlined Object.is polyfill to avoid requiring consumers ship their own
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
+ */
+function is(x, y) {
+ // SameValue algorithm
+ if (x === y) {
+ // Steps 1-5, 7-10
+ // Steps 6.b-6.e: +0 != -0
+ // Added the nonzero y check to make Flow happy, but it is redundant
+ return x !== 0 || y !== 0 || 1 / x === 1 / y;
+ } else {
+ // Step 6.a: NaN == NaN
+ return x !== x && y !== y;
+ }
+}
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+function shallowEqual(objA, objB) {
+ if (is(objA, objB)) {
+ return true;
+ }
+
+ if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
+ return false;
+ }
+
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ for (var i = 0; i < keysA.length; i++) {
+ if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export default shallowEqual
\ No newline at end of file
diff --git a/components/helpers/styleToCssString.js b/components/helpers/styleToCssString.js
new file mode 100644
index 0000000..1665cf7
--- /dev/null
+++ b/components/helpers/styleToCssString.js
@@ -0,0 +1,138 @@
+'use strict';
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+var isUnitlessNumber = {
+ boxFlex: true,
+ boxFlexGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ strokeDashoffset: true,
+ strokeOpacity: true,
+ strokeWidth: true
+};
+
+/**
+ * @param {string} prefix vendor-specific prefix, eg: Webkit
+ * @param {string} key style name, eg: transitionDuration
+ * @return {string} style name prefixed with `prefix`, properly camelCased, eg:
+ * WebkitTransitionDuration
+ */
+function prefixKey(prefix, key) {
+ return prefix + key.charAt(0).toUpperCase() + key.substring(1);
+}
+
+/**
+ * Support style names that may come passed in prefixed by adding permutations
+ * of vendor prefixes.
+ */
+var prefixes = ['Webkit', 'ms', 'Moz', 'O'];
+
+// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an
+// infinite loop, because it iterates over the newly added props too.
+Object.keys(isUnitlessNumber).forEach(function(prop) {
+ prefixes.forEach(function(prefix) {
+ isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
+ });
+});
+
+var msPattern = /^ms-/;
+
+var _uppercasePattern = /([A-Z])/g;
+
+/**
+ * Hyphenates a camelcased string, for example:
+ *
+ * > hyphenate('backgroundColor')
+ * < "background-color"
+ *
+ * For CSS style names, use `hyphenateStyleName` instead which works properly
+ * with all vendor prefixes, including `ms`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenate(string) {
+ return string.replace(_uppercasePattern, '-$1').toLowerCase();
+}
+
+/**
+ * Hyphenates a camelcased CSS property name, for example:
+ *
+ * > hyphenateStyleName('backgroundColor')
+ * < "background-color"
+ * > hyphenateStyleName('MozTransition')
+ * < "-moz-transition"
+ * > hyphenateStyleName('msTransition')
+ * < "-ms-transition"
+ *
+ * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix
+ * is converted to `-ms-`.
+ *
+ * @param {string} string
+ * @return {string}
+ */
+function hyphenateStyleName(string) {
+ return hyphenate(string).replace(msPattern, '-ms-');
+}
+
+var isArray = Array.isArray;
+var keys = Object.keys;
+
+var counter = 1;
+// Follows syntax at https://developer.mozilla.org/en-US/docs/Web/CSS/content,
+// including multiple space separated values.
+var unquotedContentValueRegex = /^(normal|none|(\b(url\([^)]*\)|chapter_counter|attr\([^)]*\)|(no-)?(open|close)-quote|inherit)((\b\s*)|$|\s+))+)$/;
+
+function buildRule(key, value) {
+ if (!isUnitlessNumber[key] && typeof value === 'number') {
+ value = '' + value + 'px';
+ } else if (key === 'content' && !unquotedContentValueRegex.test(value)) {
+ value = "'" + value.replace(/'/g, "\\'") + "'";
+ }
+
+ return hyphenateStyleName(key) + ': ' + value + '; ';
+}
+
+function styleToCssString(rules) {
+ var result = ''
+ if (typeof rules === 'string') {
+ return rules
+ }
+ if (!rules || keys(rules).length === 0) {
+ return result;
+ }
+ var styleKeys = keys(rules);
+ for (var j = 0, l = styleKeys.length; j < l; j++) {
+ var styleKey = styleKeys[j];
+ var value = rules[styleKey];
+
+ if (isArray(value)) {
+ for (var i = 0, len = value.length; i < len; i++) {
+ result += buildRule(styleKey, value[i]);
+ }
+ } else {
+ result += buildRule(styleKey, value);
+ }
+ }
+ return result;
+}
+
+export default styleToCssString
\ No newline at end of file
diff --git a/components/popover/index.js b/components/popover/index.js
new file mode 100644
index 0000000..9c79ff2
--- /dev/null
+++ b/components/popover/index.js
@@ -0,0 +1,234 @@
+import baseComponent from '../helpers/baseComponent'
+import classNames from '../helpers/classNames'
+import styleToCssString from '../helpers/styleToCssString'
+
+const getPlacements = ([a, s, b] = rects, placement = 'top') => {
+ switch (placement) {
+ case 'topLeft':
+ return {
+ top: s.scrollTop + a.top - b.height - 4,
+ left: s.scrollLeft + a.left,
+ }
+ case 'top':
+ return {
+ top: s.scrollTop + a.top - b.height - 4,
+ left: s.scrollLeft + a.left + (a.width - b.width) / 2,
+ }
+ case 'topRight':
+ return {
+ top: s.scrollTop + a.top - b.height - 4,
+ left: s.scrollLeft + a.left + a.width - b.width,
+ }
+ case 'rightTop':
+ return {
+ top: s.scrollTop + a.top,
+ left: s.scrollLeft + a.left + a.width + 4,
+ }
+ case 'right':
+ return {
+ top: s.scrollTop + a.top + (a.height - b.height) / 2,
+ left: s.scrollLeft + a.left + a.width + 4,
+ }
+ case 'rightBottom':
+ return {
+ top: s.scrollTop + a.top + a.height - b.height,
+ left: s.scrollLeft + a.left + a.width + 4,
+ }
+ case 'bottomRight':
+ return {
+ top: s.scrollTop + a.top + a.height + 4,
+ left: s.scrollLeft + a.left + a.width - b.width,
+ }
+ case 'bottom':
+ return {
+ top: s.scrollTop + a.top + a.height + 4,
+ left: s.scrollLeft + a.left + (a.width - b.width) / 2,
+ }
+ case 'bottomLeft':
+ return {
+ top: s.scrollTop + a.top + a.height + 4,
+ left: s.scrollLeft + a.left,
+ }
+ case 'leftBottom':
+ return {
+ top: s.scrollTop + a.top + a.height - b.height,
+ left: s.scrollLeft + a.left - b.width - 4,
+ }
+ case 'left':
+ return {
+ top: s.scrollTop + a.top + (a.height - b.height) / 2,
+ left: s.scrollLeft + a.left - b.width - 4,
+ }
+ case 'leftTop':
+ return {
+ top: s.scrollTop + a.top,
+ left: s.scrollLeft + a.left - b.width - 4,
+ }
+ default:
+ return {
+ left: 0,
+ top: 0,
+ }
+ }
+}
+
+baseComponent({
+ properties: {
+ prefixCls: {
+ type: String,
+ value: 'wux-popover',
+ },
+ classNames: {
+ type: null,
+ value: 'wux-animate--fadeIn',
+ },
+ theme: {
+ type: String,
+ value: 'light',
+ },
+ title: {
+ type: String,
+ value: '',
+ },
+ content: {
+ type: String,
+ value: '',
+ },
+ placement: {
+ type: String,
+ value: 'top',
+ },
+ trigger: {
+ type: String,
+ value: 'click',
+ },
+ bodyStyle: {
+ type: [String, Object],
+ value: '',
+ observer(newVal) {
+ this.setData({
+ extStyle: styleToCssString(newVal),
+ })
+ },
+ },
+ defaultVisible: {
+ type: Boolean,
+ value: false,
+ },
+ visible: {
+ type: Boolean,
+ value: false,
+ observer(newVal) {
+ if (this.data.controlled) {
+ this.updated(newVal)
+ }
+ },
+ },
+ controlled: {
+ type: Boolean,
+ value: false,
+ },
+ mask: {
+ type: Boolean,
+ value: false,
+ },
+ maskClosable: {
+ type: Boolean,
+ value: true,
+ },
+ },
+ data: {
+ extStyle: '',
+ popoverStyle: '',
+ popoverVisible: false,
+ },
+ computed: {
+ classes: ['prefixCls, theme, placement', function(prefixCls, theme, placement) {
+ const wrap = classNames(prefixCls, {
+ [`${prefixCls}--theme-${theme}`]: theme,
+ [`${prefixCls}--placement-${placement}`]: placement,
+ })
+ const content = `${prefixCls}__content`
+ const arrow = `${prefixCls}__arrow`
+ const inner = `${prefixCls}__inner`
+ const title = `${prefixCls}__title`
+ const innerContent = `${prefixCls}__inner-content`
+ const element = `${prefixCls}__element`
+
+ return {
+ wrap,
+ content,
+ arrow,
+ inner,
+ title,
+ innerContent,
+ element,
+ }
+ }],
+ },
+ methods: {
+ updated(popoverVisible) {
+ if (this.data.popoverVisible !== popoverVisible) {
+ this.setData({ popoverVisible })
+ this.setBackdropVisible(popoverVisible)
+ }
+ },
+ getPopoverStyle() {
+ const { prefixCls, placement } = this.data
+ const query = wx.createSelectorQuery().in(this)
+ query.select(`.${prefixCls}__element`).boundingClientRect()
+ query.selectViewport().scrollOffset()
+ query.select(`.${prefixCls}`).boundingClientRect()
+ query.exec((rects) => {
+ if (rects.filter((n) => !n).length) return
+
+ const placements = getPlacements(rects, placement)
+ const popoverStyle = styleToCssString(placements)
+
+ this.setData({
+ popoverStyle,
+ })
+ })
+ },
+ /**
+ * 当组件进入过渡的开始状态时,设置气泡框位置信息
+ */
+ onEnter() {
+ this.getPopoverStyle()
+ },
+ onChange() {
+ const { popoverVisible, controlled } = this.data
+ const nextVisible = !popoverVisible
+
+ if (!controlled) {
+ this.updated(nextVisible)
+ }
+
+ this.triggerEvent('change', { visible: nextVisible })
+ },
+ onClick() {
+ if (this.data.trigger === 'click') {
+ this.onChange()
+ }
+ },
+ setBackdropVisible(visible) {
+ if (this.data.mask && this.wuxBackdrop) {
+ this.wuxBackdrop[visible ? 'retain' : 'release']()
+ }
+ },
+ onMaskClick() {
+ const { maskClosable, popoverVisible } = this.data
+ if (maskClosable && popoverVisible) {
+ this.onChange()
+ }
+ },
+ },
+ ready() {
+ const { defaultVisible, visible, controlled } = this.data
+ const popoverVisible = controlled ? visible : defaultVisible
+ if (this.data.mask) {
+ this.wuxBackdrop = this.selectComponent('#wux-backdrop')
+ }
+ this.updated(popoverVisible)
+ },
+})
diff --git a/components/popover/index.json b/components/popover/index.json
new file mode 100644
index 0000000..716bdf5
--- /dev/null
+++ b/components/popover/index.json
@@ -0,0 +1,7 @@
+{
+ "component": true,
+ "usingComponents": {
+ "wux-animation-group": "../animation-group/index",
+ "wux-backdrop": "../backdrop/index"
+ }
+}
diff --git a/components/popover/index.wxml b/components/popover/index.wxml
new file mode 100644
index 0000000..c1dfa43
--- /dev/null
+++ b/components/popover/index.wxml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ {{ title }}
+
+ {{ content }}
+
+
+
+
+
+
+
+
diff --git a/components/popover/index.wxss b/components/popover/index.wxss
new file mode 100644
index 0000000..24f765a
--- /dev/null
+++ b/components/popover/index.wxss
@@ -0,0 +1,193 @@
+.wux-popover {
+ font-family: Monospaced Number,Chinese Quote,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif;
+ font-size: 28rpx;
+ line-height: 1.5;
+ color: rgba(0,0,0,.65);
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1030;
+ cursor: auto;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+ white-space: normal;
+ font-weight: 400;
+ text-align: left
+}
+.wux-popover::after {
+ content: "";
+ position: absolute;
+ background: rgba(255,255,255,.01)
+}
+.wux-popover__container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%
+}
+.wux-popover__element {
+ display: inline-block;
+ line-height: 1
+}
+.wux-popover--theme-dark .wux-popover__inner {
+ background-color: #333
+}
+.wux-popover--theme-dark.wux-popover--placement-top .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-topLeft .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-topRight .wux-popover__arrow {
+ border-top-color: #333
+}
+.wux-popover--theme-dark.wux-popover--placement-right .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-rightBottom .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-rightTop .wux-popover__arrow {
+ border-right-color: #333
+}
+.wux-popover--theme-dark.wux-popover--placement-bottom .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-bottomLeft .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-bottomRight .wux-popover__arrow {
+ border-bottom-color: #333
+}
+.wux-popover--theme-dark.wux-popover--placement-left .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-leftBottom .wux-popover__arrow,
+.wux-popover--theme-dark.wux-popover--placement-leftTop .wux-popover__arrow {
+ border-left-color: #333
+}
+.wux-popover--theme-dark .wux-popover__inner,
+.wux-popover--theme-dark .wux-popover__title {
+ color: #fff
+}
+.wux-popover--placement-top,
+.wux-popover--placement-topLeft,
+.wux-popover--placement-topRight {
+ padding-bottom: 10px
+}
+.wux-popover--placement-right,
+.wux-popover--placement-rightBottom,
+.wux-popover--placement-rightTop {
+ padding-left: 10px
+}
+.wux-popover--placement-bottom,
+.wux-popover--placement-bottomLeft,
+.wux-popover--placement-bottomRight {
+ padding-top: 10px
+}
+.wux-popover--placement-left,
+.wux-popover--placement-leftBottom,
+.wux-popover--placement-leftTop {
+ padding-right: 10px
+}
+.wux-popover__inner {
+ background-color: #fff;
+ background-clip: padding-box;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,.15);
+ color: rgba(0,0,0,.65)
+}
+.wux-popover__title {
+ position: relative;
+ min-width: 120px;
+ margin: 0;
+ padding: 5px 16px 4px;
+ min-height: 32px;
+ box-sizing: border-box;
+ color: rgba(0,0,0,.85);
+ font-weight: 500
+}
+.wux-popover__title::after {
+ content: " ";
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ height: 1PX;
+ border-bottom: 1PX solid #d9d9d9;
+ color: #d9d9d9;
+ transform-origin: 0 100%;
+ transform: scaleY(.5)
+}
+.wux-popover__inner-content {
+ padding: 24rpx 32rpx
+}
+.wux-popover__arrow {
+ width: 0;
+ height: 0;
+ position: absolute;
+ display: block;
+ border-color: transparent;
+ border-style: solid;
+ border-width: 16.97056274rpx
+}
+.wux-popover--placement-top .wux-popover__arrow,
+.wux-popover--placement-topLeft .wux-popover__arrow,
+.wux-popover--placement-topRight .wux-popover__arrow {
+ bottom: 8rpx;
+ border-bottom-width: 0;
+ border-top-color: #fff
+}
+.wux-popover--placement-top .wux-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%)
+}
+.wux-popover--placement-topLeft .wux-popover__arrow {
+ left: 32rpx
+}
+.wux-popover--placement-topRight .wux-popover__arrow {
+ right: 32rpx
+}
+.wux-popover--placement-right .wux-popover__arrow,
+.wux-popover--placement-rightBottom .wux-popover__arrow,
+.wux-popover--placement-rightTop .wux-popover__arrow {
+ left: 8rpx;
+ border-left-width: 0;
+ border-right-color: #fff
+}
+.wux-popover--placement-right .wux-popover__arrow {
+ top: 50%;
+ transform: translateY(-50%)
+}
+.wux-popover--placement-rightTop .wux-popover__arrow {
+ top: 24rpx
+}
+.wux-popover--placement-rightBottom .wux-popover__arrow {
+ bottom: 24rpx
+}
+.wux-popover--placement-bottom .wux-popover__arrow,
+.wux-popover--placement-bottomLeft .wux-popover__arrow,
+.wux-popover--placement-bottomRight .wux-popover__arrow {
+ top: 8rpx;
+ border-top-width: 0;
+ border-bottom-color: #fff
+}
+.wux-popover--placement-bottom .wux-popover__arrow {
+ left: 50%;
+ transform: translateX(-50%)
+}
+.wux-popover--placement-bottomLeft .wux-popover__arrow {
+ left: 32rpx
+}
+.wux-popover--placement-bottomRight .wux-popover__arrow {
+ right: 32rpx
+}
+.wux-popover--placement-left .wux-popover__arrow,
+.wux-popover--placement-leftBottom .wux-popover__arrow,
+.wux-popover--placement-leftTop .wux-popover__arrow {
+ right: 8rpx;
+ border-right-width: 0;
+ border-left-color: #fff
+}
+.wux-popover--placement-left .wux-popover__arrow {
+ top: 50%;
+ transform: translateY(-50%)
+}
+.wux-popover--placement-leftTop .wux-popover__arrow {
+ top: 24rpx
+}
+.wux-popover--placement-leftBottom .wux-popover__arrow {
+ bottom: 24rpx
+}
\ No newline at end of file
diff --git a/pages/storage/order-info/index.js b/pages/storage/order-info/index.js
index 412cf43..9277f05 100644
--- a/pages/storage/order-info/index.js
+++ b/pages/storage/order-info/index.js
@@ -90,7 +90,7 @@ Scene({
},
printOrderInfo: function(){
wx.showLoading({ title: '正在处理', mask: true })
- printOrder({id: this.data.form.id, printType: 3 }).then(result => {
+ printOrder({id: this.data.orderInfo.id, printType: 3 }).then(result => {
wx.hideLoading()
util.showToast('即将打印,请稍后')
}).catch(err => {