更新大理市民卡app
diff --git a/uview-ui/LICENSE b/uview-ui/LICENSE
new file mode 100644
index 0000000..8e39ead
--- /dev/null
+++ b/uview-ui/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 www.uviewui.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/uview-ui/README.md b/uview-ui/README.md
new file mode 100644
index 0000000..06d5676
--- /dev/null
+++ b/uview-ui/README.md
@@ -0,0 +1,106 @@
+<p align="center">
+ <img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
+</p>
+<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
+<h3 align="center">多平台快速开发的UI框架</h3>
+
+
+## 说明
+
+uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
+
+## 特性
+
+- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
+- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
+- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
+- 众多的常用页面和布局,让您专注逻辑,事半功倍
+- 详尽的文档支持,现代化的演示效果
+- 按需引入,精简打包体积
+
+
+## 安装
+
+```bash
+# npm方式安装
+npm i uview-ui
+```
+
+## 快速上手
+
+1. `main.js`引入uView库
+```js
+// main.js
+import uView from 'uview-ui';
+Vue.use(uView);
+```
+
+2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
+```css
+/* App.vue */
+<style lang="scss">
+@import "uview-ui/index.scss";
+</style>
+```
+
+3. `uni.scss`引入全局scss变量文件
+```css
+/* uni.scss */
+@import "uview-ui/theme.scss";
+```
+
+4. `pages.json`配置easycom规则(按需引入)
+
+```js
+// pages.json
+{
+ "easycom": {
+ // npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
+ // npm安装方式
+ "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
+ // 下载安装方式
+ // "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+ },
+ // 此为本身已有的内容
+ "pages": [
+ // ......
+ ]
+}
+```
+
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
+
+## 使用方法
+配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
+
+```html
+<template>
+ <u-button>按钮</u-button>
+</template>
+```
+
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
+
+## 链接
+
+- [官方文档](https://uviewui.com/)
+- [更新日志](https://uviewui.com/components/changelog.html)
+- [升级指南](https://uviewui.com/components/changelog.html)
+- [关于我们](https://uviewui.com/cooperation/about.html)
+
+## 预览
+
+您可以通过**微信**扫码,查看最佳的演示效果。
+<br>
+<br>
+<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
+
+<!-- ## 捐赠uView的研发
+
+uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
+
+<img src="https://uviewui.com/common/wechat.png" width="220" >
+<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
+ -->
+## 版权信息
+uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
diff --git a/uview-ui/components/u-action-sheet/u-action-sheet.vue b/uview-ui/components/u-action-sheet/u-action-sheet.vue
new file mode 100644
index 0000000..722b668
--- /dev/null
+++ b/uview-ui/components/u-action-sheet/u-action-sheet.vue
@@ -0,0 +1,190 @@
+<template>
+ <u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
+ length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
+ <view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
+ {{tips.text}}
+ </view>
+ <block v-for="(item, index) in list" :key="index">
+ <view
+ @touchmove.stop.prevent
+ @tap="itemClick(index)"
+ :style="[itemStyle(index)]"
+ class="u-action-sheet-item u-line-1"
+ :class="[index < list.length - 1 ? 'u-border-bottom' : '']"
+ :hover-stay-time="150"
+ >
+ <text>{{item.text}}</text>
+ <text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
+ </view>
+ </block>
+ <view class="u-gab" v-if="cancelBtn">
+ </view>
+ <view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
+ :hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
+ </u-popup>
+</template>
+
+<script>
+ /**
+ * actionSheet 操作菜单
+ * @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
+ * @tutorial https://www.uviewui.com/components/actionSheet.html
+ * @property {Array<Object>} list 按钮的文字数组,见官方文档示例
+ * @property {Object} tips 顶部的提示文字,见官方文档示例
+ * @property {String} cancel-text 取消按钮的提示文字
+ * @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
+ * @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
+ * @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Number String} z-index z-index值(默认1075)
+ * @property {String} cancel-text 取消按钮的提示文字
+ * @event {Function} click 点击ActionSheet列表项时触发
+ * @event {Function} close 点击取消按钮时触发
+ * @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
+ */
+ export default {
+ name: "u-action-sheet",
+ props: {
+ // 点击遮罩是否可以关闭actionsheet
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
+ list: {
+ type: Array,
+ default () {
+ // 如下
+ // return [{
+ // text: '确定',
+ // color: '',
+ // fontSize: ''
+ // }]
+ return [];
+ }
+ },
+ // 顶部的提示文字
+ tips: {
+ type: Object,
+ default () {
+ return {
+ text: '',
+ color: '',
+ fontSize: '26'
+ }
+ }
+ },
+ // 底部的取消按钮
+ cancelBtn: {
+ type: Boolean,
+ default: true
+ },
+ // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 弹出的顶部圆角值
+ borderRadius: {
+ type: [String, Number],
+ default: 0
+ },
+ // 弹出的z-index值
+ zIndex: {
+ type: [String, Number],
+ default: 0
+ },
+ // 取消按钮的文字提示
+ cancelText: {
+ type: String,
+ default: '取消'
+ }
+ },
+ computed: {
+ // 顶部提示的样式
+ tipsStyle() {
+ let style = {};
+ if (this.tips.color) style.color = this.tips.color;
+ if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
+ return style;
+ },
+ // 操作项目的样式
+ itemStyle() {
+ return (index) => {
+ let style = {};
+ if (this.list[index].color) style.color = this.list[index].color;
+ if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
+ // 选项被禁用的样式
+ if (this.list[index].disabled) style.color = '#c0c4cc';
+ return style;
+ }
+ },
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ methods: {
+ // 点击取消按钮
+ close() {
+ // 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
+ // 这是一个vue发送事件的特殊用法
+ this.popupClose();
+ this.$emit('close');
+ },
+ // 弹窗关闭
+ popupClose() {
+ this.$emit('input', false);
+ },
+ // 点击某一个item
+ itemClick(index) {
+ // disabled的项禁止点击
+ if(this.list[index].disabled) return;
+ this.$emit('click', index);
+ this.$emit('input', false);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-tips {
+ font-size: 26rpx;
+ text-align: center;
+ padding: 34rpx 0;
+ line-height: 1;
+ color: $u-tips-color;
+ }
+
+ .u-action-sheet-item {
+ @include vue-flex;;
+ line-height: 1;
+ justify-content: center;
+ align-items: center;
+ font-size: 32rpx;
+ padding: 34rpx 0;
+ flex-direction: column;
+ }
+
+ .u-action-sheet-item__subtext {
+ font-size: 24rpx;
+ color: $u-tips-color;
+ margin-top: 20rpx;
+ }
+
+ .u-gab {
+ height: 12rpx;
+ background-color: rgb(234, 234, 236);
+ }
+
+ .u-actionsheet-cancel {
+ color: $u-main-color;
+ }
+</style>
diff --git a/uview-ui/components/u-alert-tips/u-alert-tips.vue b/uview-ui/components/u-alert-tips/u-alert-tips.vue
new file mode 100644
index 0000000..e81fc37
--- /dev/null
+++ b/uview-ui/components/u-alert-tips/u-alert-tips.vue
@@ -0,0 +1,256 @@
+<template>
+ <view class="u-alert-tips" v-if="show" :class="[
+ !show ? 'u-close-alert-tips': '',
+ type ? 'u-alert-tips--bg--' + type + '-light' : '',
+ type ? 'u-alert-tips--border--' + type + '-disabled' : '',
+ ]" :style="{
+ backgroundColor: bgColor,
+ borderColor: borderColor
+ }">
+ <view class="u-icon-wrap">
+ <u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
+ </view>
+ <view class="u-alert-content" @tap.stop="click">
+ <view class="u-alert-title" :style="[uTitleStyle]">
+ {{title}}
+ </view>
+ <view v-if="description" class="u-alert-desc" :style="[descStyle]">
+ {{description}}
+ </view>
+ </view>
+ <view class="u-icon-wrap">
+ <u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
+ :size="22" class="u-close-icon" :style="{
+ top: description ? '18rpx' : '24rpx'
+ }"></u-icon>
+ </view>
+ <text v-if="closeAble && closeText" class="u-close-text" :style="{
+ top: description ? '18rpx' : '24rpx'
+ }">{{closeText}}</text>
+ </view>
+</template>
+
+<script>
+ /**
+ * alertTips 警告提示
+ * @description 警告提示,展现需要关注的信息
+ * @tutorial https://uviewui.com/components/alertTips.html
+ * @property {String} title 显示的标题文字
+ * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
+ * @property {String} type 关闭按钮(默认为叉号icon图标)
+ * @property {String} icon 图标名称
+ * @property {Object} icon-style 图标的样式,对象形式
+ * @property {Object} title-style 标题的样式,对象形式
+ * @property {Object} desc-style 描述的样式,对象形式
+ * @property {String} close-able 用文字替代关闭图标,close-able为true时有效
+ * @property {Boolean} show-icon 是否显示左边的辅助图标
+ * @property {Boolean} show 显示或隐藏组件
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 点击关闭按钮时触发
+ */
+ export default {
+ name: 'u-alert-tips',
+ props: {
+ // 显示文字
+ title: {
+ type: String,
+ default: ''
+ },
+ // 主题,success/warning/info/error
+ type: {
+ type: String,
+ default: 'warning'
+ },
+ // 辅助性文字
+ description: {
+ type: String,
+ default: ''
+ },
+ // 是否可关闭
+ closeAble: {
+ type: Boolean,
+ default: false
+ },
+ // 关闭按钮自定义文本
+ closeText: {
+ type: String,
+ default: ''
+ },
+ // 是否显示图标
+ showIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 文字颜色,如果定义了color值,icon会失效
+ color: {
+ type: String,
+ default: ''
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 边框颜色
+ borderColor: {
+ type: String,
+ default: ''
+ },
+ // 是否显示
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 左边显示的icon
+ icon: {
+ type: String,
+ default: ''
+ },
+ // icon的样式
+ iconStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 标题的样式
+ titleStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 描述文字的样式
+ descStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ },
+ data() {
+ return {
+ }
+ },
+ computed: {
+ uTitleStyle() {
+ let style = {};
+ // 如果有描述文字的话,标题进行加粗
+ style.fontWeight = this.description ? 500 : 'normal';
+ // 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
+ return this.$u.deepMerge(style, this.titleStyle);
+ },
+ uIcon() {
+ // 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
+ return this.icon ? this.icon : this.$u.type2icon(this.type);
+ },
+ uIconType() {
+ // 如果有设置图标的样式,优先使用,没有的话,则用type的样式
+ return Object.keys(this.iconStyle).length ? '' : this.type;
+ }
+ },
+ methods: {
+ // 点击内容
+ click() {
+ this.$emit('click');
+ },
+ // 点击关闭按钮
+ close() {
+ this.$emit('close');
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-alert-tips {
+ @include vue-flex;
+ align-items: center;
+ padding: 16rpx 30rpx;
+ border-radius: 8rpx;
+ position: relative;
+ transition: all 0.3s linear;
+ border: 1px solid #fff;
+
+ &--bg--primary-light {
+ background-color: $u-type-primary-light;
+ }
+
+ &--bg--info-light {
+ background-color: $u-type-info-light;
+ }
+
+ &--bg--success-light {
+ background-color: $u-type-success-light;
+ }
+
+ &--bg--warning-light {
+ background-color: $u-type-warning-light;
+ }
+
+ &--bg--error-light {
+ background-color: $u-type-error-light;
+ }
+
+ &--border--primary-disabled {
+ border-color: $u-type-primary-disabled;
+ }
+
+ &--border--success-disabled {
+ border-color: $u-type-success-disabled;
+ }
+
+ &--border--error-disabled {
+ border-color: $u-type-error-disabled;
+ }
+
+ &--border--warning-disabled {
+ border-color: $u-type-warning-disabled;
+ }
+
+ &--border--info-disabled {
+ border-color: $u-type-info-disabled;
+ }
+ }
+
+ .u-close-alert-tips {
+ opacity: 0;
+ visibility: hidden;
+ }
+
+ .u-icon {
+ margin-right: 16rpx;
+ }
+
+ .u-alert-title {
+ font-size: 28rpx;
+ color: $u-main-color;
+ }
+
+ .u-alert-desc {
+ font-size: 26rpx;
+ text-align: left;
+ color: $u-content-color;
+ }
+
+ .u-close-icon {
+ position: absolute;
+ top: 20rpx;
+ right: 20rpx;
+ }
+
+ .u-close-hover {
+ color: red;
+ }
+
+ .u-close-text {
+ font-size: 24rpx;
+ color: $u-tips-color;
+ position: absolute;
+ top: 20rpx;
+ right: 20rpx;
+ line-height: 1;
+ }
+</style>
diff --git a/uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue b/uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue
new file mode 100644
index 0000000..a48dd54
--- /dev/null
+++ b/uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue
@@ -0,0 +1,290 @@
+<template>
+ <view class="content">
+ <view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
+ <canvas
+ class="cropper"
+ :disable-scroll="true"
+ @touchstart="touchStart"
+ @touchmove="touchMove"
+ @touchend="touchEnd"
+ :style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
+ canvas-id="cropper"
+ id="cropper"
+ ></canvas>
+ <canvas
+ class="cropper"
+ :disable-scroll="true"
+ :style="{
+ position: 'fixed',
+ top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
+ width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ height: `${cropperOpt.height * cropperOpt.pixelRatio}`
+ }"
+ canvas-id="targetId"
+ id="targetId"
+ ></canvas>
+ </view>
+ <view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
+ <!-- #ifdef H5 -->
+ <view class="upload" @tap="uploadTap">选择图片</view>
+ <!-- #endif -->
+ <!-- #ifndef H5 -->
+ <view class="upload" @tap="uploadTap">重新选择</view>
+ <!-- #endif -->
+ <view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
+ </view>
+ </view>
+</template>
+
+<script>
+import WeCropper from './weCropper.js';
+export default {
+ props: {
+ // 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
+ // mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
+ boundStyle: {
+ type: Object,
+ default() {
+ return {
+ lineWidth: 4,
+ borderColor: 'rgb(245, 245, 245)',
+ mask: 'rgba(0, 0, 0, 0.35)'
+ };
+ }
+ }
+ // // 裁剪框宽度,单位rpx
+ // rectWidth: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 裁剪框高度,单位rpx
+ // rectHeight: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 输出图片宽度,单位rpx
+ // destWidth: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 输出图片高度,单位rpx
+ // destHeight: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
+ // fileType: {
+ // type: String,
+ // default: 'jpg',
+ // },
+ // // 生成的图片质量
+ // // H5上无效,目前不考虑使用此参数
+ // quality: {
+ // type: [Number, String],
+ // default: 1
+ // }
+ },
+ data() {
+ return {
+ // 底部导航的高度
+ bottomNavHeight: 50,
+ originWidth: 200,
+ width: 0,
+ height: 0,
+ cropperOpt: {
+ id: 'cropper',
+ targetId: 'targetCropper',
+ pixelRatio: 1,
+ width: 0,
+ height: 0,
+ scale: 2.5,
+ zoom: 8,
+ cut: {
+ x: (this.width - this.originWidth) / 2,
+ y: (this.height - this.originWidth) / 2,
+ width: this.originWidth,
+ height: this.originWidth
+ },
+ boundStyle: {
+ lineWidth: uni.upx2px(this.boundStyle.lineWidth),
+ mask: this.boundStyle.mask,
+ color: this.boundStyle.borderColor
+ }
+ },
+ // 裁剪框和输出图片的尺寸,高度默认等于宽度
+ // 输出图片宽度,单位px
+ destWidth: 200,
+ // 裁剪框宽度,单位px
+ rectWidth: 200,
+ // 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
+ fileType: 'jpg',
+ src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
+ };
+ },
+ onLoad(option) {
+ let rectInfo = uni.getSystemInfoSync();
+ this.width = rectInfo.windowWidth;
+ this.height = rectInfo.windowHeight - this.bottomNavHeight;
+ this.cropperOpt.width = this.width;
+ this.cropperOpt.height = this.height;
+ this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
+
+ if (option.destWidth) this.destWidth = option.destWidth;
+ if (option.rectWidth) {
+ let rectWidth = Number(option.rectWidth);
+ this.cropperOpt.cut = {
+ x: (this.width - rectWidth) / 2,
+ y: (this.height - rectWidth) / 2,
+ width: rectWidth,
+ height: rectWidth
+ };
+ }
+ this.rectWidth = option.rectWidth;
+ if (option.fileType) this.fileType = option.fileType;
+ // 初始化
+ this.cropper = new WeCropper(this.cropperOpt)
+ .on('ready', ctx => {
+ // wecropper is ready for work!
+ })
+ .on('beforeImageLoad', ctx => {
+ // before picture loaded, i can do something
+ })
+ .on('imageLoad', ctx => {
+ // picture loaded
+ })
+ .on('beforeDraw', (ctx, instance) => {
+ // before canvas draw,i can do something
+ });
+ // 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
+ uni.setNavigationBarColor({
+ frontColor: '#ffffff',
+ backgroundColor: '#000000'
+ });
+ uni.chooseImage({
+ count: 1, // 默认9
+ sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+ success: res => {
+ this.src = res.tempFilePaths[0];
+ // 获取裁剪图片资源后,给data添加src属性及其值
+ this.cropper.pushOrign(this.src);
+ }
+ });
+ },
+ methods: {
+ touchStart(e) {
+ this.cropper.touchStart(e);
+ },
+ touchMove(e) {
+ this.cropper.touchMove(e);
+ },
+ touchEnd(e) {
+ this.cropper.touchEnd(e);
+ },
+ getCropperImage(isPre = false) {
+ if(!this.src) return this.$u.toast('请先选择图片再裁剪');
+
+ let cropper_opt = {
+ destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
+ destWidth: Number(this.destWidth),
+ fileType: this.fileType
+ };
+ this.cropper.getCropperImage(cropper_opt, (path, err) => {
+ if (err) {
+ uni.showModal({
+ title: '温馨提示',
+ content: err.message
+ });
+ } else {
+ if (isPre) {
+ uni.previewImage({
+ current: '', // 当前显示图片的 http 链接
+ urls: [path] // 需要预览的图片 http 链接列表
+ });
+ } else {
+ uni.$emit('uAvatarCropper', path);
+ this.$u.route({
+ type: 'back'
+ });
+ }
+ }
+ });
+ },
+ uploadTap() {
+ const self = this;
+ uni.chooseImage({
+ count: 1, // 默认9
+ sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+ success: (res) => {
+ self.src = res.tempFilePaths[0];
+ // 获取裁剪图片资源后,给data添加src属性及其值
+
+ self.cropper.pushOrign(this.src);
+ }
+ });
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+
+.content {
+ background: rgba(255, 255, 255, 1);
+}
+
+.cropper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 11;
+}
+
+.cropper-buttons {
+ background-color: #000000;
+ color: #eee;
+}
+
+.cropper-wrapper {
+ position: relative;
+ @include vue-flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ background-color: #000;
+}
+
+.cropper-buttons {
+ width: 100vw;
+ @include vue-flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ font-size: 28rpx;
+}
+
+.cropper-buttons .upload,
+.cropper-buttons .getCropperImage {
+ width: 50%;
+ text-align: center;
+}
+
+.cropper-buttons .upload {
+ text-align: left;
+ padding-left: 50rpx;
+}
+
+.cropper-buttons .getCropperImage {
+ text-align: right;
+ padding-right: 50rpx;
+}
+</style>
diff --git a/uview-ui/components/u-avatar-cropper/weCropper.js b/uview-ui/components/u-avatar-cropper/weCropper.js
new file mode 100644
index 0000000..df02483
--- /dev/null
+++ b/uview-ui/components/u-avatar-cropper/weCropper.js
@@ -0,0 +1,1265 @@
+/**
+ * we-cropper v1.3.9
+ * (c) 2020 dlhandsome
+ * @license MIT
+ */
+(function(global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.WeCropper = factory());
+}(this, (function() {
+ 'use strict';
+
+ var device = void 0;
+ var TOUCH_STATE = ['touchstarted', 'touchmoved', 'touchended'];
+
+ function firstLetterUpper(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+ }
+
+ function setTouchState(instance) {
+ var arg = [],
+ len = arguments.length - 1;
+ while (len-- > 0) arg[len] = arguments[len + 1];
+
+ TOUCH_STATE.forEach(function(key, i) {
+ if (arg[i] !== undefined) {
+ instance[key] = arg[i];
+ }
+ });
+ }
+
+ function validator(instance, o) {
+ Object.defineProperties(instance, o);
+ }
+
+ function getDevice() {
+ if (!device) {
+ device = uni.getSystemInfoSync();
+ }
+ return device
+ }
+
+ var tmp = {};
+
+ var ref = getDevice();
+ var pixelRatio = ref.pixelRatio;
+
+ var DEFAULT = {
+ id: {
+ default: 'cropper',
+ get: function get() {
+ return tmp.id
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'string') {
+ console.error(("id:" + value + " is invalid"));
+ }
+ tmp.id = value;
+ }
+ },
+ width: {
+ default: 750,
+ get: function get() {
+ return tmp.width
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'number') {
+ console.error(("width:" + value + " is invalid"));
+ }
+ tmp.width = value;
+ }
+ },
+ height: {
+ default: 750,
+ get: function get() {
+ return tmp.height
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'number') {
+ console.error(("height:" + value + " is invalid"));
+ }
+ tmp.height = value;
+ }
+ },
+ pixelRatio: {
+ default: pixelRatio,
+ get: function get() {
+ return tmp.pixelRatio
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'number') {
+ console.error(("pixelRatio:" + value + " is invalid"));
+ }
+ tmp.pixelRatio = value;
+ }
+ },
+ scale: {
+ default: 2.5,
+ get: function get() {
+ return tmp.scale
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'number') {
+ console.error(("scale:" + value + " is invalid"));
+ }
+ tmp.scale = value;
+ }
+ },
+ zoom: {
+ default: 5,
+ get: function get() {
+ return tmp.zoom
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'number') {
+ console.error(("zoom:" + value + " is invalid"));
+ } else if (value < 0 || value > 10) {
+ console.error("zoom should be ranged in 0 ~ 10");
+ }
+ tmp.zoom = value;
+ }
+ },
+ src: {
+ default: '',
+ get: function get() {
+ return tmp.src
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'string') {
+ console.error(("src:" + value + " is invalid"));
+ }
+ tmp.src = value;
+ }
+ },
+ cut: {
+ default: {},
+ get: function get() {
+ return tmp.cut
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'object') {
+ console.error(("cut:" + value + " is invalid"));
+ }
+ tmp.cut = value;
+ }
+ },
+ boundStyle: {
+ default: {},
+ get: function get() {
+ return tmp.boundStyle
+ },
+ set: function set(value) {
+ if (typeof(value) !== 'object') {
+ console.error(("boundStyle:" + value + " is invalid"));
+ }
+ tmp.boundStyle = value;
+ }
+ },
+ onReady: {
+ default: null,
+ get: function get() {
+ return tmp.ready
+ },
+ set: function set(value) {
+ tmp.ready = value;
+ }
+ },
+ onBeforeImageLoad: {
+ default: null,
+ get: function get() {
+ return tmp.beforeImageLoad
+ },
+ set: function set(value) {
+ tmp.beforeImageLoad = value;
+ }
+ },
+ onImageLoad: {
+ default: null,
+ get: function get() {
+ return tmp.imageLoad
+ },
+ set: function set(value) {
+ tmp.imageLoad = value;
+ }
+ },
+ onBeforeDraw: {
+ default: null,
+ get: function get() {
+ return tmp.beforeDraw
+ },
+ set: function set(value) {
+ tmp.beforeDraw = value;
+ }
+ }
+ };
+
+ var ref$1 = getDevice();
+ var windowWidth = ref$1.windowWidth;
+
+ function prepare() {
+ var self = this;
+
+ // v1.4.0 版本中将不再自动绑定we-cropper实例
+ self.attachPage = function() {
+ var pages = getCurrentPages();
+ // 获取到当前page上下文
+ var pageContext = pages[pages.length - 1];
+ // 把this依附在Page上下文的wecropper属性上,便于在page钩子函数中访问
+ Object.defineProperty(pageContext, 'wecropper', {
+ get: function get() {
+ console.warn(
+ 'Instance will not be automatically bound to the page after v1.4.0\n\n' +
+ 'Please use a custom instance name instead\n\n' +
+ 'Example: \n' +
+ 'this.mycropper = new WeCropper(options)\n\n' +
+ '// ...\n' +
+ 'this.mycropper.getCropperImage()'
+ );
+ return self
+ },
+ configurable: true
+ });
+ };
+
+ self.createCtx = function() {
+ var id = self.id;
+ var targetId = self.targetId;
+
+ if (id) {
+ self.ctx = self.ctx || uni.createCanvasContext(id);
+ self.targetCtx = self.targetCtx || uni.createCanvasContext(targetId);
+ } else {
+ console.error("constructor: create canvas context failed, 'id' must be valuable");
+ }
+ };
+
+ self.deviceRadio = windowWidth / 750;
+ }
+
+ var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !==
+ 'undefined' ? self : {};
+
+
+
+
+
+ function createCommonjsModule(fn, module) {
+ return module = {
+ exports: {}
+ }, fn(module, module.exports), module.exports;
+ }
+
+ var tools = createCommonjsModule(function(module, exports) {
+ /**
+ * String type check
+ */
+ exports.isStr = function(v) {
+ return typeof v === 'string';
+ };
+ /**
+ * Number type check
+ */
+ exports.isNum = function(v) {
+ return typeof v === 'number';
+ };
+ /**
+ * Array type check
+ */
+ exports.isArr = Array.isArray;
+ /**
+ * undefined type check
+ */
+ exports.isUndef = function(v) {
+ return v === undefined;
+ };
+
+ exports.isTrue = function(v) {
+ return v === true;
+ };
+
+ exports.isFalse = function(v) {
+ return v === false;
+ };
+ /**
+ * Function type check
+ */
+ exports.isFunc = function(v) {
+ return typeof v === 'function';
+ };
+ /**
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ */
+ exports.isObj = exports.isObject = function(obj) {
+ return obj !== null && typeof obj === 'object'
+ };
+
+ /**
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ */
+ var _toString = Object.prototype.toString;
+ exports.isPlainObject = function(obj) {
+ return _toString.call(obj) === '[object Object]'
+ };
+
+ /**
+ * Check whether the object has the property.
+ */
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ exports.hasOwn = function(obj, key) {
+ return hasOwnProperty.call(obj, key)
+ };
+
+ /**
+ * Perform no operation.
+ * Stubbing args to make Flow happy without leaving useless transpiled code
+ * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)
+ */
+ exports.noop = function(a, b, c) {};
+
+ /**
+ * Check if val is a valid array index.
+ */
+ exports.isValidArrayIndex = function(val) {
+ var n = parseFloat(String(val));
+ return n >= 0 && Math.floor(n) === n && isFinite(val)
+ };
+ });
+
+ var tools_7 = tools.isFunc;
+ var tools_10 = tools.isPlainObject;
+
+ var EVENT_TYPE = ['ready', 'beforeImageLoad', 'beforeDraw', 'imageLoad'];
+
+ function observer() {
+ var self = this;
+
+ self.on = function(event, fn) {
+ if (EVENT_TYPE.indexOf(event) > -1) {
+ if (tools_7(fn)) {
+ event === 'ready' ?
+ fn(self) :
+ self[("on" + (firstLetterUpper(event)))] = fn;
+ }
+ } else {
+ console.error(("event: " + event + " is invalid"));
+ }
+ return self
+ };
+ }
+
+ function wxPromise(fn) {
+ return function(obj) {
+ var args = [],
+ len = arguments.length - 1;
+ while (len-- > 0) args[len] = arguments[len + 1];
+
+ if (obj === void 0) obj = {};
+ return new Promise(function(resolve, reject) {
+ obj.success = function(res) {
+ resolve(res);
+ };
+ obj.fail = function(err) {
+ reject(err);
+ };
+ fn.apply(void 0, [obj].concat(args));
+ })
+ }
+ }
+
+ function draw(ctx, reserve) {
+ if (reserve === void 0) reserve = false;
+
+ return new Promise(function(resolve) {
+ ctx.draw(reserve, resolve);
+ })
+ }
+
+ var getImageInfo = wxPromise(uni.getImageInfo);
+
+ var canvasToTempFilePath = wxPromise(uni.canvasToTempFilePath);
+
+ var base64 = createCommonjsModule(function(module, exports) {
+ /*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */
+ (function(root) {
+
+ // Detect free variables `exports`.
+ var freeExports = 'object' == 'object' && exports;
+
+ // Detect free variable `module`.
+ var freeModule = 'object' == 'object' && module &&
+ module.exports == freeExports && module;
+
+ // Detect free variable `global`, from Node.js or Browserified code, and use
+ // it as `root`.
+ var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal;
+ if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+ root = freeGlobal;
+ }
+
+ /*--------------------------------------------------------------------------*/
+
+ var InvalidCharacterError = function(message) {
+ this.message = message;
+ };
+ InvalidCharacterError.prototype = new Error;
+ InvalidCharacterError.prototype.name = 'InvalidCharacterError';
+
+ var error = function(message) {
+ // Note: the error messages used throughout this file match those used by
+ // the native `atob`/`btoa` implementation in Chromium.
+ throw new InvalidCharacterError(message);
+ };
+
+ var TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ // http://whatwg.org/html/common-microsyntaxes.html#space-character
+ var REGEX_SPACE_CHARACTERS = /[\t\n\f\r ]/g;
+
+ // `decode` is designed to be fully compatible with `atob` as described in the
+ // HTML Standard. http://whatwg.org/html/webappapis.html#dom-windowbase64-atob
+ // The optimized base64-decoding algorithm used is based on @atk’s excellent
+ // implementation. https://gist.github.com/atk/1020396
+ var decode = function(input) {
+ input = String(input)
+ .replace(REGEX_SPACE_CHARACTERS, '');
+ var length = input.length;
+ if (length % 4 == 0) {
+ input = input.replace(/==?$/, '');
+ length = input.length;
+ }
+ if (
+ length % 4 == 1 ||
+ // http://whatwg.org/C#alphanumeric-ascii-characters
+ /[^+a-zA-Z0-9/]/.test(input)
+ ) {
+ error(
+ 'Invalid character: the string to be decoded is not correctly encoded.'
+ );
+ }
+ var bitCounter = 0;
+ var bitStorage;
+ var buffer;
+ var output = '';
+ var position = -1;
+ while (++position < length) {
+ buffer = TABLE.indexOf(input.charAt(position));
+ bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
+ // Unless this is the first of a group of 4 characters…
+ if (bitCounter++ % 4) {
+ // …convert the first 8 bits to a single ASCII character.
+ output += String.fromCharCode(
+ 0xFF & bitStorage >> (-2 * bitCounter & 6)
+ );
+ }
+ }
+ return output;
+ };
+
+ // `encode` is designed to be fully compatible with `btoa` as described in the
+ // HTML Standard: http://whatwg.org/html/webappapis.html#dom-windowbase64-btoa
+ var encode = function(input) {
+ input = String(input);
+ if (/[^\0-\xFF]/.test(input)) {
+ // Note: no need to special-case astral symbols here, as surrogates are
+ // matched, and the input is supposed to only contain ASCII anyway.
+ error(
+ 'The string to be encoded contains characters outside of the ' +
+ 'Latin1 range.'
+ );
+ }
+ var padding = input.length % 3;
+ var output = '';
+ var position = -1;
+ var a;
+ var b;
+ var c;
+ var buffer;
+ // Make sure any padding is handled outside of the loop.
+ var length = input.length - padding;
+
+ while (++position < length) {
+ // Read three bytes, i.e. 24 bits.
+ a = input.charCodeAt(position) << 16;
+ b = input.charCodeAt(++position) << 8;
+ c = input.charCodeAt(++position);
+ buffer = a + b + c;
+ // Turn the 24 bits into four chunks of 6 bits each, and append the
+ // matching character for each of them to the output.
+ output += (
+ TABLE.charAt(buffer >> 18 & 0x3F) +
+ TABLE.charAt(buffer >> 12 & 0x3F) +
+ TABLE.charAt(buffer >> 6 & 0x3F) +
+ TABLE.charAt(buffer & 0x3F)
+ );
+ }
+
+ if (padding == 2) {
+ a = input.charCodeAt(position) << 8;
+ b = input.charCodeAt(++position);
+ buffer = a + b;
+ output += (
+ TABLE.charAt(buffer >> 10) +
+ TABLE.charAt((buffer >> 4) & 0x3F) +
+ TABLE.charAt((buffer << 2) & 0x3F) +
+ '='
+ );
+ } else if (padding == 1) {
+ buffer = input.charCodeAt(position);
+ output += (
+ TABLE.charAt(buffer >> 2) +
+ TABLE.charAt((buffer << 4) & 0x3F) +
+ '=='
+ );
+ }
+
+ return output;
+ };
+
+ var base64 = {
+ 'encode': encode,
+ 'decode': decode,
+ 'version': '0.1.0'
+ };
+
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ if (
+ typeof undefined == 'function' &&
+ typeof undefined.amd == 'object' &&
+ undefined.amd
+ ) {
+ undefined(function() {
+ return base64;
+ });
+ } else if (freeExports && !freeExports.nodeType) {
+ if (freeModule) { // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = base64;
+ } else { // in Narwhal or RingoJS v0.7.0-
+ for (var key in base64) {
+ base64.hasOwnProperty(key) && (freeExports[key] = base64[key]);
+ }
+ }
+ } else { // in Rhino or a web browser
+ root.base64 = base64;
+ }
+
+ }(commonjsGlobal));
+ });
+
+ function makeURI(strData, type) {
+ return 'data:' + type + ';base64,' + strData
+ }
+
+ function fixType(type) {
+ type = type.toLowerCase().replace(/jpg/i, 'jpeg');
+ var r = type.match(/png|jpeg|bmp|gif/)[0];
+ return 'image/' + r
+ }
+
+ function encodeData(data) {
+ var str = '';
+ if (typeof data === 'string') {
+ str = data;
+ } else {
+ for (var i = 0; i < data.length; i++) {
+ str += String.fromCharCode(data[i]);
+ }
+ }
+ return base64.encode(str)
+ }
+
+ /**
+ * 获取图像区域隐含的像素数据
+ * @param canvasId canvas标识
+ * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
+ * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
+ * @param width 将要被提取的图像数据矩形区域的宽度
+ * @param height 将要被提取的图像数据矩形区域的高度
+ * @param done 完成回调
+ */
+ function getImageData(canvasId, x, y, width, height, done) {
+ uni.canvasGetImageData({
+ canvasId: canvasId,
+ x: x,
+ y: y,
+ width: width,
+ height: height,
+ success: function success(res) {
+ done(res, null);
+ },
+ fail: function fail(res) {
+ done(null, res);
+ }
+ });
+ }
+
+ /**
+ * 生成bmp格式图片
+ * 按照规则生成图片响应头和响应体
+ * @param oData 用来描述 canvas 区域隐含的像素数据 { data, width, height } = oData
+ * @returns {*} base64字符串
+ */
+ function genBitmapImage(oData) {
+ //
+ // BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
+ // BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
+ //
+ var biWidth = oData.width;
+ var biHeight = oData.height;
+ var biSizeImage = biWidth * biHeight * 3;
+ var bfSize = biSizeImage + 54; // total header size = 54 bytes
+
+ //
+ // typedef struct tagBITMAPFILEHEADER {
+ // WORD bfType;
+ // DWORD bfSize;
+ // WORD bfReserved1;
+ // WORD bfReserved2;
+ // DWORD bfOffBits;
+ // } BITMAPFILEHEADER;
+ //
+ var BITMAPFILEHEADER = [
+ // WORD bfType -- The file type signature; must be "BM"
+ 0x42, 0x4D,
+ // DWORD bfSize -- The size, in bytes, of the bitmap file
+ bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
+ // WORD bfReserved1 -- Reserved; must be zero
+ 0, 0,
+ // WORD bfReserved2 -- Reserved; must be zero
+ 0, 0,
+ // DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
+ 54, 0, 0, 0
+ ];
+
+ //
+ // typedef struct tagBITMAPINFOHEADER {
+ // DWORD biSize;
+ // LONG biWidth;
+ // LONG biHeight;
+ // WORD biPlanes;
+ // WORD biBitCount;
+ // DWORD biCompression;
+ // DWORD biSizeImage;
+ // LONG biXPelsPerMeter;
+ // LONG biYPelsPerMeter;
+ // DWORD biClrUsed;
+ // DWORD biClrImportant;
+ // } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
+ //
+ var BITMAPINFOHEADER = [
+ // DWORD biSize -- The number of bytes required by the structure
+ 40, 0, 0, 0,
+ // LONG biWidth -- The width of the bitmap, in pixels
+ biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
+ // LONG biHeight -- The height of the bitmap, in pixels
+ biHeight & 0xff, biHeight >> 8 & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
+ // WORD biPlanes -- The number of planes for the target device. This value must be set to 1
+ 1, 0,
+ // WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
+ // has a maximum of 2^24 colors (16777216, Truecolor)
+ 24, 0,
+ // DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
+ 0, 0, 0, 0,
+ // DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
+ biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
+ // LONG biXPelsPerMeter, unused
+ 0, 0, 0, 0,
+ // LONG biYPelsPerMeter, unused
+ 0, 0, 0, 0,
+ // DWORD biClrUsed, the number of color indexes of palette, unused
+ 0, 0, 0, 0,
+ // DWORD biClrImportant, unused
+ 0, 0, 0, 0
+ ];
+
+ var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
+
+ var aImgData = oData.data;
+
+ var strPixelData = '';
+ var biWidth4 = biWidth << 2;
+ var y = biHeight;
+ var fromCharCode = String.fromCharCode;
+
+ do {
+ var iOffsetY = biWidth4 * (y - 1);
+ var strPixelRow = '';
+ for (var x = 0; x < biWidth; x++) {
+ var iOffsetX = x << 2;
+ strPixelRow += fromCharCode(aImgData[iOffsetY + iOffsetX + 2]) +
+ fromCharCode(aImgData[iOffsetY + iOffsetX + 1]) +
+ fromCharCode(aImgData[iOffsetY + iOffsetX]);
+ }
+
+ for (var c = 0; c < iPadding; c++) {
+ strPixelRow += String.fromCharCode(0);
+ }
+
+ strPixelData += strPixelRow;
+ } while (--y)
+
+ var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
+
+ return strEncoded
+ }
+
+ /**
+ * 转换为图片base64
+ * @param canvasId canvas标识
+ * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
+ * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
+ * @param width 将要被提取的图像数据矩形区域的宽度
+ * @param height 将要被提取的图像数据矩形区域的高度
+ * @param type 转换图片类型
+ * @param done 完成回调
+ */
+ function convertToImage(canvasId, x, y, width, height, type, done) {
+ if (done === void 0) done = function() {};
+
+ if (type === undefined) {
+ type = 'png';
+ }
+ type = fixType(type);
+ if (/bmp/.test(type)) {
+ getImageData(canvasId, x, y, width, height, function(data, err) {
+ var strData = genBitmapImage(data);
+ tools_7(done) && done(makeURI(strData, 'image/' + type), err);
+ });
+ } else {
+ console.error('暂不支持生成\'' + type + '\'类型的base64图片');
+ }
+ }
+
+ var CanvasToBase64 = {
+ convertToImage: convertToImage,
+ // convertToPNG: function (width, height, done) {
+ // return convertToImage(width, height, 'png', done)
+ // },
+ // convertToJPEG: function (width, height, done) {
+ // return convertToImage(width, height, 'jpeg', done)
+ // },
+ // convertToGIF: function (width, height, done) {
+ // return convertToImage(width, height, 'gif', done)
+ // },
+ convertToBMP: function(ref, done) {
+ if (ref === void 0) ref = {};
+ var canvasId = ref.canvasId;
+ var x = ref.x;
+ var y = ref.y;
+ var width = ref.width;
+ var height = ref.height;
+ if (done === void 0) done = function() {};
+
+ return convertToImage(canvasId, x, y, width, height, 'bmp', done)
+ }
+ };
+
+ function methods() {
+ var self = this;
+
+ var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
+ var boundHeight = self.height; // 裁剪框默认高度,即整个画布高度
+
+ var id = self.id;
+ var targetId = self.targetId;
+ var pixelRatio = self.pixelRatio;
+
+ var ref = self.cut;
+ var x = ref.x;
+ if (x === void 0) x = 0;
+ var y = ref.y;
+ if (y === void 0) y = 0;
+ var width = ref.width;
+ if (width === void 0) width = boundWidth;
+ var height = ref.height;
+ if (height === void 0) height = boundHeight;
+
+ self.updateCanvas = function(done) {
+ if (self.croperTarget) {
+ // 画布绘制图片
+ self.ctx.drawImage(
+ self.croperTarget,
+ self.imgLeft,
+ self.imgTop,
+ self.scaleWidth,
+ self.scaleHeight
+ );
+ }
+ tools_7(self.onBeforeDraw) && self.onBeforeDraw(self.ctx, self);
+
+ self.setBoundStyle(self.boundStyle); // 设置边界样式
+
+ self.ctx.draw(false, done);
+ return self
+ };
+
+ self.pushOrigin = self.pushOrign = function(src) {
+ self.src = src;
+
+ tools_7(self.onBeforeImageLoad) && self.onBeforeImageLoad(self.ctx, self);
+
+ return getImageInfo({
+ src: src
+ })
+ .then(function(res) {
+ var innerAspectRadio = res.width / res.height;
+ var customAspectRadio = width / height;
+
+ self.croperTarget = res.path;
+
+ if (innerAspectRadio < customAspectRadio) {
+ self.rectX = x;
+ self.baseWidth = width;
+ self.baseHeight = width / innerAspectRadio;
+ self.rectY = y - Math.abs((height - self.baseHeight) / 2);
+ } else {
+ self.rectY = y;
+ self.baseWidth = height * innerAspectRadio;
+ self.baseHeight = height;
+ self.rectX = x - Math.abs((width - self.baseWidth) / 2);
+ }
+
+ self.imgLeft = self.rectX;
+ self.imgTop = self.rectY;
+ self.scaleWidth = self.baseWidth;
+ self.scaleHeight = self.baseHeight;
+
+ self.update();
+
+ return new Promise(function(resolve) {
+ self.updateCanvas(resolve);
+ })
+ })
+ .then(function() {
+ tools_7(self.onImageLoad) && self.onImageLoad(self.ctx, self);
+ })
+ };
+
+ self.removeImage = function() {
+ self.src = '';
+ self.croperTarget = '';
+ return draw(self.ctx)
+ };
+
+ self.getCropperBase64 = function(done) {
+ if (done === void 0) done = function() {};
+
+ CanvasToBase64.convertToBMP({
+ canvasId: id,
+ x: x,
+ y: y,
+ width: width,
+ height: height
+ }, done);
+ };
+
+ self.getCropperImage = function(opt, fn) {
+ var customOptions = opt;
+
+ var canvasOptions = {
+ canvasId: id,
+ x: x,
+ y: y,
+ width: width,
+ height: height
+ };
+
+ var task = function() {
+ return Promise.resolve();
+ };
+
+ if (
+ tools_10(customOptions) &&
+ customOptions.original
+ ) {
+ // original mode
+ task = function() {
+ self.targetCtx.drawImage(
+ self.croperTarget,
+ self.imgLeft * pixelRatio,
+ self.imgTop * pixelRatio,
+ self.scaleWidth * pixelRatio,
+ self.scaleHeight * pixelRatio
+ );
+
+ canvasOptions = {
+ canvasId: targetId,
+ x: x * pixelRatio,
+ y: y * pixelRatio,
+ width: width * pixelRatio,
+ height: height * pixelRatio
+ };
+
+ return draw(self.targetCtx)
+ };
+ }
+
+ return task()
+ .then(function() {
+ if (tools_10(customOptions)) {
+ canvasOptions = Object.assign({}, canvasOptions, customOptions);
+ }
+
+ if (tools_7(customOptions)) {
+ fn = customOptions;
+ }
+
+ var arg = canvasOptions.componentContext ?
+ [canvasOptions, canvasOptions.componentContext] :
+ [canvasOptions];
+
+ return canvasToTempFilePath.apply(null, arg)
+ })
+ .then(function(res) {
+ var tempFilePath = res.tempFilePath;
+
+ return tools_7(fn) ?
+ fn.call(self, tempFilePath, null) :
+ tempFilePath
+ })
+ .catch(function(err) {
+ if (tools_7(fn)) {
+ fn.call(self, null, err);
+ } else {
+ throw err
+ }
+ })
+ };
+ }
+
+ /**
+ * 获取最新缩放值
+ * @param oldScale 上一次触摸结束后的缩放值
+ * @param oldDistance 上一次触摸结束后的双指距离
+ * @param zoom 缩放系数
+ * @param touch0 第一指touch对象
+ * @param touch1 第二指touch对象
+ * @returns {*}
+ */
+ var getNewScale = function(oldScale, oldDistance, zoom, touch0, touch1) {
+ var xMove, yMove, newDistance;
+ // 计算二指最新距离
+ xMove = Math.round(touch1.x - touch0.x);
+ yMove = Math.round(touch1.y - touch0.y);
+ newDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+
+ return oldScale + 0.001 * zoom * (newDistance - oldDistance)
+ };
+
+ function update() {
+ var self = this;
+
+ if (!self.src) {
+ return
+ }
+
+ self.__oneTouchStart = function(touch) {
+ self.touchX0 = Math.round(touch.x);
+ self.touchY0 = Math.round(touch.y);
+ };
+
+ self.__oneTouchMove = function(touch) {
+ var xMove, yMove;
+ // 计算单指移动的距离
+ if (self.touchended) {
+ return self.updateCanvas()
+ }
+ xMove = Math.round(touch.x - self.touchX0);
+ yMove = Math.round(touch.y - self.touchY0);
+
+ var imgLeft = Math.round(self.rectX + xMove);
+ var imgTop = Math.round(self.rectY + yMove);
+
+ self.outsideBound(imgLeft, imgTop);
+
+ self.updateCanvas();
+ };
+
+ self.__twoTouchStart = function(touch0, touch1) {
+ var xMove, yMove, oldDistance;
+
+ self.touchX1 = Math.round(self.rectX + self.scaleWidth / 2);
+ self.touchY1 = Math.round(self.rectY + self.scaleHeight / 2);
+
+ // 计算两指距离
+ xMove = Math.round(touch1.x - touch0.x);
+ yMove = Math.round(touch1.y - touch0.y);
+ oldDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+
+ self.oldDistance = oldDistance;
+ };
+
+ self.__twoTouchMove = function(touch0, touch1) {
+ var oldScale = self.oldScale;
+ var oldDistance = self.oldDistance;
+ var scale = self.scale;
+ var zoom = self.zoom;
+
+ self.newScale = getNewScale(oldScale, oldDistance, zoom, touch0, touch1);
+
+ // 设定缩放范围
+ self.newScale <= 1 && (self.newScale = 1);
+ self.newScale >= scale && (self.newScale = scale);
+
+ self.scaleWidth = Math.round(self.newScale * self.baseWidth);
+ self.scaleHeight = Math.round(self.newScale * self.baseHeight);
+ var imgLeft = Math.round(self.touchX1 - self.scaleWidth / 2);
+ var imgTop = Math.round(self.touchY1 - self.scaleHeight / 2);
+
+ self.outsideBound(imgLeft, imgTop);
+
+ self.updateCanvas();
+ };
+
+ self.__xtouchEnd = function() {
+ self.oldScale = self.newScale;
+ self.rectX = self.imgLeft;
+ self.rectY = self.imgTop;
+ };
+ }
+
+ var handle = {
+ // 图片手势初始监测
+ touchStart: function touchStart(e) {
+ var self = this;
+ var ref = e.touches;
+ var touch0 = ref[0];
+ var touch1 = ref[1];
+
+ if (!self.src) {
+ return
+ }
+
+ setTouchState(self, true, null, null);
+
+ // 计算第一个触摸点的位置,并参照改点进行缩放
+ self.__oneTouchStart(touch0);
+
+ // 两指手势触发
+ if (e.touches.length >= 2) {
+ self.__twoTouchStart(touch0, touch1);
+ }
+ },
+
+ // 图片手势动态缩放
+ touchMove: function touchMove(e) {
+ var self = this;
+ var ref = e.touches;
+ var touch0 = ref[0];
+ var touch1 = ref[1];
+
+ if (!self.src) {
+ return
+ }
+
+ setTouchState(self, null, true);
+
+ // 单指手势时触发
+ if (e.touches.length === 1) {
+ self.__oneTouchMove(touch0);
+ }
+ // 两指手势触发
+ if (e.touches.length >= 2) {
+ self.__twoTouchMove(touch0, touch1);
+ }
+ },
+
+ touchEnd: function touchEnd(e) {
+ var self = this;
+
+ if (!self.src) {
+ return
+ }
+
+ setTouchState(self, false, false, true);
+ self.__xtouchEnd();
+ }
+ };
+
+ function cut() {
+ var self = this;
+ var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
+ var boundHeight = self.height;
+ // 裁剪框默认高度,即整个画布高度
+ var ref = self.cut;
+ var x = ref.x;
+ if (x === void 0) x = 0;
+ var y = ref.y;
+ if (y === void 0) y = 0;
+ var width = ref.width;
+ if (width === void 0) width = boundWidth;
+ var height = ref.height;
+ if (height === void 0) height = boundHeight;
+
+ /**
+ * 设置边界
+ * @param imgLeft 图片左上角横坐标值
+ * @param imgTop 图片左上角纵坐标值
+ */
+ self.outsideBound = function(imgLeft, imgTop) {
+ self.imgLeft = imgLeft >= x ?
+ x :
+ self.scaleWidth + imgLeft - x <= width ?
+ x + width - self.scaleWidth :
+ imgLeft;
+
+ self.imgTop = imgTop >= y ?
+ y :
+ self.scaleHeight + imgTop - y <= height ?
+ y + height - self.scaleHeight :
+ imgTop;
+ };
+
+ /**
+ * 设置边界样式
+ * @param color 边界颜色
+ */
+ self.setBoundStyle = function(ref) {
+ if (ref === void 0) ref = {};
+ var color = ref.color;
+ if (color === void 0) color = '#04b00f';
+ var mask = ref.mask;
+ if (mask === void 0) mask = 'rgba(0, 0, 0, 0.3)';
+ var lineWidth = ref.lineWidth;
+ if (lineWidth === void 0) lineWidth = 1;
+
+ var half = lineWidth / 2;
+ var boundOption = [{
+ start: {
+ x: x - half,
+ y: y + 10 - half
+ },
+ step1: {
+ x: x - half,
+ y: y - half
+ },
+ step2: {
+ x: x + 10 - half,
+ y: y - half
+ }
+ },
+ {
+ start: {
+ x: x - half,
+ y: y + height - 10 + half
+ },
+ step1: {
+ x: x - half,
+ y: y + height + half
+ },
+ step2: {
+ x: x + 10 - half,
+ y: y + height + half
+ }
+ },
+ {
+ start: {
+ x: x + width - 10 + half,
+ y: y - half
+ },
+ step1: {
+ x: x + width + half,
+ y: y - half
+ },
+ step2: {
+ x: x + width + half,
+ y: y + 10 - half
+ }
+ },
+ {
+ start: {
+ x: x + width + half,
+ y: y + height - 10 + half
+ },
+ step1: {
+ x: x + width + half,
+ y: y + height + half
+ },
+ step2: {
+ x: x + width - 10 + half,
+ y: y + height + half
+ }
+ }
+ ];
+
+ // 绘制半透明层
+ self.ctx.beginPath();
+ self.ctx.setFillStyle(mask);
+ self.ctx.fillRect(0, 0, x, boundHeight);
+ self.ctx.fillRect(x, 0, width, y);
+ self.ctx.fillRect(x, y + height, width, boundHeight - y - height);
+ self.ctx.fillRect(x + width, 0, boundWidth - x - width, boundHeight);
+ self.ctx.fill();
+
+ boundOption.forEach(function(op) {
+ self.ctx.beginPath();
+ self.ctx.setStrokeStyle(color);
+ self.ctx.setLineWidth(lineWidth);
+ self.ctx.moveTo(op.start.x, op.start.y);
+ self.ctx.lineTo(op.step1.x, op.step1.y);
+ self.ctx.lineTo(op.step2.x, op.step2.y);
+ self.ctx.stroke();
+ });
+ };
+ }
+
+ var version = "1.3.9";
+
+ var WeCropper = function WeCropper(params) {
+ var self = this;
+ var _default = {};
+
+ validator(self, DEFAULT);
+
+ Object.keys(DEFAULT).forEach(function(key) {
+ _default[key] = DEFAULT[key].default;
+ });
+ Object.assign(self, _default, params);
+
+ self.prepare();
+ self.attachPage();
+ self.createCtx();
+ self.observer();
+ self.cutt();
+ self.methods();
+ self.init();
+ self.update();
+
+ return self
+ };
+
+ WeCropper.prototype.init = function init() {
+ var self = this;
+ var src = self.src;
+
+ self.version = version;
+
+ typeof self.onReady === 'function' && self.onReady(self.ctx, self);
+
+ if (src) {
+ self.pushOrign(src);
+ } else {
+ self.updateCanvas();
+ }
+ setTouchState(self, false, false, false);
+
+ self.oldScale = 1;
+ self.newScale = 1;
+
+ return self
+ };
+
+ Object.assign(WeCropper.prototype, handle);
+
+ WeCropper.prototype.prepare = prepare;
+ WeCropper.prototype.observer = observer;
+ WeCropper.prototype.methods = methods;
+ WeCropper.prototype.cutt = cut;
+ WeCropper.prototype.update = update;
+
+ return WeCropper;
+
+})));
diff --git a/uview-ui/components/u-avatar/u-avatar.vue b/uview-ui/components/u-avatar/u-avatar.vue
new file mode 100644
index 0000000..f92d318
--- /dev/null
+++ b/uview-ui/components/u-avatar/u-avatar.vue
@@ -0,0 +1,244 @@
+<template>
+ <view class="u-avatar" :style="[wrapStyle]" @tap="click">
+ <image
+ @error="loadError"
+ :style="[imgStyle]"
+ class="u-avatar__img"
+ v-if="!uText && avatar"
+ :src="avatar"
+ :mode="imgMode"
+ ></image>
+ <text class="u-line-1" v-else-if="uText" :style="{
+ fontSize: '38rpx'
+ }">{{uText}}</text>
+ <slot v-else></slot>
+ <view class="u-avatar__sex" v-if="showSex" :class="['u-avatar__sex--' + sexIcon]" :style="[uSexStyle]">
+ <u-icon :name="sexIcon" size="20"></u-icon>
+ </view>
+ <view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
+ <u-icon :name="levelIcon" size="20"></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>
+ let base64Avatar = "data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYG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";
+ /**
+ * avatar 头像
+ * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+ * @tutorial https://www.uviewui.com/components/avatar.html
+ * @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff)
+ * @property {String} src 头像路径,如加载失败,将会显示默认头像
+ * @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值,单位rpx(默认default)
+ * @property {String} mode 显示类型,见上方说明(默认circle)
+ * @property {String} sex-icon 性别图标,man-男,woman-女(默认man)
+ * @property {String} level-icon 等级图标(默认level)
+ * @property {String} sex-bg-color 性别图标背景颜色
+ * @property {String} level-bg-color 等级图标背景颜色
+ * @property {String} show-sex 是否显示性别图标(默认false)
+ * @property {String} show-level 是否显示等级图标(默认false)
+ * @property {String} img-mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值(默认aspectFill)
+ * @property {String} index 用户传递的标识符值,如果是列表循环,可穿v-for的index值
+ * @event {Function} click 头像被点击
+ * @example <u-avatar :src="src"></u-avatar>
+ */
+ export default {
+ name: 'u-avatar',
+ props: {
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: 'transparent'
+ },
+ // 头像路径
+ src: {
+ type: String,
+ default: ''
+ },
+ // 尺寸,large-大,default-中等,mini-小,如果为数值,则单位为rpx
+ // 宽度等于高度
+ size: {
+ type: [String, Number],
+ default: 'default'
+ },
+ // 头像模型,square-带圆角方形,circle-圆形
+ mode: {
+ type: String,
+ default: 'circle'
+ },
+ // 文字内容
+ text: {
+ type: String,
+ default: ''
+ },
+ // 图片的裁剪模型
+ imgMode: {
+ type: String,
+ default: 'aspectFill'
+ },
+ // 标识符
+ index: {
+ type: [String, Number],
+ default: ''
+ },
+ // 右上角性别角标,man-男,woman-女
+ sexIcon: {
+ type: String,
+ default: 'man'
+ },
+ // 右下角的等级图标
+ levelIcon: {
+ type: String,
+ default: 'level'
+ },
+ // 右下角等级图标背景颜色
+ levelBgColor: {
+ type: String,
+ default: ''
+ },
+ // 右上角性别图标的背景颜色
+ sexBgColor: {
+ type: String,
+ default: ''
+ },
+ // 是否显示性别图标
+ showSex: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示等级图标
+ showLevel: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ error: false,
+ // 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值
+ avatar: this.src ? this.src : base64Avatar,
+ }
+ },
+ watch: {
+ src(n) {
+ // 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
+ if(!n) {
+ // 如果传入null或者'',或者undefined,显示默认头像
+ this.avatar = base64Avatar;
+ this.error = true;
+ } else {
+ this.avatar = n;
+ this.error = false;
+ }
+ }
+ },
+ computed: {
+ wrapStyle() {
+ let style = {};
+ style.height = this.size == 'large' ? '120rpx' : this.size == 'default' ?
+ '90rpx' : this.size == 'mini' ? '70rpx' : this.size + 'rpx';
+ style.width = style.height;
+ style.flex = `0 0 ${style.height}`;
+ style.backgroundColor = this.bgColor;
+ style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
+ if(this.text) style.padding = `0 6rpx`;
+ return style;
+ },
+ imgStyle() {
+ let style = {};
+ style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
+ return style;
+ },
+ // 取字符串的第一个字符
+ uText() {
+ return String(this.text)[0];
+ },
+ // 性别图标的自定义样式
+ uSexStyle() {
+ let style = {};
+ if(this.sexBgColor) style.backgroundColor = this.sexBgColor;
+ return style;
+ },
+ // 等级图标的自定义样式
+ uLevelStyle() {
+ let style = {};
+ if(this.levelBgColor) style.backgroundColor = this.levelBgColor;
+ return style;
+ }
+ },
+ methods: {
+ // 图片加载错误时,显示默认头像
+ loadError() {
+ this.error = true;
+ this.avatar = base64Avatar;
+ },
+ click() {
+ this.$emit('click', this.index);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-avatar {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: $u-content-color;
+ border-radius: 10px;
+ position: relative;
+
+ &__img {
+ width: 100%;
+ height: 100%;
+ }
+
+ &__sex {
+ position: absolute;
+ width: 32rpx;
+ color: #ffffff;
+ height: 32rpx;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 100rpx;
+ top: 5%;
+ z-index: 1;
+ right: -7%;
+ border: 1px #ffffff solid;
+
+ &--man {
+ background-color: $u-type-primary;
+ }
+
+ &--woman {
+ background-color: $u-type-error;
+ }
+
+ &--none {
+ background-color: $u-type-warning;
+ }
+ }
+
+ &__level {
+ position: absolute;
+ width: 32rpx;
+ color: #ffffff;
+ height: 32rpx;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 100rpx;
+ bottom: 5%;
+ z-index: 1;
+ right: -7%;
+ border: 1px #ffffff solid;
+ background-color: $u-type-warning;
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-back-top/u-back-top.vue b/uview-ui/components/u-back-top/u-back-top.vue
new file mode 100644
index 0000000..7970fc7
--- /dev/null
+++ b/uview-ui/components/u-back-top/u-back-top.vue
@@ -0,0 +1,153 @@
+<template>
+ <view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
+ bottom: bottom + 'rpx',
+ right: right + 'rpx',
+ borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
+ zIndex: uZIndex,
+ opacity: opacity
+ }, customStyle]">
+ <view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
+ <u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
+ <view class="u-back-top__content__tips">
+ {{tips}}
+ </view>
+ </view>
+ <slot v-else />
+ </view>
+</template>
+
+<script>
+ export default {
+ name: 'u-back-top',
+ props: {
+ // 返回顶部的形状,circle-圆形,square-方形
+ mode: {
+ type: String,
+ default: 'circle'
+ },
+ // 自定义图标
+ icon: {
+ type: String,
+ default: 'arrow-upward'
+ },
+ // 提示文字
+ tips: {
+ type: String,
+ default: ''
+ },
+ // 返回顶部滚动时间
+ duration: {
+ type: [Number, String],
+ default: 100
+ },
+ // 滚动距离
+ scrollTop: {
+ type: [Number, String],
+ default: 0
+ },
+ // 距离顶部多少距离显示,单位rpx
+ top: {
+ type: [Number, String],
+ default: 400
+ },
+ // 返回顶部按钮到底部的距离,单位rpx
+ bottom: {
+ type: [Number, String],
+ default: 200
+ },
+ // 返回顶部按钮到右边的距离,单位rpx
+ right: {
+ type: [Number, String],
+ default: 40
+ },
+ // 层级
+ zIndex: {
+ type: [Number, String],
+ default: '9'
+ },
+ // 图标的样式,对象形式
+ iconStyle: {
+ type: Object,
+ default() {
+ return {
+ color: '#909399',
+ fontSize: '38rpx'
+ }
+ }
+ },
+ // 整个组件的样式
+ customStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ }
+ },
+ watch: {
+ showBackTop(nVal, oVal) {
+ // 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
+ // 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
+ if(nVal) {
+ this.uZIndex = this.zIndex;
+ this.opacity = 1;
+ } else {
+ this.uZIndex = -1;
+ this.opacity = 0;
+ }
+ }
+ },
+ computed: {
+ showBackTop() {
+ // 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
+ // 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
+ return this.scrollTop > uni.upx2px(this.top);
+ },
+ },
+ data() {
+ return {
+ // 不透明度,为了让组件有一个显示和隐藏的过渡动画
+ opacity: 0,
+ // 组件的z-index值,隐藏时设置为-1,就会看不到
+ uZIndex: -1
+ }
+ },
+ methods: {
+ backToTop() {
+ uni.pageScrollTo({
+ scrollTop: 0,
+ duration: this.duration
+ });
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-back-top {
+ width: 80rpx;
+ height: 80rpx;
+ position: fixed;
+ z-index: 9;
+ @include vue-flex;
+ flex-direction: column;
+ justify-content: center;
+ background-color: #E1E1E1;
+ color: $u-content-color;
+ align-items: center;
+ transition: opacity 0.4s;
+
+ &__content {
+ @include vue-flex;
+ flex-direction: column;
+ align-items: center;
+
+ &__tips {
+ font-size: 24rpx;
+ transform: scale(0.8);
+ line-height: 1;
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-badge/u-badge.vue b/uview-ui/components/u-badge/u-badge.vue
new file mode 100644
index 0000000..e85b133
--- /dev/null
+++ b/uview-ui/components/u-badge/u-badge.vue
@@ -0,0 +1,216 @@
+<template>
+ <view v-if="show" class="u-badge" :class="[
+ isDot ? 'u-badge-dot' : '',
+ size == 'mini' ? 'u-badge-mini' : '',
+ type ? 'u-badge--bg--' + type : ''
+ ]" :style="[{
+ top: offset[0] + 'rpx',
+ right: offset[1] + 'rpx',
+ fontSize: fontSize + 'rpx',
+ position: absolute ? 'absolute' : 'static',
+ color: color,
+ backgroundColor: bgColor
+ }, boxStyle]"
+ >
+ {{showText}}
+ </view>
+</template>
+
+<script>
+ /**
+ * badge 角标
+ * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+ * @tutorial https://www.uviewui.com/components/badge.html
+ * @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
+ * @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
+ * @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
+ * @property {String Number} overflow-count 展示封顶的数字值(默认99)
+ * @property {String} type 使用预设的背景颜色(默认error)
+ * @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
+ * @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
+ * @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
+ * @property {String} color 字体颜色(默认#ffffff)
+ * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
+ * @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
+ * @example <u-badge type="error" count="7"></u-badge>
+ */
+ export default {
+ name: 'u-badge',
+ props: {
+ // primary,warning,success,error,info
+ type: {
+ type: String,
+ default: 'error'
+ },
+ // default, mini
+ size: {
+ type: String,
+ default: 'default'
+ },
+ //是否是圆点
+ isDot: {
+ type: Boolean,
+ default: false
+ },
+ // 显示的数值内容
+ count: {
+ type: [Number, String],
+ },
+ // 展示封顶的数字值
+ overflowCount: {
+ type: Number,
+ default: 99
+ },
+ // 当数值为 0 时,是否展示 Badge
+ showZero: {
+ type: Boolean,
+ default: false
+ },
+ // 位置偏移
+ offset: {
+ type: Array,
+ default: () => {
+ return [20, 20]
+ }
+ },
+ // 是否开启绝对定位,开启了offset才会起作用
+ absolute: {
+ type: Boolean,
+ default: true
+ },
+ // 字体大小
+ fontSize: {
+ type: [String, Number],
+ default: '24'
+ },
+ // 字体演示
+ color: {
+ type: String,
+ default: '#ffffff'
+ },
+ // badge的背景颜色
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
+ isCenter: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ // 是否将badge中心与父组件右上角重合
+ boxStyle() {
+ let style = {};
+ if(this.isCenter) {
+ style.top = 0;
+ style.right = 0;
+ // Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
+ style.transform = "translateY(-50%) translateX(50%)";
+ } else {
+ style.top = this.offset[0] + 'rpx';
+ style.right = this.offset[1] + 'rpx';
+ style.transform = "translateY(0) translateX(0)";
+ }
+ // 如果尺寸为mini,后接上scal()
+ if(this.size == 'mini') {
+ style.transform = style.transform + " scale(0.8)";
+ }
+ return style;
+ },
+ // isDot类型时,不显示文字
+ showText() {
+ if(this.isDot) return '';
+ else {
+ if(this.count > this.overflowCount) return `${this.overflowCount}+`;
+ else return this.count;
+ }
+ },
+ // 是否显示组件
+ show() {
+ // 如果count的值为0,并且showZero设置为false,不显示组件
+ if(this.count == 0 && this.showZero == false) return false;
+ else return true;
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-badge {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ justify-content: center;
+ align-items: center;
+ line-height: 24rpx;
+ padding: 4rpx 8rpx;
+ border-radius: 100rpx;
+ z-index: 9;
+
+ &--bg--primary {
+ background-color: $u-type-primary;
+ }
+
+ &--bg--error {
+ background-color: $u-type-error;
+ }
+
+ &--bg--success {
+ background-color: $u-type-success;
+ }
+
+ &--bg--info {
+ background-color: $u-type-info;
+ }
+
+ &--bg--warning {
+ background-color: $u-type-warning;
+ }
+ }
+
+ .u-badge-dot {
+ height: 16rpx;
+ width: 16rpx;
+ border-radius: 100rpx;
+ line-height: 1;
+ }
+
+ .u-badge-mini {
+ transform: scale(0.8);
+ transform-origin: center center;
+ }
+
+ // .u-primary {
+ // background: $u-type-primary;
+ // color: #fff;
+ // }
+
+ // .u-error {
+ // background: $u-type-error;
+ // color: #fff;
+ // }
+
+ // .u-warning {
+ // background: $u-type-warning;
+ // color: #fff;
+ // }
+
+ // .u-success {
+ // background: $u-type-success;
+ // color: #fff;
+ // }
+
+ // .u-black {
+ // background: #585858;
+ // color: #fff;
+ // }
+
+ .u-info {
+ background-color: $u-type-info;
+ color: #fff;
+ }
+</style>
\ No newline at end of file
diff --git a/uview-ui/components/u-button/u-button.vue b/uview-ui/components/u-button/u-button.vue
new file mode 100644
index 0000000..82c3a6f
--- /dev/null
+++ b/uview-ui/components/u-button/u-button.vue
@@ -0,0 +1,596 @@
+<template>
+ <button
+ id="u-wave-btn"
+ class="u-btn u-line-1 u-fix-ios-appearance"
+ :class="[
+ 'u-size-' + size,
+ plain ? 'u-btn--' + type + '--plain' : '',
+ loading ? 'u-loading' : '',
+ shape == 'circle' ? 'u-round-circle' : '',
+ hairLine ? showHairLineBorder : 'u-btn--bold-border',
+ 'u-btn--' + type,
+ disabled ? `u-btn--${type}--disabled` : '',
+ ]"
+ :hover-start-time="Number(hoverStartTime)"
+ :hover-stay-time="Number(hoverStayTime)"
+ :disabled="disabled"
+ :form-type="formType"
+ :open-type="openType"
+ :app-parameter="appParameter"
+ :hover-stop-propagation="hoverStopPropagation"
+ :send-message-title="sendMessageTitle"
+ send-message-path="sendMessagePath"
+ :lang="lang"
+ :data-name="dataName"
+ :session-from="sessionFrom"
+ :send-message-img="sendMessageImg"
+ :show-message-card="showMessageCard"
+ @getphonenumber="getphonenumber"
+ @getuserinfo="getuserinfo"
+ @error="error"
+ @opensetting="opensetting"
+ @launchapp="launchapp"
+ :style="[customStyle, {
+ overflow: ripple ? 'hidden' : 'visible'
+ }]"
+ @tap.stop="click($event)"
+ :hover-class="getHoverClass"
+ :loading="loading"
+ >
+ <slot></slot>
+ <view
+ v-if="ripple"
+ class="u-wave-ripple"
+ :class="[waveActive ? 'u-wave-active' : '']"
+ :style="{
+ top: rippleTop + 'px',
+ left: rippleLeft + 'px',
+ width: fields.targetWidth + 'px',
+ height: fields.targetWidth + 'px',
+ 'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
+ }"
+ ></view>
+ </button>
+</template>
+
+<script>
+/**
+ * button 按钮
+ * @description Button 按钮
+ * @tutorial https://www.uviewui.com/components/button.html
+ * @property {String} size 按钮的大小
+ * @property {Boolean} ripple 是否开启点击水波纹效果
+ * @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
+ * @property {String} type 按钮的样式类型
+ * @property {Boolean} plain 按钮是否镂空,背景色透明
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
+ * @property {Boolean} shape 按钮外观形状,见文档说明
+ * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
+ * @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ * @property {String} open-type 开放能力
+ * @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ * @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
+ * @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
+ * @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
+ * @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
+ * @event {Function} click 按钮点击
+ * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
+ * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
+ * @event {Function} error 当使用开放能力时,发生错误的回调
+ * @event {Function} opensetting 在打开授权设置页并关闭后回调
+ * @event {Function} launchapp 打开 APP 成功的回调
+ * @example <u-button>月落</u-button>
+ */
+export default {
+ name: 'u-button',
+ props: {
+ // 是否细边框
+ hairLine: {
+ type: Boolean,
+ default: true
+ },
+ // 按钮的预置样式,default,primary,error,warning,success
+ type: {
+ type: String,
+ default: 'default'
+ },
+ // 按钮尺寸,default,medium,mini
+ size: {
+ type: String,
+ default: 'default'
+ },
+ // 按钮形状,circle(两边为半圆),square(带圆角)
+ shape: {
+ type: String,
+ default: 'square'
+ },
+ // 按钮是否镂空
+ plain: {
+ type: Boolean,
+ default: false
+ },
+ // 是否禁止状态
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 是否加载中
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ // 开放能力,具体请看uniapp稳定关于button组件部分说明
+ // https://uniapp.dcloud.io/component/button
+ openType: {
+ type: String,
+ default: ''
+ },
+ // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ // 取值为submit(提交表单),reset(重置表单)
+ formType: {
+ type: String,
+ default: ''
+ },
+ // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
+ // 只微信小程序、QQ小程序有效
+ appParameter: {
+ type: String,
+ default: ''
+ },
+ // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
+ hoverStopPropagation: {
+ type: Boolean,
+ default: false
+ },
+ // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
+ lang: {
+ type: String,
+ default: 'en'
+ },
+ // 会话来源,open-type="contact"时有效。只微信小程序有效
+ sessionFrom: {
+ type: String,
+ default: ''
+ },
+ // 会话内消息卡片标题,open-type="contact"时有效
+ // 默认当前标题,只微信小程序有效
+ sendMessageTitle: {
+ type: String,
+ default: ''
+ },
+ // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
+ // 默认当前分享路径,只微信小程序有效
+ sendMessagePath: {
+ type: String,
+ default: ''
+ },
+ // 会话内消息卡片图片,open-type="contact"时有效
+ // 默认当前页面截图,只微信小程序有效
+ sendMessageImg: {
+ type: String,
+ default: ''
+ },
+ // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
+ // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
+ showMessageCard: {
+ type: Boolean,
+ default: false
+ },
+ // 手指按(触摸)按钮时按钮时的背景颜色
+ hoverBgColor: {
+ type: String,
+ default: ''
+ },
+ // 水波纹的背景颜色
+ rippleBgColor: {
+ type: String,
+ default: ''
+ },
+ // 是否开启水波纹效果
+ ripple: {
+ type: Boolean,
+ default: false
+ },
+ // 按下的类名
+ hoverClass: {
+ type: String,
+ default: ''
+ },
+ // 自定义样式,对象形式
+ customStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ dataName: {
+ type: String,
+ default: ''
+ },
+ // 节流,一定时间内只能触发一次
+ throttleTime: {
+ type: [String, Number],
+ default: 1000
+ },
+ // 按住后多久出现点击态,单位毫秒
+ hoverStartTime: {
+ type: [String, Number],
+ default: 20
+ },
+ // 手指松开后点击态保留时间,单位毫秒
+ hoverStayTime: {
+ type: [String, Number],
+ default: 150
+ },
+ },
+ computed: {
+ // 当没有传bgColor变量时,按钮按下去的颜色类名
+ getHoverClass() {
+ // 如果开启水波纹效果,则不启用hover-class效果
+ if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
+ let hoverClass = '';
+ hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
+ return hoverClass;
+ },
+ // 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
+ showHairLineBorder() {
+ if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
+ return '';
+ } else {
+ return 'u-hairline-border';
+ }
+ }
+ },
+ data() {
+ return {
+ rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
+ rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
+ fields: {}, // 波纹按钮节点信息
+ waveActive: false // 激活水波纹
+ };
+ },
+ methods: {
+ // 按钮点击
+ click(e) {
+ // 进行节流控制,每this.throttle毫秒内,只在开始处执行
+ this.$u.throttle(() => {
+ // 如果按钮时disabled和loading状态,不触发水波纹效果
+ if (this.loading === true || this.disabled === true) return;
+ // 是否开启水波纹效果
+ if (this.ripple) {
+ // 每次点击时,移除上一次的类,再次添加,才能触发动画效果
+ this.waveActive = false;
+ this.$nextTick(function() {
+ this.getWaveQuery(e);
+ });
+ }
+ this.$emit('click', e);
+ }, this.throttleTime);
+ },
+ // 查询按钮的节点信息
+ getWaveQuery(e) {
+ this.getElQuery().then(res => {
+ // 查询返回的是一个数组节点
+ let data = res[0];
+ // 查询不到节点信息,不操作
+ if (!data.width || !data.width) return;
+ // 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
+ // 最终的方形(变换后的圆形)才能覆盖整个按钮
+ data.targetWidth = data.height > data.width ? data.height : data.width;
+ if (!data.targetWidth) return;
+ this.fields = data;
+ let touchesX = '',
+ touchesY = '';
+ // #ifdef MP-BAIDU
+ touchesX = e.changedTouches[0].clientX;
+ touchesY = e.changedTouches[0].clientY;
+ // #endif
+ // #ifdef MP-ALIPAY
+ touchesX = e.detail.clientX;
+ touchesY = e.detail.clientY;
+ // #endif
+ // #ifndef MP-BAIDU || MP-ALIPAY
+ touchesX = e.touches[0].clientX;
+ touchesY = e.touches[0].clientY;
+ // #endif
+ // 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
+ // 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
+ // 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
+ this.rippleTop = touchesY - data.top - data.targetWidth / 2;
+ this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
+ this.$nextTick(() => {
+ this.waveActive = true;
+ });
+ });
+ },
+ // 获取节点信息
+ getElQuery() {
+ return new Promise(resolve => {
+ let queryInfo = '';
+ // 获取元素节点信息,请查看uniapp相关文档
+ // https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
+ queryInfo = uni.createSelectorQuery().in(this);
+ //#ifdef MP-ALIPAY
+ queryInfo = uni.createSelectorQuery();
+ //#endif
+ queryInfo.select('.u-btn').boundingClientRect();
+ queryInfo.exec(data => {
+ resolve(data);
+ });
+ });
+ },
+ // 下面为对接uniapp官方按钮开放能力事件回调的对接
+ getphonenumber(res) {
+ this.$emit('getphonenumber', res);
+ },
+ getuserinfo(res) {
+ this.$emit('getuserinfo', res);
+ },
+ error(res) {
+ this.$emit('error', res);
+ },
+ opensetting(res) {
+ this.$emit('opensetting', res);
+ },
+ launchapp(res) {
+ this.$emit('launchapp', res);
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+.u-btn::after {
+ border: none;
+}
+
+.u-btn {
+ position: relative;
+ border: 0;
+ //border-radius: 10rpx;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ // 避免边框某些场景可能被“裁剪”,不能设置为hidden
+ overflow: visible;
+ line-height: 1;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ padding: 0 40rpx;
+ z-index: 1;
+ box-sizing: border-box;
+ transition: all 0.15s;
+
+ &--bold-border {
+ border: 1px solid #ffffff;
+ }
+
+ &--default {
+ color: $u-content-color;
+ border-color: #c0c4cc;
+ background-color: #ffffff;
+ }
+
+ &--primary {
+ color: #ffffff;
+ border-color: $u-type-primary;
+ background-color: $u-type-primary;
+ }
+
+ &--success {
+ color: #ffffff;
+ border-color: $u-type-success;
+ background-color: $u-type-success;
+ }
+
+ &--error {
+ color: #ffffff;
+ border-color: $u-type-error;
+ background-color: $u-type-error;
+ }
+
+ &--warning {
+ color: #ffffff;
+ border-color: $u-type-warning;
+ background-color: $u-type-warning;
+ }
+
+ &--default--disabled {
+ color: #ffffff;
+ border-color: #e4e7ed;
+ background-color: #ffffff;
+ }
+
+ &--primary--disabled {
+ color: #ffffff!important;
+ border-color: $u-type-primary-disabled!important;
+ background-color: $u-type-primary-disabled!important;
+ }
+
+ &--success--disabled {
+ color: #ffffff!important;
+ border-color: $u-type-success-disabled!important;
+ background-color: $u-type-success-disabled!important;
+ }
+
+ &--error--disabled {
+ color: #ffffff!important;
+ border-color: $u-type-error-disabled!important;
+ background-color: $u-type-error-disabled!important;
+ }
+
+ &--warning--disabled {
+ color: #ffffff!important;
+ border-color: $u-type-warning-disabled!important;
+ background-color: $u-type-warning-disabled!important;
+ }
+
+ &--primary--plain {
+ color: $u-type-primary!important;
+ border-color: $u-type-primary-disabled!important;
+ background-color: $u-type-primary-light!important;
+ }
+
+ &--success--plain {
+ color: $u-type-success!important;
+ border-color: $u-type-success-disabled!important;
+ background-color: $u-type-success-light!important;
+ }
+
+ &--error--plain {
+ color: $u-type-error!important;
+ border-color: $u-type-error-disabled!important;
+ background-color: $u-type-error-light!important;
+ }
+
+ &--warning--plain {
+ color: $u-type-warning!important;
+ border-color: $u-type-warning-disabled!important;
+ background-color: $u-type-warning-light!important;
+ }
+}
+
+.u-hairline-border:after {
+ content: ' ';
+ position: absolute;
+ pointer-events: none;
+ // 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
+ box-sizing: border-box;
+ // 中心点作为变形(scale())的原点
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
+ left: 0;
+ top: 0;
+ width: 199.8%;
+ height: 199.7%;
+ -webkit-transform: scale(0.5, 0.5);
+ transform: scale(0.5, 0.5);
+ border: 1px solid currentColor;
+ z-index: 1;
+}
+
+.u-wave-ripple {
+ z-index: 0;
+ position: absolute;
+ border-radius: 100%;
+ background-clip: padding-box;
+ pointer-events: none;
+ user-select: none;
+ transform: scale(0);
+ opacity: 1;
+ transform-origin: center;
+}
+
+.u-wave-ripple.u-wave-active {
+ opacity: 0;
+ transform: scale(2);
+ transition: opacity 1s linear, transform 0.4s linear;
+}
+
+.u-round-circle {
+ border-radius: 100rpx;
+}
+
+.u-round-circle::after {
+ border-radius: 100rpx;
+}
+
+.u-loading::after {
+ background-color: hsla(0, 0%, 100%, 0.35);
+}
+
+.u-size-default {
+ font-size: 30rpx;
+ height: 80rpx;
+ line-height: 80rpx;
+}
+
+.u-size-medium {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ width: auto;
+ font-size: 26rpx;
+ height: 70rpx;
+ line-height: 70rpx;
+ padding: 0 80rpx;
+}
+
+.u-size-mini {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ width: auto;
+ font-size: 22rpx;
+ padding-top: 1px;
+ height: 50rpx;
+ line-height: 50rpx;
+ padding: 0 20rpx;
+}
+
+.u-primary-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-primary-dark !important;
+}
+
+.u-default-plain-hover {
+ color: $u-type-primary-dark !important;
+ background: $u-type-primary-light !important;
+}
+
+.u-success-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-success-dark !important;
+}
+
+.u-warning-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-warning-dark !important;
+}
+
+.u-error-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-error-dark !important;
+}
+
+.u-info-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-info-dark !important;
+}
+
+.u-default-hover {
+ color: $u-type-primary-dark !important;
+ border-color: $u-type-primary-dark !important;
+ background-color: $u-type-primary-light !important;
+}
+
+.u-primary-hover {
+ background: $u-type-primary-dark !important;
+ color: #fff;
+}
+
+.u-success-hover {
+ background: $u-type-success-dark !important;
+ color: #fff;
+}
+
+.u-info-hover {
+ background: $u-type-info-dark !important;
+ color: #fff;
+}
+
+.u-warning-hover {
+ background: $u-type-warning-dark !important;
+ color: #fff;
+}
+
+.u-error-hover {
+ background: $u-type-error-dark !important;
+ color: #fff;
+}
+</style>
diff --git a/uview-ui/components/u-calendar/u-calendar.vue b/uview-ui/components/u-calendar/u-calendar.vue
new file mode 100644
index 0000000..6602bd0
--- /dev/null
+++ b/uview-ui/components/u-calendar/u-calendar.vue
@@ -0,0 +1,639 @@
+<template>
+ <u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
+ :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
+ <view class="u-calendar">
+ <view class="u-calendar__header">
+ <view class="u-calendar__header__text" v-if="!$slots['tooltip']">
+ {{toolTip}}
+ </view>
+ <slot v-else name="tooltip" />
+ </view>
+ <view class="u-calendar__action u-flex u-row-center">
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
+ </view>
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
+ </view>
+ <view class="u-calendar__action__text">{{ showTitle }}</view>
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
+ </view>
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
+ </view>
+ </view>
+ <view class="u-calendar__week-day">
+ <view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
+ </view>
+ <view class="u-calendar__content">
+ <!-- 前置空白部分 -->
+ <block v-for="(item, index) in weekdayArr" :key="index">
+ <view class="u-calendar__content__item"></view>
+ </block>
+ <view class="u-calendar__content__item" :class="{
+ 'u-hover-class':openDisAbled(year,month,index+1),
+ 'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
+ 'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
+ }" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
+ @tap="dateClick(index)">
+ <view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
+ <view>{{ index + 1 }}</view>
+ </view>
+ <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
+ <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
+ </view>
+ <view class="u-calendar__content__bg-month">{{month}}</view>
+ </view>
+ <view class="u-calendar__bottom">
+ <view class="u-calendar__bottom__choose">
+ <text>{{mode == 'date' ? activeDate : startDate}}</text>
+ <text v-if="endDate">至{{endDate}}</text>
+ </view>
+ <view class="u-calendar__bottom__btn">
+ <u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
+ </view>
+ </view>
+ </view>
+ </u-popup>
+</template>
+<script>
+ /**
+ * calendar 日历
+ * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
+ * @tutorial http://uviewui.com/components/calendar.html
+ * @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
+ * @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
+ * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
+ * @property {String Number} max-year 可切换的最大年份(默认2050)
+ * @property {String Number} min-year 最小可选日期(默认1950)
+ * @property {String Number} min-date 可切换的最小年份(默认1950-01-01)
+ * @property {String Number} max-date 最大可选日期(默认当前日期)
+ * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
+ * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
+ * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
+ * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
+ * @property {String} color 日期字体的默认颜色(默认#303133)
+ * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
+ * @property {String Number} z-index 弹出时的z-index值(默认10075)
+ * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
+ * @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
+ * @property {String} range-color 选择范围内字体颜色(默认#2979ff)
+ * @property {String} start-text 起始日期底部的提示文字(默认 '开始')
+ * @property {String} end-text 结束日期底部的提示文字(默认 '结束')
+ * @property {String} btn-type 底部确定按钮的主题(默认 'primary')
+ * @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
+ * @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
+ * @example <u-calendar v-model="show" :mode="mode"></u-calendar>
+ */
+
+ export default {
+ name: 'u-calendar',
+ props: {
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 是否允许通过点击遮罩关闭Picker
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 弹出的z-index值
+ zIndex: {
+ type: [String, Number],
+ default: 0
+ },
+ // 是否允许切换年份
+ changeYear: {
+ type: Boolean,
+ default: true
+ },
+ // 是否允许切换月份
+ changeMonth: {
+ type: Boolean,
+ default: true
+ },
+ // date-单个日期选择,range-开始日期+结束日期选择
+ mode: {
+ type: String,
+ default: 'date'
+ },
+ // 可切换的最大年份
+ maxYear: {
+ type: [Number, String],
+ default: 2050
+ },
+ // 可切换的最小年份
+ minYear: {
+ type: [Number, String],
+ default: 1950
+ },
+ // 最小可选日期(不在范围内日期禁用不可选)
+ minDate: {
+ type: [Number, String],
+ default: '1950-01-01'
+ },
+ /**
+ * 最大可选日期
+ * 默认最大值为今天,之后的日期不可选
+ * 2030-12-31
+ * */
+ maxDate: {
+ type: [Number, String],
+ default: ''
+ },
+ // 弹窗顶部左右两边的圆角值
+ borderRadius: {
+ type: [String, Number],
+ default: 20
+ },
+ // 月份切换按钮箭头颜色
+ monthArrowColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 年份切换按钮箭头颜色
+ yearArrowColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 默认日期字体颜色
+ color: {
+ type: String,
+ default: '#303133'
+ },
+ // 选中|起始结束日期背景色
+ activeBgColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 选中|起始结束日期字体颜色
+ activeColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 范围内日期背景色
+ rangeBgColor: {
+ type: String,
+ default: 'rgba(41,121,255,0.13)'
+ },
+ // 范围内日期字体颜色
+ rangeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // mode=range时生效,起始日期自定义文案
+ startText: {
+ type: String,
+ default: '开始'
+ },
+ // mode=range时生效,结束日期自定义文案
+ endText: {
+ type: String,
+ default: '结束'
+ },
+ //按钮样式类型
+ btnType: {
+ type: String,
+ default: 'primary'
+ },
+ // 当前选中日期带选中效果
+ isActiveCurrent: {
+ type: Boolean,
+ default: true
+ },
+ // 切换年月是否触发事件 mode=date时生效
+ isChange: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示右上角的关闭图标
+ closeable: {
+ type: Boolean,
+ default: true
+ },
+ // 顶部的提示文字
+ toolTip: {
+ type: String,
+ default: '选择日期'
+ }
+ },
+ data() {
+ return {
+ // 星期几,值为1-7
+ weekday: 1,
+ weekdayArr:[],
+ // 当前月有多少天
+ days: 0,
+ daysArr:[],
+ showTitle: '',
+ year: 2020,
+ month: 0,
+ day: 0,
+ startYear: 0,
+ startMonth: 0,
+ startDay: 0,
+ endYear: 0,
+ endMonth: 0,
+ endDay: 0,
+ today: '',
+ activeDate: '',
+ startDate: '',
+ endDate: '',
+ isStart: true,
+ min: null,
+ max: null,
+ weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
+ };
+ },
+ computed: {
+ dataChange() {
+ return `${this.mode}-${this.minDate}-${this.maxDate}`;
+ },
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ watch: {
+ dataChange(val) {
+ this.init()
+ }
+ },
+ created() {
+ this.init()
+ },
+ methods: {
+ getColor(index, type) {
+ let color = type == 1 ? '' : this.color;
+ let day = index + 1
+ let date = `${this.year}-${this.month}-${day}`
+ let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+ let start = this.startDate.replace(/\-/g, '/')
+ let end = this.endDate.replace(/\-/g, '/')
+ if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+ color = type == 1 ? this.activeBgColor : this.activeColor;
+ } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+ color = type == 1 ? this.rangeBgColor : this.rangeColor;
+ }
+ return color;
+ },
+ init() {
+ let now = new Date();
+ this.year = now.getFullYear();
+ this.month = now.getMonth() + 1;
+ this.day = now.getDate();
+ this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+ this.activeDate = this.today;
+ this.min = this.initDate(this.minDate);
+ this.max = this.initDate(this.maxDate || this.today);
+ this.startDate = "";
+ this.startYear = 0;
+ this.startMonth = 0;
+ this.startDay = 0;
+ this.endYear = 0;
+ this.endMonth = 0;
+ this.endDay = 0;
+ this.endDate = "";
+ this.isStart = true;
+ this.changeData();
+ },
+ //日期处理
+ initDate(date) {
+ let fdate = date.split('-');
+ return {
+ year: Number(fdate[0] || 1920),
+ month: Number(fdate[1] || 1),
+ day: Number(fdate[2] || 1)
+ }
+ },
+ openDisAbled: function(year, month, day) {
+ let bool = true;
+ let date = `${year}/${month}/${day}`;
+ // let today = this.today.replace(/\-/g, '/');
+ let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+ let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+ let timestamp = new Date(date).getTime();
+ if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+ bool = false;
+ }
+ return bool;
+ },
+ generateArray: function(start, end) {
+ return Array.from(new Array(end + 1).keys()).slice(start);
+ },
+ formatNum: function(num) {
+ return num < 10 ? '0' + num : num + '';
+ },
+ //一个月有多少天
+ getMonthDay(year, month) {
+ let days = new Date(year, month, 0).getDate();
+ return days;
+ },
+ getWeekday(year, month) {
+ let date = new Date(`${year}/${month}/01 00:00:00`);
+ return date.getDay();
+ },
+ checkRange(year) {
+ let overstep = false;
+ if (year < this.minYear || year > this.maxYear) {
+ uni.showToast({
+ title: "日期超出范围啦~",
+ icon: 'none'
+ })
+ overstep = true;
+ }
+ return overstep;
+ },
+ changeMonthHandler(isAdd) {
+ if (isAdd) {
+ let month = this.month + 1;
+ let year = month > 12 ? this.year + 1 : this.year;
+ if (!this.checkRange(year)) {
+ this.month = month > 12 ? 1 : month;
+ this.year = year;
+ this.changeData();
+ }
+
+ } else {
+ let month = this.month - 1;
+ let year = month < 1 ? this.year - 1 : this.year;
+ if (!this.checkRange(year)) {
+ this.month = month < 1 ? 12 : month;
+ this.year = year;
+ this.changeData();
+ }
+ }
+ },
+ changeYearHandler(isAdd) {
+ let year = isAdd ? this.year + 1 : this.year - 1;
+ if (!this.checkRange(year)) {
+ this.year = year;
+ this.changeData();
+ }
+ },
+ changeData() {
+ this.days = this.getMonthDay(this.year, this.month);
+ this.daysArr=this.generateArray(1,this.days)
+ this.weekday = this.getWeekday(this.year, this.month);
+ this.weekdayArr=this.generateArray(1,this.weekday)
+ this.showTitle = `${this.year}年${this.month}月`;
+ if (this.isChange && this.mode == 'date') {
+ this.btnFix(true);
+ }
+ },
+ dateClick: function(day) {
+ day += 1;
+ if (!this.openDisAbled(this.year, this.month, day)) {
+ this.day = day;
+ let date = `${this.year}-${this.month}-${day}`;
+ if (this.mode == 'date') {
+ this.activeDate = date;
+ } else {
+ let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
+ if (this.isStart || compare) {
+ this.startDate = date;
+ this.startYear = this.year;
+ this.startMonth = this.month;
+ this.startDay = this.day;
+ this.endYear = 0;
+ this.endMonth = 0;
+ this.endDay = 0;
+ this.endDate = "";
+ this.activeDate = "";
+ this.isStart = false;
+ } else {
+ this.endDate = date;
+ this.endYear = this.year;
+ this.endMonth = this.month;
+ this.endDay = this.day;
+ this.isStart = true;
+ }
+ }
+ }
+ },
+ close() {
+ // 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
+ this.$emit('input', false);
+ },
+ getWeekText(date) {
+ date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+ let week = date.getDay();
+ return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+ },
+ btnFix(show) {
+ if (!show) {
+ this.close();
+ }
+ if (this.mode == 'date') {
+ let arr = this.activeDate.split('-')
+ let year = this.isChange ? this.year : Number(arr[0]);
+ let month = this.isChange ? this.month : Number(arr[1]);
+ let day = this.isChange ? this.day : Number(arr[2]);
+ //当前月有多少天
+ let days = this.getMonthDay(year, month);
+ let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+ let weekText = this.getWeekText(result);
+ let isToday = false;
+ if (`${year}-${month}-${day}` == this.today) {
+ //今天
+ isToday = true;
+ }
+ this.$emit('change', {
+ year: year,
+ month: month,
+ day: day,
+ days: days,
+ result: result,
+ week: weekText,
+ isToday: isToday,
+ // switch: show //是否是切换年月操作
+ });
+ } else {
+ if (!this.startDate || !this.endDate) return;
+ let startMonth = this.formatNum(this.startMonth);
+ let startDay = this.formatNum(this.startDay);
+ let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+ let startWeek = this.getWeekText(startDate)
+
+ let endMonth = this.formatNum(this.endMonth);
+ let endDay = this.formatNum(this.endDay);
+ let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+ let endWeek = this.getWeekText(endDate);
+ this.$emit('change', {
+ startYear: this.startYear,
+ startMonth: this.startMonth,
+ startDay: this.startDay,
+ startDate: startDate,
+ startWeek: startWeek,
+ endYear: this.endYear,
+ endMonth: this.endMonth,
+ endDay: this.endDay,
+ endDate: endDate,
+ endWeek: endWeek
+ });
+ }
+ }
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-calendar {
+ color: $u-content-color;
+
+ &__header {
+ width: 100%;
+ box-sizing: border-box;
+ font-size: 30rpx;
+ background-color: #fff;
+ color: $u-main-color;
+
+ &__text {
+ margin-top: 30rpx;
+ padding: 0 60rpx;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ &__action {
+ padding: 40rpx 0 40rpx 0;
+
+ &__icon {
+ margin: 0 16rpx;
+ }
+
+ &__text {
+ padding: 0 16rpx;
+ color: $u-main-color;
+ font-size: 32rpx;
+ line-height: 32rpx;
+ font-weight: bold;
+ }
+ }
+
+ &__week-day {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 0;
+ overflow: hidden;
+
+ &__text {
+ flex: 1;
+ text-align: center;
+ }
+ }
+
+ &__content {
+ width: 100%;
+ @include vue-flex;
+ flex-wrap: wrap;
+ padding: 6px 0;
+ box-sizing: border-box;
+ background-color: #fff;
+ position: relative;
+
+ &--end-date {
+ border-top-right-radius: 8rpx;
+ border-bottom-right-radius: 8rpx;
+ }
+
+ &--start-date {
+ border-top-left-radius: 8rpx;
+ border-bottom-left-radius: 8rpx;
+ }
+
+ &__item {
+ width: 14.2857%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 0;
+ overflow: hidden;
+ position: relative;
+ z-index: 2;
+
+ &__inner {
+ height: 84rpx;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ font-size: 32rpx;
+ position: relative;
+ border-radius: 50%;
+
+ &__desc {
+ width: 100%;
+ font-size: 24rpx;
+ line-height: 24rpx;
+ transform: scale(0.75);
+ transform-origin: center center;
+ position: absolute;
+ left: 0;
+ text-align: center;
+ bottom: 2rpx;
+ }
+ }
+
+ &__tips {
+ width: 100%;
+ font-size: 24rpx;
+ line-height: 24rpx;
+ position: absolute;
+ left: 0;
+ transform: scale(0.8);
+ transform-origin: center center;
+ text-align: center;
+ bottom: 8rpx;
+ z-index: 2;
+ }
+ }
+
+ &__bg-month {
+ position: absolute;
+ font-size: 130px;
+ line-height: 130px;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ color: #e4e7ed;
+ z-index: 1;
+ }
+ }
+
+ &__bottom {
+ width: 100%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ background-color: #fff;
+ padding: 0 40rpx 30rpx;
+ box-sizing: border-box;
+ font-size: 24rpx;
+ color: $u-tips-color;
+
+ &__choose {
+ height: 50rpx;
+ }
+
+ &__btn {
+ width: 100%;
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/uview-ui/components/u-car-keyboard/u-car-keyboard.vue b/uview-ui/components/u-car-keyboard/u-car-keyboard.vue
new file mode 100644
index 0000000..84b1467
--- /dev/null
+++ b/uview-ui/components/u-car-keyboard/u-car-keyboard.vue
@@ -0,0 +1,257 @@
+<template>
+ <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
+ <view class="u-keyboard-grids">
+ <block>
+ <view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
+ <view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
+ v-for="(item, j) in group" :key="j">
+ {{ item }}
+ </view>
+ </view>
+ <view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
+ hover-class="u-hover-class">
+ <u-icon :size="38" name="backspace" :bold="true"></u-icon>
+ </view>
+ <view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
+ <text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
+ /
+ <text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
+ </view>
+ </block>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ name: "u-keyboard",
+ props: {
+ // 是否打乱键盘按键的顺序
+ random: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ // 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
+ abc: false
+ };
+ },
+ computed: {
+ areaList() {
+ let data = [
+ '京',
+ '沪',
+ '粤',
+ '津',
+ '冀',
+ '豫',
+ '云',
+ '辽',
+ '黑',
+ '湘',
+ '皖',
+ '鲁',
+ '苏',
+ '浙',
+ '赣',
+ '鄂',
+ '桂',
+ '甘',
+ '晋',
+ '陕',
+ '蒙',
+ '吉',
+ '闽',
+ '贵',
+ '渝',
+ '川',
+ '青',
+ '琼',
+ '宁',
+ '挂',
+ '藏',
+ '港',
+ '澳',
+ '新',
+ '使',
+ '学'
+ ];
+ let tmp = [];
+ // 打乱顺序
+ if (this.random) data = this.$u.randomArray(data);
+ // 切割成二维数组
+ tmp[0] = data.slice(0, 10);
+ tmp[1] = data.slice(10, 20);
+ tmp[2] = data.slice(20, 30);
+ tmp[3] = data.slice(30, 36);
+ return tmp;
+ },
+ EngKeyBoardList() {
+ let data = [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 0,
+ 'Q',
+ 'W',
+ 'E',
+ 'R',
+ 'T',
+ 'Y',
+ 'U',
+ 'I',
+ 'O',
+ 'P',
+ 'A',
+ 'S',
+ 'D',
+ 'F',
+ 'G',
+ 'H',
+ 'J',
+ 'K',
+ 'L',
+ 'Z',
+ 'X',
+ 'C',
+ 'V',
+ 'B',
+ 'N',
+ 'M'
+ ];
+ let tmp = [];
+ if (this.random) data = this.$u.randomArray(data);
+ tmp[0] = data.slice(0, 10);
+ tmp[1] = data.slice(10, 20);
+ tmp[2] = data.slice(20, 30);
+ tmp[3] = data.slice(30, 36);
+ return tmp;
+ }
+ },
+ methods: {
+ // 点击键盘按钮
+ carInputClick(i, j) {
+ let value = '';
+ // 不同模式,获取不同数组的值
+ if (this.abc) value = this.EngKeyBoardList[i][j];
+ else value = this.areaList[i][j];
+ this.$emit('change', value);
+ },
+ // 修改汽车牌键盘的输入模式,中文|英文
+ changeCarInputMode() {
+ this.abc = !this.abc;
+ },
+ // 点击退格键
+ backspaceClick() {
+ this.$emit('backspace');
+ clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+ this.timer = null;
+ this.timer = setInterval(() => {
+ this.$emit('backspace');
+ }, 250);
+ },
+ clearTimer() {
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-keyboard-grids {
+ background: rgb(215, 215, 217);
+ padding: 24rpx 0;
+ position: relative;
+ }
+
+ .u-keyboard-grids-item {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .u-keyboard-grids-btn {
+ text-decoration: none;
+ width: 62rpx;
+ flex: 0 0 64rpx;
+ height: 80rpx;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ font-size: 36rpx;
+ text-align: center;
+ line-height: 80rpx;
+ background-color: #fff;
+ margin: 8rpx 5rpx;
+ border-radius: 8rpx;
+ box-shadow: 0 2rpx 0rpx #888992;
+ font-weight: 500;
+ justify-content: center;
+ }
+
+ .u-carinput-hover {
+ background-color: rgb(185, 188, 195) !important;
+ }
+
+ .u-keyboard-back {
+ position: absolute;
+ width: 96rpx;
+ right: 22rpx;
+ bottom: 32rpx;
+ height: 80rpx;
+ background-color: rgb(185, 188, 195);
+ @include vue-flex;
+ align-items: center;
+ border-radius: 8rpx;
+ justify-content: center;
+ box-shadow: 0 2rpx 0rpx #888992;
+ }
+
+ .u-keyboard-change {
+ font-size: 24rpx;
+ box-shadow: 0 2rpx 0rpx #888992;
+ position: absolute;
+ width: 96rpx;
+ left: 22rpx;
+ line-height: 1;
+ bottom: 32rpx;
+ height: 80rpx;
+ background-color: #ffffff;
+ @include vue-flex;
+ align-items: center;
+ border-radius: 8rpx;
+ justify-content: center;
+ }
+
+ .u-keyboard-change .inactive.zh {
+ transform: scale(0.85) translateY(-10rpx);
+ }
+
+ .u-keyboard-change .inactive.en {
+ transform: scale(0.85) translateY(10rpx);
+ }
+
+ .u-keyboard-change .active {
+ color: rgb(237, 112, 64);
+ font-size: 30rpx;
+ }
+
+ .u-keyboard-change .zh {
+ transform: translateY(-10rpx);
+ }
+
+ .u-keyboard-change .en {
+ transform: translateY(10rpx);
+ }
+</style>
diff --git a/uview-ui/components/u-card/u-card.vue b/uview-ui/components/u-card/u-card.vue
new file mode 100644
index 0000000..add01cb
--- /dev/null
+++ b/uview-ui/components/u-card/u-card.vue
@@ -0,0 +1,298 @@
+<template>
+ <view
+ class="u-card"
+ @tap.stop="click"
+ :class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
+ :style="{
+ borderRadius: borderRadius + 'rpx',
+ margin: margin,
+ boxShadow: boxShadow
+ }"
+ >
+ <view
+ v-if="showHead"
+ class="u-card__head"
+ :style="[{padding: padding + 'rpx'}, headStyle]"
+ :class="{
+ 'u-border-bottom': headBorderBottom
+ }"
+ @tap="headClick"
+ >
+ <view v-if="!$slots.head" class="u-flex u-row-between">
+ <view class="u-card__head--left u-flex u-line-1" v-if="title">
+ <image
+ :src="thumb"
+ class="u-card__head--left__thumb"
+ mode="aspectfull"
+ v-if="thumb"
+ :style="{
+ height: thumbWidth + 'rpx',
+ width: thumbWidth + 'rpx',
+ borderRadius: thumbCircle ? '100rpx' : '6rpx'
+ }"
+ ></image>
+ <text
+ class="u-card__head--left__title u-line-1"
+ :style="{
+ fontSize: titleSize + 'rpx',
+ color: titleColor
+ }"
+ >
+ {{ title }}
+ </text>
+ </view>
+ <view class="u-card__head--right u-line-1" v-if="subTitle">
+ <text
+ class="u-card__head__title__text"
+ :style="{
+ fontSize: subTitleSize + 'rpx',
+ color: subTitleColor
+ }"
+ >
+ {{ subTitle }}
+ </text>
+ </view>
+ </view>
+ <slot name="head" v-else />
+ </view>
+ <view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
+ <view
+ v-if="showFoot"
+ class="u-card__foot"
+ @tap="footClick"
+ :style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
+ :class="{
+ 'u-border-top': footBorderTop
+ }"
+ >
+ <slot name="foot" />
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * card 卡片
+ * @description 卡片组件一般用于多个列表条目,且风格统一的场景
+ * @tutorial https://www.uviewui.com/components/card.html
+ * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
+ * @property {String} title 头部左边的标题
+ * @property {String} title-color 标题颜色(默认#303133)
+ * @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
+ * @property {String} sub-title 头部右边的副标题
+ * @property {String} sub-title-color 副标题颜色(默认#909399)
+ * @property {String | Number} sub-title-size 副标题字体大小(默认26)
+ * @property {Boolean} border 是否显示边框(默认true)
+ * @property {String | Number} index 用于标识点击了第几个卡片
+ * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
+ * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
+ * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
+ * @property {Object} head-style 头部自定义样式,对象形式
+ * @property {Object} body-style 中部自定义样式,对象形式
+ * @property {Object} foot-style 底部自定义样式,对象形式
+ * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
+ * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
+ * @property {Boolean} show-head 是否显示头部(默认true)
+ * @property {Boolean} show-head 是否显示尾部(默认true)
+ * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
+ * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
+ * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
+ * @event {Function} click 整个卡片任意位置被点击时触发
+ * @event {Function} head-click 卡片头部被点击时触发
+ * @event {Function} body-click 卡片主体部分被点击时触发
+ * @event {Function} foot-click 卡片底部部分被点击时触发
+ * @example <u-card padding="30" title="card"></u-card>
+ */
+export default {
+ name: 'u-card',
+ props: {
+ // 与屏幕两侧是否留空隙
+ full: {
+ type: Boolean,
+ default: false
+ },
+ // 标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 标题颜色
+ titleColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 标题字体大小,单位rpx
+ titleSize: {
+ type: [Number, String],
+ default: '30'
+ },
+ // 副标题
+ subTitle: {
+ type: String,
+ default: ''
+ },
+ // 副标题颜色
+ subTitleColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 副标题字体大小,单位rpx
+ subTitleSize: {
+ type: [Number, String],
+ default: '26'
+ },
+ // 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
+ border: {
+ type: Boolean,
+ default: true
+ },
+ // 用于标识点击了第几个
+ index: {
+ type: [Number, String, Object],
+ default: ''
+ },
+ // 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
+ margin: {
+ type: String,
+ default: '30rpx'
+ },
+ // card卡片的圆角
+ borderRadius: {
+ type: [Number, String],
+ default: '16'
+ },
+ // 头部自定义样式,对象形式
+ headStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 主体自定义样式,对象形式
+ bodyStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 底部自定义样式,对象形式
+ footStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 头部是否下边框
+ headBorderBottom: {
+ type: Boolean,
+ default: true
+ },
+ // 底部是否有上边框
+ footBorderTop: {
+ type: Boolean,
+ default: true
+ },
+ // 标题左边的缩略图
+ thumb: {
+ type: String,
+ default: ''
+ },
+ // 缩略图宽高,单位rpx
+ thumbWidth: {
+ type: [String, Number],
+ default: '60'
+ },
+ // 缩略图是否为圆形
+ thumbCircle: {
+ type: Boolean,
+ default: false
+ },
+ // 给head,body,foot的内边距
+ padding: {
+ type: [String, Number],
+ default: '30'
+ },
+ // 是否显示头部
+ showHead: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示尾部
+ showFoot: {
+ type: Boolean,
+ default: true
+ },
+ // 卡片外围阴影,字符串形式
+ boxShadow: {
+ type: String,
+ default: 'none'
+ }
+ },
+ data() {
+ return {};
+ },
+ methods: {
+ click() {
+ this.$emit('click', this.index);
+ },
+ headClick() {
+ this.$emit('head-click', this.index);
+ },
+ bodyClick() {
+ this.$emit('body-click', this.index);
+ },
+ footClick() {
+ this.$emit('foot-click', this.index);
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-card {
+ position: relative;
+ overflow: hidden;
+ font-size: 28rpx;
+ background-color: #ffffff;
+ box-sizing: border-box;
+
+ &-full {
+ // 如果是与屏幕之间不留空隙,应该设置左右边距为0
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ }
+
+ &--border:after {
+ border-radius: 16rpx;
+ }
+
+ &__head {
+ &--left {
+ color: $u-main-color;
+
+ &__thumb {
+ margin-right: 16rpx;
+ }
+
+ &__title {
+ max-width: 400rpx;
+ }
+ }
+
+ &--right {
+ color: $u-tips-color;
+ margin-left: 6rpx;
+ }
+ }
+
+ &__body {
+ color: $u-content-color;
+ }
+
+ &__foot {
+ color: $u-tips-color;
+ }
+}
+</style>
diff --git a/uview-ui/components/u-cell-group/u-cell-group.vue b/uview-ui/components/u-cell-group/u-cell-group.vue
new file mode 100644
index 0000000..3fbca72
--- /dev/null
+++ b/uview-ui/components/u-cell-group/u-cell-group.vue
@@ -0,0 +1,70 @@
+<template>
+ <view class="u-cell-box">
+ <view class="u-cell-title" v-if="title" :style="[titleStyle]">
+ {{title}}
+ </view>
+ <view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
+ <slot />
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * cellGroup 单元格父组件Group
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
+ * @tutorial https://www.uviewui.com/components/cell.html
+ * @property {String} title 分组标题
+ * @property {Boolean} border 是否显示外边框(默认true)
+ * @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
+ * @example <u-cell-group title="设置喜好">
+ */
+ export default {
+ name: "u-cell-group",
+ props: {
+ // 分组标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 是否显示分组list上下边框
+ border: {
+ type: Boolean,
+ default: true
+ },
+ // 分组标题的样式,对象形式,注意驼峰属性写法
+ // 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
+ titleStyle: {
+ type: Object,
+ default () {
+ return {};
+ }
+ }
+ },
+ data() {
+ return {
+ index: 0,
+ }
+ },
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-cell-box {
+ width: 100%;
+ }
+
+ .u-cell-title {
+ padding: 30rpx 32rpx 10rpx 32rpx;
+ font-size: 30rpx;
+ text-align: left;
+ color: $u-tips-color;
+ }
+
+ .u-cell-item-box {
+ background-color: #FFFFFF;
+ flex-direction: row;
+ }
+</style>
diff --git a/uview-ui/components/u-cell-item/u-cell-item.vue b/uview-ui/components/u-cell-item/u-cell-item.vue
new file mode 100644
index 0000000..c44fc9a
--- /dev/null
+++ b/uview-ui/components/u-cell-item/u-cell-item.vue
@@ -0,0 +1,316 @@
+<template>
+ <view
+ @tap="click"
+ class="u-cell"
+ :class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
+ hover-stay-time="150"
+ :hover-class="hoverClass"
+ :style="{
+ backgroundColor: bgColor
+ }"
+ >
+ <u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
+ <view class="u-flex" v-else>
+ <slot name="icon"></slot>
+ </view>
+ <view
+ class="u-cell_title"
+ :style="[
+ {
+ width: titleWidth ? titleWidth + 'rpx' : 'auto'
+ },
+ titleStyle
+ ]"
+ >
+ <block v-if="title">{{ title }}</block>
+ <slot name="title" v-else></slot>
+
+ <view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
+ <block v-if="label">{{ label }}</block>
+ <slot name="label" v-else></slot>
+ </view>
+ </view>
+
+ <view class="u-cell__value" :style="[valueStyle]">
+ <block class="u-cell__value" v-if="value">{{ value }}</block>
+ <slot v-else></slot>
+ </view>
+ <view class="u-flex u-cell_right" v-if="$slots['right-icon']">
+ <slot name="right-icon"></slot>
+ </view>
+ <u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
+ </view>
+</template>
+
+<script>
+/**
+ * cellItem 单元格Item
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
+ * @tutorial https://www.uviewui.com/components/cell.html
+ * @property {String} title 左侧标题
+ * @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {String} value 右侧内容
+ * @property {String} label 标题下方的描述信息
+ * @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
+ * @property {Boolean} border-top 是否显示cell的上边框(默认false)
+ * @property {Boolean} center 是否使内容垂直居中(默认false)
+ * @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
+ * // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
+ * @property {Boolean} arrow 是否显示右侧箭头(默认true)
+ * @property {Boolean} required 箭头方向,可选值(默认right)
+ * @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
+ * @property {Object} title-style 标题样式,对象形式
+ * @property {Object} value-style 右侧内容样式,对象形式
+ * @property {Object} label-style 标题下方描述信息的样式,对象形式
+ * @property {String} bg-color 背景颜色(默认transparent)
+ * @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
+ * @property {String Number} title-width 标题的宽度,单位rpx
+ * @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
+ */
+export default {
+ name: 'u-cell-item',
+ props: {
+ // 左侧图标名称(只能uView内置图标),或者图标src
+ icon: {
+ type: String,
+ default: ''
+ },
+ // 左侧标题
+ title: {
+ type: [String, Number],
+ default: ''
+ },
+ // 右侧内容
+ value: {
+ type: [String, Number],
+ default: ''
+ },
+ // 标题下方的描述信息
+ label: {
+ type: [String, Number],
+ default: ''
+ },
+ // 是否显示下边框
+ borderBottom: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示上边框
+ borderTop: {
+ type: Boolean,
+ default: false
+ },
+ // 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
+ // 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
+ // borderGap: {
+ // type: Boolean,
+ // default: true
+ // },
+ // 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
+ hoverClass: {
+ type: String,
+ default: 'u-cell-hover'
+ },
+ // 是否显示右侧箭头
+ arrow: {
+ type: Boolean,
+ default: true
+ },
+ // 内容是否垂直居中
+ center: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示左边表示必填的星号
+ required: {
+ type: Boolean,
+ default: false
+ },
+ // 标题的宽度,单位rpx
+ titleWidth: {
+ type: [Number, String],
+ default: ''
+ },
+ // 右侧箭头方向,可选值:right|up|down,默认为right
+ arrowDirection: {
+ type: String,
+ default: 'right'
+ },
+ // 控制标题的样式
+ titleStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 右侧显示内容的样式
+ valueStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 描述信息的样式
+ labelStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: 'transparent'
+ },
+ // 用于识别被点击的是第几个cell
+ index: {
+ type: [String, Number],
+ default: ''
+ },
+ // 是否使用lable插槽
+ useLabelSlot: {
+ type: Boolean,
+ default: false
+ },
+ // 左边图标的大小,单位rpx,只对传入icon字段时有效
+ iconSize: {
+ type: [Number, String],
+ default: 34
+ },
+ // 左边图标的样式,对象形式
+ iconStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ },
+ data() {
+ return {
+
+ };
+ },
+ computed: {
+ arrowStyle() {
+ let style = {};
+ if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
+ else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
+ else style.transform = 'rotate(0deg)';
+ return style;
+ }
+ },
+ methods: {
+ click() {
+ this.$emit('click', this.index);
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-cell {
+ @include vue-flex;
+ align-items: center;
+ position: relative;
+ /* #ifndef APP-NVUE */
+ box-sizing: border-box;
+ /* #endif */
+ width: 100%;
+ padding: 26rpx 32rpx;
+ font-size: 28rpx;
+ line-height: 54rpx;
+ color: $u-content-color;
+ background-color: #fff;
+ text-align: left;
+}
+
+.u-cell_title {
+ font-size: 28rpx;
+}
+
+.u-cell__left-icon-wrap {
+ margin-right: 10rpx;
+ font-size: 32rpx;
+}
+
+.u-cell__right-icon-wrap {
+ margin-left: 10rpx;
+ color: #969799;
+ font-size: 28rpx;
+}
+
+.u-cell__left-icon-wrap,
+.u-cell__right-icon-wrap {
+ @include vue-flex;
+ align-items: center;
+ height: 48rpx;
+}
+
+.u-cell-border:after {
+ position: absolute;
+ /* #ifndef APP-NVUE */
+ box-sizing: border-box;
+ content: ' ';
+ pointer-events: none;
+ border-bottom: 1px solid $u-border-color;
+ /* #endif */
+ right: 0;
+ left: 0;
+ top: 0;
+ transform: scaleY(0.5);
+}
+
+.u-cell-border {
+ position: relative;
+}
+
+.u-cell__label {
+ margin-top: 6rpx;
+ font-size: 26rpx;
+ line-height: 36rpx;
+ color: $u-tips-color;
+ /* #ifndef APP-NVUE */
+ word-wrap: break-word;
+ /* #endif */
+}
+
+.u-cell__value {
+ overflow: hidden;
+ text-align: right;
+ /* #ifndef APP-NVUE */
+ vertical-align: middle;
+ /* #endif */
+ color: $u-tips-color;
+ font-size: 26rpx;
+}
+
+.u-cell__title,
+.u-cell__value {
+ flex: 1;
+}
+
+.u-cell--required {
+ /* #ifndef APP-NVUE */
+ overflow: visible;
+ /* #endif */
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-cell--required:before {
+ position: absolute;
+ /* #ifndef APP-NVUE */
+ content: '*';
+ /* #endif */
+ left: 8px;
+ margin-top: 4rpx;
+ font-size: 14px;
+ color: $u-type-error;
+}
+
+.u-cell_right {
+ line-height: 1;
+}
+</style>
diff --git a/uview-ui/components/u-checkbox-group/u-checkbox-group.vue b/uview-ui/components/u-checkbox-group/u-checkbox-group.vue
new file mode 100644
index 0000000..6a149b3
--- /dev/null
+++ b/uview-ui/components/u-checkbox-group/u-checkbox-group.vue
@@ -0,0 +1,123 @@
+<template>
+ <view class="u-checkbox-group u-clearfix">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ import Emitter from '../../libs/util/emitter.js';
+ /**
+ * checkboxGroup 开关选择器父组件Group
+ * @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
+ * @tutorial https://www.uviewui.com/components/checkbox.html
+ * @property {String Number} max 最多能选中多少个checkbox(默认999)
+ * @property {String Number} size 组件整体的大小,单位rpx(默认40)
+ * @property {Boolean} disabled 是否禁用所有checkbox(默认false)
+ * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
+ * @property {String} width 宽度,需带单位
+ * @property {String} width 宽度,需带单位
+ * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
+ * @property {Boolean} wrap 是否每个checkbox都换行(默认false)
+ * @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
+ * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
+ * @example <u-checkbox-group></u-checkbox-group>
+ */
+ export default {
+ name: 'u-checkbox-group',
+ mixins: [Emitter],
+ props: {
+ // 最多能选中多少个checkbox
+ max: {
+ type: [Number, String],
+ default: 999
+ },
+ // 所有选中项的 name
+ // value: {
+ // default: Array,
+ // default() {
+ // return []
+ // }
+ // },
+ // 是否禁用所有复选框
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 在表单内提交时的标识符
+ name: {
+ type: [Boolean, String],
+ default: ''
+ },
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ type: Boolean,
+ default: false
+ },
+ // 形状,square为方形,circle为原型
+ shape: {
+ type: String,
+ default: 'square'
+ },
+ // 选中状态下的颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 组件的整体大小
+ size: {
+ type: [String, Number],
+ default: 34
+ },
+ // 每个checkbox占u-checkbox-group的宽度
+ width: {
+ type: String,
+ default: 'auto'
+ },
+ // 是否每个checkbox都换行
+ wrap: {
+ type: Boolean,
+ default: false
+ },
+ // 图标的大小,单位rpx
+ iconSize: {
+ type: [String, Number],
+ default: 20
+ },
+ },
+ data() {
+ return {
+ }
+ },
+ created() {
+ // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ this.children = [];
+ },
+ methods: {
+ emitEvent() {
+ let values = [];
+ this.children.map(val => {
+ if(val.value) values.push(val.name);
+ })
+ this.$emit('change', values);
+ // 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
+ // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+ setTimeout(() => {
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch('u-form-item', 'on-form-change', values);
+ }, 60)
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-checkbox-group {
+ /* #ifndef MP || APP-NVUE */
+ display: inline-flex;
+ flex-wrap: wrap;
+ /* #endif */
+ }
+</style>
diff --git a/uview-ui/components/u-checkbox/u-checkbox.vue b/uview-ui/components/u-checkbox/u-checkbox.vue
new file mode 100644
index 0000000..9414461
--- /dev/null
+++ b/uview-ui/components/u-checkbox/u-checkbox.vue
@@ -0,0 +1,284 @@
+<template>
+ <view class="u-checkbox" :style="[checkboxStyle]">
+ <view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
+ <u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
+ </view>
+ <view class="u-checkbox__label" @tap="onClickLabel" :style="{
+ fontSize: $u.addUnit(labelSize)
+ }">
+ <slot />
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * checkbox 复选框
+ * @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
+ * @tutorial https://www.uviewui.com/components/checkbox.html
+ * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+ * @property {String Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String Number} name checkbox组件的标示符
+ * @property {String} shape 形状,见官网说明(默认circle)
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
+ * @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
+ * @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
+ * @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
+ */
+ export default {
+ name: "u-checkbox",
+ props: {
+ // checkbox的名称
+ name: {
+ type: [String, Number],
+ default: ''
+ },
+ // 形状,square为方形,circle为原型
+ shape: {
+ type: String,
+ default: ''
+ },
+ // 是否为选中状态
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 是否禁用
+ disabled: {
+ type: [String, Boolean],
+ default: ''
+ },
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ type: [String, Boolean],
+ default: ''
+ },
+ // 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
+ activeColor: {
+ type: String,
+ default: ''
+ },
+ // 图标的大小,单位rpx
+ iconSize: {
+ type: [String, Number],
+ default: ''
+ },
+ // label的字体大小,rpx单位
+ labelSize: {
+ type: [String, Number],
+ default: ''
+ },
+ // 组件的整体大小
+ size: {
+ type: [String, Number],
+ default: ''
+ },
+ },
+ data() {
+ return {
+ parentDisabled: false,
+ newParams: {},
+ };
+ },
+ created() {
+ // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
+ // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
+ this.parent && this.parent.children.push(this);
+ },
+ computed: {
+ // 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
+ isDisabled() {
+ return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
+ },
+ // 是否禁用label点击
+ isLabelDisabled() {
+ return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
+ },
+ // 组件尺寸,对应size的值,默认值为34rpx
+ checkboxSize() {
+ return this.size ? this.size : (this.parent ? this.parent.size : 34);
+ },
+ // 组件的勾选图标的尺寸,默认20
+ checkboxIconSize() {
+ return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
+ },
+ // 组件选中激活时的颜色
+ elActiveColor() {
+ return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
+ },
+ // 组件的形状
+ elShape() {
+ return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
+ },
+ iconStyle() {
+ let style = {};
+ // 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
+ if (this.elActiveColor && this.value && !this.isDisabled) {
+ style.borderColor = this.elActiveColor;
+ style.backgroundColor = this.elActiveColor;
+ }
+ style.width = this.$u.addUnit(this.checkboxSize);
+ style.height = this.$u.addUnit(this.checkboxSize);
+ return style;
+ },
+ // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
+ iconColor() {
+ return this.value ? '#ffffff' : 'transparent';
+ },
+ iconClass() {
+ let classes = [];
+ classes.push('u-checkbox__icon-wrap--' + this.elShape);
+ if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
+ if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
+ if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
+ // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+ return classes.join(' ');
+ },
+ checkboxStyle() {
+ let style = {};
+ if(this.parent && this.parent.width) {
+ style.width = this.parent.width;
+ // #ifdef MP
+ // 各家小程序因为它们特殊的编译结构,使用float布局
+ style.float = 'left';
+ // #endif
+ // #ifndef MP
+ // H5和APP使用flex布局
+ style.flex = `0 0 ${this.parent.width}`;
+ // #endif
+ }
+ if(this.parent && this.parent.wrap) {
+ style.width = '100%';
+ // #ifndef MP
+ // H5和APP使用flex布局,将宽度设置100%,即可自动换行
+ style.flex = '0 0 100%';
+ // #endif
+ }
+ return style;
+ }
+ },
+ methods: {
+ onClickLabel() {
+ if (!this.isLabelDisabled && !this.isDisabled) {
+ this.setValue();
+ }
+ },
+ toggle() {
+ if (!this.isDisabled) {
+ this.setValue();
+ }
+ },
+ emitEvent() {
+ this.$emit('change', {
+ value: !this.value,
+ name: this.name
+ })
+ // 执行父组件u-checkbox-group的事件方法
+ // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
+ setTimeout(() => {
+ if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
+ }, 80);
+ },
+ // 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
+ setValue() {
+ // 判断是否超过了可选的最大数量
+ let checkedNum = 0;
+ if(this.parent && this.parent.children) {
+ // 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
+ this.parent.children.map(val => {
+ if (val.value) checkedNum++;
+ })
+ }
+ // 如果原来为选中状态,那么可以取消
+ if (this.value == true) {
+ this.emitEvent();
+ this.$emit('input', !this.value);
+ } else {
+ // 如果超出最多可选项,提示
+ if(this.parent && checkedNum >= this.parent.max) {
+ return this.$u.toast(`最多可选${this.parent.max}项`);
+ }
+ // 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
+ this.emitEvent();
+ this.$emit('input', !this.value);
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-checkbox {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ overflow: hidden;
+ user-select: none;
+ line-height: 1.8;
+
+ &__icon-wrap {
+ color: $u-content-color;
+ flex: none;
+ display: -webkit-flex;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 42rpx;
+ height: 42rpx;
+ color: transparent;
+ text-align: center;
+ transition-property: color, border-color, background-color;
+ font-size: 20px;
+ border: 1px solid #c8c9cc;
+ transition-duration: 0.2s;
+
+ /* #ifdef MP-TOUTIAO */
+ // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+ &__icon {
+ line-height: 0;
+ }
+ /* #endif */
+
+ &--circle {
+ border-radius: 100%;
+ }
+
+ &--square {
+ border-radius: 6rpx;
+ }
+
+ &--checked {
+ color: #fff;
+ background-color: $u-type-primary;
+ border-color: $u-type-primary;
+ }
+
+ &--disabled {
+ background-color: #ebedf0;
+ border-color: #c8c9cc;
+ }
+
+ &--disabled--checked {
+ color: #c8c9cc !important;
+ }
+ }
+
+ &__label {
+ word-wrap: break-word;
+ margin-left: 10rpx;
+ margin-right: 24rpx;
+ color: $u-content-color;
+ font-size: 30rpx;
+
+ &--disabled {
+ color: #c8c9cc;
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-circle-progress/u-circle-progress.vue b/uview-ui/components/u-circle-progress/u-circle-progress.vue
new file mode 100644
index 0000000..46e7c18
--- /dev/null
+++ b/uview-ui/components/u-circle-progress/u-circle-progress.vue
@@ -0,0 +1,220 @@
+<template>
+ <view
+ class="u-circle-progress"
+ :style="{
+ width: widthPx + 'px',
+ height: widthPx + 'px',
+ backgroundColor: bgColor
+ }"
+ >
+ <!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
+ <canvas
+ class="u-canvas-bg"
+ :canvas-id="elBgId"
+ :id="elBgId"
+ :style="{
+ width: widthPx + 'px',
+ height: widthPx + 'px'
+ }"
+ ></canvas>
+ <canvas
+ class="u-canvas"
+ :canvas-id="elId"
+ :id="elId"
+ :style="{
+ width: widthPx + 'px',
+ height: widthPx + 'px'
+ }"
+ ></canvas>
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+/**
+ * circleProgress 环形进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
+ * @tutorial https://www.uviewui.com/components/circleProgress.html
+ * @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
+ * @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
+ * @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
+ * @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
+ * @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
+ * @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
+ * @property {String} type 如设置,active-color值将会失效
+ * @property {String} bg-color 整个组件背景颜色,默认为白色
+ * @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
+ */
+export default {
+ name: 'u-circle-progress',
+ props: {
+ // 圆环进度百分比值
+ percent: {
+ type: Number,
+ default: 0,
+ // 限制值在0到100之间
+ validator: val => {
+ return val >= 0 && val <= 100;
+ }
+ },
+ // 底部圆环的颜色(灰色的圆环)
+ inactiveColor: {
+ type: String,
+ default: '#ececec'
+ },
+ // 圆环激活部分的颜色
+ activeColor: {
+ type: String,
+ default: '#19be6b'
+ },
+ // 圆环线条的宽度,单位rpx
+ borderWidth: {
+ type: [Number, String],
+ default: 14
+ },
+ // 整个圆形的宽度,单位rpx
+ width: {
+ type: [Number, String],
+ default: 200
+ },
+ // 整个圆环执行一圈的时间,单位ms
+ duration: {
+ type: [Number, String],
+ default: 1500
+ },
+ // 主题类型
+ type: {
+ type: String,
+ default: ''
+ },
+ // 整个圆环进度区域的背景色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ }
+ },
+ data() {
+ return {
+ // #ifdef MP-WEIXIN
+ elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
+ elId: 'uCircleProgressElId',
+ // #endif
+ // #ifndef MP-WEIXIN
+ elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
+ elId: this.$u.guid(),
+ // #endif
+ widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
+ borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
+ startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
+ progressContext: null, // 活动圆的canvas上下文
+ newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ };
+ },
+ watch: {
+ percent(nVal, oVal = 0) {
+ if (nVal > 100) nVal = 100;
+ if (nVal < 0) oVal = 0;
+ // 此值其实等于this.percent,命名一个新
+ this.newPercent = nVal;
+ this.oldPercent = oVal;
+ setTimeout(() => {
+ // 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
+ // 将此值减少或者新增到新的百分比值
+ this.drawCircleByProgress(oVal);
+ }, 50);
+ }
+ },
+ created() {
+ // 赋值,用于加载后第一个画圆使用
+ this.newPercent = this.percent;
+ this.oldPercent = 0;
+ },
+ computed: {
+ // 有type主题时,优先起作用
+ circleColor() {
+ if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
+ else return this.activeColor;
+ }
+ },
+ mounted() {
+ // 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
+ setTimeout(() => {
+ this.drawProgressBg();
+ this.drawCircleByProgress(this.oldPercent);
+ }, 50);
+ },
+ methods: {
+ drawProgressBg() {
+ let ctx = uni.createCanvasContext(this.elBgId, this);
+ ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
+ ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
+ ctx.beginPath(); // 开始描绘路径
+ // 设置一个原点(110,110),半径为100的圆的路径到当前路径
+ let radius = this.widthPx / 2;
+ ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
+ ctx.stroke(); // 对路径进行描绘
+ ctx.draw();
+ },
+ drawCircleByProgress(progress) {
+ // 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
+ let ctx = this.progressContext;
+ if (!ctx) {
+ ctx = uni.createCanvasContext(this.elId, this);
+ this.progressContext = ctx;
+ }
+ // 表示进度的两端为圆形
+ ctx.setLineCap('round');
+ // 设置线条的宽度和颜色
+ ctx.setLineWidth(this.borderWidthPx);
+ ctx.setStrokeStyle(this.circleColor);
+ // 将总过渡时间除以100,得出每修改百分之一进度所需的时间
+ let time = Math.floor(this.duration / 100);
+ // 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
+ // 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
+ let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
+ ctx.beginPath();
+ // 半径为整个canvas宽度的一半
+ let radius = this.widthPx / 2;
+ ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
+ ctx.stroke();
+ ctx.draw();
+ // 如果变更后新值大于旧值,意味着增大了百分比
+ if (this.newPercent > this.oldPercent) {
+ // 每次递增百分之一
+ progress++;
+ // 如果新增后的值,大于需要设置的值百分比值,停止继续增加
+ if (progress > this.newPercent) return;
+ } else {
+ // 同理于上面
+ progress--;
+ if (progress < this.newPercent) return;
+ }
+ setTimeout(() => {
+ // 定时器,每次操作间隔为time值,为了让进度条有动画效果
+ this.drawCircleByProgress(progress);
+ }, time);
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-circle-progress {
+ position: relative;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ justify-content: center;
+}
+
+.u-canvas-bg {
+ position: absolute;
+}
+
+.u-canvas {
+ position: absolute;
+}
+</style>
diff --git a/uview-ui/components/u-col/u-col.vue b/uview-ui/components/u-col/u-col.vue
new file mode 100644
index 0000000..3b6cc64
--- /dev/null
+++ b/uview-ui/components/u-col/u-col.vue
@@ -0,0 +1,156 @@
+<template>
+ <view class="u-col" :class="[
+ 'u-col-' + span
+ ]" :style="{
+ padding: `0 ${Number(gutter)/2 + 'rpx'}`,
+ marginLeft: 100 / 12 * offset + '%',
+ flex: `0 0 ${100 / 12 * span}%`,
+ alignItems: uAlignItem,
+ justifyContent: uJustify,
+ textAlign: textAlign
+ }"
+ @tap="click">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ /**
+ * col 布局单元格
+ * @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
+ * @tutorial https://www.uviewui.com/components/layout.html
+ * @property {String Number} span 栅格占据的列数,总12等分(默认0)
+ * @property {String} text-align 文字水平对齐方式(默认left)
+ * @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
+ * @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
+ */
+ export default {
+ name: "u-col",
+ props: {
+ // 占父容器宽度的多少等分,总分为12份
+ span: {
+ type: [Number, String],
+ default: 12
+ },
+ // 指定栅格左侧的间隔数(总12栏)
+ offset: {
+ type: [Number, String],
+ default: 0
+ },
+ // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+ justify: {
+ type: String,
+ default: 'start'
+ },
+ // 垂直对齐方式,可选值为top、center、bottom
+ align: {
+ type: String,
+ default: 'center'
+ },
+ // 文字对齐方式
+ textAlign: {
+ type: String,
+ default: 'left'
+ },
+ // 是否阻止事件传播
+ stop: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
+ }
+ },
+ created() {
+ this.parent = false;
+ },
+ mounted() {
+ // 获取父组件实例,并赋值给对应的参数
+ this.parent = this.$u.$parent.call(this, 'u-row');
+ if (this.parent) {
+ this.gutter = this.parent.gutter;
+ }
+ },
+ computed: {
+ uJustify() {
+ if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
+ else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
+ else return this.justify;
+ },
+ uAlignItem() {
+ if (this.align == 'top') return 'flex-start';
+ if (this.align == 'bottom') return 'flex-end';
+ else return this.align;
+ }
+ },
+ methods: {
+ click(e) {
+ this.$emit('click');
+ }
+ }
+ }
+</script>
+
+<style lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-col {
+ /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+ float: left;
+ /* #endif */
+ }
+
+ .u-col-0 {
+ width: 0;
+ }
+
+ .u-col-1 {
+ width: calc(100%/12);
+ }
+
+ .u-col-2 {
+ width: calc(100%/12 * 2);
+ }
+
+ .u-col-3 {
+ width: calc(100%/12 * 3);
+ }
+
+ .u-col-4 {
+ width: calc(100%/12 * 4);
+ }
+
+ .u-col-5 {
+ width: calc(100%/12 * 5);
+ }
+
+ .u-col-6 {
+ width: calc(100%/12 * 6);
+ }
+
+ .u-col-7 {
+ width: calc(100%/12 * 7);
+ }
+
+ .u-col-8 {
+ width: calc(100%/12 * 8);
+ }
+
+ .u-col-9 {
+ width: calc(100%/12 * 9);
+ }
+
+ .u-col-10 {
+ width: calc(100%/12 * 10);
+ }
+
+ .u-col-11 {
+ width: calc(100%/12 * 11);
+ }
+
+ .u-col-12 {
+ width: calc(100%/12 * 12);
+ }
+</style>
diff --git a/uview-ui/components/u-collapse-item/u-collapse-item.vue b/uview-ui/components/u-collapse-item/u-collapse-item.vue
new file mode 100644
index 0000000..3b66bfa
--- /dev/null
+++ b/uview-ui/components/u-collapse-item/u-collapse-item.vue
@@ -0,0 +1,204 @@
+<template>
+ <view class="u-collapse-item" :style="[itemStyle]">
+ <view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
+ <block v-if="!$slots['title-all']">
+ <view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
+ isShow && activeStyle && !arrow ? activeStyle : '']">
+ {{ title }}
+ </view>
+ <slot v-else name="title" />
+ <view class="u-icon-wrap">
+ <u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
+ class="u-arrow-down-icon" name="arrow-down"></u-icon>
+ </view>
+ </block>
+ <slot v-else name="title-all" />
+ </view>
+ <view class="u-collapse-body" :style="[{
+ height: isShow ? height + 'px' : '0'
+ }]">
+ <view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
+ <slot></slot>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * collapseItem 手风琴Item
+ * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
+ * @tutorial https://www.uviewui.com/components/collapse.html
+ * @property {String} title 面板标题
+ * @property {String Number} index 主要用于事件的回调,标识那个Item被点击
+ * @property {Boolean} disabled 面板是否可以打开或收起(默认false)
+ * @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
+ * @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
+ * @property {String} align 标题的对齐方式(默认left)
+ * @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
+ * @event {Function} change 某个item被打开或者收起时触发
+ * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
+ */
+ export default {
+ name: "u-collapse-item",
+ props: {
+ // 标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 标题的对齐方式
+ align: {
+ type: String,
+ default: 'left'
+ },
+ // 是否可以点击收起
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // collapse显示与否
+ open: {
+ type: Boolean,
+ default: false
+ },
+ // 唯一标识符
+ name: {
+ type: [Number, String],
+ default: ''
+ },
+ //活动样式
+ activeStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 标识当前为第几个
+ index: {
+ type: [String, Number],
+ default: ''
+ }
+ },
+ data() {
+ return {
+ isShow: false,
+ elId: this.$u.guid(),
+ height: 0, // body内容的高度
+ headStyle: {}, // 头部样式,对象形式
+ bodyStyle: {}, // 主体部分样式
+ itemStyle: {}, // 每个item的整体样式
+ arrowColor: '', // 箭头的颜色
+ hoverClass: '', // 头部按下时的效果样式类
+ arrow: true, // 是否显示右侧箭头
+
+ };
+ },
+ watch: {
+ open(val) {
+ this.isShow = val;
+ }
+ },
+ created() {
+ this.parent = false;
+ // 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
+ this.isShow = this.open;
+ },
+ methods: {
+ // 异步获取内容,或者动态修改了内容时,需要重新初始化
+ init() {
+ this.parent = this.$u.$parent.call(this, 'u-collapse');
+ if(this.parent) {
+ this.nameSync = this.name ? this.name : this.parent.childrens.length;
+ this.parent.childrens.push(this);
+ this.headStyle = this.parent.headStyle;
+ this.bodyStyle = this.parent.bodyStyle;
+ this.arrowColor = this.parent.arrowColor;
+ this.hoverClass = this.parent.hoverClass;
+ this.arrow = this.parent.arrow;
+ this.itemStyle = this.parent.itemStyle;
+ }
+ this.$nextTick(() => {
+ this.queryRect();
+ });
+ },
+ // 点击collapsehead头部
+ headClick() {
+ if (this.disabled) return;
+ if (this.parent && this.parent.accordion == true) {
+ this.parent.childrens.map(val => {
+ // 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
+ if (this != val) {
+ val.isShow = false;
+ }
+ });
+ }
+
+ this.isShow = !this.isShow;
+ // 触发本组件的事件
+ this.$emit('change', {
+ index: this.index,
+ show: this.isShow
+ })
+ // 只有在打开时才发出事件
+ if (this.isShow) this.parent && this.parent.onChange();
+ this.$forceUpdate();
+ },
+ // 查询内容高度
+ queryRect() {
+ // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+ // 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
+ this.$uGetRect('#' + this.elId).then(res => {
+ this.height = res.height;
+ })
+ }
+ },
+ mounted() {
+ this.init();
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-collapse-head {
+ position: relative;
+ @include vue-flex;
+ justify-content: space-between;
+ align-items: center;
+ color: $u-main-color;
+ font-size: 30rpx;
+ line-height: 1;
+ padding: 24rpx 0;
+ text-align: left;
+ }
+
+ .u-collapse-title {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .u-arrow-down-icon {
+ transition: all 0.3s;
+ margin-right: 20rpx;
+ margin-left: 14rpx;
+ }
+
+ .u-arrow-down-icon-active {
+ transform: rotate(180deg);
+ transform-origin: center center;
+ }
+
+ .u-collapse-body {
+ overflow: hidden;
+ transition: all 0.3s;
+ }
+
+ .u-collapse-content {
+ overflow: hidden;
+ font-size: 28rpx;
+ color: $u-tips-color;
+ text-align: left;
+ }
+</style>
diff --git a/uview-ui/components/u-collapse/u-collapse.vue b/uview-ui/components/u-collapse/u-collapse.vue
new file mode 100644
index 0000000..8572957
--- /dev/null
+++ b/uview-ui/components/u-collapse/u-collapse.vue
@@ -0,0 +1,99 @@
+<template>
+ <view class="u-collapse">
+ <slot />
+ </view>
+</template>
+
+<script>
+ /**
+ * collapse 手风琴
+ * @description 通过折叠面板收纳内容区域
+ * @tutorial https://www.uviewui.com/components/collapse.html
+ * @property {Boolean} accordion 是否手风琴模式(默认true)
+ * @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
+ * @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
+ * @property {Object} head-style 标题自定义样式,对象形式
+ * @property {Object} body-style 主体自定义样式,对象形式
+ * @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
+ * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
+ * @example <u-collapse></u-collapse>
+ */
+ export default {
+ name:"u-collapse",
+ props: {
+ // 是否手风琴模式
+ accordion: {
+ type: Boolean,
+ default: true
+ },
+ // 头部的样式
+ headStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 主体的样式
+ bodyStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 每一个item的样式
+ itemStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 是否显示右侧的箭头
+ arrow: {
+ type: Boolean,
+ default: true
+ },
+ // 箭头的颜色
+ arrowColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 标题部分按压时的样式类,"none"为无效果
+ hoverClass: {
+ type: String,
+ default: 'u-hover-class'
+ }
+ },
+ created() {
+ this.childrens = []
+ },
+ data() {
+ return {
+
+ }
+ },
+ methods: {
+ // 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
+ init() {
+ this.childrens.forEach((vm, index) => {
+ vm.init();
+ })
+ },
+ // collapse item被点击,由collapse item调用父组件方法
+ onChange() {
+ let activeItem = [];
+ this.childrens.forEach((vm, index) => {
+ if (vm.isShow) {
+ activeItem.push(vm.nameSync);
+ }
+ })
+ // 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
+ if (this.accordion) activeItem = activeItem.join('');
+ this.$emit('change', activeItem);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+</style>
diff --git a/uview-ui/components/u-column-notice/u-column-notice.vue b/uview-ui/components/u-column-notice/u-column-notice.vue
new file mode 100644
index 0000000..dd8bd31
--- /dev/null
+++ b/uview-ui/components/u-column-notice/u-column-notice.vue
@@ -0,0 +1,237 @@
+<template>
+ <view
+ class="u-notice-bar"
+ :style="{
+ background: computeBgColor,
+ padding: padding
+ }"
+ :class="[
+ type ? `u-type-${type}-light-bg` : ''
+ ]"
+ >
+ <view class="u-icon-wrap">
+ <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
+ </view>
+ <swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
+ <swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
+ <view
+ class="u-news-item u-line-1"
+ :style="[textStyle]"
+ @tap="click(index)"
+ :class="['u-type-' + type]"
+ >
+ {{ item }}
+ </view>
+ </swiper-item>
+ </swiper>
+ <view class="u-icon-wrap">
+ <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
+ <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>
+export default {
+ props: {
+ // 显示的内容,数组
+ list: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 显示的主题,success|error|primary|info|warning
+ type: {
+ type: String,
+ default: 'warning'
+ },
+ // 是否显示左侧的音量图标
+ volumeIcon: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示右侧的右箭头图标
+ moreIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示右侧的关闭图标
+ closeIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否自动播放
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 文字颜色,各图标也会使用文字颜色
+ color: {
+ type: String,
+ default: ''
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 滚动方向,row-水平滚动,column-垂直滚动
+ direction: {
+ type: String,
+ default: 'row'
+ },
+ // 是否显示
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 26
+ },
+ // 滚动一个周期的时间长,单位ms
+ duration: {
+ type: [Number, String],
+ default: 2000
+ },
+ // 音量喇叭的大小
+ volumeSize: {
+ type: [Number, String],
+ default: 34
+ },
+ // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+ speed: {
+ type: Number,
+ default: 160
+ },
+ // 水平滚动时,是否采用衔接形式滚动
+ isCircular: {
+ type: Boolean,
+ default: true
+ },
+ // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
+ mode: {
+ type: String,
+ default: 'horizontal'
+ },
+ // 播放状态,play-播放,paused-暂停
+ playState: {
+ type: String,
+ default: 'play'
+ },
+ // 是否禁止用手滑动切换
+ // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
+ disableTouch: {
+ type: Boolean,
+ default: true
+ },
+ // 通知的边距
+ padding: {
+ type: [Number, String],
+ default: '18rpx 24rpx'
+ }
+ },
+ computed: {
+ // 计算字体颜色,如果没有自定义的,就用uview主题颜色
+ computeColor() {
+ if (this.color) return this.color;
+ // 如果是无主题,就默认使用content-color
+ else if(this.type == 'none') return '#606266';
+ else return this.type;
+ },
+ // 文字内容的样式
+ textStyle() {
+ let style = {};
+ if (this.color) style.color = this.color;
+ else if(this.type == 'none') style.color = '#606266';
+ style.fontSize = this.fontSize + 'rpx';
+ return style;
+ },
+ // 垂直或者水平滚动
+ vertical() {
+ if(this.mode == 'horizontal') return false;
+ else return true;
+ },
+ // 计算背景颜色
+ computeBgColor() {
+ if (this.bgColor) return this.bgColor;
+ else if(this.type == 'none') return 'transparent';
+ }
+ },
+ data() {
+ return {
+ // animation: false
+ };
+ },
+ methods: {
+ // 点击通告栏
+ click(index) {
+ this.$emit('click', index);
+ },
+ // 点击关闭按钮
+ close() {
+ this.$emit('close');
+ },
+ // 点击更多箭头按钮
+ getMore() {
+ this.$emit('getMore');
+ },
+ change(e) {
+ let index = e.detail.current;
+ if(index == this.list.length - 1) {
+ this.$emit('end');
+ }
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-notice-bar {
+ width: 100%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: nowrap;
+ padding: 18rpx 24rpx;
+ overflow: hidden;
+}
+
+.u-swiper {
+ font-size: 26rpx;
+ height: 32rpx;
+ @include vue-flex;
+ align-items: center;
+ flex: 1;
+ margin-left: 12rpx;
+}
+
+.u-swiper-item {
+ @include vue-flex;
+ align-items: center;
+ overflow: hidden;
+}
+
+.u-news-item {
+ overflow: hidden;
+}
+
+.u-right-icon {
+ margin-left: 12rpx;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+}
+
+.u-left-icon {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+}
+</style>
diff --git a/uview-ui/components/u-count-down/u-count-down.vue b/uview-ui/components/u-count-down/u-count-down.vue
new file mode 100644
index 0000000..7285d67
--- /dev/null
+++ b/uview-ui/components/u-count-down/u-count-down.vue
@@ -0,0 +1,318 @@
+<template>
+ <view class="u-countdown">
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))">
+ <view class="u-countdown-time" :style="[letterStyle]">
+ {{ d }}
+ </view>
+ </view>
+ <view
+ class="u-countdown-colon"
+ :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+ v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
+ >
+ {{ separator == 'colon' ? ':' : '天' }}
+ </view>
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
+ <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+ {{ h }}
+ </view>
+ </view>
+ <view
+ class="u-countdown-colon"
+ :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+ v-if="showHours"
+ >
+ {{ separator == 'colon' ? ':' : '时' }}
+ </view>
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
+ <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+ {{ i }}
+ </view>
+ </view>
+ <view
+ class="u-countdown-colon"
+ :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+ v-if="showMinutes"
+ >
+ {{ separator == 'colon' ? ':' : '分' }}
+ </view>
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
+ <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+ {{ s }}
+ </view>
+ </view>
+ <view
+ class="u-countdown-colon"
+ :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+ v-if="showSeconds && separator == 'zh'"
+ >
+ 秒
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * countDown 倒计时
+ * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
+ * @tutorial https://www.uviewui.com/components/countDown.html
+ * @property {String Number} timestamp 倒计时,单位为秒
+ * @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
+ * @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
+ * @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30)
+ * @property {String} separator-color 分隔符的颜色(默认#303133)
+ * @property {String Number} font-size 倒计时字体大小,单位rpx(默认30)
+ * @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
+ * @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
+ * @property {String} border-color 数字边框的颜色(默认#303133)
+ * @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff)
+ * @property {String} color 倒计时数字的颜色(默认#303133)
+ * @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
+ * @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
+ * @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
+ * @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
+ * @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
+ * @event {Function} end 倒计时结束
+ * @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
+ * @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
+ */
+export default {
+ name: 'u-count-down',
+ props: {
+ // 倒计时的时间,秒为单位
+ timestamp: {
+ type: [Number, String],
+ default: 0
+ },
+ // 是否自动开始倒计时
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒"
+ separator: {
+ type: String,
+ default: 'colon'
+ },
+ // 分隔符的大小,单位rpx
+ separatorSize: {
+ type: [Number, String],
+ default: 30
+ },
+ // 分隔符颜色
+ separatorColor: {
+ type: String,
+ default: "#303133"
+ },
+ // 字体颜色
+ color: {
+ type: String,
+ default: '#303133'
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 30
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: '#fff'
+ },
+ // 数字框高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 'auto'
+ },
+ // 是否显示数字框
+ showBorder: {
+ type: Boolean,
+ default: false
+ },
+ // 边框颜色
+ borderColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 是否显示秒
+ showSeconds: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示分钟
+ showMinutes: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示小时
+ showHours: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示“天”
+ showDays: {
+ type: Boolean,
+ default: true
+ },
+ // 当"天"的部分为0时,不显示
+ hideZeroDay: {
+ type: Boolean,
+ default: false
+ }
+ },
+ watch: {
+ // 监听时间戳的变化
+ timestamp(newVal, oldVal) {
+ // 如果倒计时间发生变化,清除定时器,重新开始倒计时
+ this.clearTimer();
+ this.start();
+ }
+ },
+ data() {
+ return {
+ d: '00', // 天的默认值
+ h: '00', // 小时的默认值
+ i: '00', // 分钟的默认值
+ s: '00', // 秒的默认值
+ timer: null ,// 定时器
+ seconds: 0, // 记录不停倒计过程中变化的秒数
+ };
+ },
+ computed: {
+ // 倒计时item的样式,item为分别的时分秒部分的数字
+ itemStyle() {
+ let style = {};
+ if(this.height) {
+ style.height = this.height + 'rpx';
+ style.width = this.height + 'rpx';
+ }
+ if(this.showBorder) {
+ style.borderStyle = 'solid';
+ style.borderColor = this.borderColor;
+ style.borderWidth = '1px';
+ }
+ if(this.bgColor) {
+ style.backgroundColor = this.bgColor;
+ }
+ return style;
+ },
+ // 倒计时数字的样式
+ letterStyle() {
+ let style = {};
+ if(this.fontSize) style.fontSize = this.fontSize + 'rpx';
+ if(this.color) style.color = this.color;
+ return style;
+ }
+ },
+ mounted() {
+ // 如果自动倒计时
+ this.autoplay && this.timestamp && this.start();
+ },
+ methods: {
+ // 倒计时
+ start() {
+ // 避免可能出现的倒计时重叠情况
+ this.clearTimer();
+ if (this.timestamp <= 0) return;
+ this.seconds = Number(this.timestamp);
+ this.formatTime(this.seconds);
+ this.timer = setInterval(() => {
+ this.seconds--;
+ // 发出change事件
+ this.$emit('change', this.seconds);
+ if (this.seconds < 0) {
+ return this.end();
+ }
+ this.formatTime(this.seconds);
+ }, 1000);
+ },
+ // 格式化时间
+ formatTime(seconds) {
+ // 小于等于0的话,结束倒计时
+ seconds <= 0 && this.end();
+ let [day, hour, minute, second] = [0, 0, 0, 0];
+ day = Math.floor(seconds / (60 * 60 * 24));
+ // 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
+ // hour为给后面计算秒和分等用的(基于显示天的前提下计算)
+ hour = Math.floor(seconds / (60 * 60)) - day * 24;
+ // showHour为需要显示的小时
+ let showHour = null;
+ if(this.showDays) {
+ showHour = hour;
+ } else {
+ // 如果不显示天数,将“天”部分的时间折算到小时中去
+ showHour = Math.floor(seconds / (60 * 60));
+ }
+ minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
+ second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
+ // 如果小于10,在前面补上一个"0"
+ showHour = showHour < 10 ? '0' + showHour : showHour;
+ minute = minute < 10 ? '0' + minute : minute;
+ second = second < 10 ? '0' + second : second;
+ day = day < 10 ? '0' + day : day;
+ this.d = day;
+ this.h = showHour;
+ this.i = minute;
+ this.s = second;
+ },
+ // 停止倒计时
+ end() {
+ this.clearTimer();
+ this.$emit('end', {});
+ },
+ // 清除定时器
+ clearTimer() {
+ if(this.timer) {
+ // 清除定时器
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+ }
+ },
+ beforeDestroy() {
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+};
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-countdown {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ }
+
+ .u-countdown-item {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rpx;
+ border-radius: 6rpx;
+ white-space: nowrap;
+ transform: translateZ(0);
+ }
+
+ .u-countdown-time {
+ margin: 0;
+ padding: 0;
+ line-height: 1;
+ }
+
+ .u-countdown-colon {
+ @include vue-flex;
+ justify-content: center;
+ padding: 0 5rpx;
+ line-height: 1;
+ align-items: center;
+ padding-bottom: 4rpx;
+ }
+
+ .u-countdown-scale {
+ transform: scale(0.9);
+ transform-origin: center center;
+ }
+</style>
diff --git a/uview-ui/components/u-count-to/u-count-to.vue b/uview-ui/components/u-count-to/u-count-to.vue
new file mode 100644
index 0000000..053dc5f
--- /dev/null
+++ b/uview-ui/components/u-count-to/u-count-to.vue
@@ -0,0 +1,241 @@
+<template>
+ <view
+ class="u-count-num"
+ :style="{
+ fontSize: fontSize + 'rpx',
+ fontWeight: bold ? 'bold' : 'normal',
+ color: color
+ }"
+ >
+ {{ displayValue }}
+ </view>
+</template>
+
+<script>
+/**
+ * countTo 数字滚动
+ * @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
+ * @tutorial https://www.uviewui.com/components/countTo.html
+ * @property {String Number} start-val 开始值
+ * @property {String Number} end-val 结束值
+ * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
+ * @property {Boolean} autoplay 是否自动开始滚动(默认true)
+ * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
+ * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
+ * @property {String} separator 千位分隔符,见官网说明
+ * @property {String} color 字体颜色(默认#303133)
+ * @property {String Number} font-size 字体大小,单位rpx(默认50)
+ * @property {Boolean} bold 字体是否加粗(默认false)
+ * @event {Function} end 数值滚动到目标值时触发
+ * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
+ */
+export default {
+ name: 'u-count-to',
+ props: {
+ // 开始的数值,默认从0增长到某一个数
+ startVal: {
+ type: [Number, String],
+ default: 0
+ },
+ // 要滚动的目标数值,必须
+ endVal: {
+ type: [Number, String],
+ default: 0,
+ required: true
+ },
+ // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
+ duration: {
+ type: [Number, String],
+ default: 2000
+ },
+ // 设置数值后是否自动开始滚动
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 要显示的小数位数
+ decimals: {
+ type: [Number, String],
+ default: 0
+ },
+ // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
+ useEasing: {
+ type: Boolean,
+ default: true
+ },
+ // 十进制分割
+ decimal: {
+ type: [Number, String],
+ default: '.'
+ },
+ // 字体颜色
+ color: {
+ type: String,
+ default: '#303133'
+ },
+ // 字体大小
+ fontSize: {
+ type: [Number, String],
+ default: 50
+ },
+ // 是否加粗字体
+ bold: {
+ type: Boolean,
+ default: false
+ },
+ // 千位分隔符,类似金额的分割(¥23,321.05中的",")
+ separator: {
+ type: String,
+ default: ''
+ }
+ },
+ data() {
+ return {
+ localStartVal: this.startVal,
+ displayValue: this.formatNumber(this.startVal),
+ printVal: null,
+ paused: false, // 是否暂停
+ localDuration: Number(this.duration),
+ startTime: null, // 开始的时间
+ timestamp: null, // 时间戳
+ remaining: null, // 停留的时间
+ rAF: null,
+ lastTime: 0 // 上一次的时间
+ };
+ },
+ computed: {
+ countDown() {
+ return this.startVal > this.endVal;
+ }
+ },
+ watch: {
+ startVal() {
+ this.autoplay && this.start();
+ },
+ endVal() {
+ this.autoplay && this.start();
+ }
+ },
+ mounted() {
+ this.autoplay && this.start();
+ },
+ methods: {
+ easingFn(t, b, c, d) {
+ return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
+ },
+ requestAnimationFrame(callback) {
+ const currTime = new Date().getTime();
+ // 为了使setTimteout的尽可能的接近每秒60帧的效果
+ const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
+ const id = setTimeout(() => {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ this.lastTime = currTime + timeToCall;
+ return id;
+ },
+
+ cancelAnimationFrame(id) {
+ clearTimeout(id);
+ },
+ // 开始滚动数字
+ start() {
+ this.localStartVal = this.startVal;
+ this.startTime = null;
+ this.localDuration = this.duration;
+ this.paused = false;
+ this.rAF = this.requestAnimationFrame(this.count);
+ },
+ // 暂定状态,重新再开始滚动;或者滚动状态下,暂停
+ reStart() {
+ if (this.paused) {
+ this.resume();
+ this.paused = false;
+ } else {
+ this.stop();
+ this.paused = true;
+ }
+ },
+ // 暂停
+ stop() {
+ this.cancelAnimationFrame(this.rAF);
+ },
+ // 重新开始(暂停的情况下)
+ resume() {
+ this.startTime = null;
+ this.localDuration = this.remaining;
+ this.localStartVal = this.printVal;
+ this.requestAnimationFrame(this.count);
+ },
+ // 重置
+ reset() {
+ this.startTime = null;
+ this.cancelAnimationFrame(this.rAF);
+ this.displayValue = this.formatNumber(this.startVal);
+ },
+ count(timestamp) {
+ if (!this.startTime) this.startTime = timestamp;
+ this.timestamp = timestamp;
+ const progress = timestamp - this.startTime;
+ this.remaining = this.localDuration - progress;
+ if (this.useEasing) {
+ if (this.countDown) {
+ this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
+ } else {
+ this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
+ }
+ } else {
+ if (this.countDown) {
+ this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
+ } else {
+ this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
+ }
+ }
+ if (this.countDown) {
+ this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
+ } else {
+ this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
+ }
+ this.displayValue = this.formatNumber(this.printVal);
+ if (progress < this.localDuration) {
+ this.rAF = this.requestAnimationFrame(this.count);
+ } else {
+ this.$emit('end');
+ }
+ },
+ // 判断是否数字
+ isNumber(val) {
+ return !isNaN(parseFloat(val));
+ },
+ formatNumber(num) {
+ // 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
+ num = Number(num);
+ num = num.toFixed(Number(this.decimals));
+ num += '';
+ const x = num.split('.');
+ let x1 = x[0];
+ const x2 = x.length > 1 ? this.decimal + x[1] : '';
+ const rgx = /(\d+)(\d{3})/;
+ if (this.separator && !this.isNumber(this.separator)) {
+ while (rgx.test(x1)) {
+ x1 = x1.replace(rgx, '$1' + this.separator + '$2');
+ }
+ }
+ return x1 + x2;
+ },
+ destroyed() {
+ this.cancelAnimationFrame(this.rAF);
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-count-num {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ text-align: center;
+}
+</style>
diff --git a/uview-ui/components/u-divider/u-divider.vue b/uview-ui/components/u-divider/u-divider.vue
new file mode 100644
index 0000000..6f8d7e6
--- /dev/null
+++ b/uview-ui/components/u-divider/u-divider.vue
@@ -0,0 +1,153 @@
+<template>
+ <view class="u-divider" :style="{
+ height: height == 'auto' ? 'auto' : height + 'rpx',
+ backgroundColor: bgColor,
+ marginBottom: marginBottom + 'rpx',
+ marginTop: marginTop + 'rpx'
+ }" @tap="click">
+ <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+ <view v-if="useSlot" class="u-divider-text" :style="{
+ color: color,
+ fontSize: fontSize + 'rpx'
+ }"><slot /></view>
+ <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+ </view>
+</template>
+
+<script>
+/**
+ * divider 分割线
+ * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
+ * @tutorial https://www.uviewui.com/components/divider.html
+ * @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
+ * @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
+ * @property {String} color 文字颜色(默认#909399)
+ * @property {String Number} fontSize 字体大小,单位rpx(默认26)
+ * @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
+ * @property {String Number} height 整个divider的高度,单位rpx(默认40)
+ * @property {String} type 将线条设置主题色(默认primary)
+ * @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @event {Function} click divider组件被点击时触发
+ * @example <u-divider color="#fa3534">长河落日圆</u-divider>
+ */
+export default {
+ name: 'u-divider',
+ props: {
+ // 单一边divider横线的宽度(数值),单位rpx。或者百分比
+ halfWidth: {
+ type: [Number, String],
+ default: 150
+ },
+ // divider横线的颜色,如设置,
+ borderColor: {
+ type: String,
+ default: '#dcdfe6'
+ },
+ // 主题色,可以是primary|info|success|warning|error之一值
+ type: {
+ type: String,
+ default: 'primary'
+ },
+ // 文字颜色
+ color: {
+ type: String,
+ default: '#909399'
+ },
+ // 文字大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 26
+ },
+ // 整个divider的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 整个divider的高度单位rpx
+ height: {
+ type: [Number, String],
+ default: 'auto'
+ },
+ // 上边距
+ marginTop: {
+ type: [String, Number],
+ default: 0
+ },
+ // 下边距
+ marginBottom: {
+ type: [String, Number],
+ default: 0
+ },
+ // 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
+ useSlot: {
+ type: Boolean,
+ default: true
+ }
+ },
+ computed: {
+ lineStyle() {
+ let style = {};
+ if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
+ else style.width = this.halfWidth + 'rpx';
+ // borderColor优先级高于type值
+ if(this.borderColor) style.borderColor = this.borderColor;
+ return style;
+ }
+ },
+ methods: {
+ click() {
+ this.$emit('click');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-divider {
+ width: 100%;
+ position: relative;
+ text-align: center;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ flex-direction: row;
+}
+
+.u-divider-line {
+ border-bottom: 1px solid $u-border-color;
+ transform: scale(1, 0.5);
+ transform-origin: center;
+
+ &--bordercolor--primary {
+ border-color: $u-type-primary;
+ }
+
+ &--bordercolor--success {
+ border-color: $u-type-success;
+ }
+
+ &--bordercolor--error {
+ border-color: $u-type-primary;
+ }
+
+ &--bordercolor--info {
+ border-color: $u-type-info;
+ }
+
+ &--bordercolor--warning {
+ border-color: $u-type-warning;
+ }
+}
+
+.u-divider-text {
+ white-space: nowrap;
+ padding: 0 16rpx;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+}
+</style>
diff --git a/uview-ui/components/u-dropdown-item/u-dropdown-item.vue b/uview-ui/components/u-dropdown-item/u-dropdown-item.vue
new file mode 100644
index 0000000..ba60d8f
--- /dev/null
+++ b/uview-ui/components/u-dropdown-item/u-dropdown-item.vue
@@ -0,0 +1,132 @@
+<template>
+ <view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
+ <block v-if="!$slots.default && !$slots.$default">
+ <scroll-view scroll-y="true" :style="{
+ height: $u.addUnit(height)
+ }">
+ <view class="u-dropdown-item__options">
+ <u-cell-group>
+ <u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
+ :key="index" :title-style="{
+ color: value == item.value ? activeColor : inactiveColor
+ }">
+ <u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
+ </u-cell-item>
+ </u-cell-group>
+ </view>
+ </scroll-view>
+ </block>
+ <slot v-else />
+ </view>
+</template>
+
+<script>
+ /**
+ * dropdown-item 下拉菜单
+ * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+ * @tutorial http://uviewui.com/components/dropdown.html
+ * @property {String | Number} v-model 双向绑定选项卡选择值
+ * @property {String} title 菜单项标题
+ * @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
+ * @property {Boolean} disabled 是否禁用此选项卡(默认false)
+ * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+ * @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
+ * @example <u-dropdown-item title="标题"></u-dropdown-item>
+ */
+ export default {
+ name: 'u-dropdown-item',
+ props: {
+ // 当前选中项的value值
+ value: {
+ type: [Number, String, Array],
+ default: ''
+ },
+ // 菜单项标题
+ title: {
+ type: [String, Number],
+ default: ''
+ },
+ // 选项数据,如果传入了默认slot,此参数无效
+ options: {
+ type: Array,
+ default () {
+ return []
+ }
+ },
+ // 是否禁用此菜单项
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 下拉弹窗的高度
+ height: {
+ type: [Number, String],
+ default: 'auto'
+ },
+ },
+ data() {
+ return {
+ active: false, // 当前项是否处于展开状态
+ activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色
+ inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色
+ }
+ },
+ computed: {
+ // 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
+ propsChange() {
+ return `${this.title}-${this.disabled}`;
+ }
+ },
+ watch: {
+ propsChange(n) {
+ // 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
+ // 将所有子组件数据重新整理一遍
+ if (this.parent) this.parent.init();
+ }
+ },
+ created() {
+ // 父组件的实例
+ this.parent = false;
+ },
+ methods: {
+ init() {
+ // 获取父组件u-dropdown
+ let parent = this.$u.$parent.call(this, 'u-dropdown');
+ if (parent) {
+ this.parent = parent;
+ // 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
+ this.activeColor = parent.activeColor;
+ this.inactiveColor = parent.inactiveColor;
+ // 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
+ // push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
+ let exist = parent.children.find(val => {
+ return this === val;
+ })
+ if (!exist) parent.children.push(this);
+ if (parent.children.length == 1) this.active = true;
+ // 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
+ parent.menuList.push({
+ title: this.title,
+ disabled: this.disabled
+ });
+ }
+ },
+ // cell被点击
+ cellClick(value) {
+ // 修改通过v-model绑定的值
+ this.$emit('input', value);
+ // 通知父组件(u-dropdown)收起菜单
+ this.parent.close();
+ // 发出事件,抛出当前勾选项的value
+ this.$emit('change', value);
+ }
+ },
+ mounted() {
+ this.init();
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+</style>
diff --git a/uview-ui/components/u-dropdown-list/u-dropdown-list.vue b/uview-ui/components/u-dropdown-list/u-dropdown-list.vue
new file mode 100644
index 0000000..cd73c44
--- /dev/null
+++ b/uview-ui/components/u-dropdown-list/u-dropdown-list.vue
@@ -0,0 +1,242 @@
+<template>
+ <view class="dropdown-list-wapper u-flex u-flex-1">
+ <view
+ v-for="(drop, index) in dropdownListFromFather"
+ :key="drop.name"
+ :show="drop.show"
+ class="u-selected-class u-dropdown-list"
+ :style="{ zIndex: zIndex + 1 }"
+ >
+ <slot name="selectionbox">
+ <view
+ :style="{ height: top + 'rpx' }"
+ class="drop-item u-flex u-justify-center"
+ @click="handleDropClick(drop)"
+ >
+ <text :style="{ color: drop.show ? activeColor : '#999' }">
+ {{ getTitle(drop.options) }}
+ </text>
+ <view
+ class="u-animation"
+ :class="[drop.show ? 'u-animation-show' : '']"
+ >
+ <u-icon
+ v-if="drop.show"
+ name="arrow-up-fill"
+ :size="18"
+ :color="activeColor"
+ ></u-icon>
+ <u-icon v-else name="arrow-down-fill" :size="18"></u-icon>
+ </view>
+ </view>
+ </slot>
+ <view
+ class="u-dropdown-view"
+ :class="[drop.show ? 'u-dropdownlist-show' : '']"
+ :style="{
+ background: bgcolor,
+ height: drop.show ? 'auto' : 0,
+ top: top + 'rpx'
+ }"
+ >
+ <slot name="dropdownbox">
+ <view class="u-selected-list">
+ <view
+ class="select-item u-flex u-align-center u-border-bottom u-align-between"
+ :style="{ color: select.select ? activeColor : '#666666' }"
+ @tap="handleSelected(select, drop.options)"
+ v-for="(select, n) in drop.options"
+ :key="n"
+ >
+ <text>{{ select.text }}</text>
+ <u-icon
+ v-if="select.select"
+ class="select-icon"
+ :color="activeColor"
+ size="35"
+ name="checkmark"
+ ></u-icon>
+ </view>
+ </view>
+ </slot>
+ </view>
+ </view>
+ <u-mask
+ duration="100"
+ :show="dropdownShow"
+ @click="closeMask"
+ :z-index="zIndex"
+ ></u-mask>
+ </view>
+</template>
+
+<script>
+const dropdownOption1 = [
+ { id: 0, text: '类型', value: '', select: false },
+ { id: 1, text: '全场券', value: 1, select: false },
+ { id: 2, text: '品类券', value: 2, select: false },
+ { id: 3, text: '单品券', value: 3, select: false },
+ { id: 4, text: '业务券', value: 4, select: false }
+]
+const dropdownOption2 = [
+ { id: 5, text: '状态', value: '', select: false },
+ { id: 6, text: '可使用', value: 1, select: false },
+ { id: 7, text: '已过期', value: 2, select: false }
+]
+const dropdownOption3 = [
+ { id: 8, text: '优惠力度', value: '', select: false },
+ { id: 9, text: '满100-20', value: 1, select: false },
+ { id: 10, text: '满100-50', value: 2, select: false }
+]
+
+export default {
+ props: {
+ // 下拉框数据
+ dropdownList: {
+ type: Array,
+ default: () => [
+ { show: false, options: dropdownOption1 },
+ { show: false, options: dropdownOption2 },
+ { show: false, options: dropdownOption3 }
+ ],
+ required: true,
+ validator: value =>
+ value.every(item => Array.isArray(item.options) && item.options.length)
+ },
+ //背景颜色
+ bgcolor: {
+ type: String,
+ default: 'none'
+ },
+ //top rpx 选择框高度也用这个值
+ top: {
+ type: Number,
+ default: 90
+ },
+ // 菜单标题和选项的选中态颜色
+ activeColor: {
+ type: String,
+ default: '#e7141a'
+ },
+ // mask和下拉列表的z-index
+ zIndex: {
+ type: [String, Number],
+ default: 21
+ }
+ },
+ data() {
+ return {
+ dropdownShow: false,
+ dropdownListFromFather: this.dropdownList
+ }
+ },
+ computed: {},
+ methods: {
+ getTitle(item = []) {
+ const obj = item.find(v => v.select) || {}
+ if (obj.select) {
+ return obj.text
+ } else {
+ if (item[0]) {
+ item[0].select = true
+ return item[0].text
+ }
+ }
+ return ''
+ },
+ handleDropClick(item) {
+ if (item.show) {
+ item.show = false
+ this.dropdownShow = false
+ return
+ }
+ this.dropdownListFromFather.map(item => {
+ item.show = false
+ })
+ const t = setTimeout(() => {
+ item.show = true
+ this.dropdownShow = true
+ clearTimeout(t)
+ }, 100)
+ },
+ closeMask() {
+ this.dropdownShow = false
+ this.dropdownListFromFather.map(item => {
+ item.show = false
+ })
+ },
+ handleSelected(select, options) {
+ options.map(item => {
+ item.select = false
+ })
+ select.select = true
+ this.closeMask()
+ // 返回选中对象和下拉列表数组
+ this.$emit('change', select, options)
+ }
+ },
+ watch: {
+ dropdownList: {
+ handler(v) {
+ this.dropdownListFromFather = v
+ },
+ deep: true
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.dropdown-list-wapper {
+ position: relative;
+}
+.u-dropdown-view {
+ width: 100%;
+ overflow: hidden;
+ position: absolute;
+ z-index: 9999;
+ left: 0;
+ right: 0;
+ /* opacity: 0; */
+ visibility: hidden;
+ transition: height 0.5s ease-in-out;
+ .u-selected-list {
+ background-color: #fff;
+ .select-item {
+ color: #666666;
+ font-size: 28rpx;
+ padding: 30rpx 54rpx 30rpx 30rpx;
+ margin-left: 30rpx;
+ }
+ .select-item.selectActive {
+ color: #e7141a;
+ }
+ }
+}
+
+.u-dropdownlist-show {
+ /* opacity: 1; */
+ visibility: visible;
+}
+.u-dropdown-list {
+ flex: 1;
+ // z-index: 22;
+ background: #fff;
+ position: static;
+}
+.drop-item {
+ justify-content: center;
+ color: #999999;
+ font-size: 30rpx;
+ > text {
+ margin-right: 10rpx;
+ }
+ /deep/ {
+ .uicon {
+ position: relative;
+ top: -2rpx;
+ }
+ }
+}
+</style>
diff --git a/uview-ui/components/u-dropdown/u-dropdown.vue b/uview-ui/components/u-dropdown/u-dropdown.vue
new file mode 100644
index 0000000..a62e469
--- /dev/null
+++ b/uview-ui/components/u-dropdown/u-dropdown.vue
@@ -0,0 +1,298 @@
+<template>
+ <view class="u-dropdown">
+ <view class="u-dropdown__menu" :style="{
+ height: $u.addUnit(height)
+ }" :class="{
+ 'u-border-bottom': borderBottom
+ }">
+ <view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
+ <view class="u-flex">
+ <text class="u-dropdown__menu__item__text" :style="{
+ color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
+ fontSize: $u.addUnit(titleSize)
+ }">{{item.title}}</text>
+ <view class="u-dropdown__menu__item__arrow" :class="{
+ 'u-dropdown__menu__item__arrow--rotate': index === current
+ }">
+ <u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="u-dropdown__content" :style="[contentStyle, {
+ transition: `opacity ${duration / 1000}s linear`,
+ top: $u.addUnit(height),
+ height: contentHeight + 'px'
+ }]"
+ @tap="maskClick" @touchmove.stop.prevent>
+ <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
+ <slot></slot>
+ </view>
+ <view class="u-dropdown__content__mask"></view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * dropdown 下拉菜单
+ * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+ * @tutorial http://uviewui.com/components/dropdown.html
+ * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
+ * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
+ * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
+ * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
+ * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+ * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
+ * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
+ * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
+ * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
+ * @event {Function} open 下拉菜单被打开时触发
+ * @event {Function} close 下拉菜单被关闭时触发
+ * @example <u-dropdown></u-dropdown>
+ */
+ export default {
+ name: 'u-dropdown',
+ props: {
+ // 菜单标题和选项的激活态颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 菜单标题和选项的未激活态颜色
+ inactiveColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 点击遮罩是否关闭菜单
+ closeOnClickMask: {
+ type: Boolean,
+ default: true
+ },
+ // 点击当前激活项标题是否关闭菜单
+ closeOnClickSelf: {
+ type: Boolean,
+ default: true
+ },
+ // 过渡时间
+ duration: {
+ type: [Number, String],
+ default: 300
+ },
+ // 标题菜单的高度,单位任意,数值默认为rpx单位
+ height: {
+ type: [Number, String],
+ default: 80
+ },
+ // 是否显示下边框
+ borderBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 标题的字体大小
+ titleSize: {
+ type: [Number, String],
+ default: 28
+ },
+ // 下拉出来的内容部分的圆角值
+ borderRadius: {
+ type: [Number, String],
+ default: 0
+ },
+ // 菜单右侧的icon图标
+ menuIcon: {
+ type: String,
+ default: 'arrow-down'
+ },
+ // 菜单右侧图标的大小
+ menuIconSize: {
+ type: [Number, String],
+ default: 26
+ }
+ },
+ data() {
+ return {
+ showDropdown: true, // 是否打开下来菜单,
+ menuList: [], // 显示的菜单
+ active: false, // 下拉菜单的状态
+ // 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
+ // 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
+ current: 99999,
+ // 外层内容的样式,初始时处于底层,且透明
+ contentStyle: {
+ zIndex: -1,
+ opacity: 0
+ },
+ // 让某个菜单保持高亮的状态
+ highlightIndex: 99999,
+ contentHeight: 0
+ }
+ },
+ computed: {
+ // 下拉出来部分的样式
+ popupStyle() {
+ let style = {};
+ // 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
+ style.transform = `translateY(${this.active ? 0 : '-100%'})`
+ style['transition-duration'] = this.duration / 1000 + 's';
+ style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
+ return style;
+ }
+ },
+ created() {
+ // 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
+ this.children = [];
+ },
+ mounted() {
+ this.getContentHeight();
+ },
+ methods: {
+ init() {
+ // 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
+ // 以保证数据的正确性
+ this.menuList = [];
+ this.children.map(child => {
+ child.init();
+ })
+ },
+ // 点击菜单
+ menuClick(index) {
+ // 判断是否被禁用
+ if (this.menuList[index].disabled) return;
+ // 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
+ if (index === this.current && this.closeOnClickSelf) {
+ this.close();
+ // 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
+ setTimeout(() => {
+ this.children[index].active = false;
+ }, this.duration)
+ return;
+ }
+ this.open(index);
+ },
+ // 打开下拉菜单
+ open(index) {
+ // 重置高亮索引,否则会造成多个菜单同时高亮
+ // this.highlightIndex = 9999;
+ // 展开时,设置下拉内容的样式
+ this.contentStyle = {
+ zIndex: 11,
+ }
+ // 标记展开状态以及当前展开项的索引
+ this.active = true;
+ this.current = index;
+ // 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
+ // 之所以不是因display: none,是因为nvue没有display这个属性
+ this.children.map((val, idx) => {
+ val.active = index == idx ? true : false;
+ })
+ this.$emit('open', this.current);
+ },
+ // 设置下拉菜单处于收起状态
+ close() {
+ this.$emit('close', this.current);
+ // 设置为收起状态,同时current归位,设置为空字符串
+ this.active = false;
+ this.current = 99999;
+ // 下拉内容的样式进行调整,不透明度设置为0
+ this.contentStyle = {
+ zIndex: -1,
+ opacity: 0
+ }
+ },
+ // 点击遮罩
+ maskClick() {
+ // 如果不允许点击遮罩,直接返回
+ if (!this.closeOnClickMask) return;
+ this.close();
+ },
+ // 外部手动设置某个菜单高亮
+ highlight(index = undefined) {
+ this.highlightIndex = index !== undefined ? index : 99999;
+ },
+ // 获取下拉菜单内容的高度
+ getContentHeight() {
+ // 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
+ // 才能让遮罩占满菜单一下,直到屏幕底部的高度
+ // this.$u.sys()为uView封装的获取设备信息的方法
+ let windowHeight = this.$u.sys().windowHeight;
+ this.$uGetRect('.u-dropdown__menu').then(res => {
+ // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
+ // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
+ // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
+ // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
+ this.contentHeight = windowHeight - res.bottom;
+ })
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-dropdown {
+ flex: 1;
+ width: 100%;
+ position: relative;
+
+ &__menu {
+ @include vue-flex;
+ position: relative;
+ z-index: 11;
+ height: 80rpx;
+
+ &__item {
+ flex: 1;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+
+ &__text {
+ font-size: 28rpx;
+ color: $u-content-color;
+ }
+
+ &__arrow {
+ margin-left: 6rpx;
+ transition: transform .3s;
+ align-items: center;
+ @include vue-flex;
+
+ &--rotate {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+
+ &__content {
+ position: absolute;
+ z-index: 8;
+ width: 100%;
+ left: 0px;
+ bottom: 0;
+ overflow: hidden;
+
+
+ &__mask {
+ position: absolute;
+ z-index: 9;
+ background: rgba(0, 0, 0, .3);
+ width: 100%;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ }
+
+ &__popup {
+ position: relative;
+ z-index: 10;
+ transition: all 0.3s;
+ transform: translate3D(0, -100%, 0);
+ overflow: hidden;
+ }
+ }
+
+ }
+</style>
diff --git a/uview-ui/components/u-empty/u-empty.vue b/uview-ui/components/u-empty/u-empty.vue
new file mode 100644
index 0000000..2c77b24
--- /dev/null
+++ b/uview-ui/components/u-empty/u-empty.vue
@@ -0,0 +1,193 @@
+<template>
+ <view class="u-empty" v-if="show" :style="{
+ marginTop: marginTop + 'rpx'
+ }">
+ <u-icon
+ :name="src ? src : 'empty-' + mode"
+ :custom-style="iconStyle"
+ :label="text ? text : icons[mode]"
+ label-pos="bottom"
+ :label-color="color"
+ :label-size="fontSize"
+ :size="iconSize"
+ :color="iconColor"
+ margin-top="14"
+ ></u-icon>
+ <view class="u-slot-wrap">
+ <slot name="bottom"></slot>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * empty 内容为空
+ * @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
+ * @tutorial https://www.uviewui.com/components/empty.html
+ * @property {String} color 文字颜色(默认#c0c4cc)
+ * @property {String} text 文字提示(默认“无内容”)
+ * @property {String} src 自定义图标路径,如定义,mode参数会失效
+ * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
+ * @property {String} mode 内置的图标,见官网说明(默认data)
+ * @property {String Number} img-width 图标的宽度,单位rpx(默认240)
+ * @property {String} img-height 图标的高度,单位rpx(默认auto)
+ * @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
+ * @property {Boolean} show 是否显示组件(默认true)
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 点击关闭按钮时触发
+ * @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
+ */
+ export default {
+ name: "u-empty",
+ props: {
+ // 图标路径
+ src: {
+ type: String,
+ default: ''
+ },
+ // 提示文字
+ text: {
+ type: String,
+ default: ''
+ },
+ // 文字颜色
+ color: {
+ type: String,
+ default: '#c0c4cc'
+ },
+ // 图标的颜色
+ iconColor: {
+ type: String,
+ default: '#c0c4cc'
+ },
+ // 图标的大小
+ iconSize: {
+ type: [String, Number],
+ default: 120
+ },
+ // 文字大小,单位rpx
+ fontSize: {
+ type: [String, Number],
+ default: 26
+ },
+ // 选择预置的图标类型
+ mode: {
+ type: String,
+ default: 'data'
+ },
+ // 图标宽度,单位rpx
+ imgWidth: {
+ type: [String, Number],
+ default: 120
+ },
+ // 图标高度,单位rpx
+ imgHeight: {
+ type: [String, Number],
+ default: 'auto'
+ },
+ // 是否显示组件
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 组件距离上一个元素之间的距离
+ marginTop: {
+ type: [String, Number],
+ default: 0
+ },
+ iconStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ }
+ },
+ data() {
+ return {
+ icons: {
+ car: '购物车为空',
+ page: '页面不存在',
+ search: '没有搜索结果',
+ address: '没有收货地址',
+ wifi: '没有WiFi',
+ order: '订单为空',
+ coupon: '没有优惠券',
+ favor: '暂无收藏',
+ permission: '无权限',
+ history: '无历史记录',
+ news: '无新闻列表',
+ message: '消息列表为空',
+ list: '列表为空',
+ data: '数据为空'
+ },
+ // icons: [{
+ // icon: 'car',
+ // text: '购物车为空'
+ // },{
+ // icon: 'page',
+ // text: '页面不存在'
+ // },{
+ // icon: 'search',
+ // text: '没有搜索结果'
+ // },{
+ // icon: 'address',
+ // text: '没有收货地址'
+ // },{
+ // icon: 'wifi',
+ // text: '没有WiFi'
+ // },{
+ // icon: 'order',
+ // text: '订单为空'
+ // },{
+ // icon: 'coupon',
+ // text: '没有优惠券'
+ // },{
+ // icon: 'favor',
+ // text: '暂无收藏'
+ // },{
+ // icon: 'permission',
+ // text: '无权限'
+ // },{
+ // icon: 'history',
+ // text: '无历史记录'
+ // },{
+ // icon: 'news',
+ // text: '无新闻列表'
+ // },{
+ // icon: 'message',
+ // text: '消息列表为空'
+ // },{
+ // icon: 'list',
+ // text: '列表为空'
+ // },{
+ // icon: 'data',
+ // text: '数据为空'
+ // }],
+
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-empty {
+ @include vue-flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ }
+
+ .u-image {
+ margin-bottom: 20rpx;
+ }
+
+ .u-slot-wrap {
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 20rpx;
+ }
+</style>
diff --git a/uview-ui/components/u-field/u-field.vue b/uview-ui/components/u-field/u-field.vue
new file mode 100644
index 0000000..b562798
--- /dev/null
+++ b/uview-ui/components/u-field/u-field.vue
@@ -0,0 +1,384 @@
+<template>
+ <view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
+ <view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
+ <view class="u-label" :class="[required ? 'u-required' : '']" :style="{
+ justifyContent: justifyContent,
+ flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
+ }">
+ <view class="u-icon-wrap" v-if="icon">
+ <u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
+ </view>
+ <slot name="icon"></slot>
+ <text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
+ </view>
+ <view class="fild-body">
+ <view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
+ <textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
+ :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
+ :focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
+ @tap="fieldClick" />
+ <input
+ v-else
+ :style="[fieldStyle]"
+ :type="type"
+ class="u-flex-1 u-field__input-wrap"
+ :value="value"
+ :password="password || this.type === 'password'"
+ :placeholder="placeholder"
+ :placeholderStyle="placeholderStyle"
+ :disabled="disabled"
+ :maxlength="inputMaxlength"
+ :focus="focus"
+ :confirmType="confirmType"
+ @focus="onFocus"
+ @blur="onBlur"
+ @input="onInput"
+ @confirm="onConfirm"
+ @tap="fieldClick"
+ />
+ </view>
+ <u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
+ <view class="u-button-wrap"><slot name="right" /></view>
+ <u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
+ </view>
+ </view>
+ <view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
+ paddingLeft: labelWidth + 'rpx'
+ }">{{ errorMessage }}</view>
+ </view>
+</template>
+
+<script>
+/**
+ * field 输入框
+ * @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
+ * @tutorial https://www.uviewui.com/components/field.html
+ * @property {String} type 输入框的类型(默认text)
+ * @property {String} icon label左边的图标,限uView的图标名称
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
+ * @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
+ * @property {String} label 输入框左边的文字提示
+ * @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
+ * @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
+ * @property {Number String} label-width label的宽度,单位rpx(默认130)
+ * @property {String} label-align label的文字对齐方式(默认left)
+ * @property {Object} field-style 自定义输入框的样式,对象形式
+ * @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
+ * @property {String} input-align 输入框内容对齐方式(默认left)
+ * @property {Boolean} border-bottom 是否显示field的下边框(默认true)
+ * @property {Boolean} border-top 是否显示field的上边框(默认false)
+ * @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
+ * @property {String} placeholder 输入框的提示文字
+ * @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} disabled 是否不可输入(默认false)
+ * @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+ * @event {Function} input 输入框内容发生变化时触发
+ * @event {Function} focus 输入框获得焦点时触发
+ * @event {Function} blur 输入框失去焦点时触发
+ * @event {Function} confirm 点击完成按钮时触发
+ * @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
+ * @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
+ * @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
+ */
+export default {
+ name:"u-field",
+ props: {
+ icon: String,
+ rightIcon: String,
+ // arrowDirection: {
+ // type: String,
+ // default: 'right'
+ // },
+ required: Boolean,
+ label: String,
+ password: Boolean,
+ clearable: {
+ type: Boolean,
+ default: true
+ },
+ // 左边标题的宽度单位rpx
+ labelWidth: {
+ type: [Number, String],
+ default: 130
+ },
+ // 对齐方式,left|center|right
+ labelAlign: {
+ type: String,
+ default: 'left'
+ },
+ inputAlign: {
+ type: String,
+ default: 'left'
+ },
+ iconColor: {
+ type: String,
+ default: '#606266'
+ },
+ autoHeight: {
+ type: Boolean,
+ default: true
+ },
+ errorMessage: {
+ type: [String, Boolean],
+ default: ''
+ },
+ placeholder: String,
+ placeholderStyle: String,
+ focus: Boolean,
+ fixed: Boolean,
+ value: [Number, String],
+ type: {
+ type: String,
+ default: 'text'
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ maxlength: {
+ type: [Number, String],
+ default: 140
+ },
+ confirmType: {
+ type: String,
+ default: 'done'
+ },
+ // lable的位置,可选为 left-左边,top-上边
+ labelPosition: {
+ type: String,
+ default: 'left'
+ },
+ // 输入框的自定义样式
+ fieldStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 清除按钮的大小
+ clearSize: {
+ type: [Number, String],
+ default: 30
+ },
+ // lable左边的图标样式,对象形式
+ iconStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 是否显示上边框
+ borderTop: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示下边框
+ borderBottom: {
+ type: Boolean,
+ default: true
+ },
+ // 是否自动去除两端的空格
+ trim: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ focused: false,
+ itemIndex: 0,
+ };
+ },
+ computed: {
+ inputWrapStyle() {
+ let style = {};
+ style.textAlign = this.inputAlign;
+ // 判断lable的位置,如果是left的话,让input左边两边有间隙
+ if(this.labelPosition == 'left') {
+ style.margin = `0 8rpx`;
+ } else {
+ // 如果lable是top的,input的左边就没必要有间隙了
+ style.marginRight = `8rpx`;
+ }
+ return style;
+ },
+ rightIconStyle() {
+ let style = {};
+ if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
+ if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
+ else style.transform = 'roate(0deg)';
+ return style;
+ },
+ labelStyle() {
+ let style = {};
+ if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
+ if(this.labelAlign == 'center') style.justifyContent = 'center';
+ if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
+ return style;
+ },
+ // uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
+ justifyContent() {
+ if(this.labelAlign == 'left') return 'flex-start';
+ if(this.labelAlign == 'center') return 'center';
+ if(this.labelAlign == 'right') return 'flex-end';
+ },
+ // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+ inputMaxlength() {
+ return Number(this.maxlength)
+ },
+ // label的位置
+ fieldInnerStyle() {
+ let style = {};
+ if(this.labelPosition == 'left') {
+ style.flexDirection = 'row';
+ } else {
+ style.flexDirection = 'column';
+ }
+
+ return style;
+ }
+ },
+ methods: {
+ onInput(event) {
+ let value = event.detail.value;
+ // 判断是否去除空格
+ if(this.trim) value = this.$u.trim(value);
+ this.$emit('input', value);
+ },
+ onFocus(event) {
+ this.focused = true;
+ this.$emit('focus', event);
+ },
+ onBlur(event) {
+ // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+ // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+ setTimeout(() => {
+ this.focused = false;
+ }, 100)
+ this.$emit('blur', event);
+ },
+ onConfirm(e) {
+ this.$emit('confirm', e.detail.value);
+ },
+ onClear(event) {
+ this.$emit('input', '');
+ },
+ rightIconClick() {
+ this.$emit('right-icon-click');
+ this.$emit('click');
+ },
+ fieldClick() {
+ this.$emit('click');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-field {
+ font-size: 28rpx;
+ padding: 20rpx 28rpx;
+ text-align: left;
+ position: relative;
+ color: $u-main-color;
+}
+
+.u-field-inner {
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-textarea-inner {
+ align-items: flex-start;
+}
+
+.u-textarea-class {
+ min-height: 96rpx;
+ width: auto;
+ font-size: 28rpx;
+}
+
+.fild-body {
+ @include vue-flex;
+ flex: 1;
+ align-items: center;
+}
+
+.u-arror-right {
+ margin-left: 8rpx;
+}
+
+.u-label-text {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+}
+
+.u-label-left-gap {
+ margin-left: 6rpx;
+}
+
+.u-label-postion-top {
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.u-label {
+ width: 130rpx;
+ flex: 1 1 130rpx;
+ text-align: left;
+ position: relative;
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-required::before {
+ content: '*';
+ position: absolute;
+ left: -16rpx;
+ font-size: 14px;
+ color: $u-type-error;
+ height: 9px;
+ line-height: 1;
+}
+
+.u-field__input-wrap {
+ position: relative;
+ overflow: hidden;
+ font-size: 28rpx;
+ height: 48rpx;
+ flex: 1;
+ width: auto;
+}
+
+.u-clear-icon {
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-error-message {
+ color: $u-type-error;
+ font-size: 26rpx;
+ text-align: left;
+}
+
+.placeholder-style {
+ color: rgb(150, 151, 153);
+}
+
+.u-input-class {
+ font-size: 28rpx;
+}
+
+.u-button-wrap {
+ margin-left: 8rpx;
+}
+</style>
diff --git a/uview-ui/components/u-form-item/u-form-item.vue b/uview-ui/components/u-form-item/u-form-item.vue
new file mode 100644
index 0000000..d6b8c8b
--- /dev/null
+++ b/uview-ui/components/u-form-item/u-form-item.vue
@@ -0,0 +1,431 @@
+<template>
+ <view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
+ <view class="u-form-item__body" :style="{
+ flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
+ }">
+ <!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
+ <view class="u-form-item--left" :style="{
+ width: uLabelWidth,
+ flex: `0 0 ${uLabelWidth}`,
+ marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
+ }">
+ <!-- 为了块对齐 -->
+ <view class="u-form-item--left__content" v-if="required || leftIcon || label">
+ <!-- nvue不支持伪元素before -->
+ <text v-if="required" class="u-form-item--left__content--required">*</text>
+ <view class="u-form-item--left__content__icon" v-if="leftIcon">
+ <u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
+ </view>
+ <view class="u-form-item--left__content__label" :style="[elLabelStyle, {
+ 'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
+ }]">
+ {{label}}
+ </view>
+ </view>
+ </view>
+ <view class="u-form-item--right u-flex">
+ <view class="u-form-item--right__content">
+ <view class="u-form-item--right__content__slot ">
+ <slot />
+ </view>
+ <view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
+ <u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
+ <slot name="right" />
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
+ paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
+ }">{{validateMessage}}</view>
+ </view>
+</template>
+
+<script>
+ import Emitter from '../../libs/util/emitter.js';
+ import schema from '../../libs/util/async-validator';
+ // 去除警告信息
+ schema.warning = function() {};
+
+ /**
+ * form-item 表单item
+ * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+ * @tutorial http://uviewui.com/components/form.html
+ * @property {String} label 左侧提示文字
+ * @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
+ * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+ * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+ * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+ * @property {Object} label-style lable的样式,对象形式
+ * @property {String} label-align lable的对齐方式
+ * @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {Object} left-icon-style 左侧图标的样式,对象形式
+ * @property {Object} right-icon-style 右侧图标的样式,对象形式
+ * @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
+ * @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
+ */
+
+ export default {
+ name: 'u-form-item',
+ mixins: [Emitter],
+ inject: {
+ uForm: {
+ default () {
+ return null
+ }
+ }
+ },
+ props: {
+ // input的label提示语
+ label: {
+ type: String,
+ default: ''
+ },
+ // 绑定的值
+ prop: {
+ type: String,
+ default: ''
+ },
+ // 是否显示表单域的下划线边框
+ borderBottom: {
+ type: [String, Boolean],
+ default: ''
+ },
+ // label的位置,left-左边,top-上边
+ labelPosition: {
+ type: String,
+ default: ''
+ },
+ // label的宽度,单位rpx
+ labelWidth: {
+ type: [String, Number],
+ default: ''
+ },
+ // lable的样式,对象形式
+ labelStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // lable字体的对齐方式
+ labelAlign: {
+ type: String,
+ default: ''
+ },
+ // 右侧图标
+ rightIcon: {
+ type: String,
+ default: ''
+ },
+ // 左侧图标
+ leftIcon: {
+ type: String,
+ default: ''
+ },
+ // 左侧图标的样式
+ leftIconStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 左侧图标的样式
+ rightIconStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
+ required: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ initialValue: '', // 存储的默认值
+ // isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ validateState: '', // 是否校验成功
+ validateMessage: '', // 校验失败的提示语
+ // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+ errorType: ['message'],
+ fieldValue: '', // 获取当前子组件input的输入的值
+ // 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
+ parentData: {
+ borderBottom: true,
+ labelWidth: 90,
+ labelPosition: 'left',
+ labelStyle: {},
+ labelAlign: 'left',
+ }
+ };
+ },
+ watch: {
+ validateState(val) {
+ this.broadcastInputError();
+ },
+ // 监听u-form组件的errorType的变化
+ "uForm.errorType"(val) {
+ this.errorType = val;
+ this.broadcastInputError();
+ },
+ },
+ computed: {
+ // 计算后的label宽度,由于需要多个判断,故放到computed中
+ uLabelWidth() {
+ // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
+ return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
+ .elLabelWidth)) : '100%';
+ },
+ showError() {
+ return type => {
+ // 如果errorType数组中含有none,或者toast提示类型
+ if (this.errorType.indexOf('none') >= 0) return false;
+ else if (this.errorType.indexOf(type) >= 0) return true;
+ else return false;
+ }
+ },
+ // label的宽度
+ elLabelWidth() {
+ // label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
+ return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
+ .labelWidth :
+ 90);
+ },
+ // label的样式
+ elLabelStyle() {
+ return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
+ {});
+ },
+ // label的位置,左侧或者上方
+ elLabelPosition() {
+ return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
+ 'left');
+ },
+ // label的对齐方式
+ elLabelAlign() {
+ return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
+ },
+ // label的下划线
+ elBorderBottom() {
+ // 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
+ return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
+ true;
+ }
+ },
+ methods: {
+ broadcastInputError() {
+ // 子组件发出事件,第三个参数为true或者false,true代表有错误
+ this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
+ },
+ // 判断是否需要required校验
+ setRules() {
+ let that = this;
+ // 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ // 从父组件u-form拿到当前u-form-item需要验证 的规则
+ // let rules = this.getRules();
+ // if (rules.length) {
+ // this.isRequired = rules.some(rule => {
+ // // 如果有必填项,就返回,没有的话,就是undefined
+ // return rule.required;
+ // });
+ // }
+
+ // blur事件
+ this.$on('on-form-blur', that.onFieldBlur);
+ // change事件
+ this.$on('on-form-change', that.onFieldChange);
+ },
+
+ // 从u-form的rules属性中,取出当前u-form-item的校验规则
+ getRules() {
+ // 父组件的所有规则
+ let rules = this.parent.rules;
+ rules = rules ? rules[this.prop] : [];
+ // 保证返回的是一个数组形式
+ return [].concat(rules || []);
+ },
+
+ // blur事件时进行表单校验
+ onFieldBlur() {
+ this.validation('blur');
+ },
+
+ // change事件进行表单校验
+ onFieldChange() {
+ this.validation('change');
+ },
+
+ // 过滤出符合要求的rule规则
+ getFilteredRule(triggerType = '') {
+ let rules = this.getRules();
+ // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
+ if (!triggerType) return rules;
+ // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
+ // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
+ // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
+ return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
+ },
+
+ // 校验数据
+ validation(trigger, callback = () => {}) {
+ // 检验之间,先获取需要校验的值
+ this.fieldValue = this.parent.model[this.prop];
+ // blur和change是否有当前方式的校验规则
+ let rules = this.getFilteredRule(trigger);
+ // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
+ // 对count变量的统计错误而无法进入上一层的回调
+ if (!rules || rules.length === 0) {
+ return callback('');
+ }
+ // 设置当前的装填,标识为校验中
+ this.validateState = 'validating';
+ // 调用async-validator的方法
+ let validator = new schema({
+ [this.prop]: rules
+ });
+ validator.validate({
+ [this.prop]: this.fieldValue
+ }, {
+ firstFields: true
+ }, (errors, fields) => {
+ // 记录状态和报错信息
+ this.validateState = !errors ? 'success' : 'error';
+ this.validateMessage = errors ? errors[0].message : '';
+ // 调用回调方法
+ callback(this.validateMessage);
+ });
+ },
+
+ // 清空当前的u-form-item
+ resetField() {
+ this.parent.model[this.prop] = this.initialValue;
+ // 设置为`success`状态,只是为了清空错误标记
+ this.validateState = 'success';
+ }
+ },
+
+ // 组件创建完成时,将当前实例保存到u-form中
+ mounted() {
+ // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ this.parent = this.$u.$parent.call(this, 'u-form');
+ if (this.parent) {
+ // 历遍parentData中的属性,将parent中的同名属性赋值给parentData
+ Object.keys(this.parentData).map(key => {
+ this.parentData[key] = this.parent[key];
+ });
+ // 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
+ if (this.prop) {
+ // 将本实例添加到父组件中
+ this.parent.fields.push(this);
+ this.errorType = this.parent.errorType;
+ // 设置初始值
+ this.initialValue = this.fieldValue;
+ // 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
+ // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
+ this.$nextTick(() => {
+ this.setRules();
+ })
+ }
+ }
+ },
+
+ // 组件销毁前,将实例从u-form的缓存中移除
+ beforeDestroy() {
+ // 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
+ if (this.parent && this.prop) {
+ this.parent.fields.map((item, index) => {
+ if (item === this) this.parent.fields.splice(index, 1);
+ })
+ }
+ },
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-form-item {
+ @include vue-flex;
+ // align-items: flex-start;
+ padding: 20rpx 0;
+ font-size: 28rpx;
+ color: $u-main-color;
+ box-sizing: border-box;
+ line-height: $u-form-item-height;
+ flex-direction: column;
+
+ &__border-bottom--error:after {
+ border-color: $u-type-error;
+ }
+
+ &__body {
+ @include vue-flex;
+ }
+
+ &--left {
+ @include vue-flex;
+ align-items: center;
+
+ &__content {
+ position: relative;
+ @include vue-flex;
+ align-items: center;
+ padding-right: 10rpx;
+ flex: 1;
+
+ &__icon {
+ margin-right: 8rpx;
+ }
+
+ &--required {
+ position: absolute;
+ left: -16rpx;
+ vertical-align: middle;
+ color: $u-type-error;
+ padding-top: 6rpx;
+ }
+
+ &__label {
+ @include vue-flex;
+ align-items: center;
+ flex: 1;
+ }
+ }
+ }
+
+ &--right {
+ flex: 1;
+
+ &__content {
+ @include vue-flex;
+ align-items: center;
+ flex: 1;
+
+ &__slot {
+ flex: 1;
+ /* #ifndef MP */
+ @include vue-flex;
+ align-items: center;
+ /* #endif */
+ }
+
+ &__icon {
+ margin-left: 10rpx;
+ color: $u-light-color;
+ font-size: 30rpx;
+ }
+ }
+ }
+
+ &__message {
+ font-size: 24rpx;
+ line-height: 24rpx;
+ color: $u-type-error;
+ margin-top: 12rpx;
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-form/u-form.vue b/uview-ui/components/u-form/u-form.vue
new file mode 100644
index 0000000..bdbafaf
--- /dev/null
+++ b/uview-ui/components/u-form/u-form.vue
@@ -0,0 +1,134 @@
+<template>
+ <view class="u-form"><slot /></view>
+</template>
+
+<script>
+ /**
+ * form 表单
+ * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+ * @tutorial http://uviewui.com/components/form.html
+ * @property {Object} model 表单数据对象
+ * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+ * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+ * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+ * @property {Object} label-style lable的样式,对象形式
+ * @property {String} label-align lable的对齐方式
+ * @property {Object} rules 通过ref设置,见官网说明
+ * @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
+ * @example <u-form :model="form" ref="uForm"></u-form>
+ */
+
+export default {
+ name: 'u-form',
+ props: {
+ // 当前form的需要验证字段的集合
+ model: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 验证规则
+ // rules: {
+ // type: [Object, Function, Array],
+ // default() {
+ // return {};
+ // }
+ // },
+ // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+ // border-bottom-下边框呈现红色,none-无提示
+ errorType: {
+ type: Array,
+ default() {
+ return ['message', 'toast']
+ }
+ },
+ // 是否显示表单域的下划线边框
+ borderBottom: {
+ type: Boolean,
+ default: true
+ },
+ // label的位置,left-左边,top-上边
+ labelPosition: {
+ type: String,
+ default: 'left'
+ },
+ // label的宽度,单位rpx
+ labelWidth: {
+ type: [String, Number],
+ default: 90
+ },
+ // lable字体的对齐方式
+ labelAlign: {
+ type: String,
+ default: 'left'
+ },
+ // lable的样式,对象形式
+ labelStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ },
+ provide() {
+ return {
+ uForm: this
+ };
+ },
+ data() {
+ return {
+ rules: {}
+ };
+ },
+ created() {
+ // 存储当前form下的所有u-form-item的实例
+ // 不能定义在data中,否则微信小程序会造成循环引用而报错
+ this.fields = [];
+ },
+ methods: {
+ setRules(rules) {
+ this.rules = rules;
+ },
+ // 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
+ resetFields() {
+ this.fields.map(field => {
+ field.resetField();
+ });
+ },
+ // 校验全部数据
+ validate(callback) {
+ return new Promise(resolve => {
+ // 对所有的u-form-item进行校验
+ let valid = true; // 默认通过
+ let count = 0; // 用于标记是否检查完毕
+ let errorArr = []; // 存放错误信息
+ this.fields.map(field => {
+ // 调用每一个u-form-item实例的validation的校验方法
+ field.validation('', error => {
+ // 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
+ if (error) {
+ valid = false;
+ errorArr.push(error);
+ }
+ // 当历遍了所有的u-form-item时,调用promise的then方法
+ if (++count === this.fields.length) {
+ resolve(valid); // 进入promise的then方法
+ // 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
+ if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
+ this.$u.toast(errorArr[0]);
+ }
+ // 调用回调方法
+ if (typeof callback == 'function') callback(valid);
+ }
+ });
+ });
+ });
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+</style>
diff --git a/uview-ui/components/u-full-screen/u-full-screen.vue b/uview-ui/components/u-full-screen/u-full-screen.vue
new file mode 100644
index 0000000..4f7e7d9
--- /dev/null
+++ b/uview-ui/components/u-full-screen/u-full-screen.vue
@@ -0,0 +1,52 @@
+<template>
+ <u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
+ <view class="u-update-content">
+ <rich-text :nodes="content"></rich-text>
+ </view>
+ </u-modal>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ show: false,
+ content: `
+ 1. 修复badge组件的size参数无效问题<br>
+ 2. 新增Modal模态框组件<br>
+ 3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
+ 4. 修复键盘组件在微信小程序上遮罩无效的问题
+ `,
+ }
+ },
+ onReady() {
+ this.show = true;
+ },
+ methods: {
+ cancel() {
+ this.closeModal();
+ },
+ confirm() {
+ this.closeModal();
+ },
+ closeModal() {
+ uni.navigateBack();
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-full-content {
+ background-color: #00C777;
+ }
+
+ .u-update-content {
+ font-size: 26rpx;
+ color: $u-content-color;
+ line-height: 1.7;
+ padding: 30rpx;
+ }
+</style>
diff --git a/uview-ui/components/u-gap/u-gap.vue b/uview-ui/components/u-gap/u-gap.vue
new file mode 100644
index 0000000..6c01f94
--- /dev/null
+++ b/uview-ui/components/u-gap/u-gap.vue
@@ -0,0 +1,54 @@
+<template>
+ <view class="u-gap" :style="[gapStyle]"></view>
+</template>
+
+<script>
+/**
+ * gap 间隔槽
+ * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
+ * @tutorial https://www.uviewui.com/components/gap.html
+ * @property {String} bg-color 背景颜色(默认#f3f4f6)
+ * @property {String Number} height 分割槽高度,单位rpx(默认30)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @example <u-gap height="80" bg-color="#bbb"></u-gap>
+ */
+export default {
+ name: "u-gap",
+ props: {
+ bgColor: {
+ type: String,
+ default: 'transparent ' // 背景透明
+ },
+ // 高度
+ height: {
+ type: [String, Number],
+ default: 30
+ },
+ // 与上一个组件的距离
+ marginTop: {
+ type: [String, Number],
+ default: 0
+ },
+ // 与下一个组件的距离
+ marginBottom: {
+ type: [String, Number],
+ default: 0
+ },
+ },
+ computed: {
+ gapStyle() {
+ return {
+ backgroundColor: this.bgColor,
+ height: this.height + 'rpx',
+ marginTop: this.marginTop + 'rpx',
+ marginBottom: this.marginBottom + 'rpx'
+ };
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+</style>
diff --git a/uview-ui/components/u-grid-item/u-grid-item.vue b/uview-ui/components/u-grid-item/u-grid-item.vue
new file mode 100644
index 0000000..0773307
--- /dev/null
+++ b/uview-ui/components/u-grid-item/u-grid-item.vue
@@ -0,0 +1,126 @@
+<template>
+ <view class="u-grid-item" :hover-class="parentData.hoverClass"
+ :hover-stay-time="200" @tap="click" :style="{
+ background: bgColor,
+ width: width,
+ }">
+ <view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
+ <slot />
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * gridItem 提示
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
+ * @tutorial https://www.uviewui.com/components/grid.html
+ * @property {String} bg-color 宫格的背景颜色(默认#ffffff)
+ * @property {String Number} index 点击宫格时,返回的值
+ * @property {Object} custom-style 自定义样式,对象形式
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid-item></u-grid-item>
+ */
+ export default {
+ name: "u-grid-item",
+ props: {
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 点击时返回的index
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 自定义样式,对象形式
+ customStyle: {
+ type: Object,
+ default() {
+ return {
+ padding: '30rpx 0'
+ }
+ }
+ }
+ },
+ data() {
+ return {
+ parentData: {
+ hoverClass: '', // 按下去的时候,是否显示背景灰色
+ col: 3, // 父组件划分的宫格数
+ border: true, // 是否显示边框,根据父组件决定
+ }
+ };
+ },
+ created() {
+ // 父组件的实例
+ this.updateParentData();
+ // this.parent在updateParentData()中定义
+ this.parent.children.push(this);
+ },
+ computed: {
+ // 每个grid-item的宽度
+ width() {
+ return 100 / Number(this.parentData.col) + '%';
+ },
+ },
+ methods: {
+ // 获取父组件的参数
+ updateParentData() {
+ // 此方法写在mixin中
+ this.getParentData('u-grid');
+ },
+ click() {
+ this.$emit('click', this.index);
+ this.parent && this.parent.click(this.index);
+ }
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-grid-item {
+ box-sizing: border-box;
+ background: #fff;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ flex-direction: column;
+
+ /* #ifdef MP */
+ position: relative;
+ float: left;
+ /* #endif */
+ }
+
+ .u-grid-item-hover {
+ background: #f7f7f7 !important;
+ }
+
+ .u-grid-marker-box {
+ position: absolute;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ line-height: 0;
+ }
+
+ .u-grid-marker-wrap {
+ position: absolute;
+ }
+
+ .u-grid-item-box {
+ padding: 30rpx 0;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ }
+</style>
diff --git a/uview-ui/components/u-grid/u-grid.vue b/uview-ui/components/u-grid/u-grid.vue
new file mode 100644
index 0000000..6588c06
--- /dev/null
+++ b/uview-ui/components/u-grid/u-grid.vue
@@ -0,0 +1,108 @@
+<template>
+ <view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
+</template>
+
+<script>
+/**
+ * grid 宫格布局
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
+ * @tutorial https://www.uviewui.com/components/grid.html
+ * @property {String Number} col 宫格的列数(默认3)
+ * @property {Boolean} border 是否显示宫格的边框(默认true)
+ * @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid :col="3" @click="click"></u-grid>
+ */
+export default {
+ name: 'u-grid',
+ props: {
+ // 分成几列
+ col: {
+ type: [Number, String],
+ default: 3
+ },
+ // 是否显示边框
+ border: {
+ type: Boolean,
+ default: true
+ },
+ // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
+ align: {
+ type: String,
+ default: 'left'
+ },
+ // 宫格按压时的样式类,"none"为无效果
+ hoverClass: {
+ type: String,
+ default: 'u-hover-class'
+ }
+ },
+ data() {
+ return {
+ index: 0,
+ }
+ },
+ watch: {
+ // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+ parentData() {
+ if(this.children.length) {
+ this.children.map(child => {
+ // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.updateParentData) == 'function' && child.updateParentData();
+ })
+ }
+ },
+ },
+ created() {
+ // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ this.children = [];
+ },
+ computed: {
+ // 计算父组件的值是否发生变化
+ parentData() {
+ return [this.hoverClass, this.col, this.size, this.border];
+ },
+ // 宫格对齐方式
+ gridStyle() {
+ let style = {};
+ switch(this.align) {
+ case 'left':
+ style.justifyContent = 'flex-start';
+ break;
+ case 'center':
+ style.justifyContent = 'center';
+ break;
+ case 'right':
+ style.justifyContent = 'flex-end';
+ break;
+ default: style.justifyContent = 'flex-start';
+ };
+ return style;
+ }
+ },
+ methods: {
+ click(index) {
+ this.$emit('click', index);
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-grid {
+ width: 100%;
+ /* #ifdef MP */
+ position: relative;
+ box-sizing: border-box;
+ overflow: hidden;
+ /* #endif */
+
+ /* #ifndef MP */
+ @include vue-flex;
+ flex-wrap: wrap;
+ align-items: center;
+ /* #endif */
+}
+</style>
diff --git a/uview-ui/components/u-icon/u-icon.vue b/uview-ui/components/u-icon/u-icon.vue
new file mode 100644
index 0000000..db1b019
--- /dev/null
+++ b/uview-ui/components/u-icon/u-icon.vue
@@ -0,0 +1,336 @@
+<template>
+ <view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
+ <image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
+ <text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
+ @touchstart="touchstart">
+ <text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
+ class="u-icon__decimal">
+ </text>
+ </text>
+ <!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
+ <text v-if="label !== ''" class="u-icon__label" :style="{
+ color: labelColor,
+ fontSize: $u.addUnit(labelSize),
+ marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
+ marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
+ marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
+ marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
+ }">{{ label }}
+ </text>
+ </view>
+</template>
+
+<script>
+/**
+ * icon 图标
+ * @description 基于字体的图标集,包含了大多数常见场景的图标。
+ * @tutorial https://www.uviewui.com/components/icon.html
+ * @property {String} name 图标名称,见示例图标集
+ * @property {String} color 图标颜色(默认inherit)
+ * @property {String | Number} size 图标字体大小,单位rpx(默认32)
+ * @property {String | Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String} label 图标右侧的label文字(默认28)
+ * @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} label-color label字体颜色(默认#606266)
+ * @property {Object} custom-style icon的样式,对象形式
+ * @property {String} custom-prefix 自定义字体图标库时,需要写上此值
+ * @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
+ * @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
+ * @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
+ * @property {String} width 显示图片小图标时的宽度
+ * @property {String} height 显示图片小图标时的高度
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {Boolean} show-decimal-icon 是否为DecimalIcon
+ * @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
+ * @property {String | Number} percent 显示的百分比,仅Decimal时有效
+ * @event {Function} click 点击图标时触发
+ * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
+ */
+export default {
+ name: 'u-icon',
+ props: {
+ // 图标类名
+ name: {
+ type: String,
+ default: ''
+ },
+ // 图标颜色,可接受主题色
+ color: {
+ type: String,
+ default: ''
+ },
+ // 字体大小,单位rpx
+ size: {
+ type: [Number, String],
+ default: 'inherit'
+ },
+ // 是否显示粗体
+ bold: {
+ type: Boolean,
+ default: false
+ },
+ // 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 触摸图标时的类名
+ hoverClass: {
+ type: String,
+ default: ''
+ },
+ // 自定义扩展前缀,方便用户扩展自己的图标库
+ customPrefix: {
+ type: String,
+ default: 'uicon'
+ },
+ // 图标右边或者下面的文字
+ label: {
+ type: [String, Number],
+ default: ''
+ },
+ // label的位置,只能右边或者下边
+ labelPos: {
+ type: String,
+ default: 'right'
+ },
+ // label的大小
+ labelSize: {
+ type: [String, Number],
+ default: '28'
+ },
+ // label的颜色
+ labelColor: {
+ type: String,
+ default: '#606266'
+ },
+ // label与图标的距离(横向排列)
+ marginLeft: {
+ type: [String, Number],
+ default: '6'
+ },
+ // label与图标的距离(竖向排列)
+ marginTop: {
+ type: [String, Number],
+ default: '6'
+ },
+ // label与图标的距离(竖向排列)
+ marginRight: {
+ type: [String, Number],
+ default: '6'
+ },
+ // label与图标的距离(竖向排列)
+ marginBottom: {
+ type: [String, Number],
+ default: '6'
+ },
+ // 图片的mode
+ imgMode: {
+ type: String,
+ default: 'widthFix'
+ },
+ // 自定义样式
+ customStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 用于显示图片小图标时,图片的宽度
+ width: {
+ type: [String, Number],
+ default: ''
+ },
+ // 用于显示图片小图标时,图片的高度
+ height: {
+ type: [String, Number],
+ default: ''
+ },
+ // 用于解决某些情况下,让图标垂直居中的用途
+ top: {
+ type: [String, Number],
+ default: 0
+ },
+ // 是否为DecimalIcon
+ showDecimalIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 背景颜色,可接受主题色,仅Decimal时有效
+ inactiveColor: {
+ type: String,
+ default: '#ececec'
+ },
+ // 显示的百分比,仅Decimal时有效
+ percent: {
+ type: [Number, String],
+ default: '50'
+ }
+ },
+ computed: {
+ customClass() {
+ let classes = []
+ classes.push(this.customPrefix + '-' + this.name)
+ // uView的自定义图标类名为u-iconfont
+ if (this.customPrefix == 'uicon') {
+ classes.push('u-iconfont')
+ } else {
+ classes.push(this.customPrefix)
+ }
+ // 主题色,通过类配置
+ if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
+ classes.push('u-icon__icon--' + this.inactiveColor)
+ } else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+ // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+ // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+ //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+ classes = classes.join(' ')
+ //#endif
+ return classes
+ },
+ iconStyle() {
+ let style = {}
+ style = {
+ fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
+ fontWeight: this.bold ? 'bold' : 'normal',
+ // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+ top: this.$u.addUnit(this.top)
+ }
+ // 非主题色值时,才当作颜色值
+ if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
+ style.color = this.inactiveColor
+ } else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
+
+ return style
+ },
+ // 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
+ isImg() {
+ return this.name.indexOf('/') !== -1
+ },
+ imgStyle() {
+ let style = {}
+ // 如果设置width和height属性,则优先使用,否则使用size属性
+ style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
+ style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
+ return style
+ },
+ decimalIconStyle() {
+ let style = {}
+ style = {
+ fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
+ fontWeight: this.bold ? 'bold' : 'normal',
+ // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+ top: this.$u.addUnit(this.top),
+ width: this.percent + '%'
+ }
+ // 非主题色值时,才当作颜色值
+ if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
+ return style
+ },
+ decimalIconClass() {
+ let classes = []
+ classes.push(this.customPrefix + '-' + this.name)
+ // uView的自定义图标类名为u-iconfont
+ if (this.customPrefix == 'uicon') {
+ classes.push('u-iconfont')
+ } else {
+ classes.push(this.customPrefix)
+ }
+ // 主题色,通过类配置
+ if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+ else classes.push('u-icon__icon--primary')
+ // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+ // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+ //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+ classes = classes.join(' ')
+ //#endif
+ return classes
+ }
+ },
+ methods: {
+ click() {
+ this.$emit('click', this.index)
+ },
+ touchstart() {
+ this.$emit('touchstart', this.index)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+@import '../../iconfont.css';
+
+.u-icon {
+ display: inline-flex;
+ align-items: center;
+
+ &--left {
+ flex-direction: row-reverse;
+ align-items: center;
+ }
+
+ &--right {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &--top {
+ flex-direction: column-reverse;
+ justify-content: center;
+ }
+
+ &--bottom {
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ &__icon {
+ position: relative;
+
+ &--primary {
+ color: $u-type-primary;
+ }
+
+ &--success {
+ color: $u-type-success;
+ }
+
+ &--error {
+ color: $u-type-error;
+ }
+
+ &--warning {
+ color: $u-type-warning;
+ }
+
+ &--info {
+ color: $u-type-info;
+ }
+ }
+
+ &__decimal {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: inline-block;
+ overflow: hidden;
+ }
+
+ &__img {
+ height: auto;
+ will-change: transform;
+ }
+
+ &__label {
+ line-height: 1;
+ }
+}
+</style>
diff --git a/uview-ui/components/u-image/u-image.vue b/uview-ui/components/u-image/u-image.vue
new file mode 100644
index 0000000..30df88b
--- /dev/null
+++ b/uview-ui/components/u-image/u-image.vue
@@ -0,0 +1,266 @@
+<template>
+ <view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
+ <image
+ v-if="!isError"
+ :src="src"
+ :mode="mode"
+ @error="onErrorHandler"
+ @load="onLoadHandler"
+ :lazy-load="lazyLoad"
+ class="u-image__image"
+ :style="{
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
+ }"
+ ></image>
+ <view
+ v-if="showLoading && loading"
+ class="u-image__loading"
+ :style="{
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
+ backgroundColor: this.bgColor
+ }"
+ >
+ <slot v-if="$slots.loading" name="loading" />
+ <u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
+ </view>
+ <view
+ v-if="showError && isError && !loading"
+ class="u-image__error"
+ :style="{
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
+ }"
+ >
+ <slot v-if="$slots.error" name="error" />
+ <u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * Image 图片
+ * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
+ * @tutorial https://uviewui.com/components/image.html
+ * @property {String} src 图片地址
+ * @property {String} mode 裁剪模式,见官网说明
+ * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
+ * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
+ * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
+ * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
+ * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
+ * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
+ * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
+ * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
+ * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
+ * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
+ * @property {Boolean} fade 是否需要淡入效果(默认 true)
+ * @property {String Number} width 传入图片路径时图片的宽度
+ * @property {String Number} height 传入图片路径时图片的高度
+ * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
+ * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
+ * @event {Function} click 点击图片时触发
+ * @event {Function} error 图片加载失败时触发
+ * @event {Function} load 图片加载成功时触发
+ * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
+ */
+export default {
+ name: 'u-image',
+ props: {
+ // 图片地址
+ src: {
+ type: String,
+ default: ''
+ },
+ // 裁剪模式
+ mode: {
+ type: String,
+ default: 'aspectFill'
+ },
+ // 宽度,单位任意
+ width: {
+ type: [String, Number],
+ default: '100%'
+ },
+ // 高度,单位任意
+ height: {
+ type: [String, Number],
+ default: 'auto'
+ },
+ // 图片形状,circle-圆形,square-方形
+ shape: {
+ type: String,
+ default: 'square'
+ },
+ // 圆角,单位任意
+ borderRadius: {
+ type: [String, Number],
+ default: 0
+ },
+ // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
+ lazyLoad: {
+ type: Boolean,
+ default: true
+ },
+ // 开启长按图片显示识别微信小程序码菜单
+ showMenuByLongpress: {
+ type: Boolean,
+ default: true
+ },
+ // 加载中的图标,或者小图片
+ loadingIcon: {
+ type: String,
+ default: 'photo'
+ },
+ // 加载失败的图标,或者小图片
+ errorIcon: {
+ type: String,
+ default: 'error-circle'
+ },
+ // 是否显示加载中的图标或者自定义的slot
+ showLoading: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示加载错误的图标或者自定义的slot
+ showError: {
+ type: Boolean,
+ default: true
+ },
+ // 是否需要淡入效果
+ fade: {
+ type: Boolean,
+ default: true
+ },
+ // 只支持网络资源,只对微信小程序有效
+ webp: {
+ type: Boolean,
+ default: false
+ },
+ // 过渡时间,单位ms
+ duration: {
+ type: [String, Number],
+ default: 500
+ },
+ // 背景颜色,用于深色页面加载图片时,为了和背景色融合
+ bgColor: {
+ type: String,
+ default: '#f3f4f6'
+ }
+ },
+ data() {
+ return {
+ // 图片是否加载错误,如果是,则显示错误占位图
+ isError: false,
+ // 初始化组件时,默认为加载中状态
+ loading: true,
+ // 不透明度,为了实现淡入淡出的效果
+ opacity: 1,
+ // 过渡时间,因为props的值无法修改,故需要一个中间值
+ durationTime: this.duration,
+ // 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
+ backgroundStyle: {}
+ };
+ },
+ watch: {
+ src: {
+ immediate: true,
+ handler (n) {
+ if(!n) {
+ // 如果传入null或者'',或者false,或者undefined,标记为错误状态
+ this.isError = true;
+ this.loading = false;
+ } else {
+ this.isError = false;
+ }
+ }
+ }
+ },
+ computed: {
+ wrapStyle() {
+ let style = {};
+ // 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
+ style.width = this.$u.addUnit(this.width);
+ style.height = this.$u.addUnit(this.height);
+ // 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
+ style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
+ // 如果设置圆角,必须要有hidden,否则可能圆角无效
+ style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
+ if (this.fade) {
+ style.opacity = this.opacity;
+ style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
+ }
+ return style;
+ }
+ },
+ methods: {
+ // 点击图片
+ onClick() {
+ this.$emit('click');
+ },
+ // 图片加载失败
+ onErrorHandler() {
+ this.loading = false;
+ this.isError = true;
+ this.$emit('error');
+ },
+ // 图片加载完成,标记loading结束
+ onLoadHandler() {
+ this.loading = false;
+ this.isError = false;
+ this.$emit('load');
+ // 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
+ // 否则无需fade效果时,png图片依然能看到下方的背景色
+ if (!this.fade) return this.removeBgColor();
+ // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
+ this.opacity = 0;
+ // 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
+ // 到图片展示的过程中的淡入效果
+ this.durationTime = 0;
+ // 延时50ms,否则在浏览器H5,过渡效果无效
+ setTimeout(() => {
+ this.durationTime = this.duration;
+ this.opacity = 1;
+ setTimeout(() => {
+ this.removeBgColor();
+ }, this.durationTime);
+ }, 50);
+ },
+ // 移除图片的背景色
+ removeBgColor() {
+ // 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
+ this.backgroundStyle = {
+ backgroundColor: 'transparent'
+ };
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+
+.u-image {
+ position: relative;
+ transition: opacity 0.5s ease-in-out;
+
+ &__image {
+ width: 100%;
+ height: 100%;
+ }
+
+ &__loading,
+ &__error {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $u-bg-color;
+ color: $u-tips-color;
+ font-size: 46rpx;
+ }
+}
+</style>
diff --git a/uview-ui/components/u-index-anchor/u-index-anchor.vue b/uview-ui/components/u-index-anchor/u-index-anchor.vue
new file mode 100644
index 0000000..5038827
--- /dev/null
+++ b/uview-ui/components/u-index-anchor/u-index-anchor.vue
@@ -0,0 +1,89 @@
+<template>
+ <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+ <view>
+ <view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
+ <view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
+ <slot v-if="useSlot" />
+ <block v-else>
+ <text>{{ index }}</text>
+ </block>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * indexAnchor 索引列表锚点
+ * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+ * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+ * @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
+ * @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
+ * @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
+ * @event {Function} default 锚点位置显示内容,默认为索引字符
+ * @example <u-index-anchor :index="item" />
+ */
+ export default {
+ name: "u-index-anchor",
+ props: {
+ useSlot: {
+ type: Boolean,
+ default: false
+ },
+ index: {
+ type: String,
+ default: ''
+ },
+ customStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ }
+ },
+ data() {
+ return {
+ active: false,
+ wrapperStyle: {},
+ anchorStyle: {}
+ }
+ },
+ created() {
+ this.parent = false;
+ },
+ mounted() {
+ this.parent = this.$u.$parent.call(this, 'u-index-list');
+ if(this.parent) {
+ this.parent.children.push(this);
+ this.parent.updateData();
+ }
+ },
+ computed: {
+ customAnchorStyle() {
+ return Object.assign(this.anchorStyle, this.customStyle);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-index-anchor {
+ box-sizing: border-box;
+ padding: 14rpx 24rpx;
+ color: #606266;
+ width: 100%;
+ font-weight: 500;
+ font-size: 28rpx;
+ line-height: 1.2;
+ background-color: rgb(245, 245, 245);
+ }
+
+ .u-index-anchor--active {
+ right: 0;
+ left: 0;
+ color: #2979ff;
+ background-color: #fff;
+ }
+</style>
diff --git a/uview-ui/components/u-index-list/u-index-list.vue b/uview-ui/components/u-index-list/u-index-list.vue
new file mode 100644
index 0000000..30fcda0
--- /dev/null
+++ b/uview-ui/components/u-index-list/u-index-list.vue
@@ -0,0 +1,315 @@
+<template>
+ <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+ <view>
+ <view class="u-index-bar">
+ <slot />
+ <view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
+ @touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
+ <view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
+ :data-index="index">
+ {{ item }}
+ </view>
+ </view>
+ <view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
+ zIndex: alertZIndex
+ }">
+ <text>{{indexList[touchmoveIndex]}}</text>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ var indexList = function() {
+ var indexList = [];
+ var charCodeOfA = 'A'.charCodeAt(0);
+ for (var i = 0; i < 26; i++) {
+ indexList.push(String.fromCharCode(charCodeOfA + i));
+ }
+ return indexList;
+ };
+
+ /**
+ * indexList 索引列表
+ * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+ * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+ * @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
+ * @property {Array} index-list 索引字符列表,数组(默认A-Z)
+ * @property {Number String} z-index 锚点吸顶时的层级(默认965)
+ * @property {Boolean} sticky 是否开启锚点自动吸顶(默认true)
+ * @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0)
+ * @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff)
+ * @event {Function} select 选中右边索引字符时触发
+ * @example <u-index-list :scrollTop="scrollTop"></u-index-list>
+ */
+ export default {
+ name: "u-index-list",
+ props: {
+ sticky: {
+ type: Boolean,
+ default: true
+ },
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ scrollTop: {
+ type: [Number, String],
+ default: 0,
+ },
+ offsetTop: {
+ type: [Number, String],
+ default: 0
+ },
+ indexList: {
+ type: Array,
+ default () {
+ return indexList()
+ }
+ },
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ }
+ },
+ created() {
+ // #ifdef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
+ // #endif
+ // #ifndef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
+ // #endif
+ // 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错
+ this.children = [];
+ },
+ data() {
+ return {
+ activeAnchorIndex: 0,
+ showSidebar: true,
+ // children: [],
+ touchmove: false,
+ touchmoveIndex: 0,
+ }
+ },
+ watch: {
+ scrollTop() {
+ this.updateData()
+ }
+ },
+ computed: {
+ // 弹出toast的z-index值
+ alertZIndex() {
+ return this.$u.zIndex.toast;
+ }
+ },
+ methods: {
+ updateData() {
+ this.timer && clearTimeout(this.timer);
+ this.timer = setTimeout(() => {
+ this.showSidebar = !!this.children.length;
+ this.setRect().then(() => {
+ this.onScroll();
+ });
+ }, 0);
+ },
+ setRect() {
+ return Promise.all([
+ this.setAnchorsRect(),
+ this.setListRect(),
+ this.setSiderbarRect()
+ ]);
+ },
+ setAnchorsRect() {
+ return Promise.all(this.children.map((anchor, index) => anchor
+ .$uGetRect('.u-index-anchor-wrapper')
+ .then((rect) => {
+ Object.assign(anchor, {
+ height: rect.height,
+ top: rect.top
+ });
+ })));
+ },
+ setListRect() {
+ return this.$uGetRect('.u-index-bar').then((rect) => {
+ Object.assign(this, {
+ height: rect.height,
+ top: rect.top + this.scrollTop
+ });
+ });
+ },
+ setSiderbarRect() {
+ return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
+ this.sidebar = {
+ height: rect.height,
+ top: rect.top
+ };
+ });
+ },
+ getActiveAnchorIndex() {
+ const {
+ children
+ } = this;
+ const {
+ sticky
+ } = this;
+ for (let i = this.children.length - 1; i >= 0; i--) {
+ const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
+ const reachTop = sticky ? preAnchorHeight : 0;
+ if (reachTop >= children[i].top) {
+ return i;
+ }
+ }
+ return -1;
+ },
+ onScroll() {
+ const {
+ children = []
+ } = this;
+ if (!children.length) {
+ return;
+ }
+ const {
+ sticky,
+ stickyOffsetTop,
+ zIndex,
+ scrollTop,
+ activeColor
+ } = this;
+ const active = this.getActiveAnchorIndex();
+ this.activeAnchorIndex = active;
+ if (sticky) {
+ let isActiveAnchorSticky = false;
+ if (active !== -1) {
+ isActiveAnchorSticky =
+ children[active].top <= 0;
+ }
+ children.forEach((item, index) => {
+ if (index === active) {
+ let wrapperStyle = '';
+ let anchorStyle = {
+ color: `${activeColor}`
+ };
+ if (isActiveAnchorSticky) {
+ wrapperStyle = {
+ height: `${children[index].height}px`
+ };
+ anchorStyle = {
+ position: 'fixed',
+ top: `${stickyOffsetTop}px`,
+ zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+ color: `${activeColor}`
+ };
+ }
+ item.active = active;
+ item.wrapperStyle = wrapperStyle;
+ item.anchorStyle = anchorStyle;
+ } else if (index === active - 1) {
+ const currentAnchor = children[index];
+ const currentOffsetTop = currentAnchor.top;
+ const targetOffsetTop = index === children.length - 1 ?
+ this.top :
+ children[index + 1].top;
+ const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
+ const translateY = parentOffsetHeight - currentAnchor.height;
+ const anchorStyle = {
+ position: 'relative',
+ transform: `translate3d(0, ${translateY}px, 0)`,
+ zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+ color: `${activeColor}`
+ };
+ item.active = active;
+ item.anchorStyle = anchorStyle;
+ } else {
+ item.active = false;
+ item.anchorStyle = '';
+ item.wrapperStyle = '';
+ }
+ });
+ }
+ },
+ onTouchMove(event) {
+ this.touchmove = true;
+ const sidebarLength = this.children.length;
+ const touch = event.touches[0];
+ const itemHeight = this.sidebar.height / sidebarLength;
+ let clientY = 0;
+ clientY = touch.clientY;
+ let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
+ if (index < 0) {
+ index = 0;
+ } else if (index > sidebarLength - 1) {
+ index = sidebarLength - 1;
+ }
+ this.touchmoveIndex = index;
+ this.scrollToAnchor(index);
+ },
+ onTouchStop() {
+ this.touchmove = false;
+ this.scrollToAnchorIndex = null;
+ },
+ scrollToAnchor(index) {
+ if (this.scrollToAnchorIndex === index) {
+ return;
+ }
+ this.scrollToAnchorIndex = index;
+ const anchor = this.children.find((item) => item.index === this.indexList[index]);
+ if (anchor) {
+ this.$emit('select', anchor.index);
+ uni.pageScrollTo({
+ duration: 0,
+ scrollTop: anchor.top + this.scrollTop
+ });
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-index-bar {
+ position: relative
+ }
+
+ .u-index-bar__sidebar {
+ position: fixed;
+ top: 50%;
+ right: 0;
+ @include vue-flex;
+ flex-direction: column;
+ text-align: center;
+ transform: translateY(-50%);
+ user-select: none;
+ z-index: 99;
+ }
+
+ .u-index-bar__index {
+ font-weight: 500;
+ padding: 8rpx 18rpx;
+ font-size: 22rpx;
+ line-height: 1
+ }
+
+ .u-indexed-list-alert {
+ position: fixed;
+ width: 120rpx;
+ height: 120rpx;
+ right: 90rpx;
+ top: 50%;
+ margin-top: -60rpx;
+ border-radius: 24rpx;
+ font-size: 50rpx;
+ color: #fff;
+ background-color: rgba(0, 0, 0, 0.65);
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+ z-index: 9999999;
+ }
+
+ .u-indexed-list-alert text {
+ line-height: 50rpx;
+ }
+</style>
diff --git a/uview-ui/components/u-input/u-input.vue b/uview-ui/components/u-input/u-input.vue
new file mode 100644
index 0000000..f2aea72
--- /dev/null
+++ b/uview-ui/components/u-input/u-input.vue
@@ -0,0 +1,387 @@
+<template>
+ <view
+ class="u-input"
+ :class="{
+ 'u-input--border': border,
+ 'u-input--error': validateState
+ }"
+ :style="{
+ padding: `0 ${border ? 20 : 0}rpx`,
+ borderColor: borderColor,
+ textAlign: inputAlign
+ }"
+ @tap.stop="inputClick"
+ >
+ <textarea
+ v-if="type == 'textarea'"
+ class="u-input__input u-input__textarea"
+ :style="[getStyle]"
+ :value="defaultValue"
+ :placeholder="placeholder"
+ :placeholderStyle="placeholderStyle"
+ :disabled="disabled"
+ :maxlength="inputMaxlength"
+ :fixed="fixed"
+ :focus="focus"
+ :autoHeight="autoHeight"
+ :selection-end="uSelectionEnd"
+ :selection-start="uSelectionStart"
+ :cursor-spacing="getCursorSpacing"
+ :show-confirm-bar="showConfirmbar"
+ @input="handleInput"
+ @blur="handleBlur"
+ @focus="onFocus"
+ @confirm="onConfirm"
+ />
+ <input
+ v-else
+ class="u-input__input"
+ :type="type == 'password' ? 'text' : type"
+ :style="[getStyle]"
+ :value="defaultValue"
+ :password="type == 'password' && !showPassword"
+ :placeholder="placeholder"
+ :placeholderStyle="placeholderStyle"
+ :disabled="disabled || type === 'select'"
+ :maxlength="inputMaxlength"
+ :focus="focus"
+ :confirmType="confirmType"
+ :cursor-spacing="getCursorSpacing"
+ :selection-end="uSelectionEnd"
+ :selection-start="uSelectionStart"
+ :show-confirm-bar="showConfirmbar"
+ @focus="onFocus"
+ @blur="handleBlur"
+ @input="handleInput"
+ @confirm="onConfirm"
+ />
+ <view class="u-input__right-icon u-flex">
+ <view class="u-input__right-icon__clear u-input__right-icon__item" @tap="onClear" v-if="clearable && value != '' && focused">
+ <u-icon size="32" name="close-circle-fill" color="#c0c4cc"/>
+ </view>
+ <view class="u-input__right-icon__clear u-input__right-icon__item" v-if="passwordIcon && type == 'password'">
+ <u-icon size="32" :name="!showPassword ? 'eye' : 'eye-fill'" color="#c0c4cc" @click="showPassword = !showPassword"/>
+ </view>
+ <view class="u-input__right-icon--select u-input__right-icon__item" v-if="type == 'select'" :class="{
+ 'u-input__right-icon--select--reverse': selectOpen
+ }">
+ <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+import Emitter from '../../libs/util/emitter.js';
+
+/**
+ * input 输入框
+ * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
+ * @tutorial http://uviewui.com/components/input.html
+ * @property {String} type 模式选择,见官网说明
+ * @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
+ * @property {} v-model 用于双向绑定输入框的值
+ * @property {String} input-align 输入框文字的对齐方式(默认left)
+ * @property {String} placeholder placeholder显示值(默认 '请输入内容')
+ * @property {Boolean} disabled 是否禁用输入框(默认false)
+ * @property {String Number} maxlength 输入框的最大可输入长度(默认140)
+ * @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1)
+ * @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1)
+ * @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0)
+ * @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;")
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done)
+ * @property {Object} custom-style 自定义输入框的样式,对象形式
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true)
+ * @property {Boolean} border 是否显示边框(默认false)
+ * @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100)
+ * @example <u-input v-model="value" :type="type" :border="border" />
+ */
+export default {
+ name: 'u-input',
+ mixins: [Emitter],
+ props: {
+ value: {
+ type: [String, Number],
+ default: ''
+ },
+ // 输入框的类型,textarea,text,number
+ type: {
+ type: String,
+ default: 'text'
+ },
+ inputAlign: {
+ type: String,
+ default: 'left'
+ },
+ placeholder: {
+ type: String,
+ default: '请输入内容'
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ maxlength: {
+ type: [Number, String],
+ default: 140
+ },
+ placeholderStyle: {
+ type: String,
+ default: 'color: #c0c4cc;'
+ },
+ confirmType: {
+ type: String,
+ default: 'done'
+ },
+ // 输入框的自定义样式
+ customStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
+ fixed: {
+ type: Boolean,
+ default: false
+ },
+ // 是否自动获得焦点
+ focus: {
+ type: Boolean,
+ default: false
+ },
+ // 密码类型时,是否显示右侧的密码图标
+ passwordIcon: {
+ type: Boolean,
+ default: true
+ },
+ // input|textarea是否显示边框
+ border: {
+ type: Boolean,
+ default: false
+ },
+ // 输入框的边框颜色
+ borderColor: {
+ type: String,
+ default: '#dcdfe6'
+ },
+ autoHeight: {
+ type: Boolean,
+ default: true
+ },
+ // type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态
+ // open-打开,close-关闭
+ selectOpen: {
+ type: Boolean,
+ default: false
+ },
+ // 高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: ''
+ },
+ // 是否可清空
+ clearable: {
+ type: Boolean,
+ default: true
+ },
+ // 指定光标与键盘的距离,单位 px
+ cursorSpacing: {
+ type: [Number, String],
+ default: 0
+ },
+ // 光标起始位置,自动聚焦时有效,需与selection-end搭配使用
+ selectionStart: {
+ type: [Number, String],
+ default: -1
+ },
+ // 光标结束位置,自动聚焦时有效,需与selection-start搭配使用
+ selectionEnd: {
+ type: [Number, String],
+ default: -1
+ },
+ // 是否自动去除两端的空格
+ trim: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示键盘上方带有”完成“按钮那一栏
+ showConfirmbar:{
+ type:Boolean,
+ default:true
+ }
+ },
+ data() {
+ return {
+ defaultValue: this.value,
+ inputHeight: 70, // input的高度
+ textareaHeight: 100, // textarea的高度
+ validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色
+ focused: false, // 当前是否处于获得焦点的状态
+ showPassword: false, // 是否预览密码
+ lastValue: '', // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间
+ };
+ },
+ watch: {
+ value(nVal, oVal) {
+ this.defaultValue = nVal;
+ // 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
+ if(nVal != oVal && this.type == 'select') this.handleInput({
+ detail: {
+ value: nVal
+ }
+ })
+ },
+ },
+ computed: {
+ // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+ inputMaxlength() {
+ return Number(this.maxlength);
+ },
+ getStyle() {
+ let style = {};
+ // 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度
+ style.minHeight = this.height ? this.height + 'rpx' : this.type == 'textarea' ?
+ this.textareaHeight + 'rpx' : this.inputHeight + 'rpx';
+ style = Object.assign(style, this.customStyle);
+ return style;
+ },
+ //
+ getCursorSpacing() {
+ return Number(this.cursorSpacing);
+ },
+ // 光标起始位置
+ uSelectionStart() {
+ return String(this.selectionStart);
+ },
+ // 光标结束位置
+ uSelectionEnd() {
+ return String(this.selectionEnd);
+ }
+ },
+ created() {
+ // 监听u-form-item发出的错误事件,将输入框边框变红色
+ this.$on('on-form-item-error', this.onFormItemError);
+ },
+ methods: {
+ /**
+ * change 事件
+ * @param event
+ */
+ handleInput(event) {
+ let value = event.detail.value;
+ // 判断是否去除空格
+ if(this.trim) value = this.$u.trim(value);
+ // vue 原生的方法 return 出去
+ this.$emit('input', value);
+ // 当前model 赋值
+ this.defaultValue = value;
+ // 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
+ // 尚未更新到u-form-item,导致获取的值为空,从而校验混论
+ // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+ setTimeout(() => {
+ // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+ // #ifdef MP-TOUTIAO
+ if(this.$u.trim(value) == this.lastValue) return ;
+ this.lastValue = value;
+ // #endif
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch('u-form-item', 'on-form-change', value);
+ }, 40)
+ },
+ /**
+ * blur 事件
+ * @param event
+ */
+ handleBlur(event) {
+ // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+ // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+ setTimeout(() => {
+ this.focused = false;
+ }, 100)
+ // vue 原生的方法 return 出去
+ this.$emit('blur', event.detail.value);
+ setTimeout(() => {
+ // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+ // #ifdef MP-TOUTIAO
+ if(this.$u.trim(value) == this.lastValue) return ;
+ this.lastValue = value;
+ // #endif
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch('u-form-item', 'on-form-blur', event.detail.value);
+ }, 40)
+ },
+ onFormItemError(status) {
+ this.validateState = status;
+ },
+ onFocus(event) {
+ this.focused = true;
+ this.$emit('focus');
+ },
+ onConfirm(e) {
+ this.$emit('confirm', e.detail.value);
+ },
+ onClear(event) {
+ this.$emit('input', '');
+ },
+ inputClick() {
+ this.$emit('click');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-input {
+ position: relative;
+ flex: 1;
+ @include vue-flex;
+
+ &__input {
+ //height: $u-form-item-height;
+ font-size: 28rpx;
+ color: $u-main-color;
+ flex: 1;
+ }
+
+ &__textarea {
+ width: auto;
+ font-size: 28rpx;
+ color: $u-main-color;
+ padding: 10rpx 0;
+ line-height: normal;
+ flex: 1;
+ }
+
+ &--border {
+ border-radius: 6rpx;
+ border-radius: 4px;
+ border: 1px solid $u-form-item-border-color;
+ }
+
+ &--error {
+ border-color: $u-type-error!important;
+ }
+
+ &__right-icon {
+
+ &__item {
+ margin-left: 10rpx;
+ }
+
+ &--select {
+ transition: transform .4s;
+
+ &--reverse {
+ transform: rotate(-180deg);
+ }
+ }
+ }
+}
+</style>
diff --git a/uview-ui/components/u-keyboard/u-keyboard.vue b/uview-ui/components/u-keyboard/u-keyboard.vue
new file mode 100644
index 0000000..1904a2b
--- /dev/null
+++ b/uview-ui/components/u-keyboard/u-keyboard.vue
@@ -0,0 +1,217 @@
+<template>
+ <u-popup class="" :mask="mask" :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
+ :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :zIndex="uZIndex">
+ <slot />
+ <view class="u-tooltip" v-if="tooltip">
+ <view class="u-tooltip-item u-tooltip-cancel" hover-class="u-tooltip-cancel-hover" @tap="onCancel">
+ {{cancelBtn ? cancelText : ''}}
+ </view>
+ <view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
+ {{tips ? tips : mode == 'number' ? '数字键盘' : mode == 'card' ? '身份证键盘' : '车牌号键盘'}}
+ </view>
+ <view v-if="confirmBtn" @tap="onConfirm" class="u-tooltip-item u-tooltips-submit" hover-class="u-tooltips-submit-hover">
+ {{confirmBtn ? confirmText : ''}}
+ </view>
+ </view>
+ <block v-if="mode == 'number' || mode == 'card'">
+ <u-number-keyboard :random="random" @backspace="backspace" @change="change" :mode="mode" :dotEnabled="dotEnabled"></u-number-keyboard>
+ </block>
+ <block v-else>
+ <u-car-keyboard :random="random" @backspace="backspace" @change="change"></u-car-keyboard>
+ </block>
+ </u-popup>
+</template>
+
+<script>
+ /**
+ * keyboard 键盘
+ * @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。
+ * @tutorial https://www.uviewui.com/components/keyboard.html
+ * @property {String} mode 键盘类型,见官网基本使用的说明(默认number)
+ * @property {Boolean} dot-enabled 是否显示"."按键,只在mode=number时有效(默认true)
+ * @property {Boolean} tooltip 是否显示键盘顶部工具条(默认true)
+ * @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
+ * @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮(默认true)
+ * @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮(默认true)
+ * @property {Boolean} mask 是否显示遮罩(默认true)
+ * @property {String} confirm-text 确认按钮的文字
+ * @property {String} cancel-text 取消按钮的文字
+ * @property {Number String} z-index 弹出键盘的z-index值(默认1075)
+ * @property {Boolean} random 是否打乱键盘按键的顺序(默认false)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘(默认true)
+ * @event {Function} change 按键被点击(不包含退格键被点击)
+ * @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击
+ * @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击
+ * @event {Function} backspace 键盘退格键被点击
+ * @example <u-keyboard mode="number" v-model="show"></u-keyboard>
+ */
+ export default {
+ name: "u-keyboard",
+ props: {
+ // 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘
+ mode: {
+ type: String,
+ default: 'number'
+ },
+ // 是否显示键盘的"."符号
+ dotEnabled: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示顶部工具条
+ tooltip: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示工具条中间的提示
+ showTips: {
+ type: Boolean,
+ default: true
+ },
+ // 工具条中间的提示文字
+ tips: {
+ type: String,
+ default: ''
+ },
+ // 是否显示工具条左边的"取消"按钮
+ cancelBtn: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示工具条右边的"完成"按钮
+ confirmBtn: {
+ type: Boolean,
+ default: true
+ },
+ // 是否打乱键盘按键的顺序
+ random: {
+ type: Boolean,
+ default: false
+ },
+ // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 是否允许通过点击遮罩关闭键盘
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 通过双向绑定控制键盘的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
+ mask: {
+ type: Boolean,
+ default: true
+ },
+ // z-index值
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ // 取消按钮的文字
+ cancelText: {
+ type: String,
+ default: '取消'
+ },
+ // 确认按钮的文字
+ confirmText: {
+ type: String,
+ default: '确认'
+ }
+ },
+ data() {
+ return {
+ //show: false
+ }
+ },
+ computed: {
+ uZIndex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ methods: {
+ change(e) {
+ this.$emit('change', e);
+ },
+ // 键盘关闭
+ popupClose() {
+ // 通过发送input这个特殊的事件名,可以修改父组件传给props的value的变量,也即双向绑定
+ this.$emit('input', false);
+ },
+ // 输入完成
+ onConfirm() {
+ this.popupClose();
+ this.$emit('confirm');
+ },
+ // 取消输入
+ onCancel() {
+ this.popupClose();
+ this.$emit('cancel');
+ },
+ // 退格键
+ backspace() {
+ this.$emit('backspace');
+ },
+ // 关闭键盘
+ // close() {
+ // this.show = false;
+ // },
+ // // 打开键盘
+ // open() {
+ // this.show = true;
+ // }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-keyboard {
+ position: relative;
+ z-index: 1003;
+ }
+
+ .u-tooltip {
+ @include vue-flex;
+ justify-content: space-between;
+ }
+
+ .u-tooltip-item {
+ color: #333333;
+ flex: 0 0 33.333333%;
+ text-align: center;
+ padding: 20rpx 10rpx;
+ font-size: 28rpx;
+ }
+
+ .u-tooltips-submit {
+ text-align: right;
+ flex-grow: 1;
+ flex-wrap: 0;
+ padding-right: 40rpx;
+ color: $u-type-primary;
+ }
+
+ .u-tooltip-cancel {
+ text-align: left;
+ flex-grow: 1;
+ flex-wrap: 0;
+ padding-left: 40rpx;
+ color: #888888;
+ }
+
+ .u-tooltips-submit-hover {
+ color: $u-type-success;
+ }
+
+ .u-tooltip-cancel-hover {
+ color: #333333;
+ }
+</style>
diff --git a/uview-ui/components/u-lazy-load/u-lazy-load.vue b/uview-ui/components/u-lazy-load/u-lazy-load.vue
new file mode 100644
index 0000000..429a680
--- /dev/null
+++ b/uview-ui/components/u-lazy-load/u-lazy-load.vue
@@ -0,0 +1,244 @@
+<template>
+ <view class="u-wrap" :style="{
+ opacity: Number(opacity),
+ borderRadius: borderRadius + 'rpx',
+ // 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
+ transition: `opacity ${time / 1000}s ease-in-out`
+ }"
+ :class="'u-lazy-item-' + elIndex">
+ <view :class="'u-lazy-item-' + elIndex">
+ <image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" v-if="!isError" class="u-lazy-item"
+ :src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded" @error="loadError" @tap="clickImg"></image>
+ <image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" class="u-lazy-item error" v-else :src="errorImg"
+ :mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * lazyLoad 懒加载
+ * @description 懒加载使用的场景为:页面有很多图片时,APP会同时加载所有的图片,导致页面卡顿,各个位置的图片出现前后不一致等.
+ * @tutorial https://www.uviewui.com/components/lazyLoad.html
+ * @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
+ * @property {String} image 图片路径
+ * @property {String} loading-img 预加载时的占位图
+ * @property {String} error-img 图片加载出错时的占位图
+ * @property {String} threshold 触发加载时的位置,见上方说明,单位 rpx(默认300)
+ * @property {String Number} duration 图片加载成功时,淡入淡出时间,单位ms(默认)
+ * @property {String} effect 图片加载成功时,淡入淡出的css动画效果(默认ease-in-out)
+ * @property {Boolean} is-effect 图片加载成功时,是否启用淡入淡出效果(默认true)
+ * @property {String Number} border-radius 图片圆角值,单位rpx(默认0)
+ * @property {String Number} height 图片高度,注意:实际高度可能受img-mode参数影响(默认450)
+ * @property {String Number} mg-mode 图片的裁剪模式,详见image组件裁剪模式(默认widthFix)
+ * @event {Function} click 点击图片时触发
+ * @event {Function} load 图片加载成功时触发
+ * @event {Function} error 图片加载失败时触发
+ * @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
+ */
+ export default {
+ name: 'u-lazy-load',
+ props: {
+ index: {
+ type: [Number, String]
+ },
+ // 要显示的图片
+ image: {
+ type: String,
+ default: ''
+ },
+ // 图片裁剪模式
+ imgMode: {
+ type: String,
+ default: 'widthFix'
+ },
+ // 占位图片路径
+ loadingImg: {
+ type: String,
+ default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
+ },
+ // 加载失败的错误占位图
+ errorImg: {
+ type: String,
+ default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162t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'
+ },
+ // 图片进入可见区域前多少像素时,单位rpx,开始加载图片
+ // 负数为图片超出屏幕底部多少距离后触发懒加载,正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上)
+ threshold: {
+ type: [Number, String],
+ default: 100
+ },
+ // 淡入淡出动画的过渡时间
+ duration: {
+ type: [Number, String],
+ default: 500
+ },
+ // 渡效果的速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显
+ // linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
+ effect: {
+ type: String,
+ default: 'ease-in-out'
+ },
+ // 是否使用过渡效果
+ isEffect: {
+ type: Boolean,
+ default: true
+ },
+ // 圆角值
+ borderRadius: {
+ type: [Number, String],
+ default: 0
+ },
+ // 图片高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: '450'
+ }
+ },
+ data() {
+ return {
+ isShow: false,
+ opacity: 1,
+ time: this.duration,
+ loadStatus: '', // 默认是懒加载中的状态
+ isError: false, // 图片加载失败
+ elIndex: this.$u.guid()
+ }
+ },
+ computed: {
+ // 将threshold从rpx转为px
+ getThreshold() {
+ // 先取绝对值,因为threshold可能是负数,最后根据this.threshold是正数或者负数,重新还原
+ let thresholdPx = uni.upx2px(Math.abs(this.threshold));
+ return this.threshold < 0 ? -thresholdPx : thresholdPx;
+ },
+ // 计算图片的高度,可能为auto,带%,或者直接数值
+ imgHeight() {
+ return this.$u.addUnit(this.height);
+ }
+ },
+ created() {
+ // 由于一些特殊原因,不能将此变量放到data中定义
+ this.observer = {};
+ },
+ watch: {
+ isShow(nVal) {
+ // 如果是不开启过渡效果,直接返回
+ if (!this.isEffect) return;
+ this.time = 0;
+ // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果
+ this.opacity = 0;
+ // 延时30ms,否则在浏览器H5,过渡效果无效
+ setTimeout(() => {
+ this.time = this.duration;
+ this.opacity = 1;
+ }, 30)
+ },
+ // 图片路径发生变化时,需要重新标记一些变量,否则会一直卡在某一个状态,比如isError
+ image(n) {
+ if(!n) {
+ // 如果传入null或者'',或者undefined,标记为错误状态
+ this.isError = true;
+ } else {
+ this.init();
+ this.isError = false;
+ }
+ }
+ },
+ methods: {
+ // 用于重新初始化
+ init() {
+ this.isError = false;
+ this.loadStatus = '';
+ },
+ // 点击图片触发的事件,loadlazy-还是懒加载中状态,loading-图片正在加载,loaded-图片加加载完成
+ clickImg() {
+ let whichImg = '';
+ // 如果isShow为false,意味着图片还没开始加载,点击的只能是最开始的占位图
+ if (this.isShow == false) whichImg = 'lazyImg';
+ // 如果isError为true,意味着图片加载失败,这是只剩下错误的占位图,所以点击的只能是错误占位图
+ // 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
+ else if (this.isError == true) whichImg = 'errorImg';
+ // 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
+ else whichImg = 'realImg';
+ // 只通知当前图片的index
+ this.$emit('click', this.index);
+ },
+ // 图片加载完成事件,可能是加载占位图时触发,也可能是加载真正的图片完成时触发,通过isShow区分
+ imgLoaded() {
+ // 占位图加载完成
+ if (this.loadStatus == '') {
+ this.loadStatus = 'lazyed';
+ }
+ // 真正的图片加载完成
+ else if (this.loadStatus == 'lazyed') {
+ this.loadStatus = 'loaded';
+ this.$emit('load', this.index);
+ }
+ },
+ // 错误的图片加载完成
+ errorImgLoaded() {
+ this.$emit('error', this.index);
+ },
+ // 图片加载失败
+ loadError() {
+ this.isError = true;
+ },
+ disconnectObserver(observerName) {
+ const observer = this[observerName];
+ observer && observer.disconnect();
+ },
+ },
+ beforeDestroy() {
+ // 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
+ //observer.disconnect();
+ },
+ mounted() {
+ // 此uOnReachBottom事件由mixin.js发出,目的是让页面到底时,保证所有图片都进行加载,做到绝对稳定且可靠
+ this.$nextTick(() => {
+ uni.$once('uOnReachBottom', () => {
+ if (!this.isShow) this.isShow = true;
+ });
+ })
+ // mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果
+ setTimeout(() => {
+ // 这里是组件内获取布局状态,不能用uni.createIntersectionObserver,而必须用this.createIntersectionObserver
+ this.disconnectObserver('contentObserver');
+ const contentObserver = uni.createIntersectionObserver(this);
+ // 要理解这里怎么计算的,请看这个:
+ // https://blog.csdn.net/qq_25324335/article/details/83687695
+ contentObserver.relativeToViewport({
+ bottom: this.getThreshold,
+ }).observe('.u-lazy-item-' + this.elIndex, (res) => {
+ if (res.intersectionRatio > 0) {
+ // 懒加载状态改变
+ this.isShow = true;
+ // 如果图片已经加载,去掉监听,减少性能的消耗
+ this.disconnectObserver('contentObserver');
+ }
+ })
+ this.contentObserver = contentObserver;
+ }, 30)
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-wrap {
+ background-color: #eee;
+ overflow: hidden;
+ }
+
+ .u-lazy-item {
+ width: 100%;
+ // 骗系统开启硬件加速
+ transform: transition3d(0, 0, 0);
+ // 防止图片加载“闪一下”
+ will-change: transform;
+ /* #ifndef APP-NVUE */
+ display: block;
+ /* #endif */
+ }
+</style>
diff --git a/uview-ui/components/u-line-progress/u-line-progress.vue b/uview-ui/components/u-line-progress/u-line-progress.vue
new file mode 100644
index 0000000..77e2da2
--- /dev/null
+++ b/uview-ui/components/u-line-progress/u-line-progress.vue
@@ -0,0 +1,147 @@
+<template>
+ <view class="u-progress" :style="{
+ borderRadius: round ? '100rpx' : 0,
+ height: height + 'rpx',
+ backgroundColor: inactiveColor
+ }">
+ <view :class="[
+ type ? `u-type-${type}-bg` : '',
+ striped ? 'u-striped' : '',
+ striped && stripedActive ? 'u-striped-active' : ''
+ ]" class="u-active" :style="[progressStyle]">
+ <slot v-if="$slots.default || $slots.$default" />
+ <block v-else-if="showPercent">
+ {{percent + '%'}}
+ </block>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * lineProgress 线型进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
+ * @tutorial https://www.uviewui.com/components/lineProgress.html
+ * @property {String Number} percent 进度条百分比值,为数值类型,0-100
+ * @property {Boolean} round 进度条两端是否为半圆(默认true)
+ * @property {String} type 如设置,active-color值将会失效
+ * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
+ * @property {String} inactive-color 进度条的底色(默认#ececec)
+ * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
+ * @property {String Number} height 进度条的高度,单位rpx(默认28)
+ * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
+ * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
+ * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
+ */
+ export default {
+ name: "u-line-progress",
+ props: {
+ // 两端是否显示半圆形
+ round: {
+ type: Boolean,
+ default: true
+ },
+ // 主题颜色
+ type: {
+ type: String,
+ default: ''
+ },
+ // 激活部分的颜色
+ activeColor: {
+ type: String,
+ default: '#19be6b'
+ },
+ inactiveColor: {
+ type: String,
+ default: '#ececec'
+ },
+ // 进度百分比,数值
+ percent: {
+ type: Number,
+ default: 0
+ },
+ // 是否在进度条内部显示百分比的值
+ showPercent: {
+ type: Boolean,
+ default: true
+ },
+ // 进度条的高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 28
+ },
+ // 是否显示条纹
+ striped: {
+ type: Boolean,
+ default: false
+ },
+ // 条纹是否显示活动状态
+ stripedActive: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+
+ }
+ },
+ computed: {
+ progressStyle() {
+ let style = {};
+ style.width = this.percent + '%';
+ if(this.activeColor) style.backgroundColor = this.activeColor;
+ return style;
+ }
+ },
+ methods: {
+
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-progress {
+ overflow: hidden;
+ height: 15px;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ width: 100%;
+ border-radius: 100rpx;
+ }
+
+ .u-active {
+ width: 0;
+ height: 100%;
+ align-items: center;
+ @include vue-flex;
+ justify-items: flex-end;
+ justify-content: space-around;
+ font-size: 20rpx;
+ color: #ffffff;
+ transition: all 0.4s ease;
+ }
+
+ .u-striped {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 39px 39px;
+ }
+
+ .u-striped-active {
+ animation: progress-stripes 2s linear infinite;
+ }
+
+ @keyframes progress-stripes {
+ 0% {
+ background-position: 0 0;
+ }
+
+ 100% {
+ background-position: 39px 0;
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-line/u-line.vue b/uview-ui/components/u-line/u-line.vue
new file mode 100644
index 0000000..c56fbc3
--- /dev/null
+++ b/uview-ui/components/u-line/u-line.vue
@@ -0,0 +1,84 @@
+<template>
+ <view class="u-line" :style="[lineStyle]">
+
+ </view>
+</template>
+
+<script>
+ /**
+ * line 线条
+ * @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
+ * @tutorial https://www.uviewui.com/components/line.html
+ * @property {String} color 线条的颜色(默认#e4e7ed)
+ * @property {String} length 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+ * @property {String} direction 线条的方向,row-横向,col-竖向(默认row)
+ * @property {String} border-style 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线(默认solid)
+ * @property {Boolean} hair-line 是否显示细线条(默认true)
+ * @property {String} margin 线条与上下左右元素的间距,字符串形式,如"30rpx"
+ * @example <u-line color="red"></u-line>
+ */
+ export default {
+ name: 'u-line',
+ props: {
+ color: {
+ type: String,
+ default: '#e4e7ed'
+ },
+ // 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+ length: {
+ type: String,
+ default: '100%'
+ },
+ // 线条方向,col-竖向,row-横向
+ direction: {
+ type: String,
+ default: 'row'
+ },
+ // 是否显示细边框
+ hairLine: {
+ type: Boolean,
+ default: true
+ },
+ // 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx"
+ margin: {
+ type: String,
+ default: '0'
+ },
+ // 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线
+ borderStyle: {
+ type: String,
+ default: 'solid'
+ }
+ },
+ computed: {
+ lineStyle() {
+ let style = {};
+ style.margin = this.margin;
+ // 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了
+ if(this.direction == 'row') {
+ // 此处采用兼容分开写,兼容nvue的写法
+ style.borderBottomWidth = '1px';
+ style.borderBottomStyle = this.borderStyle;
+ style.width = this.$u.addUnit(this.length);
+ if(this.hairLine) style.transform = 'scaleY(0.5)';
+ } else {
+ // 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了
+ style.borderLeftWidth = '1px';
+ style.borderLeftStyle = this.borderStyle;
+ style.height = this.$u.addUnit(this.length);
+ if(this.hairLine) style.transform = 'scaleX(0.5)';
+ }
+ style.borderColor = this.color;
+ return style;
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-line {
+ vertical-align: middle;
+ }
+</style>
diff --git a/uview-ui/components/u-link/u-link.vue b/uview-ui/components/u-link/u-link.vue
new file mode 100644
index 0000000..2dd2a73
--- /dev/null
+++ b/uview-ui/components/u-link/u-link.vue
@@ -0,0 +1,89 @@
+<template>
+ <text class="u-link" @tap.stop="openLink" :style="{
+ color: color,
+ fontSize: fontSize + 'rpx',
+ borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none',
+ paddingBottom: underLine ? '0rpx' : '0'
+ }">
+ <slot></slot>
+ </text>
+</template>
+
+<script>
+ /**
+ * link 超链接
+ * @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。
+ * @tutorial https://www.uviewui.com/components/link.html
+ * @property {String} color 文字颜色(默认#606266)
+ * @property {String Number} font-size 字体大小,单位rpx(默认28)
+ * @property {Boolean} under-line 是否显示下划线(默认false)
+ * @property {String} href 跳转的链接,要带上http(s)
+ * @property {String} line-color 下划线颜色,默认同color参数颜色
+ * @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
+ * @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
+ */
+ export default {
+ name: "u-link",
+ props: {
+ // 文字颜色
+ color: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [String, Number],
+ default: 28
+ },
+ // 是否显示下划线
+ underLine: {
+ type: Boolean,
+ default: false
+ },
+ // 要跳转的链接
+ href: {
+ type: String,
+ default: ''
+ },
+ // 小程序中复制到粘贴板的提示语
+ mpTips: {
+ type: String,
+ default: '链接已复制,请在浏览器打开'
+ },
+ // 下划线颜色
+ lineColor: {
+ type: String,
+ default: ''
+ }
+ },
+ methods: {
+ openLink() {
+ // #ifdef APP-PLUS
+ plus.runtime.openURL(this.href)
+ // #endif
+ // #ifdef H5
+ window.open(this.href)
+ // #endif
+ // #ifdef MP
+ uni.setClipboardData({
+ data: this.href,
+ success: () => {
+ uni.hideToast();
+ this.$nextTick(() => {
+ this.$u.toast(this.mpTips);
+ })
+ }
+ });
+ // #endif
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-link {
+ line-height: 1;
+ }
+</style>
diff --git a/uview-ui/components/u-loading-page/u-loading-page.vue b/uview-ui/components/u-loading-page/u-loading-page.vue
new file mode 100644
index 0000000..7e04401
--- /dev/null
+++ b/uview-ui/components/u-loading-page/u-loading-page.vue
@@ -0,0 +1,25 @@
+<template>
+ <view class="u-loading-page">
+
+ </view>
+</template>
+
+<script>
+ export default {
+ props: {
+
+ },
+ data() {
+ return {
+
+ }
+ },
+ methods: {
+
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/uview-ui/components/u-loading/u-loading.vue b/uview-ui/components/u-loading/u-loading.vue
new file mode 100644
index 0000000..c747f64
--- /dev/null
+++ b/uview-ui/components/u-loading/u-loading.vue
@@ -0,0 +1,103 @@
+<template>
+ <view v-if="show" class="u-loading" :class="mode == 'circle' ? 'u-loading-circle' : 'u-loading-flower'" :style="[cricleStyle]">
+ </view>
+</template>
+
+<script>
+ /**
+ * loading 加载动画
+ * @description 警此组件为一个小动画,目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。
+ * @tutorial https://www.uviewui.com/components/loading.html
+ * @property {String} mode 模式选择,见官网说明(默认circle)
+ * @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#c7c7c7)
+ * @property {String Number} size 加载图标的大小,单位rpx(默认34)
+ * @property {Boolean} show 是否显示动画(默认true)
+ * @example <u-loading mode="circle"></u-loading>
+ */
+ export default {
+ name: "u-loading",
+ props: {
+ // 动画的类型
+ mode: {
+ type: String,
+ default: 'circle'
+ },
+ // 动画的颜色
+ color: {
+ type: String,
+ default: '#c7c7c7'
+ },
+ // 加载图标的大小,单位rpx
+ size: {
+ type: [String, Number],
+ default: '34'
+ },
+ // 是否显示动画
+ show: {
+ type: Boolean,
+ default: true
+ }
+ },
+ computed: {
+ // 加载中圆圈动画的样式
+ cricleStyle() {
+ let style = {};
+ style.width = this.size + 'rpx';
+ style.height = this.size + 'rpx';
+ if (this.mode == 'circle') style.borderColor = `#e4e4e4 #e4e4e4 #e4e4e4 ${this.color ? this.color : '#c7c7c7'}`;
+ return style;
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-loading-circle {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ vertical-align: middle;
+ width: 28rpx;
+ height: 28rpx;
+ background: 0 0;
+ border-radius: 50%;
+ border: 2px solid;
+ border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
+ animation: u-circle 1s linear infinite;
+ }
+
+ .u-loading-flower {
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ vertical-align: middle;
+ -webkit-animation: a 1s steps(12) infinite;
+ animation: u-flower 1s steps(12) infinite;
+ background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
+ background-size: 100%;
+ }
+
+ @keyframes u-flower {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ to {
+ -webkit-transform: rotate(1turn);
+ transform: rotate(1turn);
+ }
+ }
+
+ @-webkit-keyframes u-circle {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-loadmore/u-loadmore.vue b/uview-ui/components/u-loadmore/u-loadmore.vue
new file mode 100644
index 0000000..6b852a8
--- /dev/null
+++ b/uview-ui/components/u-loadmore/u-loadmore.vue
@@ -0,0 +1,203 @@
+<template>
+ <view class="u-load-more-wrap" :style="{
+ backgroundColor: bgColor,
+ marginBottom: marginBottom + 'rpx',
+ marginTop: marginTop + 'rpx',
+ height: $u.addUnit(height)
+ }">
+ <u-line color="#d4d4d4" length="50"></u-line>
+ <!-- 加载中和没有更多的状态才显示两边的横线 -->
+ <view :class="status == 'loadmore' || status == 'nomore' ? 'u-more' : ''" class="u-load-more-inner">
+ <view class="u-loadmore-icon-wrap">
+ <u-loading class="u-loadmore-icon" :color="iconColor" :mode="iconType == 'circle' ? 'circle' : 'flower'" :show="status == 'loading' && icon"></u-loading>
+ </view>
+ <!-- 如果没有更多的状态下,显示内容为dot(粗点),加载特定样式 -->
+ <view class="u-line-1" :style="[loadTextStyle]" :class="[(status == 'nomore' && isDot == true) ? 'u-dot-text' : 'u-more-text']" @tap="loadMore">
+ {{ showText }}
+ </view>
+ </view>
+ <u-line color="#d4d4d4" length="50"></u-line>
+ </view>
+</template>
+
+<script>
+ /**
+ * loadmore 加载更多
+ * @description 此组件一般用于标识页面底部加载数据时的状态。
+ * @tutorial https://www.uviewui.com/components/loadMore.html
+ * @property {String} status 组件状态(默认loadmore)
+ * @property {String} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff)
+ * @property {Boolean} icon 加载中时是否显示图标(默认true)
+ * @property {String} icon-type 加载中时的图标类型(默认circle)
+ * @property {String} icon-color icon-type为circle时有效,加载中的动画图标的颜色(默认#b7b7b7)
+ * @property {Boolean} is-dot status为nomore时,内容显示为一个"●"(默认false)
+ * @property {String} color 字体颜色(默认#606266)
+ * @property {String Number} margin-top 到上一个相邻元素的距离
+ * @property {String Number} margin-bottom 到下一个相邻元素的距离
+ * @property {Object} load-text 自定义显示的文字,见上方说明示例
+ * @event {Function} loadmore status为loadmore时,点击组件会发出此事件
+ * @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" />
+ */
+ export default {
+ name: "u-loadmore",
+ props: {
+ // 组件背景色
+ bgColor: {
+ type: String,
+ default: 'transparent'
+ },
+ // 是否显示加载中的图标
+ icon: {
+ type: Boolean,
+ default: true
+ },
+ // 字体大小
+ fontSize: {
+ type: String,
+ default: '28'
+ },
+ // 字体颜色
+ color: {
+ type: String,
+ default: '#606266'
+ },
+ // 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
+ status: {
+ type: String,
+ default: 'loadmore'
+ },
+ // 加载中状态的图标,flower-花朵状图标,circle-圆圈状图标
+ iconType: {
+ type: String,
+ default: 'circle'
+ },
+ // 显示的文字
+ loadText: {
+ type: Object,
+ default () {
+ return {
+ loadmore: '加载更多',
+ loading: '正在加载...',
+ nomore: '没有更多了'
+ }
+ }
+ },
+ // 在“没有更多”状态下,是否显示粗点
+ isDot: {
+ type: Boolean,
+ default: false
+ },
+ // 加载中显示圆圈动画时,动画的颜色
+ iconColor: {
+ type: String,
+ default: '#b7b7b7'
+ },
+ // 上边距
+ marginTop: {
+ type: [String, Number],
+ default: 0
+ },
+ // 下边距
+ marginBottom: {
+ type: [String, Number],
+ default: 0
+ },
+ // 高度,单位rpx
+ height: {
+ type: [String, Number],
+ default: 'auto'
+ }
+ },
+ data() {
+ return {
+ // 粗点
+ dotText: "●"
+ }
+ },
+ computed: {
+ // 加载的文字显示的样式
+ loadTextStyle() {
+ return {
+ color: this.color,
+ fontSize: this.fontSize + 'rpx',
+ position: 'relative',
+ zIndex: 1,
+ backgroundColor: this.bgColor,
+ // 如果是加载中状态,动画和文字需要距离近一点
+ }
+ },
+ // 加载中圆圈动画的样式
+ cricleStyle() {
+ return {
+ borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}`
+ }
+ },
+ // 加载中花朵动画形式
+ // 动画由base64图片生成,暂不支持修改
+ flowerStyle() {
+ return {
+ }
+ },
+ // 显示的提示文字
+ showText() {
+ let text = '';
+ if(this.status == 'loadmore') text = this.loadText.loadmore;
+ else if(this.status == 'loading') text = this.loadText.loading;
+ else if(this.status == 'nomore' && this.isDot) text = this.dotText;
+ else text = this.loadText.nomore;
+ return text;
+ }
+ },
+ methods: {
+ loadMore() {
+ // 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
+ if(this.status == 'loadmore') this.$emit('loadmore');
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ /* #ifdef MP */
+ // 在mp.scss中,赋予了u-line为flex: 1,这里需要一个明确的长度,所以重置掉它
+ // 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用
+ u-line {
+ flex: none;
+ }
+ /* #endif */
+
+ .u-load-more-wrap {
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .u-load-more-inner {
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0 12rpx;
+ }
+
+ .u-more {
+ position: relative;
+ @include vue-flex;
+ justify-content: center;
+ }
+
+ .u-dot-text {
+ font-size: 28rpx;
+ }
+
+ .u-loadmore-icon-wrap {
+ margin-right: 8rpx;
+ }
+
+ .u-loadmore-icon {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ }
+</style>
diff --git a/uview-ui/components/u-mask/u-mask.vue b/uview-ui/components/u-mask/u-mask.vue
new file mode 100644
index 0000000..0e76d8b
--- /dev/null
+++ b/uview-ui/components/u-mask/u-mask.vue
@@ -0,0 +1,123 @@
+<template>
+ <view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle]" @tap="click" @touchmove.stop.prevent="() => {}" :class="{
+ 'u-mask-zoom': zoom,
+ 'u-mask-show': show
+ }">
+ <slot />
+ </view>
+</template>
+
+<script>
+ /**
+ * mask 遮罩
+ * @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
+ * @tutorial https://www.uviewui.com/components/mask.html
+ * @property {Boolean} show 是否显示遮罩(默认false)
+ * @property {String Number} z-index z-index 层级(默认1070)
+ * @property {Object} custom-style 自定义样式对象,见上方说明
+ * @property {String Number} duration 动画时长,单位毫秒(默认300)
+ * @property {Boolean} zoom 是否使用scale对这招进行缩放(默认true)
+ * @property {Boolean} mask-click-able 遮罩是否可点击,为false时点击不会发送click事件(默认true)
+ * @event {Function} click mask-click-able为true时,点击遮罩发送此事件
+ * @example <u-mask :show="show" @click="show = false"></u-mask>
+ */
+ export default {
+ name: "u-mask",
+ props: {
+ // 是否显示遮罩
+ show: {
+ type: Boolean,
+ default: false
+ },
+ // 层级z-index
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ // 用户自定义样式
+ customStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放
+ zoom: {
+ type: Boolean,
+ default: true
+ },
+ // 遮罩的过渡时间,单位为ms
+ duration: {
+ type: [Number, String],
+ default: 300
+ },
+ // 是否可以通过点击遮罩进行关闭
+ maskClickAble: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ zoomStyle: {
+ transform: ''
+ },
+ scale: 'scale(1.2, 1.2)'
+ }
+ },
+ watch: {
+ show(n) {
+ if(n && this.zoom) {
+ // 当展示遮罩的时候,设置scale为1,达到缩小(原来为1.2)的效果
+ this.zoomStyle.transform = 'scale(1, 1)';
+ } else if(!n && this.zoom) {
+ // 当隐藏遮罩的时候,设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果
+ this.zoomStyle.transform = this.scale;
+ }
+ }
+ },
+ computed: {
+ maskStyle() {
+ let style = {};
+ style.backgroundColor = "rgba(0, 0, 0, 0.6)";
+ if(this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask;
+ else style.zIndex = -1;
+ style.transition = `all ${this.duration / 1000}s ease-in-out`;
+ // 判断用户传递的对象是否为空,不为空就进行合并
+ if (Object.keys(this.customStyle).length) style = {
+ ...style,
+ ...this.customStyle
+ };
+ return style;
+ }
+ },
+ methods: {
+ click() {
+ if (!this.maskClickAble) return;
+ this.$emit('click');
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ transition: transform 0.3s;
+ }
+
+ .u-mask-show {
+ opacity: 1;
+ }
+
+ .u-mask-zoom {
+ transform: scale(1.2, 1.2);
+ }
+</style>
diff --git a/uview-ui/components/u-message-input/u-message-input.vue b/uview-ui/components/u-message-input/u-message-input.vue
new file mode 100644
index 0000000..41e0719
--- /dev/null
+++ b/uview-ui/components/u-message-input/u-message-input.vue
@@ -0,0 +1,311 @@
+<template>
+ <view class="u-char-box">
+ <view class="u-char-flex">
+ <input :disabled="disabledKeyboard" :value="valueModel" type="number" :focus="focus" :maxlength="maxlength" class="u-input" @input="getVal"/>
+ <view v-for="(item, index) in loopCharArr" :key="index">
+ <view :class="[breathe && charArrLength == index ? 'u-breathe' : '', 'u-char-item',
+ charArrLength === index && mode == 'box' ? 'u-box-active' : '',
+ mode === 'box' ? 'u-box' : '']" :style="{
+ fontWeight: bold ? 'bold' : 'normal',
+ fontSize: fontSize + 'rpx',
+ width: width + 'rpx',
+ height: width + 'rpx',
+ color: inactiveColor,
+ borderColor: charArrLength === index && mode == 'box' ? activeColor : inactiveColor
+ }">
+ <view class="u-placeholder-line" :style="{
+ display: charArrLength === index ? 'block' : 'none',
+ height: width * 0.5 +'rpx'
+ }"
+ v-if="mode !== 'middleLine'"
+ ></view>
+ <view v-if="mode === 'middleLine' && charArrLength <= index" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-middle-line-active' : '']"
+ class="u-middle-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view>
+ <view v-if="mode === 'bottomLine'" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-buttom-line-active' : '']"
+ class="u-bottom-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view>
+ <block v-if="!dotFill"> {{ charArr[index] ? charArr[index] : ''}}</block>
+ <block v-else>
+ <text class="u-dot">{{ charArr[index] ? '●' : ''}}</text>
+ </block>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * messageInput 验证码输入框
+ * @description 该组件一般用于验证用户短信验证码的场景,也可以结合uView的键盘组件使用
+ * @tutorial https://www.uviewui.com/components/messageInput.html
+ * @property {String Number} maxlength 输入字符个数(默认4)
+ * @property {Boolean} dot-fill 是否用圆点填充(默认false)
+ * @property {String} mode 模式选择,见上方"基本使用"说明(默认box)
+ * @property {String Number} value 预置值
+ * @property {Boolean} breathe 是否开启呼吸效果,见上方说明(默认true)
+ * @property {Boolean} focus 是否自动获取焦点(默认false)
+ * @property {Boolean} bold 字体和输入横线是否加粗(默认true)
+ * @property {String Number} font-size 字体大小,单位rpx(默认60)
+ * @property {String} active-color 当前激活输入框的样式(默认#2979ff)
+ * @property {String} inactive-color 非激活输入框的样式,文字颜色同此值(默认#606266)
+ * @property {String | Number} width 输入框宽度,单位rpx,高等于宽(默认80)
+ * @property {Boolean} disabled-keyboard 禁止点击输入框唤起系统键盘(默认false)
+ * @event {Function} change 输入内容发生改变时触发,具体见官网说明
+ * @event {Function} finish 输入字符个数达maxlength值时触发,见官网说明
+ * @example <u-message-input mode="bottomLine"></u-message-input>
+ */
+ export default {
+ name: "u-message-input",
+ props: {
+ // 最大输入长度
+ maxlength: {
+ type: [Number, String],
+ default: 4
+ },
+ // 是否用圆点填充
+ dotFill: {
+ type: Boolean,
+ default: false
+ },
+ // 显示模式,box-盒子模式,bottomLine-横线在底部模式,middleLine-横线在中部模式
+ mode: {
+ type: String,
+ default: "box"
+ },
+ // 预置值
+ value: {
+ type: [String, Number],
+ default: ''
+ },
+ // 当前激活输入item,是否带有呼吸效果
+ breathe: {
+ type: Boolean,
+ default: true
+ },
+ // 是否自动获取焦点
+ focus: {
+ type: Boolean,
+ default: false
+ },
+ // 字体是否加粗
+ bold: {
+ type: Boolean,
+ default: false
+ },
+ // 字体大小
+ fontSize: {
+ type: [String, Number],
+ default: 60
+ },
+ // 激活样式
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 未激活的样式
+ inactiveColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 输入框的大小,单位rpx,宽等于高
+ width: {
+ type: [Number, String],
+ default: '80'
+ },
+ // 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true
+ disabledKeyboard: {
+ type: Boolean,
+ default: false
+ }
+ },
+ watch: {
+ // maxlength: {
+ // // 此值设置为true,会在组件加载后无需maxlength变化就会执行一次本监听函数,无需再created生命周期中处理
+ // immediate: true,
+ // handler(val) {
+ // this.maxlength = Number(val);
+ // }
+ // },
+ value: {
+ immediate: true,
+ handler(val) {
+ // 转为字符串
+ val = String(val);
+ // 超出部分截掉
+ this.valueModel = val.substring(0, this.maxlength);
+ }
+ },
+ },
+ data() {
+ return {
+ valueModel: ""
+ }
+ },
+ computed: {
+ // 是否显示呼吸灯效果
+ animationClass() {
+ return (index) => {
+ if (this.breathe && this.charArr.length == index) return 'u-breathe';
+ else return '';
+ }
+ },
+ // 用于显示字符
+ charArr() {
+ return this.valueModel.split('');
+ },
+ charArrLength() {
+ return this.charArr.length;
+ },
+ // 根据长度,循环输入框的个数,因为头条小程序数值不能用于v-for
+ loopCharArr() {
+ return new Array(this.maxlength);
+ }
+ },
+ methods: {
+ getVal(e) {
+ let {
+ value
+ } = e.detail
+ this.valueModel = value;
+ // 判断长度是否超出了maxlength值,理论上不会发生,因为input组件设置了maxlength属性值
+ if (String(value).length > this.maxlength) return;
+ // 未达到maxlength之前,发送change事件,达到后发送finish事件
+ this.$emit('change', value);
+ if (String(value).length == this.maxlength) {
+ this.$emit('finish', value);
+ }
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ @keyframes breathe {
+ 0% {
+ opacity: 0.3;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.3;
+ }
+ }
+
+ .u-char-box {
+ text-align: center;
+ }
+
+ .u-char-flex {
+ @include vue-flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ position: relative;
+ }
+
+ .u-input {
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 200%;
+ height: 100%;
+ text-align: left;
+ z-index: 9;
+ opacity: 0;
+ background: none;
+ }
+
+ .u-char-item {
+ position: relative;
+ width: 90rpx;
+ height: 90rpx;
+ margin: 10rpx 10rpx;
+ font-size: 60rpx;
+ font-weight: bold;
+ color: $u-main-color;
+ line-height: 90rpx;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .u-middle-line {
+ border: none;
+ }
+
+ .u-box {
+ box-sizing: border-box;
+ border: 2rpx solid #cccccc;
+ border-radius: 6rpx;
+ }
+
+ .u-box-active {
+ overflow: hidden;
+ animation-timing-function: ease-in-out;
+ animation-duration: 1500ms;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ border: 2rpx solid $u-type-primary;
+ }
+
+ .u-middle-line-active {
+ background: $u-type-primary;
+ }
+
+ .u-breathe {
+ animation: breathe 2s infinite ease;
+ }
+
+ .u-placeholder-line {
+ /* #ifndef APP-NVUE */
+ display: none;
+ /* #endif */
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 2rpx;
+ height: 40rpx;
+ background: #333333;
+ animation: twinkling 1.5s infinite ease;
+ }
+
+ .u-animation-breathe {
+ animation-name: breathe;
+ }
+
+ .u-dot {
+ font-size: 34rpx;
+ line-height: 34rpx;
+ }
+
+ .u-middle-line {
+ height: 4px;
+ background: #000000;
+ width: 80%;
+ position: absolute;
+ border-radius: 2px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .u-buttom-line-active {
+ background: $u-type-primary;
+ }
+
+ .u-bottom-line {
+ height: 4px;
+ background: #000000;
+ width: 80%;
+ position: absolute;
+ border-radius: 2px;
+ bottom: 0;
+ left: 50%;
+ transform: translate(-50%);
+ }
+</style>
diff --git a/uview-ui/components/u-modal/u-modal.vue b/uview-ui/components/u-modal/u-modal.vue
new file mode 100644
index 0000000..ce58113
--- /dev/null
+++ b/uview-ui/components/u-modal/u-modal.vue
@@ -0,0 +1,283 @@
+<template>
+ <view>
+ <u-popup :zoom="zoom" mode="center" :popup="false" :z-index="uZIndex" v-model="value" :length="width"
+ :mask-close-able="maskCloseAble" :border-radius="borderRadius" @close="popupClose" :negative-top="negativeTop">
+ <view class="u-model">
+ <view v-if="showTitle" class="u-model__title u-line-1" :style="[titleStyle]">{{ title }}</view>
+ <view class="u-model__content">
+ <view :style="[contentStyle]" v-if="$slots.default || $slots.$default">
+ <slot />
+ </view>
+ <view v-else class="u-model__content__message" :style="[contentStyle]">{{ content }}</view>
+ </view>
+ <view class="u-model__footer u-border-top" v-if="showCancelButton || showConfirmButton">
+ <view v-if="showCancelButton" :hover-stay-time="100" hover-class="u-model__btn--hover" class="u-model__footer__button"
+ :style="[cancelBtnStyle]" @tap="cancel">
+ {{cancelText}}
+ </view>
+ <view v-if="showConfirmButton || $slots['confirm-button']" :hover-stay-time="100" :hover-class="asyncClose ? 'none' : 'u-model__btn--hover'"
+ class="u-model__footer__button hairline-left" :style="[confirmBtnStyle]" @tap="confirm">
+ <slot v-if="$slots['confirm-button']" name="confirm-button"></slot>
+ <block v-else>
+ <u-loading mode="circle" :color="confirmColor" v-if="loading"></u-loading>
+ <block v-else>
+ {{confirmText}}
+ </block>
+ </block>
+ </view>
+ </view>
+ </view>
+ </u-popup>
+ </view>
+</template>
+
+<script>
+ /**
+ * modal 模态框
+ * @description 弹出模态框,常用于消息提示、消息确认、在当前页面内完成特定的交互操作
+ * @tutorial https://www.uviewui.com/components/modal.html
+ * @property {Boolean} value 是否显示模态框
+ * @property {String | Number} z-index 层级
+ * @property {String} title 模态框标题(默认"提示")
+ * @property {String | Number} width 模态框宽度(默认600)
+ * @property {String} content 模态框内容(默认"内容")
+ * @property {Boolean} show-title 是否显示标题(默认true)
+ * @property {Boolean} async-close 是否异步关闭,只对确定按钮有效(默认false)
+ * @property {Boolean} show-confirm-button 是否显示确认按钮(默认true)
+ * @property {Stringr | Number} negative-top modal往上偏移的值
+ * @property {Boolean} show-cancel-button 是否显示取消按钮(默认false)
+ * @property {Boolean} mask-close-able 是否允许点击遮罩关闭modal(默认false)
+ * @property {String} confirm-text 确认按钮的文字内容(默认"确认")
+ * @property {String} cancel-text 取消按钮的文字内容(默认"取消")
+ * @property {String} cancel-color 取消按钮的颜色(默认"#606266")
+ * @property {String} confirm-color 确认按钮的文字内容(默认"#2979ff")
+ * @property {String | Number} border-radius 模态框圆角值,单位rpx(默认16)
+ * @property {Object} title-style 自定义标题样式,对象形式
+ * @property {Object} content-style 自定义内容样式,对象形式
+ * @property {Object} cancel-style 自定义取消按钮样式,对象形式
+ * @property {Object} confirm-style 自定义确认按钮样式,对象形式
+ * @property {Boolean} zoom 是否开启缩放模式(默认true)
+ * @event {Function} confirm 确认按钮被点击
+ * @event {Function} cancel 取消按钮被点击
+ * @example <u-modal :src="title" :content="content"></u-modal>
+ */
+ export default {
+ name: 'u-modal',
+ props: {
+ // 是否显示Modal
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 层级z-index
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ // 标题
+ title: {
+ type: [String],
+ default: '提示'
+ },
+ // 弹窗宽度,可以是数值(rpx),百分比,auto等
+ width: {
+ type: [Number, String],
+ default: 600
+ },
+ // 弹窗内容
+ content: {
+ type: String,
+ default: '内容'
+ },
+ // 是否显示标题
+ showTitle: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示确认按钮
+ showConfirmButton: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示取消按钮
+ showCancelButton: {
+ type: Boolean,
+ default: false
+ },
+ // 确认文案
+ confirmText: {
+ type: String,
+ default: '确认'
+ },
+ // 取消文案
+ cancelText: {
+ type: String,
+ default: '取消'
+ },
+ // 确认按钮颜色
+ confirmColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 取消文字颜色
+ cancelColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 圆角值
+ borderRadius: {
+ type: [Number, String],
+ default: 16
+ },
+ // 标题的样式
+ titleStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 内容的样式
+ contentStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 取消按钮的样式
+ cancelStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 确定按钮的样式
+ confirmStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // 是否开启缩放效果
+ zoom: {
+ type: Boolean,
+ default: true
+ },
+ // 是否异步关闭,只对确定按钮有效
+ asyncClose: {
+ type: Boolean,
+ default: false
+ },
+ // 是否允许点击遮罩关闭modal
+ maskCloseAble: {
+ type: Boolean,
+ default: false
+ },
+ // 给一个负的margin-top,往上偏移,避免和键盘重合的情况
+ negativeTop: {
+ type: [String, Number],
+ default: 0
+ }
+ },
+ data() {
+ return {
+ loading: false, // 确认按钮是否正在加载中
+ }
+ },
+ computed: {
+ cancelBtnStyle() {
+ return Object.assign({
+ color: this.cancelColor
+ }, this.cancelStyle);
+ },
+ confirmBtnStyle() {
+ return Object.assign({
+ color: this.confirmColor
+ }, this.confirmStyle);
+ },
+ uZIndex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ watch: {
+ // 如果是异步关闭时,外部修改v-model的值为false时,重置内部的loading状态
+ // 避免下次打开的时候,状态混乱
+ value(n) {
+ if (n === true) this.loading = false;
+ }
+ },
+ methods: {
+ confirm() {
+ // 异步关闭
+ if (this.asyncClose) {
+ this.loading = true;
+ } else {
+ this.$emit('input', false);
+ }
+ this.$emit('confirm');
+ },
+ cancel() {
+ this.$emit('cancel');
+ this.$emit('input', false);
+ // 目前popup弹窗关闭有一个延时操作,此处做一个延时
+ // 避免确认按钮文字变成了"确定"字样,modal还没消失,造成视觉不好的效果
+ setTimeout(() => {
+ this.loading = false;
+ }, 300);
+ },
+ // 点击遮罩关闭modal,设置v-model的值为false,否则无法第二次弹起modal
+ popupClose() {
+ this.$emit('input', false);
+ },
+ // 清除加载中的状态
+ clearLoading() {
+ this.loading = false;
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-model {
+ height: auto;
+ overflow: hidden;
+ font-size: 32rpx;
+ background-color: #fff;
+
+ &__btn--hover {
+ background-color: rgb(230, 230, 230);
+ }
+
+ &__title {
+ padding-top: 48rpx;
+ font-weight: 500;
+ text-align: center;
+ color: $u-main-color;
+ }
+
+ &__content {
+ &__message {
+ padding: 48rpx;
+ font-size: 30rpx;
+ text-align: center;
+ color: $u-content-color;
+ }
+ }
+
+ &__footer {
+ @include vue-flex;
+
+ &__button {
+ flex: 1;
+ height: 100rpx;
+ line-height: 100rpx;
+ font-size: 32rpx;
+ box-sizing: border-box;
+ cursor: pointer;
+ text-align: center;
+ border-radius: 4rpx;
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-navbar/u-navbar.vue b/uview-ui/components/u-navbar/u-navbar.vue
new file mode 100644
index 0000000..450242e
--- /dev/null
+++ b/uview-ui/components/u-navbar/u-navbar.vue
@@ -0,0 +1,315 @@
+<template>
+ <view class="">
+ <view class="u-navbar" :style="[navbarStyle]" :class="{ 'u-navbar-fixed': isFixed, 'u-border-bottom': borderBottom }">
+ <view class="u-status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
+ <view class="u-navbar-inner" :style="[navbarInnerStyle]">
+ <view class="u-back-wrap" v-if="isBack" @tap="goBack">
+ <view class="u-icon-wrap">
+ <u-icon :name="backIconName" :color="backIconColor" :size="backIconSize"></u-icon>
+ </view>
+ <view class="u-icon-wrap u-back-text u-line-1" v-if="backText" :style="[backTextStyle]">{{ backText }}</view>
+ </view>
+ <view class="u-navbar-content-title" v-if="title" :style="[titleStyle]">
+ <view
+ class="u-title u-line-1"
+ :style="{
+ color: titleColor,
+ fontSize: titleSize + 'rpx',
+ fontWeight: titleBold ? 'bold' : 'normal'
+ }">
+ {{ title }}
+ </view>
+ </view>
+ <view class="u-slot-content">
+ <slot></slot>
+ </view>
+ <view class="u-slot-right">
+ <slot name="right"></slot>
+ </view>
+ </view>
+ </view>
+ <!-- 解决fixed定位后导航栏塌陷的问题 -->
+ <view class="u-navbar-placeholder" v-if="isFixed && !immersive" :style="{ width: '100%', height: Number(navbarHeight) + statusBarHeight + 'px' }"></view>
+ </view>
+</template>
+
+<script>
+ // 获取系统状态栏的高度
+ let systemInfo = uni.getSystemInfoSync();
+ let menuButtonInfo = {};
+ // 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
+ // #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
+ menuButtonInfo = uni.getMenuButtonBoundingClientRect();
+ // #endif
+ /**
+ * navbar 自定义导航栏
+ * @description 此组件一般用于在特殊情况下,需要自定义导航栏的时候用到,一般建议使用uniapp自带的导航栏。
+ * @tutorial https://www.uviewui.com/components/navbar.html
+ * @property {String Number} height 导航栏高度(不包括状态栏高度在内,内部自动加上),注意这里的单位是px(默认44)
+ * @property {String} back-icon-color 左边返回图标的颜色(默认#606266)
+ * @property {String} back-icon-name 左边返回图标的名称,只能为uView自带的图标(默认arrow-left)
+ * @property {String Number} back-icon-size 左边返回图标的大小,单位rpx(默认30)
+ * @property {String} back-text 返回图标右边的辅助提示文字
+ * @property {Object} back-text-style 返回图标右边的辅助提示文字的样式,对象形式(默认{ color: '#606266' })
+ * @property {String} title 导航栏标题,如设置为空字符,将会隐藏标题占位区域
+ * @property {String Number} title-width 导航栏标题的最大宽度,内容超出会以省略号隐藏,单位rpx(默认250)
+ * @property {String} title-color 标题的颜色(默认#606266)
+ * @property {String Number} title-size 导航栏标题字体大小,单位rpx(默认32)
+ * @property {Function} custom-back 自定义返回逻辑方法
+ * @property {String Number} z-index 固定在顶部时的z-index值(默认980)
+ * @property {Boolean} is-back 是否显示导航栏左边返回图标和辅助文字(默认true)
+ * @property {Object} background 导航栏背景设置,见官网说明(默认{ background: '#ffffff' })
+ * @property {Boolean} is-fixed 导航栏是否固定在顶部(默认true)
+ * @property {Boolean} immersive 沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效(默认false)
+ * @property {Boolean} border-bottom 导航栏底部是否显示下边框,如定义了较深的背景颜色,可取消此值(默认true)
+ * @example <u-navbar back-text="返回" title="剑未配妥,出门已是江湖"></u-navbar>
+ */
+ export default {
+ name: "u-navbar",
+ props: {
+ // 导航栏高度,单位px,非rpx
+ height: {
+ type: [String, Number],
+ default: ''
+ },
+ // 返回箭头的颜色
+ backIconColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 左边返回的图标
+ backIconName: {
+ type: String,
+ default: 'nav-back'
+ },
+ // 左边返回图标的大小,rpx
+ backIconSize: {
+ type: [String, Number],
+ default: '44'
+ },
+ // 返回的文字提示
+ backText: {
+ type: String,
+ default: ''
+ },
+ // 返回的文字的 样式
+ backTextStyle: {
+ type: Object,
+ default () {
+ return {
+ color: '#606266'
+ }
+ }
+ },
+ // 导航栏标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 标题的宽度,如果需要自定义右侧内容,且右侧内容很多时,可能需要减少这个宽度,单位rpx
+ titleWidth: {
+ type: [String, Number],
+ default: '250'
+ },
+ // 标题的颜色
+ titleColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 标题字体是否加粗
+ titleBold: {
+ type: Boolean,
+ default: false
+ },
+ // 标题的字体大小
+ titleSize: {
+ type: [String, Number],
+ default: 32
+ },
+ isBack: {
+ type: [Boolean, String],
+ default: true
+ },
+ // 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色
+ background: {
+ type: Object,
+ default () {
+ return {
+ background: '#ffffff'
+ }
+ }
+ },
+ // 导航栏是否固定在顶部
+ isFixed: {
+ type: Boolean,
+ default: true
+ },
+ // 是否沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效
+ immersive: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示导航栏的下边框
+ borderBottom: {
+ type: Boolean,
+ default: true
+ },
+ zIndex: {
+ type: [String, Number],
+ default: ''
+ },
+ // 自定义返回逻辑
+ customBack: {
+ type: Function,
+ default: null
+ }
+ },
+ data() {
+ return {
+ menuButtonInfo: menuButtonInfo,
+ statusBarHeight: systemInfo.statusBarHeight
+ };
+ },
+ computed: {
+ // 导航栏内部盒子的样式
+ navbarInnerStyle() {
+ let style = {};
+ // 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
+ style.height = this.navbarHeight + 'px';
+ // // 如果是各家小程序,导航栏内部的宽度需要减少右边胶囊的宽度
+ // #ifdef MP
+ let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
+ style.marginRight = rightButtonWidth + 'px';
+ // #endif
+ return style;
+ },
+ // 整个导航栏的样式
+ navbarStyle() {
+ let style = {};
+ style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.navbar;
+ // 合并用户传递的背景色对象
+ Object.assign(style, this.background);
+ return style;
+ },
+ // 导航中间的标题的样式
+ titleStyle() {
+ let style = {};
+ // #ifndef MP
+ style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
+ style.right = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
+ // #endif
+ // #ifdef MP
+ // 此处是为了让标题显示区域即使在小程序有右侧胶囊的情况下也能处于屏幕的中间,是通过绝对定位实现的
+ let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
+ style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
+ style.right = rightButtonWidth - (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + rightButtonWidth +
+ 'px';
+ // #endif
+ style.width = uni.upx2px(this.titleWidth) + 'px';
+ return style;
+ },
+ // 转换字符数值为真正的数值
+ navbarHeight() {
+ // #ifdef APP-PLUS || H5
+ return this.height ? this.height : 44;
+ // #endif
+ // #ifdef MP
+ // 小程序特别处理,让导航栏高度 = 胶囊高度 + 两倍胶囊顶部与状态栏底部的距离之差(相当于同时获得了导航栏底部与胶囊底部的距离)
+ // 此方法有缺陷,暂不用(会导致少了几个px),采用直接固定值的方式
+ // return menuButtonInfo.height + (menuButtonInfo.top - this.statusBarHeight) * 2;//导航高度
+ let height = systemInfo.platform == 'ios' ? 44 : 48;
+ return this.height ? this.height : height;
+ // #endif
+ }
+ },
+ created() {},
+ methods: {
+ goBack() {
+ // 如果自定义了点击返回按钮的函数,则执行,否则执行返回逻辑
+ if (typeof this.customBack === 'function') {
+ // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this
+ // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文
+ this.customBack.bind(this.$u.$parent.call(this))();
+ } else {
+ uni.navigateBack();
+ }
+ }
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-navbar {
+ width: 100%;
+ }
+
+ .u-navbar-fixed {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 0;
+ z-index: 991;
+ }
+
+ .u-status-bar {
+ width: 100%;
+ }
+
+ .u-navbar-inner {
+ @include vue-flex;
+ justify-content: space-between;
+ position: relative;
+ align-items: center;
+ }
+
+ .u-back-wrap {
+ @include vue-flex;
+ align-items: center;
+ flex: 1;
+ flex-grow: 0;
+ padding: 14rpx 14rpx 14rpx 24rpx;
+ }
+
+ .u-back-text {
+ padding-left: 4rpx;
+ font-size: 30rpx;
+ }
+
+ .u-navbar-content-title {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 60rpx;
+ text-align: center;
+ flex-shrink: 0;
+ }
+
+ .u-navbar-centent-slot {
+ flex: 1;
+ }
+
+ .u-title {
+ line-height: 60rpx;
+ font-size: 32rpx;
+ flex: 1;
+ }
+
+ .u-navbar-right {
+ flex: 1;
+ @include vue-flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
+
+ .u-slot-content {
+ flex: 1;
+ @include vue-flex;
+ align-items: center;
+ }
+</style>
diff --git a/uview-ui/components/u-no-network/u-no-network.vue b/uview-ui/components/u-no-network/u-no-network.vue
new file mode 100644
index 0000000..51d9f4f
--- /dev/null
+++ b/uview-ui/components/u-no-network/u-no-network.vue
@@ -0,0 +1,233 @@
+<template>
+ <view class="u-no-network" v-if="!isConnected" :style="{'z-index': uZIndex}" @touchmove.stop.prevent="() => {}">
+ <view class="u-inner">
+ <image class="u-error-icon" :src="image" mode="widthFix"></image>
+ <view class="u-tips">
+ {{tips}}
+ </view>
+ <!-- 只有APP平台,才能跳转设置页,因为需要调用plus环境 -->
+ <!-- #ifdef APP-PLUS -->
+ <view class="u-to-setting">
+ 请检查网络,或前往<text class="u-setting-btn" @tap="openSettings">设置</text>
+ </view>
+ <!-- #endif -->
+ <view class="u-retry" :hover-stay-time="150" @tap="retry" hover-class="u-retry-hover">
+ 重试
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * noNetwork 无网络提示
+ * @description 该组件无需任何配置,引入即可,内部自动处理所有功能和事件。
+ * @tutorial https://www.uviewui.com/components/noNetwork.html
+ * @property {String} tips 没有网络时的提示语(默认哎呀,网络信号丢失)
+ * @property {String Number} zIndex 组件的z-index值(默认1080)
+ * @property {String} image 无网络的图片提示,可用的src地址或base64图片
+ * @event {Function} retry 用户点击页面的"重试"按钮时触发
+ * @example <u-no-network></u-no-network>
+ */
+ export default {
+ name: "u-no-network",
+ props: {
+ // 页面文字提示
+ tips: {
+ type: String,
+ default: '哎呀,网络信号丢失'
+ },
+ // 一个z-index值,用于设置没有网络这个组件的层次,因为页面可能会有其他定位的元素层级过高,导致此组件被覆盖
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ // image 没有网络的图片提示
+ image: {
+ type: String,
+ default: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEYCAMAAABFglBLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6M0U3MjVFMzQwNEY1MTFFQUE4MTNDOUEzMTVBREMxQjIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6M0U3MjVFMzUwNEY1MTFFQUE4MTNDOUEzMTVBREMxQjIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozRTcyNUUzMjA0RjUxMUVBQTgxM0M5QTMxNUFEQzFCMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozRTcyNUUzMzA0RjUxMUVBQTgxM0M5QTMxNUFEQzFCMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkHIU9QAAAMAUExURdHW2OWiou7u7tve4dnc3/vw8N3g4sPCwvjn5+jo6M7Q0u6+vtyEhPXY2Li+wuikpPXW1uXo6dba3Pbg4Na5u+qurqqyt/HJydjb3fjo6LrAxO7Bwey1td6MjOrs7fbc3OTn6Maytf7+/vz19eqqqrzCxvzz87a8wLO6vuqxsf78/PDFxenr7L/FyNTY26+2u/z09MjMzqy0udnZ2dvb27G4vMjN0O7v8P339+Ll5u3v8NDS1ODj5frt7bC3vP76+u24uOKamtTW2MTIy9zc3N7e3vPQ0OCTk8DGyfDDw9LR0c7S1LussNbY2fPOztLU1cLHyrK6vvji4vrv78bKzPX29/np6crP0vjk5Ozu78bLzuepqczR1MHFyMDEyOq1tfTS0vP09cLIy9DU173Ex8bGxvHy88/U18vO0LK4vM7OzsTKzcPJzPLMzM/T1sbMz8HJy+7FxbW8wNTW18XKzvb3+MjLzczP0d3e3+Dh4r3Dxtna29zd3rK5vdPU1cfM0Prq6uOenuPm58HGyfHz8/ro6Lq/w8nO0bzCxcrKytjZ2uXm5vTU1LvBxbW7v+rp6eefn/39/eLi4u3t7Ozs7Pj4+Pr6+vX19fHx8erq6vPz8+Xl5ff39+fn5/v7+/z8/PLx8fX09Pb29ri4uOTk5PHw8OPj4/Py8ubm5vn5+e3s7Pb19fTz8/f29tfX1+7t7cvQ0+zr6/Ly8u3t7fr5+efm5quzuOvq6vT19uvr6/j5+a61uuy6uvb39/T09Ojn5+jq6621uvn6+t/i5KyzuPn4+Le+wvv6+s3S1fj4+fDw8OHk5vv7/Pr7++Xk5LS7v7y8vObl5fz9/ff4+OTj48DEx8XJy+Hk5euysu/x8re9wfn5+v38/Pv8/Pr6++vt7uLi4+Pi4ubl5uXk5dPV1tbV1fHMzNPX2e7u7ejn6PT19fX19PDw7/b29f39/vnr6+Hh4a+3u+7t7q61uePi48PHyf35+enp6fLz9PPz8qmxtuDg4N/f3/Dv7////////1cfN/UAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAlqElEQVR42uydCXwUVZrAk5CEJBAQshACCGyjIRwBJR0It6JBGZZBkRtm5PBgFXAXHBghqAMz6LjGTrd9n7lvwn0JQhARxYNxVJTMwcwOM+7uzK6Z3VXcrnTXVr2q6q7urrvfq26c+n4egXTSVfXv993veym4JkklKdoj0IBoogHRgGiiAdGAaKIB0YBoogHRgGiiAdFEA6IBUVuwx+vqet/v1YAkhxwmcNT9gvjnsAYkGcT7eN1TtThe+1TdL2o1IEkg+rrvUV90MF9oQBIqv6ijF4a3d51XA5J4jVVXx3z587ojGpDEm/QwkO9pQJJB2CvksAYk8fJUXT31xZG63ppRTwI5QgcgR35Rd1MDkgxSX1f31P33P1V3i3u936HUyc3edWSsjuEakKRRW3r9kVv+JrRsrwbklpKTnXs+z0wfONOtAUm8rJu55oqfkZ0TOzUgCZS3Zh7s54+S5RqQhNGYMsnPISkakCSiQcjkNzUgasvUDaVsBF1fb9GXd//B0ziliPzjEA2IujLqGRaMST+cawgLNpn4q30aEDV1Va+dYRqzNy00RMoYUmdpQNTzcb8sCtuKH8w9Z4iWLuIbVzQgapmOBedDOO6c0WjgENK2vKwBUUUmvBSiUbrqxwZuOU18d5MGhEe/dI6a2WvIxA0HX1hfvH7V8gUpA9Mz3x/VuVTRLxvwdAhH5aJjBj4hv39QAxIl743ITHm6azBPnOCfVLwlc6pR3upYFbLjvIuDlEUqRYZo38JYS4gVt5rNZhMQO46byksoMeG4w8QIjtvMpJA/wP1EdZ0bKvkitkh555mBA3zSLnBlSFnNnlJvEJJ95ItW3pJAamvtZhy3m/T0g7fhthJGiG+EviYgmEJ/wHFP6GsbnlZSoicoucxmGwHVguPTBz543C9LHky/Jnql1w5KxWGo8KsThsAFYneYMOb5mlnP11ZCrwOPjfgGI8RqsZuZtUOsFmbllFhZP+zEbc8vGcqzMibv7qos3rd7Mg+TrQv2vCVkiLYc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"
+ }
+ },
+ data() {
+ return {
+ isConnected: true, // 是否有网络连接
+ networkType: "none", // 网络类型
+ }
+ },
+ computed: {
+ uZIndex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.noNetwork;
+ }
+ },
+ mounted() {
+ this.isIOS = (uni.getSystemInfoSync().platform === 'ios');
+ uni.onNetworkStatusChange((res) => {
+ this.isConnected = res.isConnected;
+ this.networkType = res.networkType;
+ });
+ uni.getNetworkType({
+ success: (res) => {
+ this.networkType = res.networkType;
+ if (res.networkType == 'none') {
+ this.isConnected = false;
+ } else {
+ this.isConnected = true;
+ }
+ }
+ });
+ },
+ methods: {
+ retry() {
+ // 重新检查网络
+ uni.getNetworkType({
+ success: (res) => {
+ this.networkType = res.networkType;
+ if (res.networkType == 'none') {
+ uni.showToast({
+ title: '无网络连接',
+ icon: 'none',
+ position: 'top'
+ })
+ this.isConnected = false;
+ } else {
+ uni.showToast({
+ title: '网络已连接',
+ icon: 'none',
+ position: 'top'
+ })
+ this.isConnected = true;
+ }
+ }
+ });
+ this.$emit('retry');
+ },
+ async openSettings() {
+ if (this.networkType == "none") {
+ this.openSystemSettings();
+ return;
+ }
+ },
+ openAppSettings() {
+ this.gotoAppSetting();
+ },
+ openSystemSettings() {
+ // 以下方法来自5+范畴,如需深究,请自行查阅相关文档
+ // https://ask.dcloud.net.cn/docs/
+ if (this.isIOS) {
+ this.gotoiOSSetting();
+ } else {
+ this.gotoAndroidSetting();
+ }
+ },
+ network() {
+ var result = null;
+ var cellularData = plus.ios.newObject("CTCellularData");
+ var state = cellularData.plusGetAttribute("restrictedState");
+ if (state == 0) {
+ result = null;
+ } else if (state == 2) {
+ result = 1;
+ } else if (state == 1) {
+ result = 2;
+ }
+ plus.ios.deleteObject(cellularData);
+ return result;
+ },
+ gotoAppSetting() {
+ if (this.isIOS) {
+ var UIApplication = plus.ios.import("UIApplication");
+ var application2 = UIApplication.sharedApplication();
+ var NSURL2 = plus.ios.import("NSURL");
+ var setting2 = NSURL2.URLWithString("app-settings:");
+ application2.openURL(setting2);
+ plus.ios.deleteObject(setting2);
+ plus.ios.deleteObject(NSURL2);
+ plus.ios.deleteObject(application2);
+ } else {
+ var Intent = plus.android.importClass("android.content.Intent");
+ var Settings = plus.android.importClass("android.provider.Settings");
+ var Uri = plus.android.importClass("android.net.Uri");
+ var mainActivity = plus.android.runtimeMainActivity();
+ var intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
+ intent.setData(uri);
+ mainActivity.startActivity(intent);
+ }
+ },
+ gotoiOSSetting() {
+ var UIApplication = plus.ios.import("UIApplication");
+ var application2 = UIApplication.sharedApplication();
+ var NSURL2 = plus.ios.import("NSURL");
+ var setting2 = NSURL2.URLWithString("App-prefs:root=General");
+ application2.openURL(setting2);
+ plus.ios.deleteObject(setting2);
+ plus.ios.deleteObject(NSURL2);
+ plus.ios.deleteObject(application2);
+ },
+ gotoAndroidSetting() {
+ var Intent = plus.android.importClass("android.content.Intent");
+ var Settings = plus.android.importClass("android.provider.Settings");
+ var mainActivity = plus.android.runtimeMainActivity();
+ var intent = new Intent(Settings.ACTION_SETTINGS);
+ mainActivity.startActivity(intent);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-no-network {
+ background-color: #fff;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ .u-inner {
+ height: 100vh;
+ @include vue-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-top: -15%;
+ }
+
+ .u-tips {
+ color: $u-tips-color;
+ font-size: 28rpx;
+ padding: 30rpx 0;
+ }
+
+ .u-error-icon {
+ width: 300rpx;
+ }
+
+ .u-to-setting {
+ color: $u-light-color;
+ font-size: 26rpx;
+ }
+
+ .u-setting-btn {
+ font-size: 26rpx;
+ color: $u-type-primary;
+ }
+
+ .u-retry {
+ margin-top: 30rpx;
+ border: 1px solid $u-tips-color;
+ color: $u-tips-color;
+ font-size: 28rpx;
+ padding: 6rpx 30rpx;
+ border-radius: 3px;
+ }
+
+ .u-retry-hover {
+ color: #fff;
+ background-color: $u-tips-color;
+ }
+</style>
diff --git a/uview-ui/components/u-notice-bar/u-notice-bar.vue b/uview-ui/components/u-notice-bar/u-notice-bar.vue
new file mode 100644
index 0000000..41a6cc4
--- /dev/null
+++ b/uview-ui/components/u-notice-bar/u-notice-bar.vue
@@ -0,0 +1,272 @@
+<template>
+ <view class="u-notice-bar-wrap" v-if="isShow" :style="{
+ borderRadius: borderRadius + 'rpx',
+ }">
+ <block v-if="mode == 'horizontal' && isCircular">
+ <u-row-notice
+ :type="type"
+ :color="color"
+ :bgColor="bgColor"
+ :list="list"
+ :volumeIcon="volumeIcon"
+ :moreIcon="moreIcon"
+ :volumeSize="volumeSize"
+ :closeIcon="closeIcon"
+ :mode="mode"
+ :fontSize="fontSize"
+ :speed="speed"
+ :playState="playState"
+ :padding="padding"
+ @getMore="getMore"
+ @close="close"
+ @click="click"
+ ></u-row-notice>
+ </block>
+ <block v-if="mode == 'vertical' || (mode == 'horizontal' && !isCircular)">
+ <u-column-notice
+ :type="type"
+ :color="color"
+ :bgColor="bgColor"
+ :list="list"
+ :volumeIcon="volumeIcon"
+ :moreIcon="moreIcon"
+ :closeIcon="closeIcon"
+ :mode="mode"
+ :volumeSize="volumeSize"
+ :disable-touch="disableTouch"
+ :fontSize="fontSize"
+ :duration="duration"
+ :playState="playState"
+ :padding="padding"
+ @getMore="getMore"
+ @close="close"
+ @click="click"
+ @end="end"
+ ></u-column-notice>
+ </block>
+ </view>
+</template>
+<script>
+/**
+ * noticeBar 滚动通知
+ * @description 该组件用于滚动通告场景,有多种模式可供选择
+ * @tutorial https://www.uviewui.com/components/noticeBar.html
+ * @property {Array} list 滚动内容,数组形式,见上方说明
+ * @property {String} type 显示的主题(默认warning)
+ * @property {Boolean} volume-icon 是否显示小喇叭图标(默认true)
+ * @property {Boolean} more-icon 是否显示右边的向右箭头(默认false)
+ * @property {Boolean} close-icon 是否显示关闭图标(默认false)
+ * @property {Boolean} autoplay 是否自动播放(默认true)
+ * @property {String} color 文字颜色
+ * @property {String Number} bg-color 背景颜色
+ * @property {String} mode 滚动模式(默认horizontal)
+ * @property {Boolean} show 是否显示(默认true)
+ * @property {String Number} font-size 字体大小,单位rpx(默认28)
+ * @property {String Number} volume-size 左边喇叭的大小(默认34)
+ * @property {String Number} duration 滚动周期时长,只对步进模式有效,横向衔接模式无效,单位ms(默认2000)
+ * @property {String Number} speed 水平滚动时的滚动速度,即每秒移动多少距离,只对水平衔接方式有效,单位rpx(默认160)
+ * @property {String Number} font-size 字体大小,单位rpx(默认28)
+ * @property {Boolean} is-circular mode为horizontal时,指明是否水平衔接滚动(默认true)
+ * @property {String} play-state 播放状态,play - 播放,paused - 暂停(默认play)
+ * @property {String Nubmer} border-radius 通知栏圆角(默认为0)
+ * @property {String Nubmer} padding 内边距,字符串,与普通的内边距css写法一直(默认"18rpx 24rpx")
+ * @property {Boolean} no-list-hidden 列表为空时,是否显示组件(默认false)
+ * @property {Boolean} disable-touch 是否禁止通过手动滑动切换通知,只有mode = vertical,或者mode = horizontal且is-circular = false时有效(默认true)
+ * @event {Function} click 点击通告文字触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效
+ * @event {Function} close 点击右侧关闭图标触发
+ * @event {Function} getMore 点击右侧向右图标触发
+ * @event {Function} end 列表的消息每次被播放一个周期时触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效
+ * @example <u-notice-bar :more-icon="true" :list="list"></u-notice-bar>
+ */
+export default {
+ name: "u-notice-bar",
+ props: {
+ // 显示的内容,数组
+ list: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 显示的主题,success|error|primary|info|warning
+ type: {
+ type: String,
+ default: 'warning'
+ },
+ // 是否显示左侧的音量图标
+ volumeIcon: {
+ type: Boolean,
+ default: true
+ },
+ // 音量喇叭的大小
+ volumeSize: {
+ type: [Number, String],
+ default: 34
+ },
+ // 是否显示右侧的右箭头图标
+ moreIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示右侧的关闭图标
+ closeIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否自动播放
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 文字颜色,各图标也会使用文字颜色
+ color: {
+ type: String,
+ default: ''
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
+ mode: {
+ type: String,
+ default: 'horizontal'
+ },
+ // 是否显示
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 28
+ },
+ // 滚动一个周期的时间长,单位ms
+ duration: {
+ type: [Number, String],
+ default: 2000
+ },
+ // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+ speed: {
+ type: [Number, String],
+ default: 160
+ },
+ // 水平滚动时,是否采用衔接形式滚动
+ // 水平衔接模式,采用的是swiper组件,水平滚动
+ isCircular: {
+ type: Boolean,
+ default: true
+ },
+ // 播放状态,play-播放,paused-暂停
+ playState: {
+ type: String,
+ default: 'play'
+ },
+ // 是否禁止用手滑动切换
+ // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
+ disableTouch: {
+ type: Boolean,
+ default: true
+ },
+ // 滚动通知设置圆角
+ borderRadius: {
+ type: [Number, String],
+ default: 0
+ },
+ // 通知的边距
+ padding: {
+ type: [Number, String],
+ default: '18rpx 24rpx'
+ },
+ // list列表为空时,是否显示组件
+ noListHidden: {
+ type: Boolean,
+ default: true
+ }
+ },
+ computed: {
+ // 如果设置show为false,或者设置了noListHidden为true,且list长度又为零的话,隐藏组件
+ isShow() {
+ if(this.show == false || (this.noListHidden == true && this.list.length == 0)) return false;
+ else return true;
+ }
+ },
+ methods: {
+ // 点击通告栏
+ click(index) {
+ this.$emit('click', index);
+ },
+ // 点击关闭按钮
+ close() {
+ this.$emit('close');
+ },
+ // 点击更多箭头按钮
+ getMore() {
+ this.$emit('getMore');
+ },
+ // 滚动一个周期结束,只对垂直,或者水平步进形式有效
+ end() {
+ this.$emit('end');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-notice-bar-wrap {
+ overflow: hidden;
+}
+
+.u-notice-bar {
+ padding: 18rpx 24rpx;
+ overflow: hidden;
+}
+
+.u-direction-row {
+ @include vue-flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.u-left-icon {
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-notice-box {
+ flex: 1;
+ @include vue-flex;
+ overflow: hidden;
+ margin-left: 12rpx;
+}
+
+.u-right-icon {
+ margin-left: 12rpx;
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-notice-content {
+ line-height: 1;
+ white-space: nowrap;
+ font-size: 26rpx;
+ animation: u-loop-animation 10s linear infinite both;
+ text-align: right;
+ // 这一句很重要,为了能让滚动左右连接起来
+ padding-left: 100%;
+}
+
+@keyframes u-loop-animation {
+ 0% {
+ transform: translate3d(0, 0, 0);
+ }
+
+ 100% {
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+</style>
diff --git a/uview-ui/components/u-number-box/u-number-box.vue b/uview-ui/components/u-number-box/u-number-box.vue
new file mode 100644
index 0000000..54a679e
--- /dev/null
+++ b/uview-ui/components/u-number-box/u-number-box.vue
@@ -0,0 +1,363 @@
+<template>
+ <view class="u-numberbox">
+ <view class="u-icon-minus" @touchstart.stop.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal <= min }"
+ :style="{
+ background: bgColor,
+ height: inputHeight + 'rpx',
+ color: color
+ }">
+ <u-icon name="minus" :size="size"></u-icon>
+ </view>
+ <input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" :class="{ 'u-input-disabled': disabled }"
+ v-model="inputVal" class="u-number-input" @blur="onBlur" @focus="onFocus"
+ type="number" :style="{
+ color: color,
+ fontSize: size + 'rpx',
+ background: bgColor,
+ height: inputHeight + 'rpx',
+ width: inputWidth + 'rpx'
+ }" />
+ <view class="u-icon-plus" @touchstart.stop.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal >= max }"
+ :style="{
+ background: bgColor,
+ height: inputHeight + 'rpx',
+ color: color
+ }">
+ <u-icon name="plus" :size="size"></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * numberBox 步进器
+ * @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入
+ * @tutorial https://www.uviewui.com/components/numberBox.html
+ * @property {Number} value 输入框初始值(默认1)
+ * @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5)
+ * @property {Number} min 用户可输入的最小值(默认0)
+ * @property {Number} max 用户可输入的最大值(默认99999)
+ * @property {Number} step 步长,每次加或减的值(默认1)
+ * @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false)
+ * @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false)
+ * @property {Boolean} positive-integer 是否只能输入正整数(默认true)
+ * @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26)
+ * @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233)
+ * @property {String | Number} input-width 输入框宽度,单位rpx(默认80)
+ * @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50)
+ * @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
+ * @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
+ * @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250)
+ * @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200)
+ * @event {Function} change 输入框内容发生变化时触发,对象形式
+ * @event {Function} blur 输入框失去焦点时触发,对象形式
+ * @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式
+ * @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式
+ * @example <u-number-box :min="1" :max="100"></u-number-box>
+ */
+ export default {
+ name: "u-number-box",
+ props: {
+ // 预显示的数字
+ value: {
+ type: Number,
+ default: 1
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: '#F2F3F5'
+ },
+ // 最小值
+ min: {
+ type: Number,
+ default: 0
+ },
+ // 最大值
+ max: {
+ type: Number,
+ default: 99999
+ },
+ // 步进值,每次加或减的值
+ step: {
+ type: Number,
+ default: 1
+ },
+ // 是否禁用加减操作
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // input的字体大小,单位rpx
+ size: {
+ type: [Number, String],
+ default: 26
+ },
+ // 加减图标的颜色
+ color: {
+ type: String,
+ default: '#323233'
+ },
+ // input宽度,单位rpx
+ inputWidth: {
+ type: [Number, String],
+ default: 80
+ },
+ // input高度,单位rpx
+ inputHeight: {
+ type: [Number, String],
+ default: 50
+ },
+ // index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话
+ // 设置disabled为false,disabledInput为true即可
+ disabledInput: {
+ type: Boolean,
+ default: false
+ },
+ // 输入框于键盘之间的距离
+ cursorSpacing: {
+ type: [Number, String],
+ default: 100
+ },
+ // 是否开启长按连续递增或递减
+ longPress: {
+ type: Boolean,
+ default: true
+ },
+ // 开启长按触发后,每触发一次需要多久
+ pressTime: {
+ type: [Number, String],
+ default: 250
+ },
+ // 是否只能输入大于或等于0的整数(正整数)
+ positiveInteger: {
+ type: Boolean,
+ default: true
+ }
+ },
+ watch: {
+ value(v1, v2) {
+ // 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
+ if(!this.changeFromInner) {
+ this.inputVal = v1;
+ // 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
+ // 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
+ // 将changeFromInner设置为false
+ this.$nextTick(function(){
+ this.changeFromInner = false;
+ })
+ }
+ },
+ inputVal(v1, v2) {
+ // 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
+ if (v1 == '') return;
+ let value = 0;
+ // 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
+ let tmp = this.$u.test.number(v1);
+ if (tmp && v1 >= this.min && v1 <= this.max) value = v1;
+ else value = v2;
+ // 判断是否只能输入大于等于0的整数
+ if(this.positiveInteger) {
+ // 小于0,或者带有小数点,
+ if(v1 < 0 || String(v1).indexOf('.') !== -1) {
+ value = v2;
+ // 双向绑定input的值,必须要使用$nextTick修改显示的值
+ this.$nextTick(() => {
+ this.inputVal = v2;
+ })
+ }
+ }
+ // 发出change事件
+ this.handleChange(value, 'change');
+ }
+ },
+ data() {
+ return {
+ inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态
+ timer: null, // 用作长按的定时器
+ changeFromInner: false, // 值发生变化,是来自内部还是外部
+ innerChangeTimer: null, // 内部定时器
+ };
+ },
+ created() {
+ this.inputVal = Number(this.value);
+ },
+ computed: {
+ getCursorSpacing() {
+ // 先将值转为px单位,再转为数值
+ return Number(uni.upx2px(this.cursorSpacing));
+ }
+ },
+ methods: {
+ // 点击退格键
+ btnTouchStart(callback) {
+ // 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
+ this[callback]();
+ // 如果没开启长按功能,直接返回
+ if (!this.longPress) return;
+ clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+ this.timer = null;
+ this.timer = setInterval(() => {
+ // 执行加或减函数
+ this[callback]();
+ }, this.pressTime);
+ },
+ clearTimer() {
+ this.$nextTick(() => {
+ clearInterval(this.timer);
+ this.timer = null;
+ })
+ },
+ minus() {
+ this.computeVal('minus');
+ },
+ plus() {
+ this.computeVal('plus');
+ },
+ // 为了保证小数相加减出现精度溢出的问题
+ calcPlus(num1, num2) {
+ let baseNum, baseNum1, baseNum2;
+ try {
+ baseNum1 = num1.toString().split('.')[1].length;
+ } catch (e) {
+ baseNum1 = 0;
+ }
+ try {
+ baseNum2 = num2.toString().split('.')[1].length;
+ } catch (e) {
+ baseNum2 = 0;
+ }
+ baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+ let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度
+ return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision);
+ },
+ // 为了保证小数相加减出现精度溢出的问题
+ calcMinus(num1, num2) {
+ let baseNum, baseNum1, baseNum2;
+ try {
+ baseNum1 = num1.toString().split('.')[1].length;
+ } catch (e) {
+ baseNum1 = 0;
+ }
+ try {
+ baseNum2 = num2.toString().split('.')[1].length;
+ } catch (e) {
+ baseNum2 = 0;
+ }
+ baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+ let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2;
+ return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision);
+ },
+ computeVal(type) {
+ uni.hideKeyboard();
+ if (this.disabled) return;
+ let value = 0;
+ // 减
+ if (type === 'minus') {
+ value = this.calcMinus(this.inputVal, this.step);
+ } else if (type === 'plus') {
+ value = this.calcPlus(this.inputVal, this.step);
+ }
+ // 判断是否小于最小值和大于最大值
+ if (value < this.min || value > this.max) {
+ return;
+ }
+ this.inputVal = value;
+ this.handleChange(value, type);
+ },
+ // 处理用户手动输入的情况
+ onBlur(event) {
+ let val = 0;
+ let value = event.detail.value;
+ // 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
+ // 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
+ if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min;
+ val = +value;
+ if (val > this.max) {
+ val = this.max;
+ } else if (val < this.min) {
+ val = this.min;
+ }
+ this.$nextTick(() => {
+ this.inputVal = val;
+ })
+ this.handleChange(val, 'blur');
+ },
+ // 输入框获得焦点事件
+ onFocus() {
+ this.$emit('focus');
+ },
+ handleChange(value, type) {
+ if (this.disabled) return;
+ // 清除定时器,避免造成混乱
+ if(this.innerChangeTimer) {
+ clearTimeout(this.innerChangeTimer);
+ this.innerChangeTimer = null;
+ }
+ // 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果
+ this.changeFromInner = true;
+ // 一定时间内,清除changeFromInner标记,否则内部值改变后
+ // 外部通过程序修改value值,将会无效
+ this.innerChangeTimer = setTimeout(() => {
+ this.changeFromInner = false;
+ }, 150);
+ this.$emit('input', Number(value));
+ this.$emit(type, {
+ // 转为Number类型
+ value: Number(value),
+ index: this.index
+ })
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-numberbox {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .u-number-input {
+ position: relative;
+ text-align: center;
+ padding: 0;
+ margin: 0 6rpx;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .u-icon-plus,
+ .u-icon-minus {
+ width: 60rpx;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .u-icon-plus {
+ border-radius: 0 8rpx 8rpx 0;
+ }
+
+ .u-icon-minus {
+ border-radius: 8rpx 0 0 8rpx;
+ }
+
+ .u-icon-disabled {
+ color: #c8c9cc !important;
+ background: #f7f8fa !important;
+ }
+
+ .u-input-disabled {
+ color: #c8c9cc !important;
+ background-color: #f2f3f5 !important;
+ }
+</style>
diff --git a/uview-ui/components/u-number-keyboard/u-number-keyboard.vue b/uview-ui/components/u-number-keyboard/u-number-keyboard.vue
new file mode 100644
index 0000000..6425a1f
--- /dev/null
+++ b/uview-ui/components/u-number-keyboard/u-number-keyboard.vue
@@ -0,0 +1,158 @@
+<template>
+ <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
+ <view class="u-keyboard-grids">
+ <view
+ class="u-keyboard-grids-item"
+ :class="[btnBgGray(index) ? 'u-bg-gray' : '', index <= 2 ? 'u-border-top' : '', index < 9 ? 'u-border-bottom' : '', (index + 1) % 3 != 0 ? 'u-border-right' : '']"
+ :style="[itemStyle(index)]"
+ v-for="(item, index) in numList"
+ :key="index"
+ :hover-class="hoverClass(index)"
+ :hover-stay-time="100"
+ @tap="keyboardClick(item)">
+ <view class="u-keyboard-grids-btn">{{ item }}</view>
+ </view>
+ <view class="u-keyboard-grids-item u-bg-gray" hover-class="u-hover-class" :hover-stay-time="100" @touchstart.stop="backspaceClick"
+ @touchend="clearTimer">
+ <view class="u-keyboard-back u-keyboard-grids-btn">
+ <u-icon name="backspace" :size="38" :bold="true"></u-icon>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ export default {
+ props: {
+ // 键盘的类型,number-数字键盘,card-身份证键盘
+ mode: {
+ type: String,
+ default: 'number'
+ },
+ // 是否显示键盘的"."符号
+ dotEnabled: {
+ type: Boolean,
+ default: true
+ },
+ // 是否打乱键盘按键的顺序
+ random: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ backspace: 'backspace', // 退格键内容
+ dot: '.', // 点
+ timer: null, // 长按多次删除的事件监听
+ cardX: 'X' // 身份证的X符号
+ };
+ },
+ computed: {
+ // 键盘需要显示的内容
+ numList() {
+ let tmp = [];
+ if (!this.dotEnabled && this.mode == 'number') {
+ if (!this.random) {
+ return [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+ } else {
+ return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
+ }
+ } else if (this.dotEnabled && this.mode == 'number') {
+ if (!this.random) {
+ return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0];
+ } else {
+ return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]);
+ }
+ } else if (this.mode == 'card') {
+ if (!this.random) {
+ return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0];
+ } else {
+ return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]);
+ }
+ }
+ },
+ // 按键的样式,在非乱序&&数字键盘&&不显示点按钮时,index为9时,按键占位两个空间
+ itemStyle() {
+ return index => {
+ let style = {};
+ if (this.mode == 'number' && !this.dotEnabled && index == 9) style.flex = '0 0 66.6666666666%';
+ return style;
+ };
+ },
+ // 是否让按键显示灰色,只在非乱序&&数字键盘&&且允许点按键的时候
+ btnBgGray() {
+ return index => {
+ if (!this.random && index == 9 && (this.mode != 'number' || (this.mode == 'number' && this.dotEnabled))) return true;
+ else return false;
+ };
+ },
+ hoverClass() {
+ return index => {
+ if (!this.random && index == 9 && (this.mode == 'number' && this.dotEnabled || this.mode == 'card')) return 'u-hover-class';
+ else return 'u-keyboard-hover';
+ }
+ }
+ },
+ methods: {
+ // 点击退格键
+ backspaceClick() {
+ this.$emit('backspace');
+ clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+ this.timer = null;
+ this.timer = setInterval(() => {
+ this.$emit('backspace');
+ }, 250);
+ },
+ clearTimer() {
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+ // 获取键盘显示的内容
+ keyboardClick(val) {
+ // 允许键盘显示点模式和触发非点按键时,将内容转为数字类型
+ if (this.dotEnabled && val != this.dot && val != this.cardX) val = Number(val);
+ this.$emit('change', val);
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-keyboard {
+ position: relative;
+ z-index: 1003;
+ }
+
+ .u-keyboard-grids {
+ @include vue-flex;
+ flex-wrap: wrap;
+ }
+
+ .u-keyboard-grids-item {
+ flex: 0 0 33.3333333333%;
+ text-align: center;
+ font-size: 50rpx;
+ color: #333;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ height: 110rpx;
+ font-weight: 500;
+ }
+
+ .u-bg-gray {
+ background-color: $u-border-color;
+ }
+
+ .u-keyboard-back {
+ font-size: 36rpx;
+ }
+
+ .u-keyboard-hover {
+ background-color: #e7e6eb;
+ }
+</style>
diff --git a/uview-ui/components/u-parse/libs/CssHandler.js b/uview-ui/components/u-parse/libs/CssHandler.js
new file mode 100644
index 0000000..75c6015
--- /dev/null
+++ b/uview-ui/components/u-parse/libs/CssHandler.js
@@ -0,0 +1,100 @@
+const cfg = require('./config.js'),
+ isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+
+function CssHandler(tagStyle) {
+ var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
+ for (var item in tagStyle)
+ styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
+ this.styles = styles;
+}
+CssHandler.prototype.getStyle = function(data) {
+ this.styles = new parser(data, this.styles).parse();
+}
+CssHandler.prototype.match = function(name, attrs) {
+ var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
+ if (attrs.class) {
+ var items = attrs.class.split(' ');
+ for (var i = 0, item; item = items[i]; i++)
+ if (tmp = this.styles['.' + item])
+ matched += tmp + ';';
+ }
+ if (tmp = this.styles['#' + attrs.id])
+ matched += tmp + ';';
+ return matched;
+}
+module.exports = CssHandler;
+
+function parser(data, init) {
+ this.data = data;
+ this.floor = 0;
+ this.i = 0;
+ this.list = [];
+ this.res = init;
+ this.state = this.Space;
+}
+parser.prototype.parse = function() {
+ for (var c; c = this.data[this.i]; this.i++)
+ this.state(c);
+ return this.res;
+}
+parser.prototype.section = function() {
+ return this.data.substring(this.start, this.i);
+}
+// 状态机
+parser.prototype.Space = function(c) {
+ if (c == '.' || c == '#' || isLetter(c)) {
+ this.start = this.i;
+ this.state = this.Name;
+ } else if (c == '/' && this.data[this.i + 1] == '*')
+ this.Comment();
+ else if (!cfg.blankChar[c] && c != ';')
+ this.state = this.Ignore;
+}
+parser.prototype.Comment = function() {
+ this.i = this.data.indexOf('*/', this.i) + 1;
+ if (!this.i) this.i = this.data.length;
+ this.state = this.Space;
+}
+parser.prototype.Ignore = function(c) {
+ if (c == '{') this.floor++;
+ else if (c == '}' && !--this.floor) {
+ this.list = [];
+ this.state = this.Space;
+ }
+}
+parser.prototype.Name = function(c) {
+ if (cfg.blankChar[c]) {
+ this.list.push(this.section());
+ this.state = this.NameSpace;
+ } else if (c == '{') {
+ this.list.push(this.section());
+ this.Content();
+ } else if (c == ',') {
+ this.list.push(this.section());
+ this.Comma();
+ } else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
+ this.state = this.Ignore;
+}
+parser.prototype.NameSpace = function(c) {
+ if (c == '{') this.Content();
+ else if (c == ',') this.Comma();
+ else if (!cfg.blankChar[c]) this.state = this.Ignore;
+}
+parser.prototype.Comma = function() {
+ while (cfg.blankChar[this.data[++this.i]]);
+ if (this.data[this.i] == '{') this.Content();
+ else {
+ this.start = this.i--;
+ this.state = this.Name;
+ }
+}
+parser.prototype.Content = function() {
+ this.start = ++this.i;
+ if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
+ var content = this.section();
+ for (var i = 0, item; item = this.list[i++];)
+ if (this.res[item]) this.res[item] += ';' + content;
+ else this.res[item] = content;
+ this.list = [];
+ this.state = this.Space;
+}
diff --git a/uview-ui/components/u-parse/libs/MpHtmlParser.js b/uview-ui/components/u-parse/libs/MpHtmlParser.js
new file mode 100644
index 0000000..aeb0fc3
--- /dev/null
+++ b/uview-ui/components/u-parse/libs/MpHtmlParser.js
@@ -0,0 +1,580 @@
+/**
+ * html 解析器
+ * @tutorial https://github.com/jin-yufeng/Parser
+ * @version 20201029
+ * @author JinYufeng
+ * @listens MIT
+ */
+const cfg = require('./config.js'),
+ blankChar = cfg.blankChar,
+ CssHandler = require('./CssHandler.js'),
+ windowWidth = uni.getSystemInfoSync().windowWidth;
+var emoji;
+
+function MpHtmlParser(data, options = {}) {
+ this.attrs = {};
+ this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
+ this.data = data;
+ this.domain = options.domain;
+ this.DOM = [];
+ this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
+ options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
+ this.options = options;
+ this.state = this.Text;
+ this.STACK = [];
+ // 工具函数
+ this.bubble = () => {
+ for (var i = this.STACK.length, item; item = this.STACK[--i];) {
+ if (cfg.richOnlyTags[item.name]) return false;
+ item.c = 1;
+ }
+ return true;
+ }
+ this.decode = (val, amp) => {
+ var i = -1,
+ j, en;
+ while (1) {
+ if ((i = val.indexOf('&', i + 1)) == -1) break;
+ if ((j = val.indexOf(';', i + 2)) == -1) break;
+ if (val[i + 1] == '#') {
+ en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
+ if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
+ } else {
+ en = val.substring(i + 1, j);
+ if (cfg.entities[en] || en == amp)
+ val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
+ }
+ }
+ return val;
+ }
+ this.getUrl = url => {
+ if (url[0] == '/') {
+ if (url[1] == '/') url = this.options.prot + ':' + url;
+ else if (this.domain) url = this.domain + url;
+ } else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
+ url = this.domain + '/' + url;
+ return url;
+ }
+ this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
+ this.section = () => this.data.substring(this.start, this.i);
+ this.parent = () => this.STACK[this.STACK.length - 1];
+ this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
+}
+MpHtmlParser.prototype.parse = function() {
+ if (emoji) this.data = emoji.parseEmoji(this.data);
+ for (var c; c = this.data[this.i]; this.i++)
+ this.state(c);
+ if (this.state == this.Text) this.setText();
+ while (this.STACK.length) this.popNode(this.STACK.pop());
+ return this.DOM;
+}
+// 设置属性
+MpHtmlParser.prototype.setAttr = function() {
+ var name = this.attrName.toLowerCase(),
+ val = this.attrVal;
+ if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
+ else if (val) {
+ if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
+ else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
+ else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
+ }
+ this.attrVal = '';
+ while (blankChar[this.data[this.i]]) this.i++;
+ if (this.isClose()) this.setNode();
+ else {
+ this.start = this.i;
+ this.state = this.AttrName;
+ }
+}
+// 设置文本节点
+MpHtmlParser.prototype.setText = function() {
+ var back, text = this.section();
+ if (!text) return;
+ text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
+ if (back) {
+ this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
+ let j = this.start + text.length;
+ for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
+ return;
+ }
+ if (!this.pre) {
+ // 合并空白符
+ var flag, tmp = [];
+ for (let i = text.length, c; c = text[--i];)
+ if (!blankChar[c]) {
+ tmp.unshift(c);
+ if (!flag) flag = 1;
+ } else {
+ if (tmp[0] != ' ') tmp.unshift(' ');
+ if (c == '\n' && flag == void 0) flag = 0;
+ }
+ if (flag == 0) return;
+ text = tmp.join('');
+ }
+ this.siblings().push({
+ type: 'text',
+ text: this.decode(text)
+ });
+}
+// 设置元素节点
+MpHtmlParser.prototype.setNode = function() {
+ var node = {
+ name: this.tagName.toLowerCase(),
+ attrs: this.attrs
+ },
+ close = cfg.selfClosingTags[node.name];
+ if (this.options.nodes.length) node.type = 'node';
+ this.attrs = {};
+ if (!cfg.ignoreTags[node.name]) {
+ // 处理属性
+ var attrs = node.attrs,
+ style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
+ styleObj = {};
+ if (attrs.id) {
+ if (this.options.compress & 1) attrs.id = void 0;
+ else if (this.options.useAnchor) this.bubble();
+ }
+ if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
+ switch (node.name) {
+ case 'a':
+ case 'ad': // #ifdef APP-PLUS
+ case 'iframe':
+ // #endif
+ this.bubble();
+ break;
+ case 'font':
+ if (attrs.color) {
+ styleObj['color'] = attrs.color;
+ attrs.color = void 0;
+ }
+ if (attrs.face) {
+ styleObj['font-family'] = attrs.face;
+ attrs.face = void 0;
+ }
+ if (attrs.size) {
+ var size = parseInt(attrs.size);
+ if (size < 1) size = 1;
+ else if (size > 7) size = 7;
+ var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
+ styleObj['font-size'] = map[size - 1];
+ attrs.size = void 0;
+ }
+ break;
+ case 'embed':
+ // #ifndef APP-PLUS
+ var src = node.attrs.src || '',
+ type = node.attrs.type || '';
+ if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
+ node.name = 'video';
+ else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
+ '.aac'))
+ node.name = 'audio';
+ else break;
+ if (node.attrs.autostart)
+ node.attrs.autoplay = 'T';
+ node.attrs.controls = 'T';
+ // #endif
+ // #ifdef APP-PLUS
+ this.bubble();
+ break;
+ // #endif
+ case 'video':
+ case 'audio':
+ if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
+ else this[`${node.name}Num`]++;
+ if (node.name == 'video') {
+ if (this.videoNum > 3)
+ node.lazyLoad = 1;
+ if (attrs.width) {
+ styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
+ attrs.width = void 0;
+ }
+ if (attrs.height) {
+ styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
+ attrs.height = void 0;
+ }
+ }
+ if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T';
+ attrs.source = [];
+ if (attrs.src) {
+ attrs.source.push(attrs.src);
+ attrs.src = void 0;
+ }
+ this.bubble();
+ break;
+ case 'td':
+ case 'th':
+ if (attrs.colspan || attrs.rowspan)
+ for (var k = this.STACK.length, item; item = this.STACK[--k];)
+ if (item.name == 'table') {
+ item.flag = 1;
+ break;
+ }
+ }
+ if (attrs.align) {
+ if (node.name == 'table') {
+ if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';
+ else styleObj['float'] = attrs.align;
+ } else styleObj['text-align'] = attrs.align;
+ attrs.align = void 0;
+ }
+ // 压缩 style
+ var styles = style.split(';');
+ style = '';
+ for (var i = 0, len = styles.length; i < len; i++) {
+ var info = styles[i].split(':');
+ if (info.length < 2) continue;
+ let key = info[0].trim().toLowerCase(),
+ value = info.slice(1).join(':').trim();
+ if (value[0] == '-' || value.includes('safe'))
+ style += `;${key}:${value}`;
+ else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
+ styleObj[key] = value;
+ }
+ if (node.name == 'img') {
+ if (attrs.src && !attrs.ignore) {
+ if (this.bubble())
+ attrs.i = (this.imgNum++).toString();
+ else attrs.ignore = 'T';
+ }
+ if (attrs.ignore) {
+ style += ';-webkit-touch-callout:none';
+ styleObj['max-width'] = '100%';
+ }
+ var width;
+ if (styleObj.width) width = styleObj.width;
+ else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : parseFloat(attrs.width) + 'px';
+ if (width) {
+ styleObj.width = width;
+ attrs.width = '100%';
+ if (parseInt(width) > windowWidth) {
+ styleObj.height = '';
+ if (attrs.height) attrs.height = void 0;
+ }
+ }
+ if (styleObj.height) {
+ attrs.height = styleObj.height;
+ styleObj.height = '';
+ } else if (attrs.height && !attrs.height.includes('%'))
+ attrs.height = parseFloat(attrs.height) + 'px';
+ }
+ for (var key in styleObj) {
+ var value = styleObj[key];
+ if (!value) continue;
+ if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
+ // 填充链接
+ if (value.includes('url')) {
+ var j = value.indexOf('(');
+ if (j++ != -1) {
+ while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
+ value = value.substr(0, j) + this.getUrl(value.substr(j));
+ }
+ }
+ // 转换 rpx
+ else if (value.includes('rpx'))
+ value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
+ else if (key == 'white-space' && value.includes('pre') && !close)
+ this.pre = node.pre = true;
+ style += `;${key}:${value}`;
+ }
+ style = style.substr(1);
+ if (style) attrs.style = style;
+ if (!close) {
+ node.children = [];
+ if (node.name == 'pre' && cfg.highlight) {
+ this.remove(node);
+ this.pre = node.pre = true;
+ }
+ this.siblings().push(node);
+ this.STACK.push(node);
+ } else if (!cfg.filter || cfg.filter(node, this) != false)
+ this.siblings().push(node);
+ } else {
+ if (!close) this.remove(node);
+ else if (node.name == 'source') {
+ var parent = this.parent();
+ if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
+ parent.attrs.source.push(node.attrs.src);
+ } else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
+ }
+ if (this.data[this.i] == '/') this.i++;
+ this.start = this.i + 1;
+ this.state = this.Text;
+}
+// 移除标签
+MpHtmlParser.prototype.remove = function(node) {
+ var name = node.name,
+ j = this.i;
+ // 处理 svg
+ var handleSvg = () => {
+ var src = this.data.substring(j, this.i + 1);
+ node.attrs.xmlns = 'http://www.w3.org/2000/svg';
+ for (var key in node.attrs) {
+ if (key == 'viewbox') src = ` viewBox="${node.attrs.viewbox}"` + src;
+ else if (key != 'style') src = ` ${key}="${node.attrs[key]}"` + src;
+ }
+ src = '<svg' + src;
+ var parent = this.parent();
+ if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
+ parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
+ this.siblings().push({
+ name: 'img',
+ attrs: {
+ src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+ style: node.attrs.style,
+ ignore: 'T'
+ }
+ })
+ }
+ if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
+ while (1) {
+ if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
+ if (name == 'pre' || name == 'svg') this.i = j;
+ else this.i = this.data.length;
+ return;
+ }
+ this.start = (this.i += 2);
+ while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
+ if (this.section().toLowerCase() == name) {
+ // 代码块高亮
+ if (name == 'pre') {
+ this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
+ .substr(this.i - 5);
+ return this.i = j;
+ } else if (name == 'style')
+ this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
+ else if (name == 'title')
+ this.DOM.title = this.data.substring(j + 1, this.i - 7);
+ if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
+ if (name == 'svg') handleSvg();
+ return;
+ }
+ }
+}
+// 节点出栈处理
+MpHtmlParser.prototype.popNode = function(node) {
+ // 空白符处理
+ if (node.pre) {
+ node.pre = this.pre = void 0;
+ for (let i = this.STACK.length; i--;)
+ if (this.STACK[i].pre)
+ this.pre = true;
+ }
+ var siblings = this.siblings(),
+ len = siblings.length,
+ childs = node.children;
+ if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
+ return siblings.pop();
+ var attrs = node.attrs;
+ // 替换一些标签名
+ if (cfg.blockTags[node.name]) node.name = 'div';
+ else if (!cfg.trustTags[node.name]) node.name = 'span';
+ // 处理列表
+ if (node.c && (node.name == 'ul' || node.name == 'ol')) {
+ if ((node.attrs.style || '').includes('list-style:none')) {
+ for (let i = 0, child; child = childs[i++];)
+ if (child.name == 'li')
+ child.name = 'div';
+ } else if (node.name == 'ul') {
+ var floor = 1;
+ for (let i = this.STACK.length; i--;)
+ if (this.STACK[i].name == 'ul') floor++;
+ if (floor != 1)
+ for (let i = childs.length; i--;)
+ childs[i].floor = floor;
+ } else {
+ for (let i = 0, num = 1, child; child = childs[i++];)
+ if (child.name == 'li') {
+ child.type = 'ol';
+ child.num = ((num, type) => {
+ if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
+ if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
+ if (type == 'i' || type == 'I') {
+ num = (num - 1) % 99 + 1;
+ var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
+ ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
+ res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
+ if (type == 'i') return res.toLowerCase();
+ return res;
+ }
+ return num;
+ })(num++, attrs.type) + '.';
+ }
+ }
+ }
+ // 处理表格
+ if (node.name == 'table') {
+ var padding = parseFloat(attrs.cellpadding),
+ spacing = parseFloat(attrs.cellspacing),
+ border = parseFloat(attrs.border);
+ if (node.c) {
+ if (isNaN(padding)) padding = 2;
+ if (isNaN(spacing)) spacing = 2;
+ }
+ if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
+ if (node.flag && node.c) {
+ // 有 colspan 或 rowspan 且含有链接的表格转为 grid 布局实现
+ attrs.style = `${attrs.style || ''};${spacing ? `;grid-gap:${spacing}px` : ';border-left:0;border-top:0'}`;
+ var row = 1,
+ col = 1,
+ colNum,
+ trs = [],
+ children = [],
+ map = {};
+ (function f(ns) {
+ for (var i = 0; i < ns.length; i++) {
+ if (ns[i].name == 'tr') trs.push(ns[i]);
+ else f(ns[i].children || []);
+ }
+ })(node.children)
+ for (let i = 0; i < trs.length; i++) {
+ for (let j = 0, td; td = trs[i].children[j]; j++) {
+ if (td.name == 'td' || td.name == 'th') {
+ while (map[row + '.' + col]) col++;
+ var cell = {
+ name: 'div',
+ c: 1,
+ attrs: {
+ style: (td.attrs.style || '') + (border ? `;border:${border}px solid gray` + (spacing ? '' :
+ ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '')
+ },
+ children: td.children
+ }
+ if (td.attrs.colspan) {
+ cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + parseInt(td.attrs.colspan));
+ if (!td.attrs.rowspan) cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + 1);
+ col += parseInt(td.attrs.colspan) - 1;
+ }
+ if (td.attrs.rowspan) {
+ cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + parseInt(td.attrs.rowspan));
+ if (!td.attrs.colspan) cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + 1);
+ for (var k = 1; k < td.attrs.rowspan; k++) map[(row + k) + '.' + col] = 1;
+ }
+ children.push(cell);
+ col++;
+ }
+ }
+ if (!colNum) {
+ colNum = col - 1;
+ attrs.style += `;grid-template-columns:repeat(${colNum},auto)`
+ }
+ col = 1;
+ row++;
+ }
+ node.children = children;
+ } else {
+ attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
+ if (border || padding)
+ (function f(ns) {
+ for (var i = 0, n; n = ns[i]; i++) {
+ if (n.name == 'th' || n.name == 'td') {
+ if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style || ''}`;
+ if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style || ''}`;
+ } else f(n.children || []);
+ }
+ })(childs)
+ }
+ if (this.options.autoscroll) {
+ var table = Object.assign({}, node);
+ node.name = 'div';
+ node.attrs = {
+ style: 'overflow:scroll'
+ }
+ node.children = [table];
+ }
+ }
+ this.CssHandler.pop && this.CssHandler.pop(node);
+ // 自动压缩
+ if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
+ siblings[len - 1] = childs[0];
+}
+// 状态机
+MpHtmlParser.prototype.Text = function(c) {
+ if (c == '<') {
+ var next = this.data[this.i + 1],
+ isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ if (isLetter(next)) {
+ this.setText();
+ this.start = this.i + 1;
+ this.state = this.TagName;
+ } else if (next == '/') {
+ this.setText();
+ if (isLetter(this.data[++this.i + 1])) {
+ this.start = this.i + 1;
+ this.state = this.EndTag;
+ } else this.Comment();
+ } else if (next == '!' || next == '?') {
+ this.setText();
+ this.Comment();
+ }
+ }
+}
+MpHtmlParser.prototype.Comment = function() {
+ var key;
+ if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
+ else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
+ else key = '>';
+ if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
+ else this.i += key.length - 1;
+ this.start = this.i + 1;
+ this.state = this.Text;
+}
+MpHtmlParser.prototype.TagName = function(c) {
+ if (blankChar[c]) {
+ this.tagName = this.section();
+ while (blankChar[this.data[this.i]]) this.i++;
+ if (this.isClose()) this.setNode();
+ else {
+ this.start = this.i;
+ this.state = this.AttrName;
+ }
+ } else if (this.isClose()) {
+ this.tagName = this.section();
+ this.setNode();
+ }
+}
+MpHtmlParser.prototype.AttrName = function(c) {
+ if (c == '=' || blankChar[c] || this.isClose()) {
+ this.attrName = this.section();
+ if (blankChar[c])
+ while (blankChar[this.data[++this.i]]);
+ if (this.data[this.i] == '=') {
+ while (blankChar[this.data[++this.i]]);
+ this.start = this.i--;
+ this.state = this.AttrValue;
+ } else this.setAttr();
+ }
+}
+MpHtmlParser.prototype.AttrValue = function(c) {
+ if (c == '"' || c == "'") {
+ this.start++;
+ if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
+ this.attrVal = this.section();
+ this.i++;
+ } else {
+ for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
+ this.attrVal = this.section();
+ }
+ this.setAttr();
+}
+MpHtmlParser.prototype.EndTag = function(c) {
+ if (blankChar[c] || c == '>' || c == '/') {
+ var name = this.section().toLowerCase();
+ for (var i = this.STACK.length; i--;)
+ if (this.STACK[i].name == name) break;
+ if (i != -1) {
+ var node;
+ while ((node = this.STACK.pop()).name != name) this.popNode(node);
+ this.popNode(node);
+ } else if (name == 'p' || name == 'br')
+ this.siblings().push({
+ name,
+ attrs: {}
+ });
+ this.i = this.data.indexOf('>', this.i);
+ this.start = this.i + 1;
+ if (this.i == -1) this.i = this.data.length;
+ else this.state = this.Text;
+ }
+}
+module.exports = MpHtmlParser;
diff --git a/uview-ui/components/u-parse/libs/config.js b/uview-ui/components/u-parse/libs/config.js
new file mode 100644
index 0000000..1f772e9
--- /dev/null
+++ b/uview-ui/components/u-parse/libs/config.js
@@ -0,0 +1,80 @@
+/* 配置文件 */
+var cfg = {
+ // 出错占位图
+ errorImg: null,
+ // 过滤器函数
+ filter: null,
+ // 代码高亮函数
+ highlight: null,
+ // 文本处理函数
+ onText: null,
+ // 实体编码列表
+ entities: {
+ quot: '"',
+ apos: "'",
+ semi: ';',
+ nbsp: '\xA0',
+ ensp: '\u2002',
+ emsp: '\u2003',
+ ndash: '–',
+ mdash: '—',
+ middot: '·',
+ lsquo: '‘',
+ rsquo: '’',
+ ldquo: '“',
+ rdquo: '”',
+ bull: '•',
+ hellip: '…'
+ },
+ blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
+ boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
+ // 块级标签,将被转为 div
+ blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
+ // 将被移除的标签
+ ignoreTags: makeMap('area,base,canvas,frame,iframe,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'),
+ // 只能被 rich-text 显示的标签
+ richOnlyTags: makeMap('a,colgroup,fieldset,legend'),
+ // 自闭合的标签
+ selfClosingTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
+ // 信任的标签
+ trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
+ // 默认的标签样式
+ userAgentStyles: {
+ address: 'font-style:italic',
+ big: 'display:inline;font-size:1.2em',
+ blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
+ caption: 'display:table-caption;text-align:center',
+ center: 'text-align:center',
+ cite: 'font-style:italic',
+ dd: 'margin-left:40px',
+ mark: 'background-color:yellow',
+ pre: 'font-family:monospace;white-space:pre;overflow:scroll',
+ s: 'text-decoration:line-through',
+ small: 'display:inline;font-size:0.8em',
+ u: 'text-decoration:underline'
+ }
+}
+
+function makeMap(str) {
+ var map = Object.create(null),
+ list = str.split(',');
+ for (var i = list.length; i--;)
+ map[list[i]] = true;
+ return map;
+}
+
+// #ifdef MP-WEIXIN
+if (wx.canIUse('editor')) {
+ cfg.blockTags.pre = void 0;
+ cfg.ignoreTags.rp = true;
+ Object.assign(cfg.richOnlyTags, makeMap('bdi,bdo,caption,rt,ruby'));
+ Object.assign(cfg.trustTags, makeMap('bdi,bdo,caption,pre,rt,ruby'));
+}
+// #endif
+
+// #ifdef APP-PLUS
+cfg.ignoreTags.iframe = void 0;
+Object.assign(cfg.trustTags, makeMap('embed,iframe'));
+// #endif
+
+module.exports = cfg;
diff --git a/uview-ui/components/u-parse/libs/handler.wxs b/uview-ui/components/u-parse/libs/handler.wxs
new file mode 100644
index 0000000..d3b1aaa
--- /dev/null
+++ b/uview-ui/components/u-parse/libs/handler.wxs
@@ -0,0 +1,22 @@
+var inline = {
+ abbr: 1,
+ b: 1,
+ big: 1,
+ code: 1,
+ del: 1,
+ em: 1,
+ i: 1,
+ ins: 1,
+ label: 1,
+ q: 1,
+ small: 1,
+ span: 1,
+ strong: 1,
+ sub: 1,
+ sup: 1
+}
+module.exports = {
+ use: function(item) {
+ return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
+ }
+}
diff --git a/uview-ui/components/u-parse/libs/trees.vue b/uview-ui/components/u-parse/libs/trees.vue
new file mode 100644
index 0000000..2b24820
--- /dev/null
+++ b/uview-ui/components/u-parse/libs/trees.vue
@@ -0,0 +1,505 @@
+<template>
+ <view :class="'interlayer '+(c||'')" :style="s">
+ <block v-for="(n, i) in nodes" v-bind:key="i">
+ <!--图片-->
+ <view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap.stop="imgtap">
+ <rich-text v-if="ctrl[i]!=0" :nodes="[{attrs:{src:loading&&(ctrl[i]||0)<2?loading:(lazyLoad&&!ctrl[i]?placeholder:(ctrl[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
+ <image class="_image" :src="lazyLoad&&!ctrl[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
+ :show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
+ @error="error" />
+ </view>
+ <!--文本-->
+ <text v-else-if="n.type=='text'" decode>{{n.text}}</text>
+ <!--#ifndef MP-BAIDU-->
+ <text v-else-if="n.name=='br'">\n</text>
+ <!--#endif-->
+ <!--视频-->
+ <view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&ctrl[i]==undefined" :id="n.attrs.id"
+ :class="'_video '+(n.attrs.class||'')" :style="n.attrs.style" :data-i="i" @tap.stop="_loadVideo" />
+ <video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||ctrl[i]==0"
+ :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[ctrl[i]||0]"
+ :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
+ <!--音频-->
+ <audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
+ :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
+ :src="n.attrs.source[ctrl[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio" @error.native="error"
+ @play.native="play" />
+ <!--链接-->
+ <view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
+ :data-attrs="n.attrs" @tap.stop="linkpress">
+ <trees class="_span" c="_span" :nodes="n.children" />
+ </view>
+ <!--广告-->
+ <!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
+ <!--列表-->
+ <view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex;flex-direction:row'">
+ <view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
+ <view v-else class="_ul-bef">
+ <view v-if="n.floor%3==0" class="_ul-p1">█</view>
+ <view v-else-if="n.floor%3==2" class="_ul-p2" />
+ <view v-else class="_ul-p1" style="border-radius:50%">█</view>
+ </view>
+ <trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
+ </view>
+ <!--表格-->
+ <view v-else-if="n.name=='table'&&n.c&&n.flag" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:grid'">
+ <trees v-for="(cell,n) in n.children" v-bind:key="n" :class="cell.attrs.class" :c="cell.attrs.class" :style="cell.attrs.style"
+ :s="cell.attrs.style" :nodes="cell.children" />
+ </view>
+ <view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
+ <view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
+ <view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
+ <trees v-if="tr.name=='td'" :nodes="tr.children" />
+ <trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
+ :s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
+ </view>
+ </view>
+ </view>
+ <!--#ifdef APP-PLUS-->
+ <iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
+ :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+ <embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+ <!--#endif-->
+ <!--富文本-->
+ <!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
+ <rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
+ <!--#endif-->
+ <!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
+ <rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
+ <!--#endif-->
+ <trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
+ :style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
+ </block>
+ </view>
+</template>
+<script module="handler" lang="wxs" src="./handler.wxs"></script>
+<script>
+ global.Parser = {};
+ import trees from './trees'
+ const errorImg = require('../libs/config.js').errorImg;
+ export default {
+ components: {
+ trees
+ },
+ name: 'trees',
+ data() {
+ return {
+ ctrl: [],
+ placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
+ errorImg,
+ loadVideo: typeof plus == 'undefined',
+ // #ifndef MP-ALIPAY
+ c: '',
+ s: ''
+ // #endif
+ }
+ },
+ props: {
+ nodes: Array,
+ lazyLoad: Boolean,
+ loading: String,
+ // #ifdef MP-ALIPAY
+ c: String,
+ s: String
+ // #endif
+ },
+ mounted() {
+ for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
+ this.init();
+ },
+ // #ifdef APP-PLUS
+ beforeDestroy() {
+ this.observer && this.observer.disconnect();
+ },
+ // #endif
+ methods: {
+ init() {
+ for (var i = this.nodes.length, n; n = this.nodes[--i];) {
+ if (n.name == 'img') {
+ this.top.imgList.setItem(n.attrs.i, n.attrs['original-src'] || n.attrs.src);
+ // #ifdef APP-PLUS
+ if (this.lazyLoad && !this.observer) {
+ this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+ top: 500,
+ bottom: 500
+ });
+ setTimeout(() => {
+ this.observer.observe('._img', res => {
+ if (res.intersectionRatio) {
+ for (var j = this.nodes.length; j--;)
+ if (this.nodes[j].name == 'img')
+ this.$set(this.ctrl, j, 1);
+ this.observer.disconnect();
+ }
+ })
+ }, 0)
+ }
+ // #endif
+ } else if (n.name == 'video' || n.name == 'audio') {
+ var ctx;
+ if (n.name == 'video') {
+ ctx = uni.createVideoContext(n.attrs.id
+ // #ifndef MP-BAIDU
+ , this
+ // #endif
+ );
+ } else if (this.$refs[n.attrs.id])
+ ctx = this.$refs[n.attrs.id][0];
+ if (ctx) {
+ ctx.id = n.attrs.id;
+ this.top.videoContexts.push(ctx);
+ }
+ }
+ }
+ // #ifdef APP-PLUS
+ // APP 上避免 video 错位需要延时渲染
+ setTimeout(() => {
+ this.loadVideo = true;
+ }, 1000)
+ // #endif
+ },
+ play(e) {
+ var contexts = this.top.videoContexts;
+ if (contexts.length > 1 && this.top.autopause)
+ for (var i = contexts.length; i--;)
+ if (contexts[i].id != e.currentTarget.dataset.id)
+ contexts[i].pause();
+ },
+ imgtap(e) {
+ var attrs = e.currentTarget.dataset.attrs;
+ if (!attrs.ignore) {
+ var preview = true,
+ data = {
+ id: e.target.id,
+ src: attrs.src,
+ ignore: () => preview = false
+ };
+ global.Parser.onImgtap && global.Parser.onImgtap(data);
+ this.top.$emit('imgtap', data);
+ if (preview) {
+ var urls = this.top.imgList,
+ current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
+ uni.previewImage({
+ current,
+ urls
+ })
+ }
+ }
+ },
+ loadImg(e) {
+ var i = e.currentTarget.dataset.i;
+ if (this.lazyLoad && !this.ctrl[i]) {
+ // #ifdef QUICKAPP-WEBVIEW
+ this.$set(this.ctrl, i, 0);
+ this.$nextTick(function() {
+ // #endif
+ // #ifndef APP-PLUS
+ this.$set(this.ctrl, i, 1);
+ // #endif
+ // #ifdef QUICKAPP-WEBVIEW
+ })
+ // #endif
+ } else if (this.loading && this.ctrl[i] != 2) {
+ // #ifdef QUICKAPP-WEBVIEW
+ this.$set(this.ctrl, i, 0);
+ this.$nextTick(function() {
+ // #endif
+ this.$set(this.ctrl, i, 2);
+ // #ifdef QUICKAPP-WEBVIEW
+ })
+ // #endif
+ }
+ },
+ linkpress(e) {
+ var jump = true,
+ attrs = e.currentTarget.dataset.attrs;
+ attrs.ignore = () => jump = false;
+ global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
+ this.top.$emit('linkpress', attrs);
+ if (jump) {
+ // #ifdef MP
+ if (attrs['app-id']) {
+ return uni.navigateToMiniProgram({
+ appId: attrs['app-id'],
+ path: attrs.path
+ })
+ }
+ // #endif
+ if (attrs.href) {
+ if (attrs.href[0] == '#') {
+ if (this.top.useAnchor)
+ this.top.navigateTo({
+ id: attrs.href.substring(1)
+ })
+ } else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
+ // #ifdef APP-PLUS
+ plus.runtime.openWeb(attrs.href);
+ // #endif
+ // #ifndef APP-PLUS
+ uni.setClipboardData({
+ data: attrs.href,
+ success: () =>
+ uni.showToast({
+ title: '链接已复制'
+ })
+ })
+ // #endif
+ } else
+ uni.navigateTo({
+ url: attrs.href,
+ fail() {
+ uni.switchTab({
+ url: attrs.href,
+ })
+ }
+ })
+ }
+ }
+ },
+ error(e) {
+ var target = e.currentTarget,
+ source = target.dataset.source,
+ i = target.dataset.i;
+ if (source == 'video' || source == 'audio') {
+ // 加载其他 source
+ var index = this.ctrl[i] ? this.ctrl[i].i + 1 : 1;
+ if (index < this.nodes[i].attrs.source.length)
+ this.$set(this.ctrl, i, index);
+ if (e.detail.__args__)
+ e.detail = e.detail.__args__[0];
+ } else if (errorImg && source == 'img') {
+ this.top.imgList.setItem(target.dataset.index, errorImg);
+ this.$set(this.ctrl, i, 3);
+ }
+ this.top && this.top.$emit('error', {
+ source,
+ target,
+ errMsg: e.detail.errMsg
+ });
+ },
+ _loadVideo(e) {
+ this.$set(this.ctrl, e.target.dataset.i, 0);
+ }
+ }
+ }
+</script>
+
+<style>
+ /* 在这里引入自定义样式 */
+
+ /* 链接和图片效果 */
+ ._a {
+ display: inline;
+ padding: 1.5px 0 1.5px 0;
+ color: #366092;
+ word-break: break-all;
+ }
+
+ ._hover {
+ text-decoration: underline;
+ opacity: 0.7;
+ }
+
+ ._img {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ /* #ifdef MP-WEIXIN */
+ :host {
+ display: inline;
+ }
+
+ /* #endif */
+
+ /* #ifndef MP-ALIPAY || APP-PLUS */
+ .interlayer {
+ display: inherit;
+ flex-direction: inherit;
+ flex-wrap: inherit;
+ align-content: inherit;
+ align-items: inherit;
+ justify-content: inherit;
+ width: 100%;
+ white-space: inherit;
+ }
+
+ /* #endif */
+
+ ._b,
+ ._strong {
+ font-weight: bold;
+ }
+
+ /* #ifndef MP-ALIPAY */
+ ._blockquote,
+ ._div,
+ ._p,
+ ._ol,
+ ._ul,
+ ._li {
+ display: block;
+ }
+
+ /* #endif */
+
+ ._code {
+ font-family: monospace;
+ }
+
+ ._del {
+ text-decoration: line-through;
+ }
+
+ ._em,
+ ._i {
+ font-style: italic;
+ }
+
+ ._h1 {
+ font-size: 2em;
+ }
+
+ ._h2 {
+ font-size: 1.5em;
+ }
+
+ ._h3 {
+ font-size: 1.17em;
+ }
+
+ ._h5 {
+ font-size: 0.83em;
+ }
+
+ ._h6 {
+ font-size: 0.67em;
+ }
+
+ ._h1,
+ ._h2,
+ ._h3,
+ ._h4,
+ ._h5,
+ ._h6 {
+ display: block;
+ font-weight: bold;
+ }
+
+ ._image {
+ display: block;
+ width: 100%;
+ height: 360px;
+ margin-top: -360px;
+ opacity: 0;
+ }
+
+ ._ins {
+ text-decoration: underline;
+ }
+
+ ._li {
+ flex: 1;
+ width: 0;
+ }
+
+ ._ol-bef {
+ width: 36px;
+ margin-right: 5px;
+ text-align: right;
+ }
+
+ ._ul-bef {
+ display: block;
+ margin: 0 12px 0 23px;
+ line-height: normal;
+ }
+
+ ._ol-bef,
+ ._ul-bef {
+ flex: none;
+ user-select: none;
+ }
+
+ ._ul-p1 {
+ display: inline-block;
+ width: 0.3em;
+ height: 0.3em;
+ overflow: hidden;
+ line-height: 0.3em;
+ }
+
+ ._ul-p2 {
+ display: inline-block;
+ width: 0.23em;
+ height: 0.23em;
+ border: 0.05em solid black;
+ border-radius: 50%;
+ }
+
+ ._q::before {
+ content: '"';
+ }
+
+ ._q::after {
+ content: '"';
+ }
+
+ ._sub {
+ font-size: smaller;
+ vertical-align: sub;
+ }
+
+ ._sup {
+ font-size: smaller;
+ vertical-align: super;
+ }
+
+ /* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
+ ._abbr,
+ ._b,
+ ._code,
+ ._del,
+ ._em,
+ ._i,
+ ._ins,
+ ._label,
+ ._q,
+ ._span,
+ ._strong,
+ ._sub,
+ ._sup {
+ display: inline;
+ }
+
+ /* #endif */
+
+ /* #ifdef MP-WEIXIN || MP-QQ */
+ .__bdo,
+ .__bdi,
+ .__ruby,
+ .__rt {
+ display: inline-block;
+ }
+
+ /* #endif */
+ ._video {
+ position: relative;
+ display: inline-block;
+ width: 300px;
+ height: 225px;
+ background-color: black;
+ }
+
+ ._video::after {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -15px 0 0 -15px;
+ content: '';
+ border-color: transparent transparent transparent white;
+ border-style: solid;
+ border-width: 15px 0 15px 30px;
+ }
+</style>
diff --git a/uview-ui/components/u-parse/u-parse.vue b/uview-ui/components/u-parse/u-parse.vue
new file mode 100644
index 0000000..57a105e
--- /dev/null
+++ b/uview-ui/components/u-parse/u-parse.vue
@@ -0,0 +1,645 @@
+<template>
+ <view>
+ <slot v-if="!nodes.length" />
+ <!--#ifdef APP-PLUS-NVUE-->
+ <web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
+ <!--#endif-->
+ <!--#ifndef APP-PLUS-NVUE-->
+ <view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
+ <!--#ifdef H5 || MP-360-->
+ <div :id="'rtf'+uid"></div>
+ <!--#endif-->
+ <!--#ifndef H5 || MP-360-->
+ <trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
+ <!--#endif-->
+ </view>
+ <!--#endif-->
+ </view>
+</template>
+
+<script>
+ var search;
+ // #ifndef H5 || APP-PLUS-NVUE || MP-360
+ import trees from './libs/trees';
+ var cache = {},
+ // #ifdef MP-WEIXIN || MP-TOUTIAO
+ fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
+ // #endif
+ Parser = require('./libs/MpHtmlParser.js');
+ var dom;
+ // 计算 cache 的 key
+ function hash(str) {
+ for (var i = str.length, val = 5381; i--;)
+ val += (val << 5) + str.charCodeAt(i);
+ return val;
+ }
+ // #endif
+ // #ifdef H5 || APP-PLUS-NVUE || MP-360
+ var {
+ windowWidth,
+ platform
+ } = uni.getSystemInfoSync(),
+ cfg = require('./libs/config.js');
+ // #endif
+ // #ifdef APP-PLUS-NVUE
+ var weexDom = weex.requireModule('dom');
+ // #endif
+ /**
+ * Parser 富文本组件
+ * @tutorial https://github.com/jin-yufeng/Parser
+ * @property {String} html 富文本数据
+ * @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
+ * @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
+ * @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
+ * @property {Number} compress 压缩等级
+ * @property {String} domain 图片、视频等链接的主域名
+ * @property {Boolean} lazyLoad 是否开启图片懒加载
+ * @property {String} loadingImg 图片加载完成前的占位图
+ * @property {Boolean} selectable 是否开启长按复制
+ * @property {Object} tagStyle 标签的默认样式
+ * @property {Boolean} showWithAnimation 是否使用渐显动画
+ * @property {Boolean} useAnchor 是否使用锚点
+ * @property {Boolean} useCache 是否缓存解析结果
+ * @event {Function} parse 解析完成事件
+ * @event {Function} load dom 加载完成事件
+ * @event {Function} ready 所有图片加载完毕事件
+ * @event {Function} error 错误事件
+ * @event {Function} imgtap 图片点击事件
+ * @event {Function} linkpress 链接点击事件
+ * @author JinYufeng
+ * @version 20201029
+ * @listens MIT
+ */
+ export default {
+ name: 'parser',
+ data() {
+ return {
+ // #ifdef H5 || MP-360
+ uid: this._uid,
+ // #endif
+ // #ifdef APP-PLUS-NVUE
+ height: 1,
+ // #endif
+ // #ifndef APP-PLUS-NVUE
+ showAm: '',
+ // #endif
+ nodes: []
+ }
+ },
+ // #ifndef H5 || APP-PLUS-NVUE || MP-360
+ components: {
+ trees
+ },
+ // #endif
+ props: {
+ html: String,
+ autopause: {
+ type: Boolean,
+ default: true
+ },
+ autoscroll: Boolean,
+ autosetTitle: {
+ type: Boolean,
+ default: true
+ },
+ // #ifndef H5 || APP-PLUS-NVUE || MP-360
+ compress: Number,
+ loadingImg: String,
+ useCache: Boolean,
+ // #endif
+ domain: String,
+ lazyLoad: Boolean,
+ selectable: Boolean,
+ tagStyle: Object,
+ showWithAnimation: Boolean,
+ useAnchor: Boolean
+ },
+ watch: {
+ html(html) {
+ this.setContent(html);
+ }
+ },
+ created() {
+ // 图片数组
+ this.imgList = [];
+ this.imgList.each = function(f) {
+ for (var i = 0, len = this.length; i < len; i++)
+ this.setItem(i, f(this[i], i, this));
+ }
+ this.imgList.setItem = function(i, src) {
+ if (i == void 0 || !src) return;
+ // #ifndef MP-ALIPAY || APP-PLUS
+ // 去重
+ if (src.indexOf('http') == 0 && this.includes(src)) {
+ var newSrc = src.split('://')[0];
+ for (var j = newSrc.length, c; c = src[j]; j++) {
+ if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
+ newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
+ }
+ newSrc += src.substr(j);
+ return this[i] = newSrc;
+ }
+ // #endif
+ this[i] = src;
+ // 暂存 data src
+ if (src.includes('data:image')) {
+ var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
+ if (!info) return;
+ // #ifdef MP-WEIXIN || MP-TOUTIAO
+ filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
+ fs && fs.writeFile({
+ filePath,
+ data: info[3],
+ encoding: info[2],
+ success: () => this[i] = filePath
+ })
+ // #endif
+ // #ifdef APP-PLUS
+ filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
+ var bitmap = new plus.nativeObj.Bitmap();
+ bitmap.loadBase64Data(src, () => {
+ bitmap.save(filePath, {}, () => {
+ bitmap.clear()
+ this[i] = filePath;
+ })
+ })
+ // #endif
+ }
+ }
+ },
+ mounted() {
+ // #ifdef H5 || MP-360
+ this.document = document.getElementById('rtf' + this._uid);
+ // #endif
+ // #ifndef H5 || APP-PLUS-NVUE || MP-360
+ if (dom) this.document = new dom(this);
+ // #endif
+ if (search) this.search = args => search(this, args);
+ // #ifdef APP-PLUS-NVUE
+ this.document = this.$refs.web;
+ setTimeout(() => {
+ // #endif
+ if (this.html) this.setContent(this.html);
+ // #ifdef APP-PLUS-NVUE
+ }, 30)
+ // #endif
+ },
+ beforeDestroy() {
+ // #ifdef H5 || MP-360
+ if (this._observer) this._observer.disconnect();
+ // #endif
+ this.imgList.each(src => {
+ // #ifdef APP-PLUS
+ if (src && src.includes('_doc')) {
+ plus.io.resolveLocalFileSystemURL(src, entry => {
+ entry.remove();
+ });
+ }
+ // #endif
+ // #ifdef MP-WEIXIN || MP-TOUTIAO
+ if (src && src.includes(uni.env.USER_DATA_PATH))
+ fs && fs.unlink({
+ filePath: src
+ })
+ // #endif
+ })
+ clearInterval(this._timer);
+ },
+ methods: {
+ // 设置富文本内容
+ setContent(html, append) {
+ // #ifdef APP-PLUS-NVUE
+ if (!html)
+ return this.height = 1;
+ if (append)
+ this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
+ "';document.getElementById('parser').appendChild(b)");
+ else {
+ html =
+ '<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
+ this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
+ '</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
+ (this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
+ 'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
+ if (platform == 'android') html = html.replace(/%/g, '%25');
+ this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
+ }
+ this.$refs.web.evalJs(
+ 'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
+ windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
+ 'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.getAttribute("original-src")||a.src||a.getAttribute("data-src")),a.onclick=function(t){t.stopPropagation(),e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(m){m.stopPropagation();var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
+ (this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
+ ';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
+ (this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
+ 'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
+ )
+ this.nodes = [1];
+ // #endif
+ // #ifdef H5 || MP-360
+ if (!html) {
+ if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
+ return;
+ }
+ var div = document.createElement('div');
+ if (!append) {
+ if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
+ this.rtf = div;
+ } else {
+ if (!this.rtf) this.rtf = div;
+ else this.rtf.appendChild(div);
+ }
+ div.innerHTML = this._handleHtml(html, append);
+ for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
+ style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
+ style.setAttribute('scoped', 'true');
+ }
+ // 懒加载
+ if (!this._observer && this.lazyLoad && IntersectionObserver) {
+ this._observer = new IntersectionObserver(changes => {
+ for (let item, i = 0; item = changes[i++];) {
+ if (item.isIntersecting) {
+ item.target.src = item.target.getAttribute('data-src');
+ item.target.removeAttribute('data-src');
+ this._observer.unobserve(item.target);
+ }
+ }
+ }, {
+ rootMargin: '500px 0px 500px 0px'
+ })
+ }
+ var _ts = this;
+ // 获取标题
+ var title = this.rtf.getElementsByTagName('title');
+ if (title.length && this.autosetTitle)
+ uni.setNavigationBarTitle({
+ title: title[0].innerText
+ })
+ // 填充 domain
+ var fill = target => {
+ var src = target.getAttribute('src');
+ if (this.domain && src) {
+ if (src[0] == '/') {
+ if (src[1] == '/')
+ target.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
+ else target.src = this.domain + src;
+ } else if (!src.includes('://') && src.indexOf('data:') != 0) target.src = this.domain + '/' + src;
+ }
+ }
+ // 图片处理
+ this.imgList.length = 0;
+ var imgs = this.rtf.getElementsByTagName('img');
+ for (let i = 0, j = 0, img; img = imgs[i]; i++) {
+ if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
+ img.style.height = 'auto';
+ fill(img);
+ if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
+ img.i = j++;
+ _ts.imgList.push(img.getAttribute('original-src') || img.src || img.getAttribute('data-src'));
+ img.onclick = function(e) {
+ e.stopPropagation();
+ var preview = true;
+ this.ignore = () => preview = false;
+ _ts.$emit('imgtap', this);
+ if (preview) {
+ uni.previewImage({
+ current: this.i,
+ urls: _ts.imgList
+ });
+ }
+ }
+ }
+ img.onerror = function() {
+ if (cfg.errorImg)
+ _ts.imgList[this.i] = this.src = cfg.errorImg;
+ _ts.$emit('error', {
+ source: 'img',
+ target: this
+ });
+ }
+ if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
+ img.setAttribute('data-src', img.src);
+ img.removeAttribute('src');
+ this._observer.observe(img);
+ }
+ }
+ // 链接处理
+ var links = this.rtf.getElementsByTagName('a');
+ for (var link of links) {
+ link.onclick = function(e) {
+ e.stopPropagation();
+ var jump = true,
+ href = this.getAttribute('href');
+ _ts.$emit('linkpress', {
+ href,
+ ignore: () => jump = false
+ });
+ if (jump && href) {
+ if (href[0] == '#') {
+ if (_ts.useAnchor) {
+ _ts.navigateTo({
+ id: href.substr(1)
+ })
+ }
+ } else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
+ return true;
+ else
+ uni.navigateTo({
+ url: href
+ })
+ }
+ return false;
+ }
+ }
+ // 视频处理
+ var videos = this.rtf.getElementsByTagName('video');
+ _ts.videoContexts = videos;
+ for (let video, i = 0; video = videos[i++];) {
+ fill(video);
+ video.style.maxWidth = '100%';
+ video.onerror = function() {
+ _ts.$emit('error', {
+ source: 'video',
+ target: this
+ });
+ }
+ video.onplay = function() {
+ if (_ts.autopause)
+ for (let item, i = 0; item = _ts.videoContexts[i++];)
+ if (item != this) item.pause();
+ }
+ }
+ // 音频处理
+ var audios = this.rtf.getElementsByTagName('audio');
+ for (var audio of audios) {
+ fill(audio);
+ audio.onerror = function() {
+ _ts.$emit('error', {
+ source: 'audio',
+ target: this
+ });
+ }
+ }
+ // 表格处理
+ if (this.autoscroll) {
+ var tables = this.rtf.getElementsByTagName('table');
+ for (var table of tables) {
+ let div = document.createElement('div');
+ div.style.overflow = 'scroll';
+ table.parentNode.replaceChild(div, table);
+ div.appendChild(table);
+ }
+ }
+ if (!append) this.document.appendChild(this.rtf);
+ this.$nextTick(() => {
+ this.nodes = [1];
+ this.$emit('load');
+ });
+ setTimeout(() => this.showAm = '', 500);
+ // #endif
+ // #ifndef APP-PLUS-NVUE
+ // #ifndef H5 || MP-360
+ var nodes;
+ if (!html) return this.nodes = [];
+ var parser = new Parser(html, this);
+ // 缓存读取
+ if (this.useCache) {
+ var hashVal = hash(html);
+ if (cache[hashVal])
+ nodes = cache[hashVal];
+ else {
+ nodes = parser.parse();
+ cache[hashVal] = nodes;
+ }
+ } else nodes = parser.parse();
+ this.$emit('parse', nodes);
+ if (append) this.nodes = this.nodes.concat(nodes);
+ else this.nodes = nodes;
+ if (nodes.length && nodes.title && this.autosetTitle)
+ uni.setNavigationBarTitle({
+ title: nodes.title
+ })
+ if (this.imgList) this.imgList.length = 0;
+ this.videoContexts = [];
+ this.$nextTick(() => {
+ (function f(cs) {
+ for (var i = cs.length; i--;) {
+ if (cs[i].top) {
+ cs[i].controls = [];
+ cs[i].init();
+ f(cs[i].$children);
+ }
+ }
+ })(this.$children)
+ this.$emit('load');
+ })
+ // #endif
+ var height;
+ clearInterval(this._timer);
+ this._timer = setInterval(() => {
+ // #ifdef H5 || MP-360
+ this.rect = this.rtf.getBoundingClientRect();
+ // #endif
+ // #ifndef H5 || MP-360
+ uni.createSelectorQuery().in(this)
+ .select('#_top').boundingClientRect().exec(res => {
+ if (!res) return;
+ this.rect = res[0];
+ // #endif
+ if (this.rect.height == height) {
+ this.$emit('ready', this.rect)
+ clearInterval(this._timer);
+ }
+ height = this.rect.height;
+ // #ifndef H5 || MP-360
+ });
+ // #endif
+ }, 350);
+ if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
+ // #endif
+ },
+ // 获取文本内容
+ getText(ns = this.nodes) {
+ var txt = '';
+ // #ifdef APP-PLUS-NVUE
+ txt = this._text;
+ // #endif
+ // #ifdef H5 || MP-360
+ txt = this.rtf.innerText;
+ // #endif
+ // #ifndef H5 || APP-PLUS-NVUE || MP-360
+ for (var i = 0, n; n = ns[i++];) {
+ if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>')
+ .replace(/&/g, '&');
+ else if (n.type == 'br') txt += '\n';
+ else {
+ // 块级标签前后加换行
+ var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
+ '0' && n.name[1] < '7');
+ if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
+ if (n.children) txt += this.getText(n.children);
+ if (block && txt[txt.length - 1] != '\n') txt += '\n';
+ else if (n.name == 'td' || n.name == 'th') txt += '\t';
+ }
+ }
+ // #endif
+ return txt;
+ },
+ // 锚点跳转
+ in (obj) {
+ if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
+ },
+ navigateTo(obj) {
+ if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
+ // #ifdef APP-PLUS-NVUE
+ if (!obj.id)
+ weexDom.scrollToElement(this.$refs.web);
+ else
+ this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
+ '");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
+ obj.success && obj.success();
+ // #endif
+ // #ifndef APP-PLUS-NVUE
+ var d = ' ';
+ // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+ d = '>>>';
+ // #endif
+ var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
+ '#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
+ if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
+ else selector.selectViewport().scrollOffset();
+ selector.exec(res => {
+ if (!res[0]) return obj.fail && obj.fail('Label not found')
+ var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
+ if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
+ else uni.pageScrollTo({
+ scrollTop,
+ duration: 300
+ })
+ obj.success && obj.success();
+ })
+ // #endif
+ },
+ // 获取视频对象
+ getVideoContext(id) {
+ // #ifndef APP-PLUS-NVUE
+ if (!id) return this.videoContexts;
+ else
+ for (var i = this.videoContexts.length; i--;)
+ if (this.videoContexts[i].id == id) return this.videoContexts[i];
+ // #endif
+ },
+ // #ifdef H5 || APP-PLUS-NVUE || MP-360
+ _handleHtml(html, append) {
+ if (!append) {
+ // 处理 tag-style 和 userAgentStyles
+ var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
+ for (var item in cfg.userAgentStyles)
+ style += `${item}{${cfg.userAgentStyles[item]}}`;
+ for (item in this.tagStyle)
+ style += `${item}{${this.tagStyle[item]}}`;
+ style += '</style>';
+ html = style + html;
+ }
+ // 处理 rpx
+ if (html.includes('rpx'))
+ html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
+ return html;
+ },
+ // #endif
+ // #ifdef APP-PLUS-NVUE
+ _message(e) {
+ // 接收 web-view 消息
+ var d = e.detail.data[0];
+ switch (d.action) {
+ case 'load':
+ this.$emit('load');
+ this.height = d.height;
+ this._text = d.text;
+ break;
+ case 'getTitle':
+ if (this.autosetTitle)
+ uni.setNavigationBarTitle({
+ title: d.title
+ })
+ break;
+ case 'getImgList':
+ this.imgList.length = 0;
+ for (var i = d.imgList.length; i--;)
+ this.imgList.setItem(i, d.imgList[i]);
+ break;
+ case 'preview':
+ var preview = true;
+ d.img.ignore = () => preview = false;
+ this.$emit('imgtap', d.img);
+ if (preview)
+ uni.previewImage({
+ current: d.img.i,
+ urls: this.imgList
+ })
+ break;
+ case 'linkpress':
+ var jump = true,
+ href = d.href;
+ this.$emit('linkpress', {
+ href,
+ ignore: () => jump = false
+ })
+ if (jump && href) {
+ if (href[0] == '#') {
+ if (this.useAnchor)
+ weexDom.scrollToElement(this.$refs.web, {
+ offset: d.offset
+ })
+ } else if (href.includes('://'))
+ plus.runtime.openWeb(href);
+ else
+ uni.navigateTo({
+ url: href
+ })
+ }
+ break;
+ case 'error':
+ if (d.source == 'img' && cfg.errorImg)
+ this.imgList.setItem(d.target.i, cfg.errorImg);
+ this.$emit('error', {
+ source: d.source,
+ target: d.target
+ })
+ break;
+ case 'ready':
+ this.height = d.height;
+ if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
+ this.rect = res[0];
+ this.$emit('ready', res[0]);
+ })
+ break;
+ case 'click':
+ this.$emit('click');
+ this.$emit('tap');
+ }
+ },
+ // #endif
+ }
+ }
+</script>
+
+<style>
+ @keyframes _show {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+ }
+
+ /* #ifdef MP-WEIXIN */
+ :host {
+ display: block;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ /* #endif */
+</style>
diff --git a/uview-ui/components/u-picker/u-picker.vue b/uview-ui/components/u-picker/u-picker.vue
new file mode 100644
index 0000000..68212a4
--- /dev/null
+++ b/uview-ui/components/u-picker/u-picker.vue
@@ -0,0 +1,676 @@
+<template>
+ <u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
+ <view class="u-datetime-picker">
+ <view class="u-picker-header" @touchmove.stop.prevent="">
+ <view class="u-btn-picker u-btn-picker--tips"
+ :style="{ color: cancelColor }"
+ hover-class="u-opacity"
+ :hover-stay-time="150"
+ @tap="getResult('cancel')"
+ >{{cancelText}}</view>
+ <view class="u-picker__title">{{ title }}</view>
+ <view
+ class="u-btn-picker u-btn-picker--primary"
+ :style="{ color: moving ? cancelColor : confirmColor }"
+ hover-class="u-opacity"
+ :hover-stay-time="150"
+ @touchmove.stop=""
+ @tap.stop="getResult('confirm')"
+ >
+ {{confirmText}}
+ </view>
+ </view>
+ <view class="u-picker-body">
+ <picker-view v-if="mode == 'region'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
+ <picker-view-column v-if="!reset && params.province">
+ <view class="u-column-item" v-for="(item, index) in provinces" :key="index">
+ <view class="u-line-1">{{ item.label }}</view>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.city">
+ <view class="u-column-item" v-for="(item, index) in citys" :key="index">
+ <view class="u-line-1">{{ item.label }}</view>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.area">
+ <view class="u-column-item" v-for="(item, index) in areas" :key="index">
+ <view class="u-line-1">{{ item.label }}</view>
+ </view>
+ </picker-view-column>
+ </picker-view>
+ <picker-view v-else-if="mode == 'time'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
+ <picker-view-column v-if="!reset && params.year">
+ <view class="u-column-item" v-for="(item, index) in years" :key="index">
+ {{ item }}
+ <text class="u-text" v-if="showTimeTag">年</text>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.month">
+ <view class="u-column-item" v-for="(item, index) in months" :key="index">
+ {{ formatNumber(item) }}
+ <text class="u-text" v-if="showTimeTag">月</text>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.day">
+ <view class="u-column-item" v-for="(item, index) in days" :key="index">
+ {{ formatNumber(item) }}
+ <text class="u-text" v-if="showTimeTag">日</text>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.hour">
+ <view class="u-column-item" v-for="(item, index) in hours" :key="index">
+ {{ formatNumber(item) }}
+ <text class="u-text" v-if="showTimeTag">时</text>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.minute">
+ <view class="u-column-item" v-for="(item, index) in minutes" :key="index">
+ {{ formatNumber(item) }}
+ <text class="u-text" v-if="showTimeTag">分</text>
+ </view>
+ </picker-view-column>
+ <picker-view-column v-if="!reset && params.second">
+ <view class="u-column-item" v-for="(item, index) in seconds" :key="index">
+ {{ formatNumber(item) }}
+ <text class="u-text" v-if="showTimeTag">秒</text>
+ </view>
+ </picker-view-column>
+ </picker-view>
+ <picker-view v-else-if="mode == 'selector'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
+ <picker-view-column v-if="!reset">
+ <view class="u-column-item" v-for="(item, index) in range" :key="index">
+ <view class="u-line-1">{{ getItemValue(item, 'selector') }}</view>
+ </view>
+ </picker-view-column>
+ </picker-view>
+ <picker-view v-else-if="mode == 'multiSelector'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
+ <picker-view-column v-if="!reset" v-for="(item, index) in range" :key="index">
+ <view class="u-column-item" v-for="(item1, index1) in item" :key="index1">
+ <view class="u-line-1">{{ getItemValue(item1, 'multiSelector') }}</view>
+ </view>
+ </picker-view-column>
+ </picker-view>
+ </view>
+ </view>
+ </u-popup>
+</template>
+
+<script>
+import provinces from '../../libs/util/province.js';
+import citys from '../../libs/util/city.js';
+import areas from '../../libs/util/area.js';
+
+/**
+ * picker picker弹出选择器
+ * @description 此选择器有两种弹出模式:一是时间模式,可以配置年,日,月,时,分,秒参数 二是地区模式,可以配置省,市,区参数
+ * @tutorial https://www.uviewui.com/components/picker.html
+ * @property {Object} params 需要显示的参数,见官网说明
+ * @property {String} mode 模式选择,region-地区类型,time-时间类型(默认time)
+ * @property {String Number} start-year 可选的开始年份,mode=time时有效(默认1950)
+ * @property {String Number} end-year 可选的结束年份,mode=time时有效(默认2050)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} show-time-tag 时间模式时,是否显示后面的年月日中文提示
+ * @property {String} cancel-color 取消按钮的颜色(默认#606266)
+ * @property {String} confirm-color 确认按钮的颜色(默认#2979ff)
+ * @property {String} default-time 默认选中的时间,mode=time时有效
+ * @property {String} confirm-text 确认按钮的文字
+ * @property {String} cancel-text 取消按钮的文字
+ * @property {String} default-region 默认选中的地区,中文形式,mode=region时有效
+ * @property {String} default-code 默认选中的地区,编号形式,mode=region时有效
+ * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
+ * @property {String Number} z-index 弹出时的z-index值(默认1075)
+ * @property {Array} default-selector 数组形式,其中每一项表示选择了range对应项中的第几个
+ * @property {Array} range 自定义选择的数据,mode=selector或mode=multiSelector时有效
+ * @property {String} range-key 当range参数的元素为对象时,指定Object中的哪个key的值作为选择器显示内容
+ * @event {Function} confirm 点击确定按钮,返回当前选择的值
+ * @event {Function} cancel 点击取消按钮,返回当前选择的值
+ * @example <u-picker v-model="show" mode="time"></u-picker>
+ */
+export default {
+ name: 'u-picker',
+ props: {
+ // picker中需要显示的参数
+ params: {
+ type: Object,
+ default() {
+ return {
+ year: true,
+ month: true,
+ day: true,
+ hour: false,
+ minute: false,
+ second: false,
+ province: true,
+ city: true,
+ area: true,
+ timestamp: true,
+ };
+ }
+ },
+ // 当mode=selector或者mode=multiSelector时,提供的数组
+ range: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 当mode=selector或者mode=multiSelector时,提供的默认选中的下标
+ defaultSelector: {
+ type: Array,
+ default() {
+ return [0];
+ }
+ },
+ // 当 range 是一个 Array<Object> 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
+ rangeKey: {
+ type: String,
+ default: ''
+ },
+ // 模式选择,region-地区类型,time-时间类型,selector-单列模式,multiSelector-多列模式
+ mode: {
+ type: String,
+ default: 'time'
+ },
+ // 年份开始时间
+ startYear: {
+ type: [String, Number],
+ default: 1950
+ },
+ // 年份结束时间
+ endYear: {
+ type: [String, Number],
+ default: 2050
+ },
+ // "取消"按钮的颜色
+ cancelColor: {
+ type: String,
+ default: '#606266'
+ },
+ // "确定"按钮的颜色
+ confirmColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 默认显示的时间,2025-07-02 || 2025-07-02 13:01:00 || 2025/07/02
+ defaultTime: {
+ type: String,
+ default: ''
+ },
+ // 默认显示的地区,可传类似["河北省", "秦皇岛市", "北戴河区"]
+ defaultRegion: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 时间模式时,是否显示后面的年月日中文提示
+ showTimeTag: {
+ type: Boolean,
+ default: true
+ },
+ // 默认显示地区的编码,defaultRegion和areaCode同时存在,areaCode优先,可传类似["13", "1303", "130304"]
+ areaCode: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 是否允许通过点击遮罩关闭Picker
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 弹出的z-index值
+ zIndex: {
+ type: [String, Number],
+ default: 0
+ },
+ // 顶部标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 取消按钮的文字
+ cancelText: {
+ type: String,
+ default: '取消'
+ },
+ // 确认按钮的文字
+ confirmText: {
+ type: String,
+ default: '确认'
+ }
+ },
+ data() {
+ return {
+ years: [],
+ months: [],
+ days: [],
+ hours: [],
+ minutes: [],
+ seconds: [],
+ year: 0,
+ month: 0,
+ day: 0,
+ hour: 0,
+ minute: 0,
+ second: 0,
+ reset: false,
+ startDate: '',
+ endDate: '',
+ valueArr: [],
+ provinces: provinces,
+ citys: citys[0],
+ areas: areas[0][0],
+ province: 0,
+ city: 0,
+ area: 0,
+ moving: false // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
+ };
+ },
+ mounted() {
+ this.init();
+ },
+ computed: {
+ propsChange() {
+ // 引用这几个变量,是为了监听其变化
+ return `${this.mode}-${this.defaultTime}-${this.startYear}-${this.endYear}-${this.defaultRegion}-${this.areaCode}`;
+ },
+ regionChange() {
+ // 引用这几个变量,是为了监听其变化
+ return `${this.province}-${this.city}`;
+ },
+ yearAndMonth() {
+ return `${this.year}-${this.month}`;
+ },
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ watch: {
+ propsChange() {
+ this.reset = true;
+ setTimeout(() => this.init(), 10);
+ },
+ // 如果地区发生变化,为了让picker联动起来,必须重置this.citys和this.areas
+ regionChange(val) {
+ this.citys = citys[this.province];
+ this.areas = areas[this.province][this.city];
+ },
+ // watch监听月份的变化,实时变更日的天数,因为不同月份,天数不一样
+ // 一个月可能有30,31天,甚至闰年2月的29天,平年2月28天
+ yearAndMonth(val) {
+ if (this.params.year) this.setDays();
+ },
+ // 微信和QQ小程序由于一些奇怪的原因(故同时对所有平台均初始化一遍),需要重新初始化才能显示正确的值
+ value(n) {
+ if (n) {
+ this.reset = true;
+ setTimeout(() => this.init(), 10);
+ }
+ }
+ },
+ methods: {
+ // 标识滑动开始,只有微信小程序才有这样的事件
+ pickstart() {
+ // #ifdef MP-WEIXIN
+ this.moving = true;
+ // #endif
+ },
+ // 标识滑动结束
+ pickend() {
+ // #ifdef MP-WEIXIN
+ this.moving = false;
+ // #endif
+ },
+ // 对单列和多列形式的判断是否有传入变量的情况
+ getItemValue(item, mode) {
+ // 目前(2020-05-25)uni-app对微信小程序编译有错误,导致v-if为false中的内容也执行,错误导致
+ // 单列模式或者多列模式中的getItemValue同时被执行,故在这里再加一层判断
+ if (this.mode == mode) {
+ return typeof item == 'object' ? item[this.rangeKey] : item;
+ }
+ },
+ // 小于10前面补0,用于月份,日期,时分秒等
+ formatNumber(num) {
+ return +num < 10 ? '0' + num : String(num);
+ },
+ // 生成递进的数组
+ generateArray: function(start, end) {
+ // 转为数值格式,否则用户给end-year等传递字符串值时,下面的end+1会导致字符串拼接,而不是相加
+ start = Number(start);
+ end = Number(end);
+ end = end > start ? end : start;
+ // 生成数组,获取其中的索引,并剪出来
+ return [...Array(end + 1).keys()].slice(start);
+ },
+ getIndex: function(arr, val) {
+ let index = arr.indexOf(val);
+ // 如果index为-1(即找不到index值),~(-1)=-(-1)-1=0,导致条件不成立
+ return ~index ? index : 0;
+ },
+ //日期时间处理
+ initTimeValue() {
+ // 格式化时间,在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
+ let fdate = this.defaultTime.replace(/\-/g, '/');
+ fdate = fdate && fdate.indexOf('/') == -1 ? `2020/01/01 ${fdate}` : fdate;
+ let time = null;
+ if (fdate) time = new Date(fdate);
+ else time = new Date();
+ // 获取年日月时分秒
+ this.year = time.getFullYear();
+ this.month = Number(time.getMonth()) + 1;
+ this.day = time.getDate();
+ this.hour = time.getHours();
+ this.minute = time.getMinutes();
+ this.second = time.getSeconds();
+ },
+ init() {
+ this.valueArr = [];
+ this.reset = false;
+ if (this.mode == 'time') {
+ this.initTimeValue();
+ if (this.params.year) {
+ this.valueArr.push(0);
+ this.setYears();
+ }
+ if (this.params.month) {
+ this.valueArr.push(0);
+ this.setMonths();
+ }
+ if (this.params.day) {
+ this.valueArr.push(0);
+ this.setDays();
+ }
+ if (this.params.hour) {
+ this.valueArr.push(0);
+ this.setHours();
+ }
+ if (this.params.minute) {
+ this.valueArr.push(0);
+ this.setMinutes();
+ }
+ if (this.params.second) {
+ this.valueArr.push(0);
+ this.setSeconds();
+ }
+ } else if (this.mode == 'region') {
+ if (this.params.province) {
+ this.valueArr.push(0);
+ this.setProvinces();
+ }
+ if (this.params.city) {
+ this.valueArr.push(0);
+ this.setCitys();
+ }
+ if (this.params.area) {
+ this.valueArr.push(0);
+ this.setAreas();
+ }
+ } else if (this.mode == 'selector') {
+ this.valueArr = this.defaultSelector;
+ } else if (this.mode == 'multiSelector') {
+ this.valueArr = this.defaultSelector;
+ this.multiSelectorValue = this.defaultSelector;
+ }
+ this.$forceUpdate();
+ },
+ // 设置picker的某一列值
+ setYears() {
+ // 获取年份集合
+ this.years = this.generateArray(this.startYear, this.endYear);
+ // 设置this.valueArr某一项的值,是为了让picker预选中某一个值
+ this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.years, this.year));
+ },
+ setMonths() {
+ this.months = this.generateArray(1, 12);
+ this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.months, this.month));
+ },
+ setDays() {
+ let totalDays = new Date(this.year, this.month, 0).getDate();
+ this.days = this.generateArray(1, totalDays);
+ let index = 0;
+ // 这里不能使用类似setMonths()中的this.valueArr.splice(this.valueArr.length - 1, xxx)做法
+ // 因为this.month和this.year变化时,会触发watch中的this.setDays(),导致this.valueArr.length计算有误
+ if (this.params.year && this.params.month) index = 2;
+ else if (this.params.month) index = 1;
+ else if (this.params.year) index = 1;
+ else index = 0;
+ // 当月份变化时,会导致日期的天数也会变化,如果原来选的天数大于变化后的天数,则重置为变化后的最大值
+ // 比如原来选中3月31日,调整为2月后,日期变为最大29,这时如果day值继续为31显然不合理,于是将其置为29(picker-column从1开始)
+ if(this.day > this.days.length) this.day = this.days.length;
+ this.valueArr.splice(index, 1, this.getIndex(this.days, this.day));
+ },
+ setHours() {
+ this.hours = this.generateArray(0, 23);
+ this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.hours, this.hour));
+ },
+ setMinutes() {
+ this.minutes = this.generateArray(0, 59);
+ this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.minutes, this.minute));
+ },
+ setSeconds() {
+ this.seconds = this.generateArray(0, 59);
+ this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.seconds, this.second));
+ },
+ setProvinces() {
+ // 判断是否需要province参数
+ if (!this.params.province) return;
+ let tmp = '';
+ let useCode = false;
+ // 如果同时配置了defaultRegion和areaCode,优先使用areaCode参数
+ if (this.areaCode.length) {
+ tmp = this.areaCode[0];
+ useCode = true;
+ } else if (this.defaultRegion.length) tmp = this.defaultRegion[0];
+ else tmp = 0;
+ // 历遍省份数组匹配
+ provinces.map((v, k) => {
+ if (useCode ? v.value == tmp : v.label == tmp) {
+ tmp = k;
+ }
+ });
+ this.province = tmp;
+ this.provinces = provinces;
+ // 设置默认省份的值
+ this.valueArr.splice(0, 1, this.province);
+ },
+ setCitys() {
+ if (!this.params.city) return;
+ let tmp = '';
+ let useCode = false;
+ if (this.areaCode.length) {
+ tmp = this.areaCode[1];
+ useCode = true;
+ } else if (this.defaultRegion.length) tmp = this.defaultRegion[1];
+ else tmp = 0;
+ citys[this.province].map((v, k) => {
+ if (useCode ? v.value == tmp : v.label == tmp) {
+ tmp = k;
+ }
+ });
+ this.city = tmp;
+ this.citys = citys[this.province];
+ this.valueArr.splice(1, 1, this.city);
+ },
+ setAreas() {
+ if (!this.params.area) return;
+ let tmp = '';
+ let useCode = false;
+ if (this.areaCode.length) {
+ tmp = this.areaCode[2];
+ useCode = true;
+ } else if (this.defaultRegion.length) tmp = this.defaultRegion[2];
+ else tmp = 0;
+ areas[this.province][this.city].map((v, k) => {
+ if (useCode ? v.value == tmp : v.label == tmp) {
+ tmp = k;
+ }
+ });
+ this.area = tmp;
+ this.areas = areas[this.province][this.city];
+ this.valueArr.splice(2, 1, this.area);
+ },
+ close() {
+ this.$emit('input', false);
+ },
+ // 用户更改picker的列选项
+ change(e) {
+ this.valueArr = e.detail.value;
+ let i = 0;
+ if (this.mode == 'time') {
+ // 这里使用i++,是因为this.valueArr数组的长度是不确定长度的,它根据this.params的值来配置长度
+ // 进入if规则,i会加1,保证了能获取准确的值
+ if (this.params.year) this.year = this.years[this.valueArr[i++]];
+ if (this.params.month) this.month = this.months[this.valueArr[i++]];
+ if (this.params.day) this.day = this.days[this.valueArr[i++]];
+ if (this.params.hour) this.hour = this.hours[this.valueArr[i++]];
+ if (this.params.minute) this.minute = this.minutes[this.valueArr[i++]];
+ if (this.params.second) this.second = this.seconds[this.valueArr[i++]];
+ } else if (this.mode == 'region') {
+ if (this.params.province) this.province = this.valueArr[i++];
+ if (this.params.city) this.city = this.valueArr[i++];
+ if (this.params.area) this.area = this.valueArr[i++];
+ } else if (this.mode == 'multiSelector') {
+ let index = null;
+ // 对比前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
+ this.defaultSelector.map((val, idx) => {
+ if (val != e.detail.value[idx]) index = idx;
+ });
+ // 为了让用户对多列变化时,对动态设置其他列的变更
+ if (index != null) {
+ this.$emit('columnchange', {
+ column: index,
+ index: e.detail.value[index]
+ });
+ }
+ }
+ },
+ // 用户点击确定按钮
+ getResult(event = null) {
+ // #ifdef MP-WEIXIN
+ if (this.moving) return;
+ // #endif
+ let result = {};
+ // 只返回用户在this.params中配置了为true的字段
+ if (this.mode == 'time') {
+ if (this.params.year) result.year = this.formatNumber(this.year || 0);
+ if (this.params.month) result.month = this.formatNumber(this.month || 0);
+ if (this.params.day) result.day = this.formatNumber(this.day || 0);
+ if (this.params.hour) result.hour = this.formatNumber(this.hour || 0);
+ if (this.params.minute) result.minute = this.formatNumber(this.minute || 0);
+ if (this.params.second) result.second = this.formatNumber(this.second || 0);
+ if (this.params.timestamp) result.timestamp = this.getTimestamp();
+ } else if (this.mode == 'region') {
+ if (this.params.province) result.province = provinces[this.province];
+ if (this.params.city) result.city = citys[this.province][this.city];
+ if (this.params.area) result.area = areas[this.province][this.city][this.area];
+ } else if (this.mode == 'selector') {
+ result = this.valueArr;
+ } else if (this.mode == 'multiSelector') {
+ result = this.valueArr;
+ }
+ if (event) this.$emit(event, result);
+ this.close();
+ },
+ // 获取时间戳
+ getTimestamp() {
+ // yyyy-mm-dd为安卓写法,不支持iOS,需要使用"/"分隔,才能二者兼容
+ let time = this.year + '/' + this.month + '/' + this.day + ' ' + this.hour + ':' + this.minute + ':' + this.second;
+ return new Date(time).getTime() / 1000;
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import '../../libs/css/style.components.scss';
+
+.u-datetime-picker {
+ position: relative;
+ z-index: 999;
+}
+
+.u-picker-view {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.u-picker-header {
+ width: 100%;
+ height: 90rpx;
+ padding: 0 40rpx;
+ @include vue-flex;
+ justify-content: space-between;
+ align-items: center;
+ box-sizing: border-box;
+ font-size: 30rpx;
+ background: #fff;
+ position: relative;
+}
+
+.u-picker-header::after {
+ content: '';
+ position: absolute;
+ border-bottom: 1rpx solid #eaeef1;
+ -webkit-transform: scaleY(0.5);
+ transform: scaleY(0.5);
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+.u-picker__title {
+ color: $u-content-color;
+}
+
+.u-picker-body {
+ width: 100%;
+ height: 500rpx;
+ overflow: hidden;
+ background-color: #fff;
+}
+
+.u-column-item {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: $u-main-color;
+ padding: 0 8rpx;
+}
+
+.u-text {
+ font-size: 24rpx;
+ padding-left: 8rpx;
+}
+
+.u-btn-picker {
+ padding: 16rpx;
+ box-sizing: border-box;
+ text-align: center;
+ text-decoration: none;
+}
+
+.u-opacity {
+ opacity: 0.5;
+}
+
+.u-btn-picker--primary {
+ color: $u-type-primary;
+}
+
+.u-btn-picker--tips {
+ color: $u-tips-color;
+}
+</style>
diff --git a/uview-ui/components/u-popup/u-popup.vue b/uview-ui/components/u-popup/u-popup.vue
new file mode 100644
index 0000000..69c0ec8
--- /dev/null
+++ b/uview-ui/components/u-popup/u-popup.vue
@@ -0,0 +1,456 @@
+<template>
+ <view v-if="visibleSync" :style="[customStyle, {
+ zIndex: uZindex - 1
+ }]" class="u-drawer" hover-stop-propagation>
+ <u-mask :duration="duration" :custom-style="maskCustomStyle" :maskClickAble="maskCloseAble" :z-index="uZindex - 2" :show="showDrawer && mask" @click="maskClick"></u-mask>
+ <view
+ class="u-drawer-content"
+ @tap="modeCenterClose(mode)"
+ :class="[
+ safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
+ 'u-drawer-' + mode,
+ showDrawer ? 'u-drawer-content-visible' : '',
+ zoom && mode == 'center' ? 'u-animation-zoom' : ''
+ ]"
+ @touchmove.stop.prevent
+ @tap.stop.prevent
+ :style="[style]"
+ >
+ <view class="u-mode-center-box" @tap.stop.prevent @touchmove.stop.prevent v-if="mode == 'center'" :style="[centerStyle]">
+ <u-icon
+ @click="close"
+ v-if="closeable"
+ class="u-close"
+ :class="['u-close--' + closeIconPos]"
+ :name="closeIcon"
+ :color="closeIconColor"
+ :size="closeIconSize"
+ ></u-icon>
+ <scroll-view class="u-drawer__scroll-view" scroll-y="true">
+ <slot />
+ </scroll-view>
+ </view>
+ <scroll-view class="u-drawer__scroll-view" scroll-y="true" v-else>
+ <slot />
+ </scroll-view>
+ <view @tap="close" class="u-close" :class="['u-close--' + closeIconPos]">
+ <u-icon
+ v-if="mode != 'center' && closeable"
+ :name="closeIcon"
+ :color="closeIconColor"
+ :size="closeIconSize"
+ ></u-icon>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * popup 弹窗
+ * @description 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义
+ * @tutorial https://www.uviewui.com/components/popup.html
+ * @property {String} mode 弹出方向(默认left)
+ * @property {Boolean} mask 是否显示遮罩(默认true)
+ * @property {Stringr | Number} length mode=left | 见官网说明(默认auto)
+ * @property {Boolean} zoom 是否开启缩放动画,只在mode为center时有效(默认true)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层(默认true)
+ * @property {Object} custom-style 用户自定义样式
+ * @property {Stringr | Number} negative-top 中部弹出时,往上偏移的值
+ * @property {Numberr | String} border-radius 弹窗圆角值(默认0)
+ * @property {Numberr | String} z-index 弹出内容的z-index值(默认1075)
+ * @property {Boolean} closeable 是否显示关闭图标(默认false)
+ * @property {String} close-icon 关闭图标的名称,只能uView的内置图标
+ * @property {String} close-icon-pos 自定义关闭图标位置(默认top-right)
+ * @property {String} close-icon-color 关闭图标的颜色(默认#909399)
+ * @property {Number | String} close-icon-size 关闭图标的大小,单位rpx(默认30)
+ * @event {Function} open 弹出层打开
+ * @event {Function} close 弹出层收起
+ * @example <u-popup v-model="show"><view>出淤泥而不染,濯清涟而不妖</view></u-popup>
+ */
+export default {
+ name: 'u-popup',
+ props: {
+ /**
+ * 显示状态
+ */
+ show: {
+ type: Boolean,
+ default: false
+ },
+ /**
+ * 弹出方向,left|right|top|bottom|center
+ */
+ mode: {
+ type: String,
+ default: 'left'
+ },
+ /**
+ * 是否显示遮罩
+ */
+ mask: {
+ type: Boolean,
+ default: true
+ },
+ // 抽屉的宽度(mode=left|right),或者高度(mode=top|bottom),单位rpx,或者"auto"
+ // 或者百分比"50%",表示由内容撑开高度或者宽度
+ length: {
+ type: [Number, String],
+ default: 'auto'
+ },
+ // 是否开启缩放动画,只在mode=center时有效
+ zoom: {
+ type: Boolean,
+ default: true
+ },
+ // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 是否可以通过点击遮罩进行关闭
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 用户自定义样式
+ customStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
+ // 对v-model双向绑定多层调用造成报错不能修改props值的问题
+ popup: {
+ type: Boolean,
+ default: true
+ },
+ // 显示显示弹窗的圆角,单位rpx
+ borderRadius: {
+ type: [Number, String],
+ default: 0
+ },
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ // 是否显示关闭图标
+ closeable: {
+ type: Boolean,
+ default: false
+ },
+ // 关闭图标的名称,只能uView的内置图标
+ closeIcon: {
+ type: String,
+ default: 'close'
+ },
+ // 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
+ closeIconPos: {
+ type: String,
+ default: 'top-right'
+ },
+ // 关闭图标的颜色
+ closeIconColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 关闭图标的大小,单位rpx
+ closeIconSize: {
+ type: [String, Number],
+ default: '30'
+ },
+ // 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto"
+ // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
+ width: {
+ type: String,
+ default: ''
+ },
+ // 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto"
+ // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
+ height: {
+ type: String,
+ default: ''
+ },
+ // 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
+ negativeTop: {
+ type: [String, Number],
+ default: 0
+ },
+ // 遮罩的样式,一般用于修改遮罩的透明度
+ maskCustomStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 遮罩打开或收起的动画过渡时间,单位ms
+ duration: {
+ type: [String, Number],
+ default: 250
+ }
+ },
+ data() {
+ return {
+ visibleSync: false,
+ showDrawer: false,
+ timer: null,
+ closeFromInner: false, // value的值改变,是发生在内部还是外部
+ };
+ },
+ computed: {
+ // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
+ style() {
+ let style = {};
+ // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
+ if (this.mode == 'left' || this.mode == 'right') {
+ style = {
+ width: this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length),
+ height: '100%',
+ transform: `translate3D(${this.mode == 'left' ? '-100%' : '100%'},0px,0px)`
+ };
+ } else if (this.mode == 'top' || this.mode == 'bottom') {
+ style = {
+ width: '100%',
+ height: this.height ? this.getUnitValue(this.height) : this.getUnitValue(this.length),
+ transform: `translate3D(0px,${this.mode == 'top' ? '-100%' : '100%'},0px)`
+ };
+ }
+ style.zIndex = this.uZindex;
+ // 如果用户设置了borderRadius值,添加弹窗的圆角
+ if (this.borderRadius) {
+ switch (this.mode) {
+ case 'left':
+ style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`;
+ break;
+ case 'top':
+ style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`;
+ break;
+ case 'right':
+ style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`;
+ break;
+ case 'bottom':
+ style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`;
+ break;
+ default:
+ }
+ // 不加可能圆角无效
+ style.overflow = 'hidden';
+ }
+ if(this.duration) style.transition = `all ${this.duration / 1000}s linear`;
+ return style;
+ },
+ // 中部弹窗的特有样式
+ centerStyle() {
+ let style = {};
+ style.width = this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length);
+ // 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开高度
+ style.height = this.height ? this.getUnitValue(this.height) : 'auto';
+ style.zIndex = this.uZindex;
+ style.marginTop = `-${this.$u.addUnit(this.negativeTop)}`;
+ if (this.borderRadius) {
+ style.borderRadius = `${this.borderRadius}rpx`;
+ // 不加可能圆角无效
+ style.overflow = 'hidden';
+ }
+ return style;
+ },
+ // 计算整理后的z-index值
+ uZindex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ }
+ },
+ watch: {
+ value(val) {
+ if (val) {
+ this.open();
+ } else if(!this.closeFromInner) {
+ this.close();
+ }
+ this.closeFromInner = false;
+ }
+ },
+ mounted() {
+ // 组件渲染完成时,检查value是否为true,如果是,弹出popup
+ this.value && this.open();
+ },
+ methods: {
+ // 判断传入的值,是否带有单位,如果没有,就默认用rpx单位
+ getUnitValue(val) {
+ if(/(%|px|rpx|auto)$/.test(val)) return val;
+ else return val + 'rpx'
+ },
+ // 遮罩被点击
+ maskClick() {
+ this.close();
+ },
+ close() {
+ // 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
+ // 造成@close事件触发两次
+ this.closeFromInner = true;
+ this.change('showDrawer', 'visibleSync', false);
+ },
+ // 中部弹出时,需要.u-drawer-content将居中内容,此元素会铺满屏幕,点击需要关闭弹窗
+ // 让其只在mode=center时起作用
+ modeCenterClose(mode) {
+ if (mode != 'center' || !this.maskCloseAble) return;
+ this.close();
+ },
+ open() {
+ this.change('visibleSync', 'showDrawer', true);
+ },
+ // 此处的原理是,关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
+ // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
+ change(param1, param2, status) {
+ // 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件
+ if (this.popup == true) {
+ this.$emit('input', status);
+ }
+ this[param1] = status;
+ if(status) {
+ // #ifdef H5 || MP
+ this.timer = setTimeout(() => {
+ this[param2] = status;
+ this.$emit(status ? 'open' : 'close');
+ }, 50);
+ // #endif
+ // #ifndef H5 || MP
+ this.$nextTick(() => {
+ this[param2] = status;
+ this.$emit(status ? 'open' : 'close');
+ })
+ // #endif
+ } else {
+ this.timer = setTimeout(() => {
+ this[param2] = status;
+ this.$emit(status ? 'open' : 'close');
+ }, this.duration);
+ }
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-drawer {
+ /* #ifndef APP-NVUE */
+ display: block;
+ /* #endif */
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+}
+
+.u-drawer-content {
+ /* #ifndef APP-NVUE */
+ display: block;
+ /* #endif */
+ position: absolute;
+ z-index: 1003;
+ transition: all 0.25s linear;
+}
+
+.u-drawer__scroll-view {
+ width: 100%;
+ height: 100%;
+}
+
+.u-drawer-left {
+ top: 0;
+ bottom: 0;
+ left: 0;
+ background-color: #ffffff;
+}
+
+.u-drawer-right {
+ right: 0;
+ top: 0;
+ bottom: 0;
+ background-color: #ffffff;
+}
+
+.u-drawer-top {
+ top: 0;
+ left: 0;
+ right: 0;
+ background-color: #ffffff;
+}
+
+.u-drawer-bottom {
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: #ffffff;
+}
+
+.u-drawer-center {
+ @include vue-flex;
+ flex-direction: column;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ z-index: 99999;
+}
+
+.u-mode-center-box {
+ min-width: 100rpx;
+ min-height: 100rpx;
+ /* #ifndef APP-NVUE */
+ display: block;
+ /* #endif */
+ position: relative;
+ background-color: #ffffff;
+}
+
+.u-drawer-content-visible.u-drawer-center {
+ transform: scale(1);
+ opacity: 1;
+}
+
+.u-animation-zoom {
+ transform: scale(1.15);
+}
+
+.u-drawer-content-visible {
+ transform: translate3D(0px, 0px, 0px) !important;
+}
+
+.u-close {
+ position: absolute;
+ z-index: 3;
+}
+
+.u-close--top-left {
+ top: 30rpx;
+ left: 30rpx;
+}
+
+.u-close--top-right {
+ top: 30rpx;
+ right: 30rpx;
+}
+
+.u-close--bottom-left {
+ bottom: 30rpx;
+ left: 30rpx;
+}
+
+.u-close--bottom-right {
+ right: 30rpx;
+ bottom: 30rpx;
+}
+</style>
diff --git a/uview-ui/components/u-radio-group/u-radio-group.vue b/uview-ui/components/u-radio-group/u-radio-group.vue
new file mode 100644
index 0000000..2172eb2
--- /dev/null
+++ b/uview-ui/components/u-radio-group/u-radio-group.vue
@@ -0,0 +1,128 @@
+<template>
+ <view class="u-radio-group u-clearfix">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ import Emitter from '../../libs/util/emitter.js';
+ /**
+ * radioRroup 单选框父组件
+ * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio使用
+ * @tutorial https://www.uviewui.com/components/radio.html
+ * @property {Boolean} disabled 是否禁用所有radio(默认false)
+ * @property {String Number} size 组件整体的大小,单位rpx(默认40)
+ * @property {String} active-color 选中时的颜色,应用到所有子Radio组件(默认#2979ff)
+ * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+ * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
+ * @property {String} width 宽度,需带单位
+ * @property {Boolean} wrap 是否每个radio都换行(默认false)
+ * @event {Function} change 任一个radio状态发生变化时触发
+ * @example <u-radio-group v-model="value"></u-radio-group>
+ */
+ export default {
+ name: "u-radio-group",
+ mixins: [Emitter],
+ props: {
+ // 是否禁用所有单选框
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 匹配某一个radio组件,如果某个radio的name值等于此值,那么这个radio就被会选中
+ value: {
+ type: [String, Number],
+ default: ''
+ },
+ // 选中状态下的颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 组件的整体大小
+ size: {
+ type: [String, Number],
+ default: 34
+ },
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ type: Boolean,
+ default: false
+ },
+ // 形状,square为方形,circle为原型
+ shape: {
+ type: String,
+ default: 'circle'
+ },
+ // 图标的大小,单位rpx
+ iconSize: {
+ type: [String, Number],
+ default: 20
+ },
+ // 每个checkbox占u-checkbox-group的宽度
+ width: {
+ type: [String, Number],
+ default: 'auto'
+ },
+ // 是否每个checkbox都换行
+ wrap: {
+ type: Boolean,
+ default: false
+ }
+ },
+ created() {
+ // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ this.children = [];
+ },
+ watch: {
+ // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+ parentData() {
+ if(this.children.length) {
+ this.children.map(child => {
+ // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.updateParentData) == 'function' && child.updateParentData();
+ })
+ }
+ },
+ },
+ computed: {
+ // 这里computed的变量,都是子组件u-radio需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
+ // 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(u-radio-group)
+ // 拉取父组件新的变化后的参数
+ parentData() {
+ return [this.value, this.disabled, this.activeColor, this.size, this.labelDisabled, this.shape, this.iconSize, this.width, this.wrap];
+ }
+ },
+ methods: {
+ // 该方法有子组件radio调用,当一个radio被选中的时候,给父组件设置value值(props传递的value)
+ setValue(val) {
+ // 通过子组件传递过来的val值(此被选中的子组件内部已将parentValue设置等于val的值),将其他
+ // u-radio设置未选中的状态
+ this.children.map(child => {
+ if(child.parentData.value != val) child.parentData.value = '';
+ })
+ // 通过emit事件,设置父组件通过v-model双向绑定的值
+ this.$emit('input', val);
+ this.$emit('change', val);
+ // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
+ // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+ setTimeout(() => {
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch('u-form-item', 'on-form-change', val);
+ }, 60)
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-radio-group {
+ /* #ifndef MP || APP-NVUE */
+ display: inline-flex;
+ flex-wrap: wrap;
+ /* #endif */
+ }
+</style>
diff --git a/uview-ui/components/u-radio/u-radio.vue b/uview-ui/components/u-radio/u-radio.vue
new file mode 100644
index 0000000..1d9a439
--- /dev/null
+++ b/uview-ui/components/u-radio/u-radio.vue
@@ -0,0 +1,271 @@
+<template>
+ <view class="u-radio" :style="[radioStyle]">
+ <view class="u-radio__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
+ <u-icon
+ class="u-radio__icon-wrap__icon"
+ name="checkbox-mark"
+ :size="elIconSize"
+ :color="iconColor"/>
+ </view>
+ <view class="u-radio__label" @tap="onClickLabel" :style="{
+ fontSize: $u.addUnit(labelSize)
+ }">
+ <slot />
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * radio 单选框
+ * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio-group使用
+ * @tutorial https://www.uviewui.com/components/radio.html
+ * @property {String Number} icon-size 图标大小,单位rpx(默认24)
+ * @property {String Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String Number} name radio组件的标示符
+ * @property {String} shape 形状,见上方说明(默认circle)
+ * @property {Boolean} disabled 是否禁用(默认false)
+ * @property {Boolean} label-disabled 点击文本是否可以操作radio(默认true)
+ * @property {String} active-color 选中时的颜色,如设置parent的active-color将失效
+ * @event {Function} change 某个radio状态发生变化时触发(选中状态)
+ * @example <u-radio :label-disabled="false">门掩黄昏,无计留春住</u-radio>
+ */
+ export default {
+ name: "u-radio",
+ props: {
+ // radio的名称
+ name: {
+ type: [String, Number],
+ default: ''
+ },
+ // 形状,square为方形,circle为原型
+ shape: {
+ type: String,
+ default: ''
+ },
+ // 是否禁用
+ disabled: {
+ type: [String, Boolean],
+ default: ''
+ },
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ type: [String, Boolean],
+ default: ''
+ },
+ // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
+ activeColor: {
+ type: String,
+ default: ''
+ },
+ // 图标的大小,单位rpx
+ iconSize: {
+ type: [String, Number],
+ default: ''
+ },
+ // label的字体大小,rpx单位
+ labelSize: {
+ type: [String, Number],
+ default: ''
+ },
+ },
+ data() {
+ return {
+ // 父组件的默认值,因为头条小程序不支持在computed中使用this.parent.shape的形式
+ // 故只能使用如此方法
+ parentData: {
+ iconSize: null,
+ labelDisabled: null,
+ disabled: null,
+ shape: null,
+ activeColor: null,
+ size: null,
+ width: null,
+ height: null,
+ value: null,
+ wrap: null
+ }
+ };
+ },
+ created() {
+ this.parent = false;
+ // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
+ this.updateParentData();
+ this.parent.children.push(this);
+ },
+ computed: {
+ // 是否禁用,如果父组件u-raios-group禁用的话,将会忽略子组件的配置
+ elDisabled() {
+ return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
+ },
+ // 是否禁用label点击
+ elLabelDisabled() {
+ return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled : false;
+ },
+ // 组件尺寸,对应size的值,默认值为34rpx
+ elSize() {
+ return this.size ? this.size : (this.parentData.size ? this.parentData.size : 34);
+ },
+ // 组件的勾选图标的尺寸,默认20
+ elIconSize() {
+ return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 20);
+ },
+ // 组件选中激活时的颜色
+ elActiveColor() {
+ return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : 'primary');
+ },
+ // 组件的形状
+ elShape() {
+ return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle');
+ },
+ // 设置radio的状态,要求radio的name等于parent的value时才为选中状态
+ iconStyle() {
+ let style = {};
+ if (this.elActiveColor && this.parentData.value == this.name && !this.elDisabled) {
+ style.borderColor = this.elActiveColor;
+ style.backgroundColor = this.elActiveColor;
+ }
+ style.width = this.$u.addUnit(this.elSize);
+ style.height = this.$u.addUnit(this.elSize);
+ return style;
+ },
+ iconColor() {
+ return this.name == this.parentData.value ? '#ffffff' : 'transparent';
+ },
+ iconClass() {
+ let classes = [];
+ classes.push('u-radio__icon-wrap--' + this.elShape);
+ if (this.name == this.parentData.value) classes.push('u-radio__icon-wrap--checked');
+ if (this.elDisabled) classes.push('u-radio__icon-wrap--disabled');
+ if (this.name == this.parentData.value && this.elDisabled) classes.push(
+ 'u-radio__icon-wrap--disabled--checked');
+ // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+ return classes.join(' ');
+ },
+ radioStyle() {
+ let style = {};
+ if (this.parentData.width) {
+ style.width = this.$u.addUnit(this.parentData.width);
+ // #ifdef MP
+ // 各家小程序因为它们特殊的编译结构,使用float布局
+ style.float = 'left';
+ // #endif
+ // #ifndef MP
+ // H5和APP使用flex布局
+ style.flex = `0 0 ${this.$u.addUnit(this.parentData.width)}`;
+ // #endif
+ }
+ if (this.parentData.wrap) {
+ style.width = '100%';
+ // #ifndef MP
+ // H5和APP使用flex布局,将宽度设置100%,即可自动换行
+ style.flex = '0 0 100%';
+ // #endif
+ }
+ return style;
+ }
+ },
+ methods: {
+ updateParentData() {
+ this.getParentData('u-radio-group');
+ },
+ onClickLabel() {
+ if (!this.elLabelDisabled && !this.elDisabled) {
+ this.setRadioCheckedStatus();
+ }
+ },
+ toggle() {
+ if (!this.elDisabled) {
+ this.setRadioCheckedStatus();
+ }
+ },
+ emitEvent() {
+ // u-radio的name不等于父组件的v-model的值时(意味着未选中),才发出事件,避免多次点击触发事件
+ if(this.parentData.value != this.name) this.$emit('change', this.name);
+ },
+ // 改变组件选中状态
+ // 这里的改变的依据是,更改本组件的parentData.value值为本组件的name值,同时通过父组件遍历所有u-radio实例
+ // 将本组件外的其他u-radio的parentData.value都设置为空(由computed计算后,都被取消选中状态),因而只剩下一个为选中状态
+ setRadioCheckedStatus() {
+ this.emitEvent();
+ if(this.parent) {
+ this.parent.setValue(this.name);
+ this.parentData.value = this.name;
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-radio {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+ overflow: hidden;
+ user-select: none;
+ line-height: 1.8;
+
+ &__icon-wrap {
+ color: $u-content-color;
+ @include vue-flex;
+ flex: none;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 42rpx;
+ height: 42rpx;
+ color: transparent;
+ text-align: center;
+ transition-property: color, border-color, background-color;
+ font-size: 20px;
+ border: 1px solid #c8c9cc;
+ transition-duration: 0.2s;
+
+ /* #ifdef MP-TOUTIAO */
+ // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+ &__icon {
+ line-height: 0;
+ }
+ /* #endif */
+
+ &--circle {
+ border-radius: 100%;
+ }
+
+ &--square {
+ border-radius: 3px;
+ }
+
+ &--checked {
+ color: #fff;
+ background-color: #2979ff;
+ border-color: #2979ff;
+ }
+
+ &--disabled {
+ background-color: #ebedf0;
+ border-color: #c8c9cc;
+ }
+
+ &--disabled--checked {
+ color: #c8c9cc !important;
+ }
+ }
+
+ &__label {
+ word-wrap: break-word;
+ margin-left: 10rpx;
+ margin-right: 24rpx;
+ color: $u-content-color;
+ font-size: 30rpx;
+
+ &--disabled {
+ color: #c8c9cc;
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-rate/u-rate.vue b/uview-ui/components/u-rate/u-rate.vue
new file mode 100644
index 0000000..1e44d56
--- /dev/null
+++ b/uview-ui/components/u-rate/u-rate.vue
@@ -0,0 +1,275 @@
+<template>
+ <view class="u-rate" :id="elId" @touchmove.stop.prevent="touchMove">
+ <view class="u-star-wrap" v-for="(item, index) in count" :key="index" :class="[elClass]">
+ <u-icon
+ :name="activeIndex > index ? elActiveIcon : inactiveIcon"
+ @click="click(index + 1, $event)"
+ :color="activeIndex > index ? elActiveColor : inactiveColor"
+ :custom-style="{
+ fontSize: size + 'rpx',
+ padding: `0 ${gutter / 2 + 'rpx'}`
+ }"
+ :custom-prefix="customPrefix"
+ :show-decimal-icon="showDecimalIcon(index)"
+ :percent="decimal"
+ :inactive-color="inactiveColor"
+ ></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>/**
+ * rate 评分
+ * @description 该组件一般用于满意度调查,星型评分的场景
+ * @tutorial https://www.uviewui.com/components/rate.html
+ * @property {String Number} count 最多可选的星星数量(默认5)
+ * @property {String Number} current 默认选中的星星数量(默认0)
+ * @property {Boolean} disabled 是否禁止用户操作(默认false)
+ * @property {String Number} size 星星的大小,单位rpx(默认32)
+ * @property {String} inactive-color 未选中星星的颜色(默认#b2b2b2)
+ * @property {String} active-color 选中的星星颜色(默认#FA3534)
+ * @property {String} active-icon 选中时的图标名,只能为uView的内置图标(默认star-fill)
+ * @property {String} inactive-icon 未选中时的图标名,只能为uView的内置图标(默认star)
+ * @property {String} gutter 星星之间的距离(默认10)
+ * @property {String Number} min-count 最少选中星星的个数(默认0)
+ * @property {Boolean} allow-half 是否允许半星选择(默认false)
+ * @event {Function} change 选中的星星发生变化时触发
+ * @example <u-rate :count="count" :current="2"></u-rate>
+ */
+
+export default {
+ name: 'u-rate',
+ props: {
+ // 用于v-model双向绑定选中的星星数量
+ // 1.4.5版新增
+ value: {
+ type: [Number, String],
+ default: -1
+ },
+ // 要显示的星星数量
+ count: {
+ type: [Number, String],
+ default: 5
+ },
+ // 当前需要默认选中的星星(选中的个数)
+ // 1.4.5后通过value双向绑定,不再建议使用此参数
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 是否不可选中
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 星星的大小,单位rpx
+ size: {
+ type: [Number, String],
+ default: 32
+ },
+ // 未选中时的颜色
+ inactiveColor: {
+ type: String,
+ default: '#b2b2b2'
+ },
+ // 选中的颜色
+ activeColor: {
+ type: String,
+ default: '#FA3534'
+ },
+ // 星星之间的间距,单位rpx
+ gutter: {
+ type: [Number, String],
+ default: 10
+ },
+ // 最少能选择的星星个数
+ minCount: {
+ type: [Number, String],
+ default: 0
+ },
+ // 是否允许半星(功能尚未实现)
+ allowHalf: {
+ type: Boolean,
+ default: false
+ },
+ // 选中时的图标(星星)
+ activeIcon: {
+ type: String,
+ default: 'star-fill'
+ },
+ // 未选中时的图标(星星)
+ inactiveIcon: {
+ type: String,
+ default: 'star'
+ },
+ // 自定义扩展前缀,方便用户扩展自己的图标库
+ customPrefix: {
+ type: String,
+ default: 'uicon'
+ },
+ colors: {
+ type: Array,
+ default() {
+ return []
+ }
+ },
+ icons: {
+ type: Array,
+ default() {
+ return []
+ }
+ }
+ },
+ data() {
+ return {
+ // 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
+ elId: this.$u.guid(),
+ elClass: this.$u.guid(),
+ starBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
+ // 当前激活的星星的index,如果存在value,优先使用value,因为它可以双向绑定(1.4.5新增)
+ activeIndex: this.value != -1 ? this.value : this.current,
+ starWidth: 0, // 每个星星的宽度
+ starWidthArr: [] //每个星星最右边到组件盒子最左边的距离
+ }
+ },
+ watch: {
+ current(val) {
+ this.activeIndex = val
+ },
+ value(val) {
+ this.activeIndex = val
+ }
+ },
+ computed: {
+ decimal() {
+ if (this.disabled) {
+ return this.activeIndex * 100 % 100
+ } else if (this.allowHalf) {
+ return 50
+ }
+ },
+ elActiveIcon() {
+ const len = this.icons.length
+ // 此处规则类似于下方的elActiveColor参数,都是根据一定的规则,显示不同的图标
+ // 结果可能如此:icons参数传递了3个图标,当选中两个时,用第一个图标,4个时,用第二个图标
+ // 第三个时,用第三个图标作为激活的图标
+ if (len && len <= this.count) {
+ const step = Math.round(this.activeIndex / Math.round(this.count / len))
+ if (step < 1) return this.icons[0]
+ if (step > len) return this.icons[len - 1]
+ return this.icons[step - 1]
+ }
+ return this.activeIcon
+ },
+ elActiveColor() {
+ const len = this.colors.length
+ // 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
+ // 4颗星为第二个颜色值,5颗星为第三个颜色值)
+ if (len && len <= this.count) {
+ const step = Math.round(this.activeIndex / Math.round(this.count / len))
+ if (step < 1) return this.colors[0]
+ if (step > len) return this.colors[len - 1]
+ return this.colors[step - 1]
+ }
+ return this.activeColor
+ }
+ },
+ methods: {
+ // 获取评分组件盒子的布局信息
+ getElRectById() {
+ // uView封装的获取节点的方法,详见文档
+ this.$u.getRect('#' + this.elId).then(res => {
+ this.starBoxLeft = res.left
+ })
+ },
+ // 获取单个星星的尺寸
+ getElRectByClass() {
+ // uView封装的获取节点的方法,详见文档
+ this.$u.getRect('.' + this.elClass).then(res => {
+ this.starWidth = res.width
+ // 把每个星星右边到组件盒子左边的距离放入数组中
+ for (let i = 0; i < this.count; i++) {
+ this.starWidthArr[i] = (i + 1) * this.starWidth
+ }
+ })
+ },
+ // 手指滑动
+ touchMove(e) {
+ if (this.disabled) {
+ return
+ }
+ if (!e.changedTouches[0]) {
+ return
+ }
+ const movePageX = e.changedTouches[0].pageX
+ // 滑动点相对于评分盒子左边的距离
+ const distance = movePageX - this.starBoxLeft
+
+ // 如果滑动到了评分盒子的左边界,就设置为0星
+ if (distance <= 0) {
+ this.activeIndex = 0
+ }
+ // 滑动的距离,相当于多少颗星星
+ let index = Math.ceil(distance / this.starWidth)
+ this.activeIndex = index > this.count ? this.count : index
+ // 对最少颗星星的限制
+ if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
+ this.emitEvent()
+ },
+ // 通过点击,直接选中
+ click(index, e) {
+ if (this.disabled) {
+ return
+ }
+ // 半星选择,尚未实现
+ if (this.allowHalf) {
+ }
+ // 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
+ if (index == 1) {
+ if (this.activeIndex == 1) {
+ this.activeIndex = 0
+ } else {
+ this.activeIndex = 1
+ }
+ } else {
+ this.activeIndex = index
+ }
+ // 对最少颗星星的限制
+ if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
+ this.emitEvent()
+ },
+ // 发出事件
+ emitEvent() {
+ // 发出change事件
+ this.$emit('change', this.activeIndex)
+ // 同时修改双向绑定的value的值
+ if (this.value != -1) {
+ this.$emit('input', this.activeIndex)
+ }
+ },
+ showDecimalIcon(index) {
+ return this.disabled && parseInt(this.activeIndex) === index
+ }
+ },
+ mounted() {
+ this.getElRectById()
+ this.getElRectByClass()
+ }
+}
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-rate {
+ display: -webkit-inline-flex;
+ display: inline-flex;
+ align-items: center;
+ margin: 0;
+ padding: 0;
+}
+
+.u-icon {
+ box-sizing: border-box;
+}
+</style>
diff --git a/uview-ui/components/u-read-more/u-read-more.vue b/uview-ui/components/u-read-more/u-read-more.vue
new file mode 100644
index 0000000..a77a44f
--- /dev/null
+++ b/uview-ui/components/u-read-more/u-read-more.vue
@@ -0,0 +1,179 @@
+<template>
+ <view class="">
+ <view class="u-content" :class="[elId]" :style="{
+ height: isLongContent && !showMore ? showHeight + 'rpx' : 'auto',
+ textIndent: textIndent
+ }">
+ <slot></slot>
+ </view>
+ <view @tap="toggleReadMore" v-if="isLongContent" class="u-content__showmore-wrap"
+ :class="{ 'u-content__show-more': showMore }"
+ :style="[innerShadowStyle]">
+ <text class="u-content__showmore-wrap__readmore-btn" :style="{
+ fontSize: fontSize + 'rpx',
+ color: color
+ }">
+ {{ showMore ? openText : closeText }}
+ </text>
+ <view class="u-content__showmore-wrap__readmore-btn__icon u-flex">
+ <u-icon :color="color" :size="fontSize" :name="showMore ? 'arrow-up' : 'arrow-down'"></u-icon>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * readMore 阅读更多
+ * @description 该组件一般用于内容较长,预先收起一部分,点击展开全部内容的场景。
+ * @tutorial https://www.uviewui.com/components/readMore.html
+ * @property {String Number} show-height 内容超出此高度才会显示展开全文按钮,单位rpx(默认400)
+ * @property {Boolean} toggle 展开后是否显示收起按钮(默认false)
+ * @property {String} close-text 关闭时的提示文字(默认“展开阅读全文”)
+ * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
+ * @property {String} text-indent 段落首行缩进的字符个数(默认2em)
+ * @property {String} open-text 展开时的提示文字(默认“收起”)
+ * @property {String} color 提示文字的颜色(默认#2979ff)
+ * @example <u-read-more><rich-text :nodes="content"></rich-text></u-read-more>
+ */
+ export default {
+ name: "u-read-more",
+ props: {
+ // 默认的显示占位高度,单位为rpx
+ showHeight: {
+ type: [Number, String],
+ default: 400
+ },
+ // 展开后是否显示"收起"按钮
+ toggle: {
+ type: Boolean,
+ default: false
+ },
+ // 关闭时的提示文字
+ closeText: {
+ type: String,
+ default: '展开阅读全文'
+ },
+ // 展开时的提示文字
+ openText: {
+ type: String,
+ default: '收起'
+ },
+ // 提示的文字颜色
+ color: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 提示文字的大小
+ fontSize: {
+ type: [String, Number],
+ default: 28
+ },
+ // 是否显示阴影
+ shadowStyle: {
+ type: Object,
+ default () {
+ return {
+ backgroundImage: "linear-gradient(-180deg, rgba(255, 255, 255, 0) 0%, #fff 80%)",
+ paddingTop: "300rpx",
+ marginTop: "-300rpx"
+ }
+ }
+ },
+ // 段落首行缩进的字符个数
+ textIndent: {
+ type: String,
+ default: '2em'
+ },
+ // open和close事件时,将此参数返回在回调参数中
+ index: {
+ type: [Number, String],
+ default: ''
+ }
+ },
+ watch: {
+ paramsChange(val) {
+ this.init();
+ }
+ },
+ computed: {
+ paramsChange() {
+ return `${this.toggle}-${this.showHeight}`;
+ },
+ // 展开后无需阴影,收起时才需要阴影样式
+ innerShadowStyle() {
+ if (this.showMore) return {};
+ else return this.shadowStyle
+ }
+ },
+ data() {
+ return {
+ isLongContent: false, // 是否需要隐藏一部分内容
+ showMore: false, // 当前隐藏与显示的状态,true-显示,false-收起
+ elId: this.$u.guid(), // 生成唯一class
+ };
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.init();
+ })
+ },
+ methods: {
+ init() {
+ this.$uGetRect('.' + this.elId).then(res => {
+ // 判断高度,如果真实内容高度大于占位高度,则显示收起与展开的控制按钮
+ if (res.height > uni.upx2px(this.showHeight)) {
+ this.isLongContent = true;
+ this.showMore = false;
+ }
+ })
+ },
+ // 展开或者收起
+ toggleReadMore() {
+ this.showMore = !this.showMore;
+ // 如果toggle为false,隐藏"收起"部分的内容
+ if (this.toggle == false) this.isLongContent = false;
+ // 发出打开或者收齐的事件
+ this.$emit(this.showMore ? 'open' : 'close', this.index);
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-content {
+ font-size: 30rpx;
+ color: $u-content-color;
+ line-height: 1.8;
+ text-align: left;
+ overflow: hidden;
+
+ &__show-more {
+ padding-top: 0;
+ background: none;
+ margin-top: 20rpx;
+ }
+
+ &__showmore-wrap {
+ position: relative;
+ width: 100%;
+ padding-bottom: 26rpx;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+
+ &__readmore-btn {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+
+ &__icon {
+ margin-left: 14rpx;
+ }
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-row-notice/u-row-notice.vue b/uview-ui/components/u-row-notice/u-row-notice.vue
new file mode 100644
index 0000000..f4683f1
--- /dev/null
+++ b/uview-ui/components/u-row-notice/u-row-notice.vue
@@ -0,0 +1,269 @@
+<template>
+ <view
+ v-if="show"
+ class="u-notice-bar"
+ :style="{
+ background: computeBgColor,
+ padding: padding
+ }"
+ :class="[
+ type ? `u-type-${type}-light-bg` : ''
+ ]"
+ >
+ <view class="u-direction-row">
+ <view class="u-icon-wrap">
+ <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
+ </view>
+ <view class="u-notice-box" id="u-notice-box">
+ <view
+ class="u-notice-content"
+ id="u-notice-content"
+ :style="{
+ animationDuration: animationDuration,
+ animationPlayState: animationPlayState,
+ }"
+ >
+ <text class="u-notice-text" @tap="click" :style="[textStyle]"
+ :class="['u-type-' + type]">{{showText}}</text>
+ </view>
+ </view>
+ <view class="u-icon-wrap">
+ <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
+ <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
+ </view>
+ </view>
+ </view>
+</template>
+<script>
+export default {
+ props: {
+ // 显示的内容,数组
+ list: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 显示的主题,success|error|primary|info|warning|none
+ // none主题默认为透明背景,黑色(contentColor)字体
+ type: {
+ type: String,
+ default: 'warning'
+ },
+ // 是否显示左侧的音量图标
+ volumeIcon: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示右侧的右箭头图标
+ moreIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示右侧的关闭图标
+ closeIcon: {
+ type: Boolean,
+ default: false
+ },
+ // 是否自动播放
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 文字颜色,各图标也会使用文字颜色
+ color: {
+ type: String,
+ default: ''
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 是否显示
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 26
+ },
+ // 音量喇叭的大小
+ volumeSize: {
+ type: [Number, String],
+ default: 34
+ },
+ // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+ speed: {
+ type: [Number, String],
+ default: 160
+ },
+ // 播放状态,play-播放,paused-暂停
+ playState: {
+ type: String,
+ default: 'play'
+ },
+ // 通知的边距
+ padding: {
+ type: [Number, String],
+ default: '18rpx 24rpx'
+ }
+ },
+ data() {
+ return {
+ textWidth: 0, // 滚动的文字宽度
+ boxWidth: 0, // 供文字滚动的父盒子的宽度,和前者一起为了计算滚动速度
+ animationDuration: '10s', // 动画执行时间
+ animationPlayState: 'paused', // 动画的开始和结束执行
+ showText: '' // 显示的文本
+ };
+ },
+ watch: {
+ list: {
+ immediate: true,
+ handler(val) {
+ this.showText = val.join(',');
+ this.$nextTick(() => {
+ this.initSize();
+ });
+ }
+ },
+ playState(val) {
+ if(val == 'play') this.animationPlayState = 'running';
+ else this.animationPlayState = 'paused';
+ },
+ speed(val) {
+ this.initSize();
+ }
+ },
+ computed: {
+ // 计算字体颜色,如果没有自定义的,就用uview主题颜色
+ computeColor() {
+ if (this.color) return this.color;
+ // 如果是无主题,就默认使用content-color
+ else if(this.type == 'none') return '#606266';
+ else return this.type;
+ },
+ // 文字内容的样式
+ textStyle() {
+ let style = {};
+ if (this.color) style.color = this.color;
+ else if(this.type == 'none') style.color = '#606266';
+ style.fontSize = this.fontSize + 'rpx';
+ return style;
+ },
+ // 计算背景颜色
+ computeBgColor() {
+ if (this.bgColor) return this.bgColor;
+ else if(this.type == 'none') return 'transparent';
+ }
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.initSize();
+ });
+ },
+ methods: {
+ initSize() {
+ let query = [],
+ boxWidth = 0,
+ textWidth = 0;
+ let textQuery = new Promise((resolve, reject) => {
+ uni.createSelectorQuery()
+ .in(this)
+ .select(`#u-notice-content`)
+ .boundingClientRect()
+ .exec(ret => {
+ this.textWidth = ret[0].width;
+ resolve();
+ });
+ });
+ query.push(textQuery);
+ Promise.all(query).then(() => {
+ // 根据t=s/v(时间=路程/速度),这里为何不需要加上#u-notice-box的宽度,因为中设置了.u-notice-content样式中设置了padding-left: 100%
+ // 恰巧计算出来的结果中已经包含了#u-notice-box的宽度
+ this.animationDuration = `${this.textWidth / uni.upx2px(this.speed)}s`;
+ // 这里必须这样开始动画,否则在APP上动画速度不会改变(HX版本2.4.6,IOS13)
+ this.animationPlayState = 'paused';
+ setTimeout(() => {
+ if(this.playState == 'play' && this.autoplay) this.animationPlayState = 'running';
+ }, 10);
+ });
+ },
+ // 点击通告栏
+ click(index) {
+ this.$emit('click');
+ },
+ // 点击关闭按钮
+ close() {
+ this.$emit('close');
+ },
+ // 点击更多箭头按钮
+ getMore() {
+ this.$emit('getMore');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-notice-bar {
+ padding: 18rpx 24rpx;
+ overflow: hidden;
+}
+
+.u-direction-row {
+ @include vue-flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.u-left-icon {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ align-items: center;
+}
+
+.u-notice-box {
+ flex: 1;
+ @include vue-flex;
+ overflow: hidden;
+ margin-left: 12rpx;
+}
+
+.u-right-icon {
+ margin-left: 12rpx;
+ display: inline-flex;
+ align-items: center;
+}
+
+.u-notice-content {
+ animation: u-loop-animation 10s linear infinite both;
+ text-align: right;
+ // 这一句很重要,为了能让滚动左右连接起来
+ padding-left: 100%;
+ @include vue-flex;
+ flex-wrap: nowrap;
+}
+
+.u-notice-text {
+ font-size: 26rpx;
+ word-break: keep-all;
+ white-space: nowrap
+}
+
+@keyframes u-loop-animation {
+ 0% {
+ transform: translate3d(0, 0, 0);
+ }
+
+ 100% {
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+</style>
diff --git a/uview-ui/components/u-row/u-row.vue b/uview-ui/components/u-row/u-row.vue
new file mode 100644
index 0000000..915dfa6
--- /dev/null
+++ b/uview-ui/components/u-row/u-row.vue
@@ -0,0 +1,84 @@
+<template>
+ <view class="u-row" :style="{
+ alignItems: uAlignItem,
+ justifyContent: uJustify
+ }"
+ @tap="click"
+ >
+ <slot />
+ </view>
+</template>
+
+<script>
+ /**
+ * row 行布局
+ * @description 通过基础的 12 分栏,迅速简便地创建布局。
+ * @tutorial https://www.uviewui.com/components/layout.html#row-props
+ * @property {String Number} gutter 栅格间隔,左右各为此值的一半,单位rpx(默认0)
+ * @property {String} justify 水平排列方式(微信小程序暂不支持)默认(start(或flex-start))
+ * @property {String} align 垂直排列方式(默认center)
+ * @example <u-row gutter="16"></u-row>
+ */
+ export default {
+ name: "u-row",
+ props: {
+ // 给col添加间距,左右边距各占一半
+ gutter: {
+ type: [String, Number],
+ default: 20
+ },
+ // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+ justify: {
+ type: String,
+ default: 'start'
+ },
+ // 垂直对齐方式,可选值为top、center、bottom
+ align: {
+ type: String,
+ default: 'center'
+ },
+ // 是否阻止事件传播
+ stop: {
+ type: Boolean,
+ default: true
+ }
+ },
+ computed: {
+ uJustify() {
+ if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
+ else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
+ else return this.justify;
+ },
+ uAlignItem() {
+ if (this.align == 'top') return 'flex-start';
+ if (this.align == 'bottom') return 'flex-end';
+ else return this.align;
+ }
+ },
+ methods: {
+ click(e) {
+ this.$emit('click');
+ }
+ }
+ }
+</script>
+
+<style lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-row {
+ // 由于微信小程序编译后奇怪的页面结构,只能使用float布局实现,flex无法实现
+ /* #ifndef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+ @include vue-flex;
+ /* #endif */
+ flex-wrap: wrap;
+ }
+
+ .u-row:after {
+ /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+ display: table;
+ clear: both;
+ content: "";
+ /* #endif */
+ }
+</style>
diff --git a/uview-ui/components/u-search/u-search.vue b/uview-ui/components/u-search/u-search.vue
new file mode 100644
index 0000000..206f661
--- /dev/null
+++ b/uview-ui/components/u-search/u-search.vue
@@ -0,0 +1,342 @@
+<template>
+ <view class="u-search" @tap="clickHandler" :style="{
+ margin: margin,
+ }">
+ <view
+ class="u-content"
+ :style="{
+ backgroundColor: bgColor,
+ borderRadius: shape == 'round' ? '100rpx' : '10rpx',
+ border: borderStyle,
+ height: height + 'rpx'
+ }"
+ >
+ <view class="u-icon-wrap">
+ <u-icon class="u-clear-icon" :size="30" :name="searchIcon" :color="searchIconColor ? searchIconColor : color"></u-icon>
+ </view>
+ <input
+ confirm-type="search"
+ @blur="blur"
+ :value="value"
+ @confirm="search"
+ @input="inputChange"
+ :disabled="disabled"
+ @focus="getFocus"
+ :focus="focus"
+ :maxlength="maxlength"
+ placeholder-class="u-placeholder-class"
+ :placeholder="placeholder"
+ :placeholder-style="`color: ${placeholderColor}`"
+ class="u-input"
+ type="text"
+ :style="[{
+ textAlign: inputAlign,
+ color: color,
+ backgroundColor: bgColor,
+ }, inputStyle]"
+ />
+ <view class="u-close-wrap" v-if="keyword && clearabled && focused" @tap="clear">
+ <u-icon class="u-clear-icon" name="close-circle-fill" size="34" color="#c0c4cc"></u-icon>
+ </view>
+ </view>
+ <view :style="[actionStyle]" class="u-action"
+ :class="[showActionBtn || show ? 'u-action-active' : '']"
+ @tap.stop.prevent="custom"
+ >{{ actionText }}</view>
+ </view>
+</template>
+
+<script>
+/**
+ * search 搜索框
+ * @description 搜索组件,集成了常见搜索框所需功能,用户可以一键引入,开箱即用。
+ * @tutorial https://www.uviewui.com/components/search.html
+ * @property {String} shape 搜索框形状,round-圆形,square-方形(默认round)
+ * @property {String} bg-color 搜索框背景颜色(默认#f2f2f2)
+ * @property {String} border-color 边框颜色,配置了颜色,才会有边框
+ * @property {String} placeholder 占位文字内容(默认“请输入关键字”)
+ * @property {Boolean} clearabled 是否启用清除控件(默认true)
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} show-action 是否显示右侧控件(默认true)
+ * @property {String} action-text 右侧控件文字(默认“搜索”)
+ * @property {Object} action-style 右侧控件的样式,对象形式
+ * @property {String} input-align 输入框内容水平对齐方式(默认left)
+ * @property {Object} input-style 自定义输入框样式,对象形式
+ * @property {Boolean} disabled 是否启用输入框(默认false)
+ * @property {String} search-icon-color 搜索图标的颜色,默认同输入框字体颜色
+ * @property {String} color 输入框字体颜色(默认#606266)
+ * @property {String} placeholder-color placeholder的颜色(默认#909399)
+ * @property {String} search-icon 输入框左边的图标,可以为uView图标名称或图片路径
+ * @property {String} margin 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"
+ * @property {Boolean} animation 是否开启动画,见上方说明(默认false)
+ * @property {String} value 输入框初始值
+ * @property {String | Number} maxlength 输入框最大能输入的长度,-1为不限制长度
+ * @property {Boolean} input-style input输入框的样式,可以定义文字颜色,大小等,对象形式
+ * @property {String | Number} height 输入框高度,单位rpx(默认64)
+ * @event {Function} change 输入框内容发生变化时触发
+ * @event {Function} search 用户确定搜索时触发,用户按回车键,或者手机键盘右下角的"搜索"键时触发
+ * @event {Function} custom 用户点击右侧控件时触发
+ * @event {Function} clear 用户点击清除按钮时触发
+ * @example <u-search placeholder="日照香炉生紫烟" v-model="keyword"></u-search>
+ */
+export default {
+ name: "u-search",
+ props: {
+ // 搜索框形状,round-圆形,square-方形
+ shape: {
+ type: String,
+ default: 'round'
+ },
+ // 搜索框背景色,默认值#f2f2f2
+ bgColor: {
+ type: String,
+ default: '#f2f2f2'
+ },
+ // 占位提示文字
+ placeholder: {
+ type: String,
+ default: '请输入关键字'
+ },
+ // 是否启用清除控件
+ clearabled: {
+ type: Boolean,
+ default: true
+ },
+ // 是否自动聚焦
+ focus: {
+ type: Boolean,
+ default: false
+ },
+ // 是否在搜索框右侧显示取消按钮
+ showAction: {
+ type: Boolean,
+ default: true
+ },
+ // 右边控件的样式
+ actionStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 取消按钮文字
+ actionText: {
+ type: String,
+ default: '搜索'
+ },
+ // 输入框内容对齐方式,可选值为 left|center|right
+ inputAlign: {
+ type: String,
+ default: 'left'
+ },
+ // 是否启用输入框
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 开启showAction时,是否在input获取焦点时才显示
+ animation: {
+ type: Boolean,
+ default: false
+ },
+ // 边框颜色,只要配置了颜色,才会有边框
+ borderColor: {
+ type: String,
+ default: 'none'
+ },
+ // 输入框的初始化内容
+ value: {
+ type: String,
+ default: ''
+ },
+ // 搜索框高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 64
+ },
+ // input输入框的样式,可以定义文字颜色,大小等,对象形式
+ inputStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 输入框最大能输入的长度,-1为不限制长度(来自uniapp文档)
+ maxlength: {
+ type: [Number, String],
+ default: '-1'
+ },
+ // 搜索图标的颜色,默认同输入框字体颜色
+ searchIconColor: {
+ type: String,
+ default: ''
+ },
+ // 输入框字体颜色
+ color: {
+ type: String,
+ default: '#606266'
+ },
+ // placeholder的颜色
+ placeholderColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"、"30rpx 20rpx"等写法
+ margin: {
+ type: String,
+ default: '0'
+ },
+ // 左边输入框的图标,可以为uView图标名称或图片路径
+ searchIcon: {
+ type: String,
+ default: 'search'
+ }
+ },
+ data() {
+ return {
+ keyword: '',
+ showClear: false, // 是否显示右边的清除图标
+ show: false,
+ // 标记input当前状态是否处于聚焦中,如果是,才会显示右侧的清除控件
+ focused: this.focus
+ // 绑定输入框的值
+ // inputValue: this.value
+ };
+ },
+ watch: {
+ keyword(nVal) {
+ // 双向绑定值,让v-model绑定的值双向变化
+ this.$emit('input', nVal);
+ // 触发change事件,事件效果和v-model双向绑定的效果一样,让用户多一个选择
+ this.$emit('change', nVal);
+ },
+ value: {
+ immediate: true,
+ handler(nVal) {
+ this.keyword = nVal;
+ }
+ }
+ },
+ computed: {
+ showActionBtn() {
+ if (!this.animation && this.showAction) return true;
+ else return false;
+ },
+ // 样式,根据用户传入的颜色值生成,如果不传入,默认为none
+ borderStyle() {
+ if (this.borderColor) return `1px solid ${this.borderColor}`;
+ else return 'none';
+ },
+ },
+ methods: {
+ // 目前HX2.6.9 v-model双向绑定无效,故监听input事件获取输入框内容的变化
+ inputChange(e) {
+ this.keyword = e.detail.value;
+ },
+ // 清空输入
+ // 也可以作为用户通过this.$refs形式调用清空输入框内容
+ clear() {
+ this.keyword = '';
+ // 延后发出事件,避免在父组件监听clear事件时,value为更新前的值(不为空)
+ this.$nextTick(() => {
+ this.$emit('clear');
+ })
+ },
+ // 确定搜索
+ search(e) {
+ this.$emit('search', e.detail.value);
+ try{
+ // 收起键盘
+ uni.hideKeyboard();
+ }catch(e){}
+ },
+ // 点击右边自定义按钮的事件
+ custom() {
+ this.$emit('custom', this.keyword);
+ try{
+ // 收起键盘
+ uni.hideKeyboard();
+ }catch(e){}
+ },
+ // 获取焦点
+ getFocus() {
+ this.focused = true;
+ // 开启右侧搜索按钮展开的动画效果
+ if (this.animation && this.showAction) this.show = true;
+ this.$emit('focus', this.keyword);
+ },
+ // 失去焦点
+ blur() {
+ // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+ // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+ setTimeout(() => {
+ this.focused = false;
+ }, 100)
+ this.show = false;
+ this.$emit('blur', this.keyword);
+ },
+ // 点击搜索框,只有disabled=true时才发出事件,因为禁止了输入,意味着是想跳转真正的搜索页
+ clickHandler() {
+ if(this.disabled) this.$emit('click');
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-search {
+ @include vue-flex;
+ align-items: center;
+ flex: 1;
+}
+
+.u-content {
+ @include vue-flex;
+ align-items: center;
+ padding: 0 18rpx;
+ flex: 1;
+}
+
+.u-clear-icon {
+ @include vue-flex;
+ align-items: center;
+}
+
+.u-input {
+ flex: 1;
+ font-size: 28rpx;
+ line-height: 1;
+ margin: 0 10rpx;
+ color: $u-tips-color;
+}
+
+.u-close-wrap {
+ width: 40rpx;
+ height: 100%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+}
+
+.u-placeholder-class {
+ color: $u-tips-color;
+}
+
+.u-action {
+ font-size: 28rpx;
+ color: $u-main-color;
+ width: 0;
+ overflow: hidden;
+ transition: all 0.3s;
+ white-space: nowrap;
+ text-align: center;
+}
+
+.u-action-active {
+ width: 80rpx;
+ margin-left: 10rpx;
+}
+</style>
diff --git a/uview-ui/components/u-section/u-section.vue b/uview-ui/components/u-section/u-section.vue
new file mode 100644
index 0000000..02293ce
--- /dev/null
+++ b/uview-ui/components/u-section/u-section.vue
@@ -0,0 +1,154 @@
+<template>
+ <view class="u-section">
+ <view class="u-section__title" :style="{
+ fontWeight: bold ? 'bold' : 'normal',
+ color: color,
+ fontSize: fontSize + 'rpx',
+ paddingLeft: showLine ? (fontSize * 0.7) + 'rpx' : 0
+ }" :class="{
+ 'u-section--line': showLine
+ }">
+ <view class="u-section__title__icon-wrap u-flex" :style="[lineStyle]" v-if="showLine">
+ <u-icon top="0" name="column-line" :size="fontSize * 1.25" bold :color="lineColor ? lineColor : color"></u-icon>
+ </view>
+ <text class="u-flex u-section__title__text">{{title}}</text>
+ </view>
+ <view class="u-section__right-info" v-if="right || $slots.right" :style="{
+ color: subColor
+ }" @tap="rightClick">
+ <slot name="right" v-if="$slots.right" />
+ <block v-else>
+ {{subTitle}}
+ <view class="u-section__right-info__icon-arrow u-flex" v-if="arrow">
+ <u-icon name="arrow-right" size="24" :color="subColor"></u-icon>
+ </view>
+ </block>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * section 查看更多
+ * @description 该组件一般用于分类信息有很多,但是限于篇幅只能列出一部分,让用户通过"查看更多"获得更多信息的场景,实际效果见演示。
+ * @tutorial https://www.uviewui.com/components/section.html
+ * @property {String} title 左边主标题
+ * @property {String} sub-title 右边副标题(默认更多)
+ * @property {Boolean} right 是否显示右边的内容(默认true)
+ * @property {Boolean} showLine 是否显示左边的竖条(默认true)
+ * @property {Boolean} arrow 是否显示右边箭头(默认true)
+ * @property {String Number} font-size 主标题的字体大小(默认28)
+ * @property {Boolean} bold 主标题是否加粗(默认true)
+ * @property {String} color 主标题颜色(默认#303133)
+ * @event {Function} click 组件右侧的内容被点击时触发,用于跳转"更多"
+ * @example <u-section title="今日热门" :right="false"></u-section>
+ */
+ export default {
+ name: "u-section",
+ props: {
+ // 标题信息
+ title: {
+ type: String,
+ default: ''
+ },
+ // 右边副标题内容
+ subTitle: {
+ type: String,
+ default: '更多'
+ },
+ // 是否显示右边的内容
+ right: {
+ type: Boolean,
+ default: true
+ },
+ fontSize: {
+ type: [Number, String],
+ default: 28
+ },
+ // 主标题是否加粗
+ bold: {
+ type: Boolean,
+ default: true
+ },
+ // 主标题的颜色
+ color: {
+ type: String,
+ default: '#303133'
+ },
+ // 右边副标题的颜色
+ subColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 是否显示左边的竖条
+ showLine: {
+ type: Boolean,
+ default: true
+ },
+ // 左边竖线的颜色
+ lineColor: {
+ type: String,
+ default: ''
+ },
+ // 是否显示右边箭头
+ arrow: {
+ type: Boolean,
+ default: true
+ },
+ },
+ computed: {
+ // 左边竖条的样式
+ lineStyle() {
+ // 由于安卓和iOS的,需要稍微调整绝对定位的top值,才能让左边的竖线和右边的文字垂直居中
+ return {
+ // 由于竖线为字体图标,具有比实际线宽更宽的宽度,所以也需要根据字体打下动态调整
+ left: -(Number(this.fontSize) * 0.9) + 'rpx',
+ top: -(Number(this.fontSize) * (this.$u.os() == 'ios' ? 0.14 : 0.15)) + 'rpx',
+ }
+ }
+ },
+ methods: {
+ rightClick() {
+ this.$emit('click');
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-section {
+ @include vue-flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+
+ &__title {
+ position: relative;
+ font-size: 28rpx;
+ padding-left: 20rpx;
+ @include vue-flex;
+ align-items: center;
+
+ &__icon-wrap {
+ position: absolute;
+ }
+
+ &__text {
+ line-height: 1;
+ }
+ }
+
+ &__right-info {
+ color: $u-tips-color;
+ font-size: 26rpx;
+ @include vue-flex;
+ align-items: center;
+
+ &__icon-arrow {
+ margin-left: 6rpx;
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-select/u-select.vue b/uview-ui/components/u-select/u-select.vue
new file mode 100644
index 0000000..1285845
--- /dev/null
+++ b/uview-ui/components/u-select/u-select.vue
@@ -0,0 +1,417 @@
+<template>
+ <view class="u-select">
+ <!-- <view class="u-select__action" :class="{
+ 'u-select--border': border
+ }" @tap.stop="selectHandler">
+ <view class="u-select__action__icon" :class="{
+ 'u-select__action__icon--reverse': value == true
+ }">
+ <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
+ </view>
+ </view> -->
+ <u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
+ <view class="u-select">
+ <view class="u-select__header" @touchmove.stop.prevent="">
+ <view
+ class="u-select__header__cancel u-select__header__btn"
+ :style="{ color: cancelColor }"
+ hover-class="u-hover-class"
+ :hover-stay-time="150"
+ @tap="getResult('cancel')"
+ >
+ {{cancelText}}
+ </view>
+ <view class="u-select__header__title">
+ {{title}}
+ </view>
+ <view
+ class="u-select__header__confirm u-select__header__btn"
+ :style="{ color: moving ? cancelColor : confirmColor }"
+ hover-class="u-hover-class"
+ :hover-stay-time="150"
+ @touchmove.stop=""
+ @tap.stop="getResult('confirm')"
+ >
+ {{confirmText}}
+ </view>
+ </view>
+ <view class="u-select__body">
+ <picker-view @change="columnChange" class="u-select__body__picker-view" :value="defaultSelector" @pickstart="pickstart" @pickend="pickend">
+ <picker-view-column v-for="(item, index) in columnData" :key="index">
+ <view class="u-select__body__picker-view__item" v-for="(item1, index1) in item" :key="index1">
+ <view class="u-line-1">{{ item1[labelName] }}</view>
+ </view>
+ </picker-view-column>
+ </picker-view>
+ </view>
+ </view>
+ </u-popup>
+ </view>
+</template>
+
+<script>
+ /**
+ * select 列选择器
+ * @description 此选择器用于单列,多列,多列联动的选择场景。(从1.3.0版本起,不建议使用Picker组件的单列和多列模式,Select组件是专门为列选择而构造的组件,更简单易用。)
+ * @tutorial http://uviewui.com/components/select.html
+ * @property {String} mode 模式选择,"single-column"-单列模式,"mutil-column"-多列模式,"mutil-column-auto"-多列联动模式
+ * @property {Array} list 列数据,数组形式,见官网说明
+ * @property {Boolean} v-model 布尔值变量,用于控制选择器的弹出与收起
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {String} cancel-color 取消按钮的颜色(默认#606266)
+ * @property {String} confirm-color 确认按钮的颜色(默认#2979ff)
+ * @property {String} confirm-text 确认按钮的文字
+ * @property {String} cancel-text 取消按钮的文字
+ * @property {String} default-value 提供的默认选中的下标,见官网说明
+ * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
+ * @property {String Number} z-index 弹出时的z-index值(默认10075)
+ * @property {String} value-name 自定义list数据的value属性名 1.3.6
+ * @property {String} label-name 自定义list数据的label属性名 1.3.6
+ * @property {String} child-name 自定义list数据的children属性名,只对多列联动模式有效 1.3.7
+ * @event {Function} confirm 点击确定按钮,返回当前选择的值
+ * @example <u-select v-model="show" :list="list"></u-select>
+ */
+
+export default {
+ props: {
+ // 列数据
+ list: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 是否显示边框
+ border: {
+ type: Boolean,
+ default: true
+ },
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // "取消"按钮的颜色
+ cancelColor: {
+ type: String,
+ default: '#606266'
+ },
+ // "确定"按钮的颜色
+ confirmColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 弹出的z-index值
+ zIndex: {
+ type: [String, Number],
+ default: 0
+ },
+ safeAreaInsetBottom: {
+ type: Boolean,
+ default: false
+ },
+ // 是否允许通过点击遮罩关闭Picker
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ },
+ // 提供的默认选中的下标
+ defaultValue: {
+ type: Array,
+ default() {
+ return [0];
+ }
+ },
+ // 模式选择,single-column-单列,mutil-column-多列,mutil-column-auto-多列联动
+ mode: {
+ type: String,
+ default: 'single-column'
+ },
+ // 自定义value属性名
+ valueName: {
+ type: String,
+ default: 'value'
+ },
+ // 自定义label属性名
+ labelName: {
+ type: String,
+ default: 'label'
+ },
+ // 自定义多列联动模式的children属性名
+ childName: {
+ type: String,
+ default: 'children'
+ },
+ // 顶部标题
+ title: {
+ type: String,
+ default: ''
+ },
+ // 取消按钮的文字
+ cancelText: {
+ type: String,
+ default: '取消'
+ },
+ // 确认按钮的文字
+ confirmText: {
+ type: String,
+ default: '确认'
+ }
+ },
+ data() {
+ return {
+ // 用于列改变时,保存当前的索引,下一次变化时比较得出是哪一列发生了变化
+ defaultSelector: [0],
+ // picker-view的数据
+ columnData: [],
+ // 每次队列发生变化时,保存选择的结果
+ selectValue: [],
+ // 上一次列变化时的index
+ lastSelectIndex: [],
+ // 列数
+ columnNum: 0,
+ // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
+ moving: false
+ };
+ },
+ watch: {
+ // 在select弹起的时候,重新初始化所有数据
+ value: {
+ immediate: true,
+ handler(val) {
+ if(val) setTimeout(() => this.init(), 10);
+ }
+ },
+ },
+ computed: {
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ },
+ },
+ methods: {
+ // 标识滑动开始,只有微信小程序才有这样的事件
+ pickstart() {
+ // #ifdef MP-WEIXIN
+ this.moving = true;
+ // #endif
+ },
+ // 标识滑动结束
+ pickend() {
+ // #ifdef MP-WEIXIN
+ this.moving = false;
+ // #endif
+ },
+ init() {
+ this.setColumnNum();
+ this.setDefaultSelector();
+ this.setColumnData();
+ this.setSelectValue();
+ },
+ // 获取默认选中列下标
+ setDefaultSelector() {
+ // 如果没有传入默认选中的值,生成长度为columnNum,用0填充的数组
+ this.defaultSelector = this.defaultValue.length == this.columnNum ? this.defaultValue : Array(this.columnNum).fill(0);
+ this.lastSelectIndex = this.$u.deepClone(this.defaultSelector);
+ },
+ // 计算列数
+ setColumnNum() {
+ // 单列的列数为1
+ if(this.mode == 'single-column') this.columnNum = 1;
+ // 多列时,this.list数组长度就是列数
+ else if(this.mode == 'mutil-column') this.columnNum = this.list.length;
+ // 多列联动时,通过历遍this.list的第一个元素,得出有多少列
+ else if(this.mode == 'mutil-column-auto') {
+ let num = 1;
+ let column = this.list;
+ // 只要有元素并且第一个元素有children属性,继续历遍
+ while(column[0][this.childName]) {
+ column = column[0] ? column[0][this.childName] : {};
+ num ++;
+ }
+ this.columnNum = num;
+ }
+ },
+ // 获取需要展示在picker中的列数据
+ setColumnData() {
+ let data = [];
+ this.selectValue = [];
+ if(this.mode == 'mutil-column-auto') {
+ // 获得所有数据中的第一个元素
+ let column = this.list[this.defaultSelector.length ? this.defaultSelector[0] : 0];
+ // 通过循环所有的列数,再根据设定列的数组,得出当前需要渲染的整个列数组
+ for (let i = 0; i < this.columnNum; i++) {
+ // 第一列默认为整个list数组
+ if (i == 0) {
+ data[i] = this.list;
+ column = column[this.childName];
+ } else {
+ // 大于第一列时,判断是否有默认选中的,如果没有就用该列的第一项
+ data[i] = column;
+ column = column[this.defaultSelector[i]][this.childName];
+ }
+ }
+ } else if(this.mode == 'single-column') {
+ data[0] = this.list;
+ } else {
+ data = this.list;
+ }
+ this.columnData = data;
+ },
+ // 获取默认选中的值,如果没有设置defaultValue,就默认选中每列的第一个
+ setSelectValue() {
+ let tmp = null;
+ for(let i = 0; i < this.columnNum; i++) {
+ tmp = this.columnData[i][this.defaultSelector[i]];
+ let data = {
+ value: tmp ? tmp[this.valueName] : null,
+ label: tmp ? tmp[this.labelName] : null
+ };
+ // 判断是否存在额外的参数,如果存在,就返回
+ if(tmp && tmp.extra) data.extra = tmp.extra;
+ this.selectValue.push(data)
+ }
+ },
+ // 列选项
+ columnChange(e) {
+ let index = null;
+ let columnIndex = e.detail.value;
+ // 由于后面是需要push进数组的,所以需要先清空数组
+ this.selectValue = [];
+ if(this.mode == 'mutil-column-auto') {
+ // 对比前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
+ this.lastSelectIndex.map((val, idx) => {
+ if (val != columnIndex[idx]) index = idx;
+ });
+ this.defaultSelector = columnIndex;
+ for (let i = index + 1; i < this.columnNum; i++) {
+ // 当前变化列的下一列的数据,需要获取上一列的数据,同时需要指定是上一列的第几个的children,再往后的
+ // 默认是队列的第一个为默认选项
+ this.columnData[i] = this.columnData[i - 1][i - 1 == index ? columnIndex[index] : 0][this.childName];
+ // 改变的列之后的所有列,默认选中第一个
+ this.defaultSelector[i] = 0;
+ }
+ // 在历遍的过程中,可能由于上一步修改this.columnData,导致产生连锁反应,程序触发columnChange,会有多次调用
+ // 只有在最后一次数据稳定后的结果是正确的,此前的历遍中,可能会产生undefined,故需要判断
+ columnIndex.map((item, index) => {
+ let data = this.columnData[index][columnIndex[index]];
+ let tmp = {
+ value: data ? data[this.valueName] : null,
+ label: data ? data[this.labelName] : null,
+ };
+ // 判断是否有需要额外携带的参数
+ if(data && data.extra !== undefined) tmp.extra = data.extra;
+ this.selectValue.push(tmp);
+
+ })
+ // 保存这一次的结果,用于下次列发生变化时作比较
+ this.lastSelectIndex = columnIndex;
+ } else if(this.mode == 'single-column') {
+ let data = this.columnData[0][columnIndex[0]];
+ // 初始默认选中值
+ let tmp = {
+ value: data ? data[this.valueName] : null,
+ label: data ? data[this.labelName] : null,
+ };
+ // 判断是否有需要额外携带的参数
+ if(data && data.extra !== undefined) tmp.extra = data.extra;
+ this.selectValue.push(tmp);
+ } else if(this.mode == 'mutil-column') {
+ // 初始默认选中值
+ columnIndex.map((item, index) => {
+ let data = this.columnData[index][columnIndex[index]];
+ // 初始默认选中值
+ let tmp = {
+ value: data ? data[this.valueName] : null,
+ label: data ? data[this.labelName] : null,
+ };
+ // 判断是否有需要额外携带的参数
+ if(data && data.extra !== undefined) tmp.extra = data.extra;
+ this.selectValue.push(tmp);
+ })
+ }
+ },
+ close() {
+ this.$emit('input', false);
+ },
+ // 点击确定或者取消
+ getResult(event = null) {
+ // #ifdef MP-WEIXIN
+ if (this.moving) return;
+ // #endif
+ if (event) this.$emit(event, this.selectValue);
+ this.close();
+ },
+ selectHandler() {
+ this.$emit('click');
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-select {
+
+ &__action {
+ position: relative;
+ line-height: $u-form-item-height;
+ height: $u-form-item-height;
+
+ &__icon {
+ position: absolute;
+ right: 20rpx;
+ top: 50%;
+ transition: transform .4s;
+ transform: translateY(-50%);
+ z-index: 1;
+
+ &--reverse {
+ transform: rotate(-180deg) translateY(50%);
+ }
+ }
+ }
+
+ &__hader {
+ &__title {
+ color: $u-content-color;
+ }
+ }
+
+ &--border {
+ border-radius: 6rpx;
+ border-radius: 4px;
+ border: 1px solid $u-form-item-border-color;
+ }
+
+ &__header {
+ @include vue-flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 80rpx;
+ padding: 0 40rpx;
+ }
+
+ &__body {
+ width: 100%;
+ height: 500rpx;
+ overflow: hidden;
+ background-color: #fff;
+
+ &__picker-view {
+ height: 100%;
+ box-sizing: border-box;
+
+ &__item {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: $u-main-color;
+ padding: 0 8rpx;
+ }
+ }
+ }
+}
+</style>
diff --git a/uview-ui/components/u-skeleton/u-skeleton.vue b/uview-ui/components/u-skeleton/u-skeleton.vue
new file mode 100644
index 0000000..c8cb19e
--- /dev/null
+++ b/uview-ui/components/u-skeleton/u-skeleton.vue
@@ -0,0 +1,199 @@
+<template>
+ <view v-if="loading" :style="{
+ width: windowWinth + 'px',
+ height: windowHeight + 'px',
+ backgroundColor: bgColor,
+ position: 'absolute',
+ left: left + 'px',
+ top: top + 'px',
+ zIndex: 9998,
+ overflow: 'hidden'
+ }"
+ @touchmove.stop.prevent>
+ <view v-for="(item, index) in RectNodes" :key="$u.guid()" :class="[animation ? 'skeleton-fade' : '']" :style="{
+ width: item.width + 'px',
+ height: item.height + 'px',
+ backgroundColor: elColor,
+ position: 'absolute',
+ left: (item.left - left) + 'px',
+ top: (item.top - top) + 'px'
+ }"></view>
+ <view v-for="(item, index) in circleNodes" :key="$u.guid()" :class="animation ? 'skeleton-fade' : ''" :style="{
+ width: item.width + 'px',
+ height: item.height + 'px',
+ backgroundColor: elColor,
+ borderRadius: item.width/2 + 'px',
+ position: 'absolute',
+ left: (item.left - left) + 'px',
+ top: (item.top - top) + 'px'
+ }"></view>
+ <view v-for="(item, index) in filletNodes" :key="$u.guid()" :class="animation ? 'skeleton-fade' : ''" :style="{
+ width: item.width + 'px',
+ height: item.height + 'px',
+ backgroundColor: elColor,
+ borderRadius: borderRadius + 'rpx',
+ position: 'absolute',
+ left: (item.left - left) + 'px',
+ top: (item.top - top) + 'px'
+ }"></view>
+ </view>
+</template>
+
+<script>
+ /**
+ * skeleton 骨架屏
+ * @description 骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。
+ * @tutorial https://www.uviewui.com/components/skeleton.html
+ * @property {String} el-color 骨架块状元素的背景颜色(默认#e5e5e5)
+ * @property {String} bg-color 骨架组件背景颜色(默认#ffffff)
+ * @property {Boolean} animation 骨架块是否显示动画效果(默认false)
+ * @property {String Number} border-radius u-skeleton-fillet类名元素,对应的骨架块的圆角大小,单位rpx(默认10)
+ * @property {Boolean} loading 是否显示骨架组件,请求完成后,将此值设置为false(默认true)
+ * @example <u-skeleton :loading="true" :animation="true"></u-skeleton>
+ */
+ export default {
+ name: "u-skeleton",
+ props: {
+ // 需要渲染的元素背景颜色,十六进制或者rgb等都可以
+ elColor: {
+ type: String,
+ default: '#e5e5e5'
+ },
+ // 整个骨架屏页面的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 是否显示加载动画
+ animation: {
+ type: Boolean,
+ default: false
+ },
+ // 圆角值,只对类名为u-skeleton-fillet的元素生效,为数值,不带单位
+ borderRadius: {
+ type: [String, Number],
+ default: "10"
+ },
+ // 是否显示骨架,true-显示,false-隐藏
+ loading: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ windowWinth: 750, // 骨架屏宽度
+ windowHeight: 1500, // 骨架屏高度
+ filletNodes: [], // 圆角元素
+ circleNodes: [], // 圆形元素
+ RectNodes: [], // 矩形元素
+ top: 0,
+ left: 0,
+ }
+ },
+ methods: {
+ // 查询各节点的信息
+ selecterQueryInfo() {
+ // 获取整个父组件容器的高度,当做骨架屏的高度
+ // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
+ let query = '';
+ // #ifdef MP-WEIXIN
+ query = uni.createSelectorQuery().in(this.$parent);
+ // #endif
+ // #ifndef MP-WEIXIN
+ query = uni.createSelectorQuery()
+ // #endif
+ query.selectAll('.u-skeleton').boundingClientRect().exec((res) => {
+ this.windowHeight = res[0][0].height;
+ this.windowWinth = res[0][0].width;
+ this.top = res[0][0].bottom - res[0][0].height;
+ this.left = res[0][0].left;
+ });
+ // 矩形骨架元素
+ this.getRectEls();
+ // 圆形骨架元素
+ this.getCircleEls();
+ // 圆角骨架元素
+ this.getFilletEls();
+ },
+ // 矩形元素列表
+ getRectEls() {
+ let query = '';
+ // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
+ // #ifdef MP-WEIXIN
+ query = uni.createSelectorQuery().in(this.$parent);
+ // #endif
+ // #ifndef MP-WEIXIN
+ query = uni.createSelectorQuery()
+ // #endif
+ query.selectAll('.u-skeleton-rect').boundingClientRect().exec((res) => {
+ this.RectNodes = res[0];
+ });
+ },
+ // 圆角元素列表
+ getFilletEls() {
+ let query = '';
+ // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
+ // #ifdef MP-WEIXIN
+ query = uni.createSelectorQuery().in(this.$parent);
+ // #endif
+ // #ifndef MP-WEIXIN
+ query = uni.createSelectorQuery()
+ // #endif
+ query.selectAll('.u-skeleton-fillet').boundingClientRect().exec((res) => {
+ this.filletNodes = res[0];
+ });
+ },
+ // 圆形元素列表
+ getCircleEls() {
+ let query = '';
+ // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
+ // #ifdef MP-WEIXIN
+ query = uni.createSelectorQuery().in(this.$parent);
+ // #endif
+ // #ifndef MP-WEIXIN
+ query = uni.createSelectorQuery()
+ // #endif
+ query.selectAll('.u-skeleton-circle').boundingClientRect().exec((res) => {
+ this.circleNodes = res[0];
+ });
+ }
+ },
+ // 组件被挂载
+ mounted() {
+ // 获取系统信息
+ let systemInfo = uni.getSystemInfoSync();
+ this.windowHeight = systemInfo.windowHeight;
+ this.windowWinth = systemInfo.windowWidth;
+ this.selecterQueryInfo();
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .skeleton-fade {
+ width: 100%;
+ height: 100%;
+ background: rgb(194, 207, 214);
+ animation-duration: 1.5s;
+ animation-name: blink;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+ }
+
+ @keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.4;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-slider/u-slider.vue b/uview-ui/components/u-slider/u-slider.vue
new file mode 100644
index 0000000..6d21b92
--- /dev/null
+++ b/uview-ui/components/u-slider/u-slider.vue
@@ -0,0 +1,257 @@
+<template>
+ <view class="u-slider" @tap="onClick" :class="[disabled ? 'u-slider--disabled' : '']" :style="{
+ backgroundColor: inactiveColor
+ }">
+ <view
+ class="u-slider__gap"
+ :style="[
+ barStyle,
+ {
+ height: height + 'rpx',
+ backgroundColor: activeColor
+ }
+ ]"
+ >
+ <view class="u-slider__button-wrap" @touchstart="onTouchStart"
+ @touchmove="onTouchMove" @touchend="onTouchEnd"
+ @touchcancel="onTouchEnd">
+ <slot v-if="$slots.default || $slots.$default"/>
+ <view v-else class="u-slider__button" :style="[blockStyle, {
+ height: blockWidth + 'rpx',
+ width: blockWidth + 'rpx',
+ backgroundColor: blockColor
+ }]"></view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * slider 滑块选择器
+ * @tutorial https://uviewui.com/components/slider.html
+ * @property {Number | String} value 滑块默认值(默认0)
+ * @property {Number | String} min 最小值(默认0)
+ * @property {Number | String} max 最大值(默认100)
+ * @property {Number | String} step 步长(默认1)
+ * @property {Number | String} blockWidth 滑块宽度,高等于宽(30)
+ * @property {Number | String} height 滑块条高度,单位rpx(默认6)
+ * @property {String} inactiveColor 底部条背景颜色(默认#c0c4cc)
+ * @property {String} activeColor 底部选择部分的背景颜色(默认#2979ff)
+ * @property {String} blockColor 滑块颜色(默认#ffffff)
+ * @property {Object} blockStyle 给滑块自定义样式,对象形式
+ * @property {Boolean} disabled 是否禁用滑块(默认为false)
+ * @event {Function} start 滑动触发
+ * @event {Function} moving 正在滑动中
+ * @event {Function} end 滑动结束
+ * @example <u-slider v-model="value" />
+ */
+export default {
+ name: 'u-slider',
+ props: {
+ // 当前进度百分比值,范围0-100
+ value: {
+ type: [Number, String],
+ default: 0
+ },
+ // 是否禁用滑块
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 滑块宽度,高等于宽,单位rpx
+ blockWidth: {
+ type: [Number, String],
+ default: 30
+ },
+ // 最小值
+ min: {
+ type: [Number, String],
+ default: 0
+ },
+ // 最大值
+ max: {
+ type: [Number, String],
+ default: 100
+ },
+ // 步进值
+ step: {
+ type: [Number, String],
+ default: 1
+ },
+ // 滑块条高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 6
+ },
+ // 进度条的激活部分颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 进度条的背景颜色
+ inactiveColor: {
+ type: String,
+ default: '#c0c4cc'
+ },
+ // 滑块的背景颜色
+ blockColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 用户对滑块的自定义颜色
+ blockStyle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ },
+ data() {
+ return {
+ startX: 0,
+ status: 'end',
+ newValue: 0,
+ distanceX: 0,
+ startValue: 0,
+ barStyle: {},
+ sliderRect: {
+ left: 0,
+ width: 0
+ }
+ };
+ },
+ watch: {
+ value(n) {
+ // 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
+ if(this.status == 'end') this.updateValue(this.value, false);
+ }
+ },
+ created() {
+ this.updateValue(this.value, false);
+ },
+ mounted() {
+ // 获取滑块条的尺寸信息
+ this.$uGetRect('.u-slider').then(rect => {
+ this.sliderRect = rect;
+ });
+ },
+ methods: {
+ onTouchStart(event) {
+ if (this.disabled) return;
+ this.startX = 0;
+ // 触摸点集
+ let touches = event.touches[0];
+ // 触摸点到屏幕左边的距离
+ this.startX = touches.clientX;
+ // 此处的this.value虽为props值,但是通过$emit('input')进行了修改
+ this.startValue = this.format(this.value);
+ // 标示当前的状态为开始触摸滑动
+ this.status = 'start';
+ },
+ onTouchMove(event) {
+ if (this.disabled) return;
+ // 连续触摸的过程会一直触发本方法,但只有手指触发且移动了才被认为是拖动了,才发出事件
+ // 触摸后第一次移动已经将status设置为moving状态,故触摸第二次移动不会触发本事件
+ if (this.status == 'start') this.$emit('start');
+ let touches = event.touches[0];
+ // 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
+ this.distanceX = touches.clientX - this.sliderRect.left;
+ // 获得移动距离对整个滑块的百分比值,此为带有多位小数的值,不能用此更新视图
+ // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
+ this.newValue = (this.distanceX / this.sliderRect.width) * 100;
+ this.status = 'moving';
+ // 发出moving事件
+ this.$emit('moving');
+ this.updateValue(this.newValue, true);
+ },
+ onTouchEnd() {
+ if (this.disabled) return;
+ if (this.status === 'moving') {
+ this.updateValue(this.newValue, false);
+ this.$emit('end');
+ }
+ this.status = 'end';
+ },
+ updateValue(value, drag) {
+ // 去掉小数部分,同时也是对step步进的处理
+ const width = this.format(value);
+ // 不允许滑动的值超过max最大值,百分比也不能超过100
+ if(width > this.max || width > 100) return;
+ // 设置移动的百分比值
+ let barStyle = {
+ width: width + '%'
+ };
+ // 移动期间无需过渡动画
+ if (drag == true) {
+ barStyle.transition = 'none';
+ } else {
+ // 非移动期间,删掉对过渡为空的声明,让css中的声明起效
+ delete barStyle.transition;
+ }
+ // 修改value值
+ this.$emit('input', width);
+ this.barStyle = barStyle;
+ },
+ format(value) {
+ // 将小数变成整数,为了减少对视图的更新,造成视图层与逻辑层的阻塞
+ return Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * this.step;
+ },
+ onClick(event) {
+ if (this.disabled) return;
+ // 直接点击滑块的情况,计算方式与onTouchMove方法相同
+ const value = ((event.detail.x - this.sliderRect.left) / this.sliderRect.width) * 100;
+ this.updateValue(value, false);
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-slider {
+ position: relative;
+ border-radius: 999px;
+ border-radius: 999px;
+ background-color: #ebedf0;
+}
+
+.u-slider:before {
+ position: absolute;
+ right: 0;
+ left: 0;
+ content: '';
+ top: -8px;
+ bottom: -8px;
+ z-index: -1;
+}
+
+.u-slider__gap {
+ position: relative;
+ border-radius: inherit;
+ transition: width 0.2s;
+ transition: width 0.2s;
+ background-color: #1989fa;
+}
+
+.u-slider__button {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+ background-color: #fff;
+ cursor: pointer;
+}
+
+.u-slider__button-wrap {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translate3d(50%, -50%, 0);
+}
+
+.u-slider--disabled {
+ opacity: 0.5;
+}
+</style>
diff --git a/uview-ui/components/u-steps/u-steps.vue b/uview-ui/components/u-steps/u-steps.vue
new file mode 100644
index 0000000..a72cfcf
--- /dev/null
+++ b/uview-ui/components/u-steps/u-steps.vue
@@ -0,0 +1,200 @@
+<template>
+ <view class="">
+ <view
+ class="u-steps"
+ :style="{
+ flexDirection: direction
+ }"
+ >
+ <view class="u-steps__item"
+ :class="['u-steps__item--' + direction]"
+ v-for="(item, index) in list" :key="index"
+ >
+ <view
+ class="u-steps__item__num"
+ v-if="mode == 'number'"
+ :style="{
+ backgroundColor: current < index ? 'transparent' : activeColor,
+ borderColor: current < index ? unActiveColor : activeColor
+ }"
+ >
+ <text v-if="current < index" :style="{
+ color: current < index ? unActiveColor : activeColor,
+ }">
+ {{ index + 1 }}
+ </text>
+ <u-icon v-else size="22" color="#ffffff" :name="icon"></u-icon>
+ </view>
+ <view class="u-steps__item__dot" v-if="mode == 'dot'" :style="{
+ backgroundColor: index <= current ? activeColor : unActiveColor
+ }"></view>
+ <text class="u-line-1" :style="{
+ color: index <= current ? activeColor : unActiveColor,
+ }" :class="['u-steps__item__text--' + direction]">
+ {{ item.name }}
+ </text>
+ <view class="u-steps__item__line" :class="['u-steps__item__line--' + mode]" v-if="index < list.length - 1">
+ <u-line :direction="direction" length="100%" :hair-line="false" :color="unActiveColor"></u-line>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * steps 步骤条
+ * @description 该组件一般用于完成一个任务要分几个步骤,标识目前处于第几步的场景。
+ * @tutorial https://www.uviewui.com/components/steps.html
+ * @property {String} mode 设置模式(默认dot)
+ * @property {Array} list 数轴条数据,数组。具体见上方示例
+ * @property {String} type type主题(默认primary)
+ * @property {String} direction row-横向,column-竖向(默认row)
+ * @property {Number String} current 设置当前处于第几步
+ * @property {String} active-color 已完成步骤的激活颜色,如设置,type值会失效
+ * @property {String} un-active-color 未激活的颜色,用于表示未完成步骤的颜色(默认#606266)
+ * @example <u-steps :list="numList" active-color="#fa3534"></u-steps>
+ */
+export default {
+ name: 'u-steps',
+ props: {
+ // 步骤条的类型,dot|number
+ mode: {
+ type: String,
+ default: 'dot'
+ },
+ // 步骤条的数据
+ list: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 主题类型, primary|success|info|warning|error
+ type: {
+ type: String,
+ default: 'primary'
+ },
+ // 当前哪一步是激活的
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 激活步骤的颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 未激活的颜色
+ unActiveColor: {
+ type: String,
+ default: '#909399'
+ },
+ // 自定义图标
+ icon: {
+ type: String,
+ default: 'checkmark'
+ },
+ // step的排列方向,row-横向,column-竖向
+ direction: {
+ type: String,
+ default: 'row'
+ }
+ },
+ data() {
+ return {};
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+@import '../../libs/css/style.components.scss';
+
+$u-steps-item-number-width: 44rpx;
+$u-steps-item-dot-width: 20rpx;
+
+.u-steps {
+ @include vue-flex;
+
+ .u-steps__item {
+ flex: 1;
+ text-align: center;
+ position: relative;
+ min-width: 100rpx;
+ font-size: 26rpx;
+ color: #8799a3;
+ @include vue-flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+
+ &--row {
+ @include vue-flex;
+ flex-direction: column;
+
+ .u-steps__item__line {
+ position: absolute;
+ z-index: 0;
+ left: 75%;
+ width: 50%;
+
+ &--dot {
+ top: calc(#{$u-steps-item-dot-width} / 2);
+ }
+
+ &--number {
+ top: calc(#{$u-steps-item-number-width} / 2);
+ }
+ }
+ }
+
+ &--column {
+ @include vue-flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ min-height: 120rpx;
+
+ .u-steps__item__line {
+ position: absolute;
+ z-index: 0;
+ height: 50%;
+ top: 75%;
+
+ &--dot {
+ left: calc(#{$u-steps-item-dot-width} / 2);
+ }
+
+ &--number {
+ left: calc(#{$u-steps-item-number-width} / 2);
+ }
+ }
+ }
+
+ &__num {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ width: $u-steps-item-number-width;
+ height: $u-steps-item-number-width;
+ border: 1px solid #8799a3;
+ border-radius: 50%;
+ overflow: hidden;
+ }
+
+ &__dot {
+ width: $u-steps-item-dot-width;
+ height: $u-steps-item-dot-width;
+ @include vue-flex;
+ border-radius: 50%;
+ }
+
+ &__text--row {
+ margin-top: 14rpx;
+ }
+
+ &__text--column {
+ margin-left: 14rpx;
+ }
+ }
+}
+</style>
diff --git a/uview-ui/components/u-sticky/u-sticky.vue b/uview-ui/components/u-sticky/u-sticky.vue
new file mode 100644
index 0000000..d9bc34c
--- /dev/null
+++ b/uview-ui/components/u-sticky/u-sticky.vue
@@ -0,0 +1,157 @@
+<template>
+ <view class="">
+ <view class="u-sticky-wrap" :class="[elClass]" :style="{
+ height: fixed ? height + 'px' : 'auto',
+ backgroundColor: bgColor
+ }">
+ <view class="u-sticky" :style="{
+ position: fixed ? 'fixed' : 'static',
+ top: stickyTop + 'px',
+ left: left + 'px',
+ width: width == 'auto' ? 'auto' : width + 'px',
+ zIndex: uZIndex
+ }">
+ <slot></slot>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * sticky 吸顶
+ * @description 该组件与CSS中position: sticky属性实现的效果一致,当组件达到预设的到顶部距离时, 就会固定在指定位置,组件位置大于预设的顶部距离时,会重新按照正常的布局排列。
+ * @tutorial https://www.uviewui.com/components/sticky.html
+ * @property {String Number} offset-top 吸顶时与顶部的距离,单位rpx(默认0)
+ * @property {String Number} index 自定义标识,用于区分是哪一个组件
+ * @property {Boolean} enable 是否开启吸顶功能(默认true)
+ * @property {String} bg-color 组件背景颜色(默认#ffffff)
+ * @property {String Number} z-index 吸顶时的z-index值(默认970)
+ * @property {String Number} h5-nav-height 导航栏高度,自定义导航栏时(无导航栏时需设置为0),需要传入此值,单位px(默认44)
+ * @event {Function} fixed 组件吸顶时触发
+ * @event {Function} unfixed 组件取消吸顶时触发
+ * @example <u-sticky offset-top="200"><view>塞下秋来风景异,衡阳雁去无留意</view></u-sticky>
+ */
+ export default {
+ name: "u-sticky",
+ props: {
+ // 吸顶容器到顶部某个距离的时候,进行吸顶,在H5平台,NavigationBar为44px
+ offsetTop: {
+ type: [Number, String],
+ default: 0
+ },
+ //列表中的索引值
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 是否开启吸顶功能
+ enable: {
+ type: Boolean,
+ default: true
+ },
+ // h5顶部导航栏的高度
+ h5NavHeight: {
+ type: [Number, String],
+ default: 44
+ },
+ // 吸顶区域的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // z-index值
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ }
+ },
+ data() {
+ return {
+ fixed: false,
+ height: 'auto',
+ stickyTop: 0,
+ elClass: this.$u.guid(),
+ left: 0,
+ width: 'auto',
+ };
+ },
+ watch: {
+ offsetTop(val) {
+ this.initObserver();
+ },
+ enable(val) {
+ if (val == false) {
+ this.fixed = false;
+ this.disconnectObserver('contentObserver');
+ } else {
+ this.initObserver();
+ }
+ }
+ },
+ computed: {
+ uZIndex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.sticky;
+ }
+ },
+ mounted() {
+ this.initObserver();
+ },
+ methods: {
+ initObserver() {
+ if (!this.enable) return;
+ // #ifdef H5
+ this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) + this.h5NavHeight : this.h5NavHeight;
+ // #endif
+ // #ifndef H5
+ this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) : 0;
+ // #endif
+
+ this.disconnectObserver('contentObserver');
+ this.$uGetRect('.' + this.elClass).then((res) => {
+ this.height = res.height;
+ this.left = res.left;
+ this.width = res.width;
+ this.$nextTick(() => {
+ this.observeContent();
+ });
+ });
+ },
+ observeContent() {
+ this.disconnectObserver('contentObserver');
+ const contentObserver = this.createIntersectionObserver({
+ thresholds: [0.95, 0.98, 1]
+ });
+ contentObserver.relativeToViewport({
+ top: -this.stickyTop
+ });
+ contentObserver.observe('.' + this.elClass, res => {
+ if (!this.enable) return;
+ this.setFixed(res.boundingClientRect.top);
+ });
+ this.contentObserver = contentObserver;
+ },
+ setFixed(top) {
+ const fixed = top < this.stickyTop;
+ if (fixed) this.$emit('fixed', this.index);
+ else if(this.fixed) this.$emit('unfixed', this.index);
+ this.fixed = fixed;
+ },
+ disconnectObserver(observerName) {
+ const observer = this[observerName];
+ observer && observer.disconnect();
+ },
+ },
+ beforeDestroy() {
+ this.disconnectObserver('contentObserver');
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ .u-sticky {
+ z-index: 9999999999;
+ }
+</style>
diff --git a/uview-ui/components/u-subsection/u-subsection.vue b/uview-ui/components/u-subsection/u-subsection.vue
new file mode 100644
index 0000000..b8fbd79
--- /dev/null
+++ b/uview-ui/components/u-subsection/u-subsection.vue
@@ -0,0 +1,355 @@
+<template>
+ <view class="u-subsection" :style="[subsectionStyle]">
+ <view class="u-item u-line-1" :style="[itemStyle(index)]" @tap="click(index)" :class="[noBorderRight(index), 'u-item-' + index]"
+ v-for="(item, index) in listInfo" :key="index">
+ <view :style="[textStyle(index)]" class="u-item-text u-line-1">{{ item.name }}</view>
+ </view>
+ <view class="u-item-bg" :style="[itemBarStyle]"></view>
+ </view>
+</template>
+
+<script>
+ /**
+ * subsection 分段器
+ * @description 该分段器一般用于用户从几个选项中选择某一个的场景
+ * @tutorial https://www.uviewui.com/components/subsection.html
+ * @property {Array} list 选项的数组,形式见上方"基本使用"
+ * @property {String Number} current 初始化时默认选中的选项索引值(默认0)
+ * @property {String} active-color 激活时的颜色,mode为subsection时固定为白色(默认#303133)
+ * @property {String} inactive-color 未激活时字体的颜色,mode为subsection时无效(默认#606266)
+ * @property {String} mode 模式选择,见官网"模式选择"说明(默认button)
+ * @property {String Number} font-size 字体大小,单位rpx(默认28)
+ * @property {String Number} height 组件高度,单位rpx(默认70)
+ * @property {Boolean} animation 是否开启动画效果,见上方说明(默认true)
+ * @property {Boolean} bold 激活选项的字体是否加粗(默认true)
+ * @property {String} bg-color 组件背景颜色,mode为button时有效(默认#eeeeef)
+ * @property {String} button-color 按钮背景颜色,mode为button时有效(默认#ffffff)
+ * @event {Function} change 分段器选项发生改变时触发
+ * @example <u-subsection active-color="#ff9900"></u-subsection>
+ */
+ export default {
+ name: "u-subsection",
+ props: {
+ // tab的数据
+ list: {
+ type: Array,
+ default () {
+ return [];
+ }
+ },
+ // 当前活动的tab的index
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 激活的颜色
+ activeColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 未激活的颜色
+ inactiveColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 模式选择,mode=button为按钮形式,mode=subsection时为分段模式
+ mode: {
+ type: String,
+ default: 'button'
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 28
+ },
+ // 是否开启动画效果
+ animation: {
+ type: Boolean,
+ default: true
+ },
+ // 组件的高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 70
+ },
+ // 激活tab的字体是否加粗
+ bold: {
+ type: Boolean,
+ default: true
+ },
+ // mode=button时,组件背景颜色
+ bgColor: {
+ type: String,
+ default: '#eeeeef'
+ },
+ // mode = button时,滑块背景颜色
+ buttonColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 在切换分段器的时候,是否让设备震一下
+ vibrateShort: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ listInfo: [],
+ itemBgStyle: {
+ width: 0,
+ left: 0,
+ backgroundColor: '#ffffff',
+ height: '100%',
+ transition: ''
+ },
+ currentIndex: this.current,
+ buttonPadding: 3, // mode = button 时,组件的内边距
+ borderRadius: 5, // 圆角值
+ firstTimeVibrateShort: true // 组件初始化时,会触发current变化,此时不应震动
+ };
+ },
+ watch: {
+ current: {
+ immediate: true,
+ handler(nVal) {
+ this.currentIndex = nVal;
+ this.changeSectionStatus(nVal);
+ }
+ }
+ },
+ created() {
+ // 将list的数据,传入listInfo数组,因为不能修改props传递的list值
+ // 可以接受直接数组形式,或者数组元素为对象的形式,如:['简介', '评论'],或者[{name: '简介'}, {name: '评论'}]
+ this.listInfo = this.list.map((val, index) => {
+ if (typeof val != 'object') {
+ let obj = {
+ width: 0,
+ name: val
+ };
+ return obj;
+ } else {
+ val.width = 0;
+ return val;
+ }
+ });
+ },
+ computed: {
+ // 设置mode=subsection时,滑块特有的样式
+ noBorderRight() {
+ return index => {
+ if (this.mode != 'subsection') return;
+ let classs = '';
+ // 不显示右边的边框
+ if (index < this.list.length - 1) classs += ' u-none-border-right';
+ // 显示整个组件的左右边圆角
+ if (index == 0) classs += ' u-item-first';
+ if (index == this.list.length - 1) classs += ' u-item-last';
+ return classs;
+ };
+ },
+ // 文字的样式
+ textStyle() {
+ return index => {
+ let style = {};
+ // 设置字体颜色
+ if (this.mode == 'subsection') {
+ if (index == this.currentIndex) {
+ style.color = '#ffffff';
+ } else {
+ style.color = this.activeColor;
+ }
+ } else {
+ if (index == this.currentIndex) {
+ style.color = this.activeColor;
+ } else {
+ style.color = this.inactiveColor;
+ }
+ }
+ // 字体加粗
+ if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
+ // 文字大小
+ style.fontSize = this.fontSize + 'rpx';
+ return style;
+ };
+ },
+ // 每个分段器item的样式
+ itemStyle() {
+ return index => {
+ let style = {};
+ if (this.mode == 'subsection') {
+ // 设置border的样式
+ style.borderColor = this.activeColor;
+ style.borderWidth = '1px';
+ style.borderStyle = 'solid';
+ }
+ return style;
+ };
+ },
+ // mode=button时,外层view的样式
+ subsectionStyle() {
+ let style = {};
+ style.height = uni.upx2px(this.height) + 'px';
+ if (this.mode == 'button') {
+ style.backgroundColor = this.bgColor;
+ style.padding = `${this.buttonPadding}px`;
+ style.borderRadius = `${this.borderRadius}px`;
+ }
+ return style;
+ },
+ // 滑块的样式
+ itemBarStyle() {
+ let style = {};
+ style.backgroundColor = this.activeColor;
+ style.zIndex = 1;
+ if (this.mode == 'button') {
+ style.backgroundColor = this.buttonColor;
+ style.borderRadius = `${this.borderRadius}px`;
+ style.bottom = `${this.buttonPadding}px`;
+ style.height = uni.upx2px(this.height) - this.buttonPadding * 2 + 'px';
+ style.zIndex = 0;
+ }
+ return Object.assign(this.itemBgStyle, style);
+ }
+ },
+ mounted() {
+ setTimeout(() => {
+ this.getTabsInfo();
+ }, 10);
+ },
+ methods: {
+ // 改变滑块的样式
+ changeSectionStatus(nVal) {
+ if (this.mode == 'subsection') {
+ // 根据滑块在最左边和最右边时,显示左边和右边的圆角
+ if (nVal == this.list.length - 1) {
+ this.itemBgStyle.borderRadius = `0 ${this.buttonPadding}px ${this.buttonPadding}px 0`;
+ }
+ if (nVal == 0) {
+ this.itemBgStyle.borderRadius = `${this.buttonPadding}px 0 0 ${this.buttonPadding}px`;
+ }
+ if (nVal > 0 && nVal < this.list.length - 1) {
+ this.itemBgStyle.borderRadius = '0';
+ }
+ }
+ // 更新滑块的位置
+ setTimeout(() => {
+ this.itemBgLeft();
+ }, 10);
+ if (this.vibrateShort && !this.firstTimeVibrateShort) {
+ // 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
+ // #ifndef H5
+ uni.vibrateShort();
+ // #endif
+ }
+ // 第一次过后,设置firstTimeVibrateShort为false,让其下一次可以震动(如果允许震动的话)
+ this.firstTimeVibrateShort = false;
+ },
+ click(index) {
+ // 不允许点击当前激活选项
+ if (index == this.currentIndex) return;
+ this.currentIndex = index;
+ this.changeSectionStatus(index);
+ this.$emit('change', Number(index));
+ },
+ // 获取各个tab的节点信息
+ getTabsInfo() {
+ let view = uni.createSelectorQuery().in(this);
+ for (let i = 0; i < this.list.length; i++) {
+ view.select('.u-item-' + i).boundingClientRect();
+ }
+ view.exec(res => {
+ if (!res.length) {
+ setTimeout(() => {
+ this.getTabsInfo();
+ return;
+ }, 10);
+ }
+ // 将分段器每个item的宽度,放入listInfo数组
+ res.map((val, index) => {
+ this.listInfo[index].width = val.width;
+ });
+ // 初始化滑块的宽度
+ if (this.mode == 'subsection') {
+ this.itemBgStyle.width = this.listInfo[0].width + 'px';
+ } else if (this.mode == 'button') {
+ this.itemBgStyle.width = this.listInfo[0].width + 'px';
+ }
+ // 初始化滑块的位置
+ this.itemBgLeft();
+ });
+ },
+ itemBgLeft() {
+ // 根据是否开启动画效果,
+ if (this.animation) {
+ this.itemBgStyle.transition = 'all 0.35s';
+ } else {
+ this.itemBgStyle.transition = 'all 0s';
+ }
+ let left = 0;
+ // 计算当前活跃item到组件左边的距离
+ this.listInfo.map((val, index) => {
+ if (index < this.currentIndex) left += val.width;
+ });
+ // 根据mode不同模式,计算滑块需要移动的距离
+ if (this.mode == 'subsection') {
+ this.itemBgStyle.left = left + 'px';
+ } else if (this.mode == 'button') {
+ this.itemBgStyle.left = left + this.buttonPadding + 'px';
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-subsection {
+ @include vue-flex;
+ align-items: center;
+ overflow: hidden;
+ position: relative;
+ }
+
+ .u-item {
+ flex: 1;
+ text-align: center;
+ font-size: 26rpx;
+ height: 100%;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ color: $u-main-color;
+ padding: 0 6rpx;
+ }
+
+ .u-item-bg {
+ background-color: $u-type-primary;
+ position: absolute;
+ z-index: -1;
+ }
+
+ .u-none-border-right {
+ border-right: none !important;
+ }
+
+ .u-item-first {
+ border-top-left-radius: 8rpx;
+ border-bottom-left-radius: 8rpx;
+ }
+
+ .u-item-last {
+ border-top-right-radius: 8rpx;
+ border-bottom-right-radius: 8rpx;
+ }
+
+ .u-item-text {
+ transition: all 0.35s;
+ color: $u-main-color;
+ @include vue-flex;
+ align-items: center;
+ position: relative;
+ z-index: 3;
+ }
+</style>
diff --git a/uview-ui/components/u-swipe-action/u-swipe-action.vue b/uview-ui/components/u-swipe-action/u-swipe-action.vue
new file mode 100644
index 0000000..13fb33c
--- /dev/null
+++ b/uview-ui/components/u-swipe-action/u-swipe-action.vue
@@ -0,0 +1,255 @@
+<template>
+ <view class="">
+ <movable-area class="u-swipe-action" :style="{ backgroundColor: bgColor }">
+ <movable-view
+ class="u-swipe-view"
+ @change="change"
+ @touchend="touchend"
+ @touchstart="touchstart"
+ direction="horizontal"
+ :disabled="disabled"
+ :x="moveX"
+ :style="{
+ width: movableViewWidth ? movableViewWidth : '100%'
+ }"
+ >
+ <view
+ class="u-swipe-content"
+ @tap.stop="contentClick"
+ >
+ <slot></slot>
+ </view>
+ <view class="u-swipe-del" v-if="showBtn" @tap.stop="btnClick(index)" :style="[btnStyle(item.style)]" v-for="(item, index) in options" :key="index">
+ <view class="u-btn-text">{{ item.text }}</view>
+ </view>
+ </movable-view>
+ </movable-area>
+ </view>
+</template>
+
+<script>
+/**
+ * swipeAction 左滑单元格
+ * @description 该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。
+ * @tutorial https://www.uviewui.com/components/swipeAction.html
+ * @property {String} bg-color 整个组件背景颜色(默认#ffffff)
+ * @property {Array} options 数组形式,可以配置背景颜色和文字
+ * @property {String Number} index 标识符,点击时候用于区分点击了哪一个,用v-for循环时的index即可
+ * @property {String Number} btn-width 按钮宽度,单位rpx(默认180)
+ * @property {Boolean} disabled 是否禁止某个swipeAction滑动(默认false)
+ * @property {Boolean} show 打开或者关闭某个组件(默认false)
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 组件触发关闭状态时
+ * @event {Function} content-click 点击内容时触发
+ * @event {Function} open 组件触发打开状态时
+ * @example <u-swipe-action btn-text="收藏">...</u-swipe-action>
+ */
+export default {
+ name: 'u-swipe-action',
+ props: {
+ // index值,用于得知点击删除的是哪个按钮
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 滑动按钮的宽度,单位为rpx
+ btnWidth: {
+ type: [String, Number],
+ default: 180
+ },
+ // 是否禁止某个action滑动
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 打开或者关闭组件
+ show: {
+ type: Boolean,
+ default: false
+ },
+ // 组件背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 是否使手机发生短促震动,目前只在iOS的微信小程序有效(2020-05-06)
+ vibrateShort: {
+ type: Boolean,
+ default: false
+ },
+ // 按钮操作参数
+ options: {
+ type: Array,
+ default() {
+ return [];
+ }
+ }
+ },
+ watch: {
+ show: {
+ immediate: true,
+ handler(nVal, oVal) {
+ if (nVal) {
+ this.open();
+ } else {
+ this.close();
+ }
+ }
+ }
+ },
+ data() {
+ return {
+ moveX: 0, // movable-view元素在x轴上需要移动的目标移动距离,用于展开或收起滑动的按钮
+ scrollX: 0, // movable-view移动过程中产生的change事件中的x轴移动值
+ status: false, // 滑动的状态,表示当前是展开还是关闭按钮的状态
+ movableAreaWidth: 0, // 滑动区域
+ elId: this.$u.guid(), // id,用于通知另外组件关闭时的识别
+ showBtn: false, // 刚开始渲染视图时不显示右边的按钮,避免视图闪动
+ };
+ },
+ computed: {
+ movableViewWidth() {
+ return this.movableAreaWidth + this.allBtnWidth + 'px';
+ },
+ innerBtnWidth() {
+ return uni.upx2px(this.btnWidth);
+ },
+ allBtnWidth() {
+ return uni.upx2px(this.btnWidth) * this.options.length;
+ },
+ btnStyle() {
+ return style => {
+ let css = {};
+ style.width = this.btnWidth + 'rpx';
+ return style;
+ };
+ }
+ },
+ mounted() {
+ this.getActionRect();
+ },
+ methods: {
+ // 点击按钮
+ btnClick(index) {
+ this.status = false;
+ // this.index为点击的几个组件,index为点击某个组件的第几个按钮(options数组的索引)
+ this.$emit('click', this.index, index);
+ },
+ // movable-view元素移动事件
+ change(e) {
+ this.scrollX = e.detail.x;
+ },
+ // 关闭按钮状态
+ close() {
+ this.moveX = 0;
+ this.status = false;
+ },
+ // 打开按钮的状态
+ open() {
+ if (this.disabled) return;
+ this.moveX = -this.allBtnWidth;
+ this.status = true;
+ },
+ // 用户手指离开movable-view元素,停止触摸
+ touchend() {
+ this.moveX = this.scrollX;
+ // 停止触摸时候,判断当前是展开还是关闭状态
+ // 关闭状态
+ // 这一步很重要,需要先给this.moveX一个变化的随机值,否则因为前后设置的为同一个值
+ // props单向数据流的原因,导致movable-view元素不会发生变化,切记,详见文档:
+ // https://uniapp.dcloud.io/use?id=%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98
+ this.$nextTick(function() {
+ if (this.status == false) {
+ // 关闭状态左滑,产生的x轴位移为负值,也就是说滑动的距离大于按钮的四分之一宽度,自动展开按钮
+ if (this.scrollX <= -this.allBtnWidth / 4) {
+ this.moveX = -this.allBtnWidth; // 按钮宽度的负值,即为展开状态movable-view元素左滑的距离
+ this.status = true; // 标志当前为展开状态
+ this.emitOpenEvent();
+ // 产生震动效果
+ if (this.vibrateShort) uni.vibrateShort();
+ } else {
+ this.moveX = 0; // 如果距离没有按钮宽度的四分之一,自动收起
+ this.status = false;
+ this.emitCloseEvent();
+ }
+ } else {
+ // 如果在打开的状态下,右滑动的距离X轴偏移超过按钮的四分之一(负值反过来的四分之三),自动收起按钮
+ if (this.scrollX > (-this.allBtnWidth * 3) / 4) {
+ this.moveX = 0;
+ this.$nextTick(() => {
+ this.moveX = 101;
+ });
+ this.status = false;
+ this.emitCloseEvent();
+ } else {
+ this.moveX = -this.allBtnWidth;
+ this.status = true;
+ this.emitOpenEvent();
+ }
+ }
+ });
+ },
+ emitOpenEvent() {
+ this.$emit('open', this.index);
+ },
+ emitCloseEvent() {
+ this.$emit('close', this.index);
+ },
+ // 开始触摸
+ touchstart() {},
+ getActionRect() {
+ this.$uGetRect('.u-swipe-action').then(res => {
+ this.movableAreaWidth = res.width;
+ // 等视图更新完后,再显示右边的可滑动按钮,防止这些按钮会"闪一下"
+ this.$nextTick(() => {
+ this.showBtn = true;
+ })
+ });
+ },
+ // 点击内容触发事件
+ contentClick() {
+ // 点击内容时,如果当前为打开状态,收起组件
+ if (this.status == true) {
+ this.status = 'close';
+ this.moveX = 0;
+ }
+ this.$emit('content-click', this.index);
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-swipe-action {
+ width: auto;
+ height: initial;
+ position: relative;
+ overflow: hidden;
+}
+
+.u-swipe-view {
+ @include vue-flex;
+ height: initial;
+ position: relative;
+ /* 这一句很关键,覆盖默认的绝对定位 */
+}
+
+.u-swipe-content {
+ flex: 1;
+}
+
+.u-swipe-del {
+ position: relative;
+ font-size: 30rpx;
+ color: #ffffff;
+}
+
+.u-btn-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+</style>
diff --git a/uview-ui/components/u-swiper/u-swiper.vue b/uview-ui/components/u-swiper/u-swiper.vue
new file mode 100644
index 0000000..b6e7e81
--- /dev/null
+++ b/uview-ui/components/u-swiper/u-swiper.vue
@@ -0,0 +1,340 @@
+<template>
+ <view class="u-swiper-wrap" :style="{
+ borderRadius: `${borderRadius}rpx`
+ }">
+ <swiper :current="elCurrent" @change="change" @animationfinish="animationfinish" :interval="interval" :circular="circular" :duration="duration" :autoplay="autoplay"
+ :previous-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'" :next-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
+ :style="{
+ height: height + 'rpx',
+ backgroundColor: bgColor
+ }">
+ <swiper-item class="u-swiper-item" v-for="(item, index) in list" :key="index">
+ <view class="u-list-image-wrap" @tap.stop.prevent="listClick(index)" :class="[uCurrent != index ? 'u-list-scale' : '']" :style="{
+ borderRadius: `${borderRadius}rpx`,
+ transform: effect3d && uCurrent != index ? 'scaleY(0.9)' : 'scaleY(1)',
+ margin: effect3d && uCurrent != index ? '0 20rpx' : 0,
+ }">
+ <image class="u-swiper-image" :src="item[name] || item" :mode="imgMode"></image>
+ <view v-if="title && item.title" class="u-swiper-title u-line-1" :style="[{
+ 'padding-bottom': titlePaddingBottom
+ }, titleStyle]">
+ {{ item.title }}
+ </view>
+ </view>
+ </swiper-item>
+ </swiper>
+ <view class="u-swiper-indicator" :style="{
+ top: indicatorPos == 'topLeft' || indicatorPos == 'topCenter' || indicatorPos == 'topRight' ? '12rpx' : 'auto',
+ bottom: indicatorPos == 'bottomLeft' || indicatorPos == 'bottomCenter' || indicatorPos == 'bottomRight' ? '12rpx' : 'auto',
+ justifyContent: justifyContent,
+ padding: `0 ${effect3d ? '74rpx' : '24rpx'}`
+ }">
+ <block v-if="mode == 'rect'">
+ <view class="u-indicator-item-rect" :class="{ 'u-indicator-item-rect-active': index == uCurrent }" v-for="(item, index) in list"
+ :key="index"></view>
+ </block>
+ <block v-if="mode == 'dot'">
+ <view class="u-indicator-item-dot" :class="{ 'u-indicator-item-dot-active': index == uCurrent }" v-for="(item, index) in list"
+ :key="index"></view>
+ </block>
+ <block v-if="mode == 'round'">
+ <view class="u-indicator-item-round" :class="{ 'u-indicator-item-round-active': index == uCurrent }" v-for="(item, index) in list"
+ :key="index"></view>
+ </block>
+ <block v-if="mode == 'number'">
+ <view class="u-indicator-item-number">{{ uCurrent + 1 }}/{{ list.length }}</view>
+ </block>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * swiper 轮播图
+ * @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用
+ * @tutorial https://www.uviewui.com/components/swiper.html
+ * @property {Array} list 轮播图数据,见官网"基本使用"说明
+ * @property {Boolean} title 是否显示标题文字,需要配合list参数,见官网说明(默认false)
+ * @property {String} mode 指示器模式,见官网说明(默认round)
+ * @property {String Number} height 轮播图组件高度,单位rpx(默认250)
+ * @property {String} indicator-pos 指示器的位置(默认bottomCenter)
+ * @property {Boolean} effect3d 是否开启3D效果(默认false)
+ * @property {Boolean} autoplay 是否自动播放(默认true)
+ * @property {String Number} interval 自动轮播时间间隔,单位ms(默认2500)
+ * @property {Boolean} circular 是否衔接播放,见官网说明(默认true)
+ * @property {String} bg-color 背景颜色(默认#f3f4f6)
+ * @property {String Number} border-radius 轮播图圆角值,单位rpx(默认8)
+ * @property {Object} title-style 自定义标题样式
+ * @property {String Number} effect3d-previous-margin mode = true模式的情况下,激活项与前后项之间的距离,单位rpx(默认50)
+ * @property {String} img-mode 图片的裁剪模式,详见image组件裁剪模式(默认aspectFill)
+ * @event {Function} click 点击轮播图时触发
+ * @example <u-swiper :list="list" mode="dot" indicator-pos="bottomRight"></u-swiper>
+ */
+ export default {
+ name: "u-swiper",
+ props: {
+ // 轮播图的数据,格式如:[{image: 'xxxx', title: 'xxxx'},{image: 'yyyy', title: 'yyyy'}],其中title字段可选
+ list: {
+ type: Array,
+ default () {
+ return [];
+ }
+ },
+ // 是否显示title标题
+ title: {
+ type: Boolean,
+ default: false
+ },
+ // 用户自定义的指示器的样式
+ indicator: {
+ type: Object,
+ default () {
+ return {};
+ }
+ },
+ // 圆角值
+ borderRadius: {
+ type: [Number, String],
+ default: 8
+ },
+ // 隔多久自动切换
+ interval: {
+ type: [String, Number],
+ default: 3000
+ },
+ // 指示器的模式,rect|dot|number|round
+ mode: {
+ type: String,
+ default: 'round'
+ },
+ // list的高度,单位rpx
+ height: {
+ type: [Number, String],
+ default: 250
+ },
+ // 指示器的位置,topLeft|topCenter|topRight|bottomLeft|bottomCenter|bottomRight
+ indicatorPos: {
+ type: String,
+ default: 'bottomCenter'
+ },
+ // 是否开启缩放效果
+ effect3d: {
+ type: Boolean,
+ default: false
+ },
+ // 3D模式的情况下,激活item与前后item之间的距离,单位rpx
+ effect3dPreviousMargin: {
+ type: [Number, String],
+ default: 50
+ },
+ // 是否自动播放
+ autoplay: {
+ type: Boolean,
+ default: true
+ },
+ // 自动轮播时间间隔,单位ms
+ duration: {
+ type: [Number, String],
+ default: 500
+ },
+ // 是否衔接滑动,即到最后一张时接着滑动,是否自动切换到第一张
+ circular: {
+ type: Boolean,
+ default: true
+ },
+ // 图片的裁剪模式
+ imgMode: {
+ type: String,
+ default: 'aspectFill'
+ },
+ // 从list数组中读取的图片的属性名
+ name: {
+ type: String,
+ default: 'image'
+ },
+ // 背景颜色
+ bgColor: {
+ type: String,
+ default: '#f3f4f6'
+ },
+ // 初始化时,默认显示第几项
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 标题的样式,对象形式
+ titleStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ }
+ },
+ watch: {
+ // 如果外部的list发生变化,判断长度是否被修改,如果前后长度不一致,重置uCurrent值,避免溢出
+ list(nVal, oVal) {
+ if(nVal.length !== oVal.length) this.uCurrent = 0;
+ },
+ // 监听外部current的变化,实时修改内部依赖于此测uCurrent值,如果更新了current,而不是更新uCurrent,
+ // 就会错乱,因为指示器是依赖于uCurrent的
+ current(n) {
+ this.uCurrent = n;
+ }
+ },
+ data() {
+ return {
+ uCurrent: this.current // 当前活跃的swiper-item的index
+ };
+ },
+ computed: {
+ justifyContent() {
+ if (this.indicatorPos == 'topLeft' || this.indicatorPos == 'bottomLeft') return 'flex-start';
+ if (this.indicatorPos == 'topCenter' || this.indicatorPos == 'bottomCenter') return 'center';
+ if (this.indicatorPos == 'topRight' || this.indicatorPos == 'bottomRight') return 'flex-end';
+ },
+ titlePaddingBottom() {
+ let tmp = 0;
+ if (this.mode == 'none') return '12rpx';
+ if (['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode == 'number') {
+ tmp = '60rpx';
+ } else if (['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode != 'number') {
+ tmp = '40rpx';
+ } else {
+ tmp = '12rpx';
+ }
+ return tmp;
+ },
+ // 因为uni的swiper组件的current参数只接受Number类型,这里做一个转换
+ elCurrent() {
+ return Number(this.current);
+ }
+ },
+ methods: {
+ listClick(index) {
+ this.$emit('click', index);
+ },
+ change(e) {
+ let current = e.detail.current;
+ this.uCurrent = current;
+ // 发出change事件,表示当前自动切换的index,从0开始
+ this.$emit('change', current);
+ },
+ // 头条小程序不支持animationfinish事件,改由change事件
+ // 暂不监听此事件,因为不再给swiper绑定uCurrent属性
+ animationfinish(e) {
+ // #ifndef MP-TOUTIAO
+ // this.uCurrent = e.detail.current;
+ // #endif
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-swiper-wrap {
+ position: relative;
+ overflow: hidden;
+ transform: translateY(0);
+ }
+
+ .u-swiper-image {
+ width: 100%;
+ will-change: transform;
+ height: 100%;
+ /* #ifndef APP-NVUE */
+ display: block;
+ /* #endif */
+ /* #ifdef H5 */
+ pointer-events: none;
+ /* #endif */
+ }
+
+ .u-swiper-indicator {
+ padding: 0 24rpx;
+ position: absolute;
+ @include vue-flex;
+ width: 100%;
+ z-index: 1;
+ }
+
+ .u-indicator-item-rect {
+ width: 26rpx;
+ height: 8rpx;
+ margin: 0 6rpx;
+ transition: all 0.5s;
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+
+ .u-indicator-item-rect-active {
+ background-color: rgba(255, 255, 255, 0.8);
+ }
+
+ .u-indicator-item-dot {
+ width: 14rpx;
+ height: 14rpx;
+ margin: 0 6rpx;
+ border-radius: 20rpx;
+ transition: all 0.5s;
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+
+ .u-indicator-item-dot-active {
+ background-color: rgba(255, 255, 255, 0.8);
+ }
+
+ .u-indicator-item-round {
+ width: 14rpx;
+ height: 14rpx;
+ margin: 0 6rpx;
+ border-radius: 20rpx;
+ transition: all 0.5s;
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+
+ .u-indicator-item-round-active {
+ width: 34rpx;
+ background-color: rgba(255, 255, 255, 0.8);
+ }
+
+ .u-indicator-item-number {
+ padding: 6rpx 16rpx;
+ line-height: 1;
+ background-color: rgba(0, 0, 0, 0.3);
+ border-radius: 100rpx;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.8);
+ }
+
+ .u-list-scale {
+ transform-origin: center center;
+ }
+
+ .u-list-image-wrap {
+ width: 100%;
+ height: 100%;
+ flex: 1;
+ transition: all 0.5s;
+ overflow: hidden;
+ box-sizing: content-box;
+ position: relative;
+ }
+
+ .u-swiper-title {
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0.3);
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ font-size: 28rpx;
+ padding: 12rpx 24rpx;
+ color: rgba(255, 255, 255, 0.9);
+ }
+
+ .u-swiper-item {
+ @include vue-flex;
+ overflow: hidden;
+ align-items: center;
+ }
+</style>
diff --git a/uview-ui/components/u-switch/u-switch.vue b/uview-ui/components/u-switch/u-switch.vue
new file mode 100644
index 0000000..dd8e4bb
--- /dev/null
+++ b/uview-ui/components/u-switch/u-switch.vue
@@ -0,0 +1,163 @@
+<template>
+ <view class="u-switch" :class="[value == true ? 'u-switch--on' : '', disabled ? 'u-switch--disabled' : '']" @tap="onClick"
+ :style="[switchStyle]">
+ <view class="u-switch__node node-class" :style="{
+ width: $u.addUnit(this.size),
+ height: $u.addUnit(this.size)
+ }">
+ <u-loading :show="loading" class="u-switch__loading" :size="size * 0.6" :color="loadingColor" />
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * switch 开关选择器
+ * @description 选择开关一般用于只有两个选择,且只能选其一的场景。
+ * @tutorial https://www.uviewui.com/components/switch.html
+ * @property {Boolean} loading 是否处于加载中(默认false)
+ * @property {Boolean} disabled 是否禁用(默认false)
+ * @property {String Number} size 开关尺寸,单位rpx(默认50)
+ * @property {String} active-color 打开时的背景色(默认#2979ff)
+ * @property {Boolean} inactive-color 关闭时的背景色(默认#ffffff)
+ * @property {Boolean | Number | String} active-value 打开选择器时通过change事件发出的值(默认true)
+ * @property {Boolean | Number | String} inactive-value 关闭选择器时通过change事件发出的值(默认false)
+ * @event {Function} change 在switch打开或关闭时触发
+ * @example <u-switch v-model="checked" active-color="red" inactive-color="#eee"></u-switch>
+ */
+ export default {
+ name: "u-switch",
+ props: {
+ // 是否为加载中状态
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ // 是否为禁用装填
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 开关尺寸,单位rpx
+ size: {
+ type: [Number, String],
+ default: 50
+ },
+ // 打开时的背景颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 关闭时的背景颜色
+ inactiveColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 通过v-model双向绑定的值
+ value: {
+ type: Boolean,
+ default: false
+ },
+ // 是否使手机发生短促震动,目前只在iOS的微信小程序有效(2020-05-06)
+ vibrateShort: {
+ type: Boolean,
+ default: false
+ },
+ // 打开选择器时的值
+ activeValue: {
+ type: [Number, String, Boolean],
+ default: true
+ },
+ // 关闭选择器时的值
+ inactiveValue: {
+ type: [Number, String, Boolean],
+ default: false
+ },
+ },
+ data() {
+ return {
+
+ }
+ },
+ computed: {
+ switchStyle() {
+ let style = {};
+ style.fontSize = this.size + 'rpx';
+ style.backgroundColor = this.value ? this.activeColor : this.inactiveColor;
+ return style;
+ },
+ loadingColor() {
+ return this.value ? this.activeColor : null;
+ }
+ },
+ methods: {
+ onClick() {
+ if (!this.disabled && !this.loading) {
+ // 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
+ if(this.vibrateShort) uni.vibrateShort();
+ this.$emit('input', !this.value);
+ // 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
+ this.$nextTick(() => {
+ this.$emit('change', this.value ? this.activeValue : this.inactiveValue);
+ })
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-switch {
+ position: relative;
+ /* #ifndef APP-NVUE */
+ display: inline-block;
+ /* #endif */
+ box-sizing: initial;
+ width: 2em;
+ height: 1em;
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 1em;
+ transition: background-color 0.3s;
+ font-size: 50rpx;
+ }
+
+ .u-switch__node {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-radius: 100%;
+ z-index: 1;
+ background-color: #fff;
+ background-color: #fff;
+ box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05);
+ box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05);
+ transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
+ transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05), -webkit-transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
+ transition: transform cubic-bezier(0.3, 1.05, 0.4, 1.05);
+ transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05)
+ }
+
+ .u-switch__loading {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .u-switch--on {
+ background-color: #1989fa;
+ }
+
+ .u-switch--on .u-switch__node {
+ transform: translateX(100%);
+ }
+
+ .u-switch--disabled {
+ opacity: 0.4;
+ }
+</style>
diff --git a/uview-ui/components/u-tabbar/u-tabbar.vue b/uview-ui/components/u-tabbar/u-tabbar.vue
new file mode 100644
index 0000000..448206c
--- /dev/null
+++ b/uview-ui/components/u-tabbar/u-tabbar.vue
@@ -0,0 +1,328 @@
+<template>
+ <view v-if="show" class="u-tabbar" @touchmove.stop.prevent="() => {}">
+ <view class="u-tabbar__content safe-area-inset-bottom" :style="{
+ height: $u.addUnit(height),
+ backgroundColor: bgColor,
+ }" :class="{
+ 'u-border-top': borderTop
+ }">
+ <view class="u-tabbar__content__item" v-for="(item, index) in list" :key="index" :class="{
+ 'u-tabbar__content__circle': midButton &&item.midButton
+ }" @tap.stop="clickHandler(index)" :style="{
+ backgroundColor: bgColor
+ }">
+ <view :class="[
+ midButton && item.midButton ? 'u-tabbar__content__circle__button' : 'u-tabbar__content__item__button'
+ ]">
+ <u-icon
+ :size="midButton && item.midButton ? midButtonSize : iconSize"
+ :name="elIconPath(index)"
+ img-mode="scaleToFill"
+ :color="elColor(index)"
+ :custom-prefix="item.customIcon ? 'custom-icon' : 'uicon'"
+ ></u-icon>
+ <u-badge :count="item.count" :is-dot="item.isDot"
+ v-if="item.count"
+ :offset="[-2, getOffsetRight(item.count, item.isDot)]"
+ ></u-badge>
+ </view>
+ <view class="u-tabbar__content__item__text" :style="{
+ color: elColor(index)
+ }">
+ <text class="u-line-1">{{item.text}}</text>
+ </view>
+ </view>
+ <view v-if="midButton" class="u-tabbar__content__circle__border" :class="{
+ 'u-border': borderTop,
+ }" :style="{
+ backgroundColor: bgColor,
+ left: midButtonLeft
+ }">
+ </view>
+ </view>
+ <!-- 这里加上一个48rpx的高度,是为了增高有凸起按钮时的防塌陷高度(也即按钮凸出来部分的高度) -->
+ <view class="u-fixed-placeholder safe-area-inset-bottom" :style="{
+ height: `calc(${$u.addUnit(height)} + ${midButton ? 48 : 0}rpx)`,
+ }"></view>
+ </view>
+</template>
+
+<script>
+ export default {
+ props: {
+ // 显示与否
+ show: {
+ type: Boolean,
+ default: true
+ },
+ // 通过v-model绑定current值
+ value: {
+ type: [String, Number],
+ default: 0
+ },
+ // 整个tabbar的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // tabbar的高度,默认50px,单位任意,如果为数值,则为rpx单位
+ height: {
+ type: [String, Number],
+ default: '50px'
+ },
+ // 非凸起图标的大小,单位任意,数值默认rpx
+ iconSize: {
+ type: [String, Number],
+ default: 40
+ },
+ // 凸起的图标的大小,单位任意,数值默认rpx
+ midButtonSize: {
+ type: [String, Number],
+ default: 90
+ },
+ // 激活时的演示,包括字体图标,提示文字等的演示
+ activeColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 未激活时的颜色
+ inactiveColor: {
+ type: String,
+ default: '#606266'
+ },
+ // 是否显示中部的凸起按钮
+ midButton: {
+ type: Boolean,
+ default: false
+ },
+ // 配置参数
+ list: {
+ type: Array,
+ default () {
+ return []
+ }
+ },
+ // 切换前的回调
+ beforeSwitch: {
+ type: Function,
+ default: null
+ },
+ // 是否显示顶部的横线
+ borderTop: {
+ type: Boolean,
+ default: true
+ },
+ // 是否隐藏原生tabbar
+ hideTabBar: {
+ type: Boolean,
+ default: true
+ },
+ },
+ data() {
+ return {
+ // 由于安卓太菜了,通过css居中凸起按钮的外层元素有误差,故通过js计算将其居中
+ midButtonLeft: '50%',
+ pageUrl: '', // 当前页面URL
+ }
+ },
+ created() {
+ // 是否隐藏原生tabbar
+ if(this.hideTabBar) uni.hideTabBar();
+ // 获取引入了u-tabbar页面的路由地址,该地址没有路径前面的"/"
+ let pages = getCurrentPages();
+ // 页面栈中的最后一个即为项为当前页面,route属性为页面路径
+ this.pageUrl = pages[pages.length - 1].route;
+ },
+ computed: {
+ elIconPath() {
+ return (index) => {
+ // 历遍u-tabbar的每一项item时,判断是否传入了pagePath参数,如果传入了
+ // 和data中的pageUrl参数对比,如果相等,即可判断当前的item对应当前的tabbar页面,设置高亮图标
+ // 采用这个方法,可以无需使用v-model绑定的value值
+ let pagePath = this.list[index].pagePath;
+ // 如果定义了pagePath属性,意味着使用系统自带tabbar方案,否则使用一个页面用几个组件模拟tabbar页面的方案
+ // 这两个方案对处理tabbar item的激活与否方式不一样
+ if(pagePath) {
+ if(pagePath == this.pageUrl || pagePath == '/' + this.pageUrl) {
+ return this.list[index].selectedIconPath;
+ } else {
+ return this.list[index].iconPath;
+ }
+ } else {
+ // 普通方案中,索引等于v-model值时,即为激活项
+ return index == this.value ? this.list[index].selectedIconPath : this.list[index].iconPath
+ }
+ }
+ },
+ elColor() {
+ return (index) => {
+ // 判断方法同理于elIconPath
+ let pagePath = this.list[index].pagePath;
+ if(pagePath) {
+ if(pagePath == this.pageUrl || pagePath == '/' + this.pageUrl) return this.activeColor;
+ else return this.inactiveColor;
+ } else {
+ return index == this.value ? this.activeColor : this.inactiveColor;
+ }
+ }
+ }
+ },
+ mounted() {
+ this.midButton && this.getMidButtonLeft();
+ },
+ methods: {
+ async clickHandler(index) {
+ if(this.beforeSwitch && typeof(this.beforeSwitch) === 'function') {
+ // 执行回调,同时传入索引当作参数
+ // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this
+ // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文
+ let beforeSwitch = this.beforeSwitch.bind(this.$u.$parent.call(this))(index);
+ // 判断是否返回了promise
+ if (!!beforeSwitch && typeof beforeSwitch.then === 'function') {
+ await beforeSwitch.then(res => {
+ // promise返回成功,
+ this.switchTab(index);
+ }).catch(err => {
+
+ })
+ } else if(beforeSwitch === true) {
+ // 如果返回true
+ this.switchTab(index);
+ }
+ } else {
+ this.switchTab(index);
+ }
+ },
+ // 切换tab
+ switchTab(index) {
+ // 发出事件和修改v-model绑定的值
+ this.$emit('change', index);
+ // 如果有配置pagePath属性,使用uni.switchTab进行跳转
+ if(this.list[index].pagePath) {
+ uni.switchTab({
+ url: this.list[index].pagePath
+ })
+ } else {
+ // 如果配置了papgePath属性,将不会双向绑定v-model传入的value值
+ // 因为这个模式下,不再需要v-model绑定的value值了,而是通过getCurrentPages()适配
+ this.$emit('input', index);
+ }
+ },
+ // 计算角标的right值
+ getOffsetRight(count, isDot) {
+ // 点类型,count大于9(两位数),分别设置不同的right值,避免位置太挤
+ if(isDot) {
+ return -20;
+ } else if(count > 9) {
+ return -40;
+ } else {
+ return -30;
+ }
+ },
+ // 获取凸起按钮外层元素的left值,让其水平居中
+ getMidButtonLeft() {
+ let windowWidth = this.$u.sys().windowWidth;
+ // 由于安卓中css计算left: 50%的结果不准确,故用js计算
+ this.midButtonLeft = (windowWidth / 2) + 'px';
+ }
+ }
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+ .u-fixed-placeholder {
+ /* #ifndef APP-NVUE */
+ box-sizing: content-box;
+ /* #endif */
+ }
+
+ .u-tabbar {
+
+ &__content {
+ @include vue-flex;
+ align-items: center;
+ position: relative;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 998;
+ /* #ifndef APP-NVUE */
+ box-sizing: content-box;
+ /* #endif */
+
+ &__circle__border {
+ border-radius: 100%;
+ width: 110rpx;
+ height: 110rpx;
+ top: -48rpx;
+ position: absolute;
+ z-index: 4;
+ background-color: #ffffff;
+ // 由于安卓的无能,导致只有3个tabbar item时,此css计算方式有误差
+ // 故使用js计算的形式来定位,此处不注释,是因为js计算有延后,避免出现位置闪动
+ left: 50%;
+ transform: translateX(-50%);
+
+ &:after {
+ border-radius: 100px;
+ }
+ }
+
+ &__item {
+ flex: 1;
+ justify-content: center;
+ height: 100%;
+ padding: 12rpx 0;
+ @include vue-flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+
+ &__button {
+ position: absolute;
+ top: 14rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &__text {
+ color: $u-content-color;
+ font-size: 26rpx;
+ line-height: 28rpx;
+ position: absolute;
+ bottom: 14rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+ }
+
+ &__circle {
+ position: relative;
+ @include vue-flex;
+ flex-direction: column;
+ justify-content: space-between;
+ z-index: 10;
+ /* #ifndef APP-NVUE */
+ height: calc(100% - 1px);
+ /* #endif */
+
+ &__button {
+ width: 90rpx;
+ height: 90rpx;
+ border-radius: 100%;
+ @include vue-flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ background-color: #ffffff;
+ top: -40rpx;
+ left: 50%;
+ z-index: 6;
+ transform: translateX(-50%);
+ }
+ }
+ }
+ }
+</style>
diff --git a/uview-ui/components/u-table/u-table.vue b/uview-ui/components/u-table/u-table.vue
new file mode 100644
index 0000000..79df16f
--- /dev/null
+++ b/uview-ui/components/u-table/u-table.vue
@@ -0,0 +1,84 @@
+<template>
+ <view class="u-table" :style="[tableStyle]">
+ <slot />
+ </view>
+</template>
+
+<script>
+ /**
+ * table 表格
+ * @description 表格组件一般用于展示大量结构化数据的场景
+ * @tutorial https://www.uviewui.com/components/table.html
+ * @property {String} border-color 表格边框的颜色(默认#e4e7ed)
+ * @property {String} bg-color 表格的背景颜色(默认#ffffff)
+ * @property {String} align 单元格的内容对齐方式,作用类似css的text-align(默认center)
+ * @property {String} padding 单元格的内边距,同css的padding写法(默认10rpx 0)
+ * @property {String Number} font-size 单元格字体大小,单位rpx(默认28)
+ * @property {String} color 单元格字体颜色(默认#606266)
+ * @property {Object} th-style th单元格的样式,对象形式(将th所需参数放在table组件,是为了避免每一个th组件要写一遍)
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 点击关闭按钮时触发
+ * @example <u-table></u-table>
+ */
+ export default {
+ name: "u-table",
+ props: {
+ borderColor: {
+ type: String,
+ default: '#e4e7ed'
+ },
+ align: {
+ type: String,
+ default: 'center'
+ },
+ // td的内边距
+ padding: {
+ type: String,
+ default: '10rpx 6rpx'
+ },
+ // 字体大小
+ fontSize: {
+ type: [String, Number],
+ default: 28
+ },
+ // 字体颜色
+ color: {
+ type: String,
+ default: '#606266'
+ },
+ // th的自定义样式
+ thStyle: {
+ type: Object,
+ default () {
+ return {}
+ }
+ },
+ // table的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ }
+ },
+ data() {
+ return {}
+ },
+ computed: {
+ tableStyle() {
+ let style = {};
+ style.borderLeft = `solid 1px ${this.borderColor}`;
+ style.borderTop = `solid 1px ${this.borderColor}`;
+ style.backgroundColor = this.bgColor;;
+ return style;
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-table {
+ width: 100%;
+ box-sizing: border-box;
+ }
+</style>
diff --git a/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue b/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue
new file mode 100644
index 0000000..a53e261
--- /dev/null
+++ b/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue
@@ -0,0 +1,488 @@
+<template>
+ <view class="u-tabs" :style="{
+ zIndex: zIndex,
+ background: bgColor
+ }">
+ <scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{ zIndex: zIndex + 1 }">
+ <view class="u-tabs-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
+ <view class="u-tabs-item" :style="[tabItemStyle(index)]"
+ v-for="(item, index) in getTabs" :key="index" :class="[preId + index]" @tap="emit(index)">
+ <u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge>
+ {{ item[name] || item['name']}}
+ </view>
+ <view v-if="showBar" class="u-scroll-bar" :style="[tabBarStyle]"></view>
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script>
+ import colorGradient from '../../libs/function/colorGradient';
+ let color = colorGradient;
+ const { windowWidth } = uni.getSystemInfoSync();
+ const preId = 'UEl_';
+
+ /**
+ * tabsSwiper 全屏选项卡
+ * @description 该组件内部实现主要依托于uniapp的scroll-view和swiper组件,主要特色是切换过程中,tabsSwiper文字的颜色可以渐变,底部滑块可以 跟随式滑动,活动tab滚动居中等。应用场景可以用于需要左右切换页面,比如商城的订单中心(待收货-待付款-待评价-已退货)等应用场景。
+ * @tutorial https://www.uviewui.com/components/tabsSwiper.html
+ * @property {Boolean} is-scroll tabs是否可以左右拖动(默认true)
+ * @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
+ * @property {String Number} current 指定哪个tab为激活状态(默认0)
+ * @property {String Number} height 导航栏的高度,单位rpx(默认80)
+ * @property {String Number} font-size tab文字大小,单位rpx(默认30)
+ * @property {String Number} swiper-width tabs组件外部swiper的宽度,默认为屏幕宽度,单位rpx(默认750)
+ * @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff)
+ * @property {String} inactive-color tabs文字颜色(默认#303133)
+ * @property {String Number} bar-width 滑块宽度,单位rpx(默认40)
+ * @property {String Number} bar-height 滑块高度,单位rpx(默认6)
+ * @property {Object} bar-style 底部滑块的样式,对象形式
+ * @property {Object} active-item-style 活动tabs item的样式,对象形式
+ * @property {Boolean} show-bar 是否显示底部的滑块(默认true)
+ * @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40)
+ * @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff)
+ * @property {String} name 组件内部读取的list参数中的属性名,见官网说明(默认name)
+ * @property {String} count 组件内部读取的list参数中的属性名(badge徽标数),同name属性的使用,见官网说明(默认count)
+ * @property {Array} offset 设置badge徽标数的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx(默认[5, 20])
+ * @property {Boolean} bold 激活选项的字体是否加粗(默认true)
+ * @event {Function} change 点击标签时触发
+ * @example <u-tabs-swiper ref="tabs" :list="list" :is-scroll="false"></u-tabs-swiper>
+ */
+ export default {
+ name: "u-tabs-swiper",
+ props: {
+ // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
+ isScroll: {
+ type: Boolean,
+ default: true
+ },
+ //需循环的标签列表
+ list: {
+ type: Array,
+ default () {
+ return [];
+ }
+ },
+ // 当前活动tab的索引
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 导航栏的高度和行高,单位rpx
+ height: {
+ type: [Number, String],
+ default: 80
+ },
+ // 字体大小,单位rpx
+ fontSize: {
+ type: [Number, String],
+ default: 30
+ },
+ // 过渡动画时长, 单位s
+ // duration: {
+ // type: [Number, String],
+ // default: 0.5
+ // },
+ swiperWidth: {
+ //line3生效, 外部swiper的宽度, 单位rpx
+ type: [String, Number],
+ default: 750
+ },
+ // 选中项的主题颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 未选中项的颜色
+ inactiveColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 菜单底部移动的bar的宽度,单位rpx
+ barWidth: {
+ type: [Number, String],
+ default: 40
+ },
+ // 移动bar的高度
+ barHeight: {
+ type: [Number, String],
+ default: 6
+ },
+ // 单个tab的左或右内边距(各占一半),单位rpx
+ gutter: {
+ type: [Number, String],
+ default: 40
+ },
+ // 如果是绝对定位,添加z-index值
+ zIndex: {
+ type: [Number, String],
+ default: 1
+ },
+ // 导航栏的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ //滚动至中心目标类型
+ autoCenterMode: {
+ type: String,
+ default: 'window'
+ },
+ // 读取传入的数组对象的属性(tab名称)
+ name: {
+ type: String,
+ default: 'name'
+ },
+ // 读取传入的数组对象的属性(徽标数)
+ count: {
+ type: String,
+ default: 'count'
+ },
+ // 徽标数位置偏移
+ offset: {
+ type: Array,
+ default: () => {
+ return [5, 20]
+ }
+ },
+ // 活动tab字体是否加粗
+ bold: {
+ type: Boolean,
+ default: true
+ },
+ // 当前活动tab item的样式
+ activeItemStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 是否显示底部的滑块
+ showBar: {
+ type: Boolean,
+ default: true
+ },
+ // 底部滑块的自定义样式
+ barStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ }
+ },
+ data() {
+ return {
+ scrollLeft: 0, // 滚动scroll-view的左边滚动距离
+ tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
+ windowWidth: 0, // 屏幕宽度,单位为px
+ //scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
+ animationFinishCurrent: this.current,
+ componentsWidth: 0,
+ line3AddDx: 0,
+ line3Dx: 0,
+ preId,
+ sW: 0,
+ tabsInfo: [],
+ colorGradientArr: [],
+ colorStep: 100 // 两个颜色之间的渐变等分
+ };
+ },
+ computed: {
+ // 获取当前活跃的current值
+ getCurrent() {
+ const current = Number(this.current);
+ // 判断是否超出边界
+ if (current > this.getTabs.length - 1) {
+ return this.getTabs.length - 1;
+ }
+ if (current < 0) return 0;
+ return current;
+ },
+ getTabs() {
+ return this.list;
+ },
+ // 滑块需要移动的距离
+ scrollBarLeft() {
+ return Number(this.line3Dx) + Number(this.line3AddDx);
+ },
+ // 滑块的宽度转为px单位
+ barWidthPx() {
+ return uni.upx2px(this.barWidth);
+ },
+ // tab的样式
+ tabItemStyle() {
+ return (index) => {
+ let style = {
+ height: this.height + 'rpx',
+ lineHeight: this.height + 'rpx',
+ padding: `0 ${this.gutter / 2}rpx`,
+ color: this.tabsInfo.length > 0 ? (this.tabsInfo[index] ? this.tabsInfo[index].color : this.activeColor) : this.inactiveColor,
+ fontSize: this.fontSize + 'rpx',
+ zIndex: this.zIndex + 2,
+ fontWeight: (index == this.getCurrent && this.bold) ? 'bold' : 'normal'
+ };
+ if(index == this.getCurrent) {
+ // 给选中的tab item添加外部自定义的样式
+ style = Object.assign(style, this.activeItemStyle);
+ }
+ return style;
+ }
+ },
+ // 底部滑块的样式
+ tabBarStyle() {
+ let style = {
+ width: this.barWidthPx + 'px',
+ height: this.barHeight + 'rpx',
+ borderRadius: '100px',
+ backgroundColor: this.activeColor,
+ left: this.scrollBarLeft + 'px'
+ };
+ return Object.assign(style, this.barStyle);
+ }
+ },
+ watch: {
+ current(n, o) {
+ this.change(n);
+ this.setFinishCurrent(n);
+ },
+ list() {
+ this.$nextTick(() => {
+ this.init();
+ })
+ }
+ },
+ mounted() {
+ this.init();
+ },
+ methods: {
+ async init() {
+ this.countPx();
+ await this.getTabsInfo();
+ this.countLine3Dx();
+ this.getQuery(() => {
+ this.setScrollViewToCenter();
+ });
+ // 颜色渐变过程数组
+ this.colorGradientArr = color.colorGradient(this.inactiveColor, this.activeColor, this.colorStep);
+ },
+ // 获取各个tab的节点信息
+ getTabsInfo() {
+ return new Promise((resolve, reject) => {
+ let view = uni.createSelectorQuery().in(this);
+ for (let i = 0; i < this.list.length; i++) {
+ view.select('.' + preId + i).boundingClientRect();
+ }
+ view.exec(res => {
+ const arr = [];
+ for (let i = 0; i < res.length; i++) {
+ // 给每个tab添加其文字颜色属性
+ res[i].color = this.inactiveColor;
+ // 当前tab直接赋予activeColor
+ if (i == this.getCurrent) res[i].color = this.activeColor;
+ arr.push(res[i]);
+ }
+ this.tabsInfo = arr;
+ resolve();
+ });
+ })
+ },
+ // 当swiper滑动结束,计算滑块最终要停留的位置
+ countLine3Dx() {
+ const tab = this.tabsInfo[this.animationFinishCurrent];
+ // 让滑块中心点和当前tab中心重合
+ if (tab) this.line3Dx = tab.left + tab.width / 2 - this.barWidthPx / 2 - this.tabsInfo[0].left;
+ },
+ countPx() {
+ // swiper宽度由rpx转为px单位,因为dx等,都是px单位
+ this.sW = uni.upx2px(Number(this.swiperWidth));
+ },
+ emit(index) {
+ this.$emit('change', index);
+ },
+ change() {
+ this.setScrollViewToCenter();
+ },
+ getQuery(cb) {
+ try {
+ let view = uni.createSelectorQuery().in(this).select('.u-tabs');
+ view.fields({
+ size: true
+ },
+ data => {
+ if (data) {
+ this.componentsWidth = data.width;
+ if (cb && typeof cb === 'function') cb(data);
+ } else {
+ this.getQuery(cb);
+ }
+ }
+ ).exec();
+ } catch (e) {
+ this.componentsWidth = windowWidth;
+ }
+ },
+ // 把活动tab移动到屏幕中心点
+ setScrollViewToCenter() {
+ let tab;
+ tab = this.tabsInfo[this.animationFinishCurrent];
+ if (tab) {
+ let tabCenter = tab.left + tab.width / 2;
+ let fatherWidth;
+ // 活动tab移动到中心时,以屏幕还是tab组件为宽度为基准
+ if (this.autoCenterMode === 'window') {
+ fatherWidth = windowWidth;
+ } else {
+ fatherWidth = this.componentsWidth;
+ }
+ this.scrollLeft = tabCenter - fatherWidth / 2;
+ }
+ },
+ setDx(dx) {
+ let nextTabIndex = dx > 0 ? this.animationFinishCurrent + 1 : this.animationFinishCurrent - 1;
+ // 判断索引是否超出边界
+ nextTabIndex = nextTabIndex <= 0 ? 0 : nextTabIndex;
+ nextTabIndex = nextTabIndex >= this.list.length ? this.list.length - 1 : nextTabIndex;
+ const tab = this.tabsInfo[nextTabIndex];
+ // 当前tab中心点x轴坐标
+ let nowTab = this.tabsInfo[this.animationFinishCurrent];
+ let nowTabX = nowTab.left + nowTab.width / 2;
+ // 下一个tab
+ let nextTab = this.tabsInfo[nextTabIndex];
+ let nextTabX = nextTab.left + nextTab.width / 2;
+ // 两个tab之间的距离,因为下一个tab可能在当前tab的左边或者右边,取绝对值即可
+ let distanceX = Math.abs(nextTabX - nowTabX);
+ this.line3AddDx = (dx / this.sW) * distanceX;
+ this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx);
+ },
+ // 设置tab的颜色
+ setTabColor(nowTabIndex, nextTabIndex, dx) {
+ let colorIndex = Math.abs(Math.ceil((dx / this.sW) * 100));
+ let colorLength = this.colorGradientArr.length;
+ // 处理超出索引边界的情况
+ colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex;
+ // 设置下一个tab的颜色
+ this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex];
+ // 设置当前tab的颜色
+ this.tabsInfo[nowTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex];
+ },
+ // swiper结束滑动
+ setFinishCurrent(current) {
+ // 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况
+ this.tabsInfo.map((val, index) => {
+ if (current == index) val.color = this.activeColor;
+ else val.color = this.inactiveColor;
+ return val;
+ });
+ this.line3AddDx = 0;
+ this.animationFinishCurrent = current;
+ this.countLine3Dx();
+ }
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "../../libs/css/style.components.scss";
+
+ view,
+ scroll-view {
+ box-sizing: border-box;
+ }
+
+ .u-tabs {
+ width: 100%;
+ transition-property: background-color, color;
+ }
+
+ /* #ifndef APP-NVUE */
+ ::-webkit-scrollbar,
+ ::-webkit-scrollbar,
+ ::-webkit-scrollbar {
+ display: none;
+ width: 0 !important;
+ height: 0 !important;
+ -webkit-appearance: none;
+ background: transparent;
+ }
+ /* #endif */
+
+ /* #ifdef H5 */
+ // 通过样式穿透,隐藏H5下,scroll-view下的滚动条
+ scroll-view ::v-deep ::-webkit-scrollbar {
+ display: none;
+ width: 0 !important;
+ height: 0 !important;
+ -webkit-appearance: none;
+ background: transparent;
+ }
+
+ /* #endif */
+
+ .u-scroll-view {
+ width: 100%;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ .u-tabs-scroll-box {
+ position: relative;
+ }
+
+ .u-tabs-scorll-flex {
+ @include vue-flex;
+ justify-content: space-between;
+ }
+
+ .u-tabs-scorll-flex .u-tabs-item {
+ flex: 1;
+ }
+
+ .u-tabs-item {
+ position: relative;
+ display: inline-block;
+ text-align: center;
+ transition-property: background-color, color, font-weight;
+ }
+
+ .content {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .boxStyle {
+ pointer-events: none;
+ position: absolute;
+ transition-property: all;
+ }
+
+ .boxStyle2 {
+ pointer-events: none;
+ position: absolute;
+ bottom: 0;
+ transition-property: all;
+ transform: translateY(-100%);
+ }
+
+ .itemBackgroundBox {
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ transition-property: left, background-color;
+ @include vue-flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .itemBackground {
+ height: 100%;
+ width: 100%;
+ transition-property: all;
+ }
+
+ .u-scroll-bar {
+ position: absolute;
+ bottom: 4rpx;
+ }
+</style>
diff --git a/uview-ui/components/u-tabs/u-tabs.vue b/uview-ui/components/u-tabs/u-tabs.vue
new file mode 100644
index 0000000..d25d616
--- /dev/null
+++ b/uview-ui/components/u-tabs/u-tabs.vue
@@ -0,0 +1,368 @@
+<template>
+ <view class="u-tabs" :style="{
+ background: bgColor
+ }">
+ <!-- $u.getRect()对组件根节点无效,因为写了.in(this),故这里获取内层接点尺寸 -->
+ <view :id="id">
+ <scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
+ <view class="u-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
+ <view class="u-tab-item u-line-1" :id="'u-tab-item-' + index" v-for="(item, index) in list" :key="index" @tap="clickTab(index)"
+ :style="[tabItemStyle(index)]">
+ <u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge>
+ {{ item[name] || item['name']}}
+ </view>
+ <view v-if="showBar" class="u-tab-bar" :style="[tabBarStyle]"></view>
+ </view>
+ </scroll-view>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * tabs 标签
+ * @description 该组件,是一个tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
+ * @tutorial https://www.uviewui.com/components/tabs.html
+ * @property {Boolean} is-scroll tabs是否可以左右拖动(默认true)
+ * @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
+ * @property {String Number} current 指定哪个tab为激活状态(默认0)
+ * @property {String Number} height 导航栏的高度,单位rpx(默认80)
+ * @property {String Number} font-size tab文字大小,单位rpx(默认30)
+ * @property {String Number} duration 滑块移动一次所需的时间,单位秒(默认0.5)
+ * @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff)
+ * @property {String} inactive-color tabs文字颜色(默认#303133)
+ * @property {String Number} bar-width 滑块宽度,单位rpx(默认40)
+ * @property {Object} active-item-style 活动tabs item的样式,对象形式
+ * @property {Object} bar-style 底部滑块的样式,对象形式
+ * @property {Boolean} show-bar 是否显示底部的滑块(默认true)
+ * @property {String Number} bar-height 滑块高度,单位rpx(默认6)
+ * @property {String Number} item-width 标签的宽度(默认auto)
+ * @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40)
+ * @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff)
+ * @property {String} name 组件内部读取的list参数中的属性名(tab名称),见官网说明(默认name)
+ * @property {String} count 组件内部读取的list参数中的属性名(badge徽标数),同name属性的使用,见官网说明(默认count)
+ * @property {Array} offset 设置badge徽标数的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx(默认[5, 20])
+ * @property {Boolean} bold 激活选项的字体是否加粗(默认true)
+ * @event {Function} change 点击标签时触发
+ * @example <u-tabs ref="tabs" :list="list" :is-scroll="false"></u-tabs>
+ */
+ export default {
+ name: "u-tabs",
+ props: {
+ // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
+ isScroll: {
+ type: Boolean,
+ default: true
+ },
+ //需循环的标签列表
+ list: {
+ type: Array,
+ default () {
+ return [];
+ }
+ },
+ // 当前活动tab的索引
+ current: {
+ type: [Number, String],
+ default: 0
+ },
+ // 导航栏的高度和行高
+ height: {
+ type: [String, Number],
+ default: 80
+ },
+ // 字体大小
+ fontSize: {
+ type: [String, Number],
+ default: 30
+ },
+ // 过渡动画时长, 单位ms
+ duration: {
+ type: [String, Number],
+ default: 0.5
+ },
+ // 选中项的主题颜色
+ activeColor: {
+ type: String,
+ default: '#2979ff'
+ },
+ // 未选中项的颜色
+ inactiveColor: {
+ type: String,
+ default: '#303133'
+ },
+ // 菜单底部移动的bar的宽度,单位rpx
+ barWidth: {
+ type: [String, Number],
+ default: 40
+ },
+ // 移动bar的高度
+ barHeight: {
+ type: [String, Number],
+ default: 6
+ },
+ // 单个tab的左或有内边距(左右相同)
+ gutter: {
+ type: [String, Number],
+ default: 30
+ },
+ // 导航栏的背景颜色
+ bgColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 读取传入的数组对象的属性(tab名称)
+ name: {
+ type: String,
+ default: 'name'
+ },
+ // 读取传入的数组对象的属性(徽标数)
+ count: {
+ type: String,
+ default: 'count'
+ },
+ // 徽标数位置偏移
+ offset: {
+ type: Array,
+ default: () => {
+ return [5, 20]
+ }
+ },
+ // 活动tab字体是否加粗
+ bold: {
+ type: Boolean,
+ default: true
+ },
+ // 当前活动tab item的样式
+ activeItemStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 是否显示底部的滑块
+ showBar: {
+ type: Boolean,
+ default: true
+ },
+ // 底部滑块的自定义样式
+ barStyle: {
+ type: Object,
+ default() {
+ return {}
+ }
+ },
+ // 标签的宽度
+ itemWidth: {
+ type: [Number, String],
+ default: 'auto'
+ }
+ },
+ data() {
+ return {
+ scrollLeft: 0, // 滚动scroll-view的左边滚动距离
+ tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
+ componentWidth: 0, // 屏幕宽度,单位为px
+ scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
+ parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
+ id: this.$u.guid(), // id值
+ currentIndex: this.current,
+ barFirstTimeMove: true, // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
+ };
+ },
+ watch: {
+ // 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
+ // 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
+ list(n, o) {
+ // list变动时,重制内部索引,否则可能导致超出数组边界的情况
+ if(n.length !== o.length) this.currentIndex = 0;
+ // 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
+ this.$nextTick(() => {
+ this.init();
+ });
+ },
+ current: {
+ immediate: true,
+ handler(nVal, oVal) {
+ // 视图更新后再执行移动操作
+ this.$nextTick(() => {
+ this.currentIndex = nVal;
+ this.scrollByIndex();
+ });
+ }
+ },
+ },
+ computed: {
+ // 移动bar的样式
+ tabBarStyle() {
+ let style = {
+ width: this.barWidth + 'rpx',
+ transform: `translate(${this.scrollBarLeft}px, -100%)`,
+ // 滑块在页面渲染后第一次滑动时,无需动画效果
+ 'transition-duration': `${this.barFirstTimeMove ? 0 : this.duration }s`,
+ 'background-color': this.activeColor,
+ height: this.barHeight + 'rpx',
+ // 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
+ 'border-radius': `${this.barHeight / 2}px`
+ };
+ Object.assign(style, this.barStyle);
+ return style;
+ },
+ // tab的样式
+ tabItemStyle() {
+ return (index) => {
+ let style = {
+ height: this.height + 'rpx',
+ 'line-height': this.height + 'rpx',
+ 'font-size': this.fontSize + 'rpx',
+ 'transition-duration': `${this.duration}s`,
+ padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
+ flex: this.isScroll ? 'auto' : '1',
+ width: this.$u.addUnit(this.itemWidth)
+ };
+ // 字体加粗
+ if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
+ if (index == this.currentIndex) {
+ style.color = this.activeColor;
+ // 给选中的tab item添加外部自定义的样式
+ style = Object.assign(style, this.activeItemStyle);
+ } else {
+ style.color = this.inactiveColor;
+ }
+ return style;
+ }
+ }
+ },
+ methods: {
+ // 设置一个init方法,方便多处调用
+ async init() {
+ // 获取tabs组件的尺寸信息
+ let tabRect = await this.$uGetRect('#' + this.id);
+ // tabs组件距离屏幕左边的宽度
+ this.parentLeft = tabRect.left;
+ // tabs组件的宽度
+ this.componentWidth = tabRect.width;
+ this.getTabRect();
+ },
+ // 点击某一个tab菜单
+ clickTab(index) {
+ // 点击当前活动tab,不触发事件
+ if(index == this.currentIndex) return ;
+ // 发送事件给父组件
+ this.$emit('change', index);
+ },
+ // 查询tab的布局信息
+ getTabRect() {
+ // 创建节点查询
+ let query = uni.createSelectorQuery().in(this);
+ // 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
+ for (let i = 0; i < this.list.length; i++) {
+ // 只要size和rect两个参数
+ query.select(`#u-tab-item-${i}`).fields({
+ size: true,
+ rect: true
+ });
+ }
+ // 执行查询,一次性获取多个结果
+ query.exec(
+ function(res) {
+ this.tabQueryInfo = res;
+ // 初始化滚动条和移动bar的位置
+ this.scrollByIndex();
+ }.bind(this)
+ );
+ },
+ // 滚动scroll-view,让活动的tab处于屏幕的中间位置
+ scrollByIndex() {
+ // 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
+ let tabInfo = this.tabQueryInfo[this.currentIndex];
+ if (!tabInfo) return;
+ // 活动tab的宽度
+ let tabWidth = tabInfo.width;
+ // 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
+ let offsetLeft = tabInfo.left - this.parentLeft;
+ // 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
+ let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
+ this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
+ // 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
+ let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
+ // 计算当前活跃item到组件左边的距离
+ this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
+ // 第一次移动滑块的时候,barFirstTimeMove为true,放到延时中将其设置false
+ // 延时是因为scrollBarLeft作用于computed计算时,需要一个过程需,否则导致出错
+ if(this.barFirstTimeMove == true) {
+ setTimeout(() => {
+ this.barFirstTimeMove = false;
+ }, 100)
+ }
+ }
+ },
+ mounted() {
+ this.init();
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ view,
+ scroll-view {
+ box-sizing: border-box;
+ }
+
+ /* #ifndef APP-NVUE */
+ ::-webkit-scrollbar,
+ ::-webkit-scrollbar,
+ ::-webkit-scrollbar {
+ display: none;
+ width: 0 !important;
+ height: 0 !important;
+ -webkit-appearance: none;
+ background: transparent;
+ }
+ /* #endif */
+
+ .u-scroll-box {
+ position: relative;
+ /* #ifdef MP-TOUTIAO */
+ white-space: nowrap;
+ /* #endif */
+ }
+
+ /* #ifdef H5 */
+ // 通过样式穿透,隐藏H5下,scroll-view下的滚动条
+ scroll-view ::v-deep ::-webkit-scrollbar {
+ display: none;
+ width: 0 !important;
+ height: 0 !important;
+ -webkit-appearance: none;
+ background: transparent;
+ }
+ /* #endif */
+
+ .u-scroll-view {
+ width: 100%;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ .u-tab-item {
+ position: relative;
+ /* #ifndef APP-NVUE */
+ display: inline-block;
+ /* #endif */
+ text-align: center;
+ transition-property: background-color, color;
+ }
+
+ .u-tab-bar {
+ position: absolute;
+ bottom: 0;
+ }
+
+ .u-tabs-scorll-flex {
+ @include vue-flex;
+ justify-content: space-between;
+ }
+</style>
diff --git a/uview-ui/components/u-tag/u-tag.vue b/uview-ui/components/u-tag/u-tag.vue
new file mode 100644
index 0000000..90ec3f4
--- /dev/null
+++ b/uview-ui/components/u-tag/u-tag.vue
@@ -0,0 +1,294 @@
+<template>
+ <view v-if="show" :class="[
+ disabled ? 'u-disabled' : '',
+ 'u-size-' + size,
+ 'u-shape-' + shape,
+ 'u-mode-' + mode + '-' + type
+ ]"
+ class="u-tag" :style="[customStyle]" @tap="clickTag">
+ {{text}}
+ <view class="u-icon-wrap" @tap.stop>
+ <u-icon @click="close" size="22" v-if="closeable" :color="closeIconColor"
+ name="close" class="u-close-icon" :style="[iconStyle]"></u-icon>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * tag 提示
+ * @description 该组件一般用于标记和选择
+ * @tutorial https://www.uviewui.com/components/tag.html
+ * @property {String} type 主题类型(默认primary)
+ * @property {String} size 标签大小(默认default)
+ * @property {String} shape 标签形状(默认square)
+ * @property {String} text 标签的文字内容
+ * @property {String} bg-color 自定义标签的背景颜色
+ * @property {String} border-color 标签的边框颜色
+ * @property {String} close-color 关闭按钮的颜色
+ * @property {String Number} index 点击标签时,会通过click事件返回该值
+ * @property {String} mode 模式选择,见官网说明(默认light)
+ * @property {Boolean} closeable 是否可关闭,设置为true,文字右边会出现一个关闭图标(默认false)
+ * @property {Boolean} show 标签显示与否(默认true)
+ * @event {Function} click 点击标签触发
+ * @event {Function} close closeable为true时,点击标签关闭按钮触发
+ * @example <u-tag text="雪月夜" type="success" />
+ */
+ export default {
+ name: 'u-tag',
+ // 是否禁用这个标签,禁用的话,会屏蔽点击事件
+ props: {
+ // 标签类型info、primary、success、warning、error
+ type: {
+ type: String,
+ default: 'primary'
+ },
+ disabled: {
+ type: [Boolean, String],
+ default: false
+ },
+ // 标签的大小,分为default(默认),mini(较小)
+ size: {
+ type: String,
+ default: 'default'
+ },
+ // tag的形状,circle(两边半圆形), square(方形,带圆角),circleLeft(左边是半圆),circleRight(右边是半圆)
+ shape: {
+ type: String,
+ default: 'square'
+ },
+ // 标签文字
+ text: {
+ type: [String, Number],
+ default: ''
+ },
+ // 背景颜色,默认为空字符串,即不处理
+ bgColor: {
+ type: String,
+ default: ''
+ },
+ // 标签字体颜色,默认为空字符串,即不处理
+ color: {
+ type: String,
+ default: ''
+ },
+ // 镂空形式标签的边框颜色
+ borderColor: {
+ type: String,
+ default: ''
+ },
+ // 关闭按钮图标的颜色
+ closeColor: {
+ type: String,
+ default: ''
+ },
+ // 点击时返回的索引值,用于区分例遍的数组哪个元素被点击了
+ index: {
+ type: [Number, String],
+ default: ''
+ },
+ // 模式选择,dark|light|plain
+ mode: {
+ type: String,
+ default: 'light'
+ },
+ // 是否可关闭
+ closeable: {
+ type: Boolean,
+ default: false
+ },
+ // 是否显示
+ show: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+
+ }
+ },
+ computed: {
+ customStyle() {
+ let style = {};
+ // 文字颜色(如果有此值,会覆盖type值的颜色)
+ if(this.color) style.color = this.color;
+ // tag的背景颜色(如果有此值,会覆盖type值的颜色)
+ if(this.bgColor) style.backgroundColor = this.bgColor;
+ // 如果是镂空型tag,没有传递边框颜色(borderColor)的话,使用文字的颜色(color属性)
+ if(this.mode == 'plain' && this.color && !this.borderColor) style.borderColor = this.color;
+ else style.borderColor = this.borderColor;
+ return style;
+ },
+ iconStyle() {
+ if(!this.closeable) return ;
+ let style = {};
+ if(this.size == 'mini') style.fontSize = '20rpx';
+ else style.fontSize = '22rpx';
+ if(this.mode == 'plain' || this.mode == 'light') style.color = this.type;
+ else if(this.mode == 'dark') style.color = "#ffffff";
+ if(this.closeColor) style.color = this.closeColor;
+ return style;
+ },
+ // 关闭图标的颜色
+ closeIconColor() {
+ // 如果定义了关闭图标的颜色,就用此值,否则用字体颜色的值
+ // 如果上面的二者都没有,如果是dark深色模式,图标就为白色
+ // 最后如果上面的三者都不合适,就返回type值给图标获取颜色
+ let color = '';
+ if(this.closeColor) return this.closeColor;
+ else if(this.color) return this.color;
+ else if(this.mode == 'dark') return '#ffffff';
+ else return this.type;
+ }
+ },
+ methods: {
+ // 标签被点击
+ clickTag() {
+ // 如果是disabled状态,不发送点击事件
+ if(this.disabled) return ;
+ this.$emit('click', this.index);
+ },
+ // 点击标签关闭按钮
+ close() {
+ this.$emit('close', this.index);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-tag {
+ box-sizing: border-box;
+ align-items: center;
+ border-radius: 6rpx;
+ /* #ifndef APP-NVUE */
+ display: inline-block;
+ /* #endif */
+ line-height: 1;
+ }
+
+ .u-size-default {
+ font-size: 22rpx;
+ padding: 12rpx 22rpx;
+ }
+
+ .u-size-mini {
+ font-size: 20rpx;
+ padding: 6rpx 12rpx;
+ }
+
+ .u-mode-light-primary {
+ background-color: $u-type-primary-light;
+ color: $u-type-primary;
+ border: 1px solid $u-type-primary-disabled;
+ }
+
+ .u-mode-light-success {
+ background-color: $u-type-success-light;
+ color: $u-type-success;
+ border: 1px solid $u-type-success-disabled;
+ }
+
+ .u-mode-light-error {
+ background-color: $u-type-error-light;
+ color: $u-type-error;
+ border: 1px solid $u-type-error-disabled;
+ }
+
+ .u-mode-light-warning {
+ background-color: $u-type-warning-light;
+ color: $u-type-warning;
+ border: 1px solid $u-type-warning-disabled;
+ }
+
+ .u-mode-light-info {
+ background-color: $u-type-info-light;
+ color: $u-type-info;
+ border: 1px solid $u-type-info-disabled;
+ }
+
+ .u-mode-dark-primary {
+ background-color: $u-type-primary;
+ color: #FFFFFF;
+ }
+
+ .u-mode-dark-success {
+ background-color: $u-type-success;
+ color: #FFFFFF;
+ }
+
+ .u-mode-dark-error {
+ background-color: $u-type-error;
+ color: #FFFFFF;
+ }
+
+ .u-mode-dark-warning {
+ background-color: $u-type-warning;
+ color: #FFFFFF;
+ }
+
+ .u-mode-dark-info {
+ background-color: $u-type-info;
+ color: #FFFFFF;
+ }
+
+ .u-mode-plain-primary {
+ background-color: #FFFFFF;
+ color: $u-type-primary;
+ border: 1px solid $u-type-primary;
+ }
+
+ .u-mode-plain-success {
+ background-color: #FFFFFF;
+ color: $u-type-success;
+ border: 1px solid $u-type-success;
+ }
+
+ .u-mode-plain-error {
+ background-color: #FFFFFF;
+ color: $u-type-error;
+ border: 1px solid $u-type-error;
+ }
+
+ .u-mode-plain-warning {
+ background-color: #FFFFFF;
+ color: $u-type-warning;
+ border: 1px solid $u-type-warning;
+ }
+
+ .u-mode-plain-info {
+ background-color: #FFFFFF;
+ color: $u-type-info;
+ border: 1px solid $u-type-info;
+ }
+
+ .u-disabled {
+ opacity: 0.55;
+ }
+
+ .u-shape-circle {
+ border-radius: 100rpx;
+ }
+
+ .u-shape-circleRight {
+ border-radius: 0 100rpx 100rpx 0;
+ }
+
+ .u-shape-circleLeft {
+ border-radius: 100rpx 0 0 100rpx;
+ }
+
+ .u-close-icon {
+ margin-left: 14rpx;
+ font-size: 22rpx;
+ color: $u-type-success;
+ }
+
+ .u-icon-wrap {
+ display: inline-flex;
+ transform: scale(0.86);
+ }
+</style>
diff --git a/uview-ui/components/u-td/u-td.vue b/uview-ui/components/u-td/u-td.vue
new file mode 100644
index 0000000..b00ad8b
--- /dev/null
+++ b/uview-ui/components/u-td/u-td.vue
@@ -0,0 +1,66 @@
+<template>
+ <view class="u-td" :style="[tdStyle]">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ /**
+ * td td单元格
+ * @description 表格组件一般用于展示大量结构化数据的场景(搭配u-table使用)
+ * @tutorial https://www.uviewui.com/components/table.html#td-props
+ * @property {String Number} width 单元格宽度百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比,单元格宽度默认为均分tr的长度(默认auto)
+ * @example <u-td>二年级</u-td>
+ */
+ export default {
+ name: "u-td",
+ props: {
+ // 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
+ width: {
+ type: [Number, String],
+ default: 'auto'
+ }
+ },
+ data() {
+ return {
+ tdStyle: {
+
+ }
+ }
+ },
+ created() {
+ this.parent = false;
+ },
+ mounted() {
+ this.parent = this.$u.$parent.call(this, 'u-table');
+ if (this.parent) {
+ // 将父组件的相关参数,合并到本组件
+ let style = {};
+ if (this.width != "auto") style.flex = `0 0 ${this.width}`;
+ style.textAlign = this.parent.align;
+ style.fontSize = this.parent.fontSize + 'rpx';
+ style.padding = this.parent.padding;
+ style.borderBottom = `solid 1px ${this.parent.borderColor}`;
+ style.borderRight = `solid 1px ${this.parent.borderColor}`;
+ style.color = this.parent.color;
+ this.tdStyle = style;
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-td {
+ @include vue-flex;
+ flex-direction: column;
+ flex: 1;
+ justify-content: center;
+ font-size: 28rpx;
+ color: $u-content-color;
+ align-self: stretch;
+ box-sizing: border-box;
+ height: 100%;
+ }
+</style>
diff --git a/uview-ui/components/u-th/u-th.vue b/uview-ui/components/u-th/u-th.vue
new file mode 100644
index 0000000..9fe5a16
--- /dev/null
+++ b/uview-ui/components/u-th/u-th.vue
@@ -0,0 +1,62 @@
+<template>
+ <view class="u-th" :style="[thStyle]">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ /**
+ * th th单元格
+ * @description 表格组件一般用于展示大量结构化数据的场景(搭配u-table使用)
+ * @tutorial https://www.uviewui.com/components/table.html#td-props
+ * @property {String Number} width 标题单元格宽度百分比或者具体带单位的值,如30%,200rpx等,一般使用百分比,单元格宽度默认为均分tr的长度
+ * @example 暂无示例
+ */
+ export default {
+ name: "u-th",
+ props: {
+ // 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
+ width: {
+ type: [Number, String],
+ default: ''
+ }
+ },
+ data() {
+ return {
+ thStyle: {}
+ }
+ },
+ created() {
+ this.parent = false;
+ },
+ mounted() {
+ this.parent = this.$u.$parent.call(this, 'u-table');
+ if (this.parent) {
+ // 将父组件的相关参数,合并到本组件
+ let style = {};
+ if (this.width) style.flex = `0 0 ${this.width}`;
+ style.textAlign = this.parent.align;
+ style.padding = this.parent.padding;
+ style.borderBottom = `solid 1px ${this.parent.borderColor}`;
+ style.borderRight = `solid 1px ${this.parent.borderColor}`;
+ Object.assign(style, this.parent.style);
+ this.thStyle = style;
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-th {
+ @include vue-flex;
+ flex-direction: column;
+ flex: 1;
+ justify-content: center;
+ font-size: 28rpx;
+ color: $u-main-color;
+ font-weight: bold;
+ background-color: rgb(245, 246, 248);
+ }
+</style>
diff --git a/uview-ui/components/u-time-line-item/u-time-line-item.vue b/uview-ui/components/u-time-line-item/u-time-line-item.vue
new file mode 100644
index 0000000..be86fdc
--- /dev/null
+++ b/uview-ui/components/u-time-line-item/u-time-line-item.vue
@@ -0,0 +1,83 @@
+<template>
+ <view class="u-time-axis-item">
+ <slot name="content" />
+ <view class="u-time-axis-node" :style="[nodeStyle]">
+ <slot name="node">
+ <view class="u-dot">
+ </view>
+ </slot>
+ </view>
+ </view>
+</template>
+
+<script>
+ /**
+ * timeLineItem 时间轴Item
+ * @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。(搭配u-time-line使用)
+ * @tutorial https://www.uviewui.com/components/timeLine.html
+ * @property {String} bg-color 左边节点的背景颜色,一般通过slot内容自定义背景颜色即可(默认#ffffff)
+ * @property {String Number} node-top 节点左边图标绝对定位的top值,单位rpx
+ * @example <u-time-line-item node-top="2">...</u-time-line-item>
+ */
+ export default {
+ name: "u-time-line-item",
+ props: {
+ // 节点的背景颜色
+ bgColor: {
+ type: String,
+ default: "#ffffff"
+ },
+ // 节点左边图标绝对定位的top值
+ nodeTop: {
+ type: [String, Number],
+ default: ""
+ }
+ },
+ data() {
+ return {
+
+ }
+ },
+ computed: {
+ nodeStyle() {
+ let style = {
+ backgroundColor: this.bgColor,
+ };
+ if (this.nodeTop != "") style.top = this.nodeTop + 'rpx';
+ return style;
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-time-axis-item {
+ @include vue-flex;
+ flex-direction: column;
+ width: 100%;
+ position: relative;
+ margin-bottom: 32rpx;
+ }
+
+ .u-time-axis-node {
+ position: absolute;
+ top: 12rpx;
+ left: -40rpx;
+ transform-origin: 0;
+ transform: translateX(-50%);
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1;
+ font-size: 24rpx;
+ }
+
+ .u-dot {
+ height: 16rpx;
+ width: 16rpx;
+ border-radius: 100rpx;
+ background: #ddd;
+ }
+</style>
diff --git a/uview-ui/components/u-time-line/u-time-line.vue b/uview-ui/components/u-time-line/u-time-line.vue
new file mode 100644
index 0000000..f3c7587
--- /dev/null
+++ b/uview-ui/components/u-time-line/u-time-line.vue
@@ -0,0 +1,43 @@
+<template>
+ <view class="u-time-axis">
+ <slot />
+ </view>
+</template>
+
+<script>
+ /**
+ * timeLine 时间轴
+ * @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。
+ * @tutorial https://www.uviewui.com/components/timeLine.html
+ * @example <u-time-line></u-time-line>
+ */
+ export default {
+ name: "u-time-line",
+ data() {
+ return {
+
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-time-axis {
+ padding-left: 40rpx;
+ position: relative;
+ }
+
+ .u-time-axis::before {
+ content: " ";
+ position: absolute;
+ left: 0;
+ top: 12rpx;
+ width: 1px;
+ bottom: 0;
+ border-left: 1px solid #ddd;
+ transform-origin: 0 0;
+ transform: scaleX(0.5);
+ }
+</style>
diff --git a/uview-ui/components/u-toast/u-toast.vue b/uview-ui/components/u-toast/u-toast.vue
new file mode 100644
index 0000000..a2209a5
--- /dev/null
+++ b/uview-ui/components/u-toast/u-toast.vue
@@ -0,0 +1,220 @@
+<template>
+ <view class="u-toast" :class="[isShow ? 'u-show' : '', 'u-type-' + tmpConfig.type, 'u-position-' + tmpConfig.position]" :style="{
+ zIndex: uZIndex
+ }">
+ <view class="u-icon-wrap">
+ <u-icon v-if="tmpConfig.icon" class="u-icon" :name="iconName" :size="30" :color="tmpConfig.type"></u-icon>
+ </view>
+ <text class="u-text">{{tmpConfig.title}}</text>
+ </view>
+</template>
+
+<script>
+ /**
+ * toast 消息提示
+ * @description 此组件表现形式类似uni的uni.showToastAPI,但也有不同的地方。
+ * @tutorial https://www.uviewui.com/components/toast.html
+ * @property {String} z-index toast展示时的z-index值
+ * @event {Function} show 显示toast,如需一进入页面就显示toast,请在onReady生命周期调用
+ * @example <u-toast ref="uToast" />
+ */
+ export default {
+ name: "u-toast",
+ props: {
+ // z-index值
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ },
+ },
+ data() {
+ return {
+ isShow: false,
+ timer: null, // 定时器
+ config: {
+ params: {}, // URL跳转的参数,对象
+ title: '', // 显示文本
+ type: '', // 主题类型,primary,success,error,warning,black
+ duration: 2000, // 显示的时间,毫秒
+ isTab: false, // 是否跳转tab页面
+ url: '', // toast消失后是否跳转页面,有则跳转,优先级高于back参数
+ icon: true, // 显示的图标
+ position: 'center', // toast出现的位置
+ callback: null, // 执行完后的回调函数
+ back: false, // 结束toast是否自动返回上一页
+ },
+ tmpConfig: {}, // 将用户配置和内置配置合并后的临时配置变量
+ };
+ },
+ computed: {
+ iconName() {
+ // 只有不为none,并且type为error|warning|succes|info时候,才显示图标
+ if (['error', 'warning', 'success', 'info'].indexOf(this.tmpConfig.type) >= 0 && this.tmpConfig.icon) {
+ let icon = this.$u.type2icon(this.tmpConfig.type);
+ return icon;
+ }
+ },
+ uZIndex() {
+ // 显示toast时候,如果用户有传递z-index值,有限使用
+ return this.isShow ? (this.zIndex ? this.zIndex : this.$u.zIndex.toast) : '999999';
+ }
+ },
+ methods: {
+ // 显示toast组件,由父组件通过this.$refs.xxx.show(options)形式调用
+ show(options) {
+ // 不降结果合并到this.config变量,避免多次条用u-toast,前后的配置造成混论
+ this.tmpConfig = this.$u.deepMerge(this.config, options);
+ if (this.timer) {
+ // 清除定时器
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ this.isShow = true;
+ this.timer = setTimeout(() => {
+ // 倒计时结束,清除定时器,隐藏toast组件
+ this.isShow = false;
+ clearTimeout(this.timer);
+ this.timer = null;
+ // 判断是否存在callback方法,如果存在就执行
+ typeof(this.tmpConfig.callback) === 'function' && this.tmpConfig.callback();
+ this.timeEnd();
+ }, this.tmpConfig.duration);
+ },
+ // 隐藏toast组件,由父组件通过this.$refs.xxx.hide()形式调用
+ hide() {
+ this.isShow = false;
+ if (this.timer) {
+ // 清除定时器
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ // 倒计时结束之后,进行的一些操作
+ timeEnd() {
+ // 如果带有url值,根据isTab为true或者false进行跳转
+ if (this.tmpConfig.url) {
+ // 如果url没有"/"开头,添加上,因为uni的路由跳转需要"/"开头
+ if (this.tmpConfig.url[0] != '/') this.tmpConfig.url = '/' + this.tmpConfig.url;
+ // 判断是否有传递显式的参数
+ if (Object.keys(this.tmpConfig.params).length) {
+ // 判断用户传递的url中,是否带有参数
+ // 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
+ // 如果有params参数,转换后无需带上"?"
+ let query = '';
+ if (/.*\/.*\?.*=.*/.test(this.tmpConfig.url)) {
+ // object对象转为get类型的参数
+ query = this.$u.queryParams(this.tmpConfig.params, false);
+ this.tmpConfig.url = this.tmpConfig.url + "&" + query;
+ } else {
+ query = this.$u.queryParams(this.tmpConfig.params);
+ this.tmpConfig.url += query;
+ }
+ }
+ // 如果是跳转tab页面,就使用uni.switchTab
+ if (this.tmpConfig.isTab) {
+ uni.switchTab({
+ url: this.tmpConfig.url
+ });
+ } else {
+ uni.navigateTo({
+ url: this.tmpConfig.url
+ });
+ }
+ } else if(this.tmpConfig.back) {
+ // 回退到上一页
+ this.$u.route({
+ type: 'back'
+ })
+ }
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-toast {
+ position: fixed;
+ z-index: -1;
+ transition: opacity 0.3s;
+ text-align: center;
+ color: #fff;
+ border-radius: 8rpx;
+ background: #585858;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ opacity: 0;
+ pointer-events: none;
+ padding: 18rpx 40rpx;
+ }
+
+ .u-toast.u-show {
+ opacity: 1;
+ }
+
+ .u-icon {
+ margin-right: 10rpx;
+ @include vue-flex;
+ align-items: center;
+ line-height: normal;
+ }
+
+ .u-position-center {
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%,-50%);
+ /* #ifndef APP-NVUE */
+ max-width: 70%;
+ /* #endif */
+ }
+
+ .u-position-top {
+ left: 50%;
+ top: 20%;
+ transform: translate(-50%,-50%);
+ }
+
+ .u-position-bottom {
+ left: 50%;
+ bottom: 20%;
+ transform: translate(-50%,-50%);
+ }
+
+ .u-type-primary {
+ color: $u-type-primary;
+ background-color: $u-type-primary-light;
+ border: 1px solid rgb(215, 234, 254);
+ }
+
+ .u-type-success {
+ color: $u-type-success;
+ background-color: $u-type-success-light;
+ border: 1px solid #BEF5C8;
+ }
+
+ .u-type-error {
+ color: $u-type-error;
+ background-color: $u-type-error-light;
+ border: 1px solid #fde2e2;
+ }
+
+ .u-type-warning {
+ color: $u-type-warning;
+ background-color: $u-type-warning-light;
+ border: 1px solid #faecd8;
+ }
+
+ .u-type-info {
+ color: $u-type-info;
+ background-color: $u-type-info-light;
+ border: 1px solid #ebeef5;
+ }
+
+ .u-type-default {
+ color: #fff;
+ background-color: #585858;
+ }
+</style>
diff --git a/uview-ui/components/u-top-tips/u-top-tips.vue b/uview-ui/components/u-top-tips/u-top-tips.vue
new file mode 100644
index 0000000..98d58df
--- /dev/null
+++ b/uview-ui/components/u-top-tips/u-top-tips.vue
@@ -0,0 +1,121 @@
+<template>
+ <view class="u-tips" :class="['u-' + type, isShow ? 'u-tip-show' : '']" :style="{
+ top: navbarHeight + 'px',
+ zIndex: uZIndex
+ }">{{ title }}</view>
+</template>
+
+<script>
+ /**
+ * topTips 顶部提示
+ * @description 该组件一般用于页面顶部向下滑出一个提示,尔后自动收起的场景。
+ * @tutorial https://www.uviewui.com/components/topTips.html
+ * @property {String Number} navbar-height 导航栏高度(包含状态栏高度在内),单位PX
+ * @property {String Number} z-index z-index值(默认975)
+ * @example <u-top-tips ref="uTips"></u-top-tips>
+ */
+ export default {
+ name: "u-top-tips",
+ props: {
+ // 导航栏高度,用于提示的初始化
+ navbarHeight: {
+ type: [Number, String],
+ // #ifndef H5
+ default: 0,
+ // #endif
+ // #ifdef H5
+ default: 44,
+ // #endif
+ },
+ // z-index值
+ zIndex: {
+ type: [Number, String],
+ default: ''
+ }
+ },
+ data() {
+ return {
+ timer: null, // 定时器
+ isShow: false, // 是否显示消息组件
+ title: '', // 组件中显示的消息内容
+ type: 'primary', // 消息的类型(颜色不同),primary,success,error,warning,info
+ duration: 2000, // 组件显示的时间,单位为毫秒
+ };
+ },
+ computed: {
+ uZIndex() {
+ return this.zIndex ? this.zIndex : this.$u.zIndex.topTips;
+ }
+ },
+ methods: {
+ show(config = {}) {
+ // 先清除定时器(可能是上一次定义的,需要清除了再开始新的)
+ clearTimeout(this.timer);
+ // 时间,内容,类型主题(type)等参数
+ if (config.duration) this.duration = config.duration;
+ if (config.type) this.type = config.type;
+ this.title = config.title;
+ this.isShow = true;
+ // 倒计时
+ this.timer = setTimeout(() => {
+ this.isShow = false;
+ clearTimeout(this.timer);
+ this.timer = null;
+ }, this.duration);
+ }
+ }
+ };
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ view {
+ box-sizing: border-box;
+ }
+
+ // 顶部弹出类型样式
+ .u-tips {
+ width: 100%;
+ position: fixed;
+ z-index: 1;
+ padding: 20rpx 30rpx;
+ color: #FFFFFF;
+ font-size: 28rpx;
+ left: 0;
+ right: 0;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ // 此处为最核心点,translateY(-100%)意味着将其从Y轴隐藏(隐藏到顶部(h5)或者说导航栏(app)下面)
+ transform: translateY(-100%);
+ transition: all 0.35s linear;
+ }
+
+ .u-tip-show {
+ transform: translateY(0);
+ opacity: 1;
+ z-index: 99;
+ }
+
+ .u-primary {
+ background: $u-type-primary;
+ }
+
+ .u-success {
+ background: $u-type-success;
+ }
+
+ .u-warning {
+ background: $u-type-warning;
+ }
+
+ .u-error {
+ background: $u-type-error;
+ }
+
+ .u-info {
+ background: $u-type-info;
+ }
+</style>
diff --git a/uview-ui/components/u-tr/u-tr.vue b/uview-ui/components/u-tr/u-tr.vue
new file mode 100644
index 0000000..06aed4a
--- /dev/null
+++ b/uview-ui/components/u-tr/u-tr.vue
@@ -0,0 +1,25 @@
+<template>
+ <view class="u-tr">
+ <slot></slot>
+ </view>
+</template>
+
+<script>
+ /**
+ * tr 表格行标签
+ * @description 表格组件一般用于展示大量结构化数据的场景(搭配<u-table>使用)
+ * @tutorial https://www.uviewui.com/components/table.html
+ * @example <u-tr></u-tr>
+ */
+ export default {
+ name: "u-tr",
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-tr {
+ @include vue-flex;
+ }
+</style>
diff --git a/uview-ui/components/u-upload/u-upload.vue b/uview-ui/components/u-upload/u-upload.vue
new file mode 100644
index 0000000..2fec19d
--- /dev/null
+++ b/uview-ui/components/u-upload/u-upload.vue
@@ -0,0 +1,652 @@
+<template>
+ <view class="u-upload" v-if="!disabled">
+ <view
+ v-if="showUploadList"
+ class="u-list-item u-preview-wrap"
+ v-for="(item, index) in lists"
+ :key="index"
+ :style="{
+ width: $u.addUnit(width),
+ height: $u.addUnit(height)
+ }"
+ >
+ <view
+ v-if="deletable"
+ class="u-delete-icon"
+ @tap.stop="deleteItem(index)"
+ :style="{
+ background: delBgColor
+ }"
+ >
+ <u-icon class="u-icon" :name="delIcon" size="20" :color="delColor"></u-icon>
+ </view>
+ <u-line-progress
+ v-if="showProgress && item.progress > 0 && !item.error"
+ :show-percent="false"
+ height="16"
+ class="u-progress"
+ :percent="item.progress"
+ ></u-line-progress>
+ <view @tap.stop="retry(index)" v-if="item.error" class="u-error-btn">点击重试</view>
+ <image @tap.stop="doPreviewImage(item.url || item.path, index)" class="u-preview-image" v-if="!item.isImage" :src="item.url || item.path" :mode="imageMode"></image>
+ </view>
+ <slot name="file" :file="lists"></slot>
+ <view style="display: inline-block;" @tap="selectFile" v-if="maxCount > lists.length">
+ <slot name="addBtn"></slot>
+ <view
+ v-if="!customBtn"
+ class="u-list-item u-add-wrap"
+ hover-class="u-add-wrap__hover"
+ hover-stay-time="150"
+ :style="{
+ width: $u.addUnit(width),
+ height: $u.addUnit(height)
+ }"
+ >
+ <u-icon name="plus" class="u-add-btn" size="40"></u-icon>
+ <view class="u-add-tips">{{ uploadText }}</view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script>
+/**
+ * upload 图片上传
+ * @description 该组件用于上传图片场景
+ * @tutorial https://www.uviewui.com/components/upload.html
+ * @property {String} action 服务器上传地址
+ * @property {String Number} max-count 最大选择图片的数量(默认99)
+ * @property {Boolean} custom-btn 如果需要自定义选择图片的按钮,设置为true(默认false)
+ * @property {Boolean} show-progress 是否显示进度条(默认true)
+ * @property {Boolean} disabled 是否启用(显示/移仓)组件(默认false)
+ * @property {String} image-mode 预览图片等显示模式,可选值为uni的image的mode属性值(默认aspectFill)
+ * @property {String} del-icon 右上角删除图标名称,只能为uView内置图标
+ * @property {String} del-bg-color 右上角关闭按钮的背景颜色
+ * @property {String | Number} index 在各个回调事件中的最后一个参数返回,用于区别是哪一个组件的事件
+ * @property {String} del-color 右上角关闭按钮图标的颜色
+ * @property {Object} header 上传携带的头信息,对象形式
+ * @property {Object} form-data 上传额外携带的参数
+ * @property {String} name 上传文件的字段名,供后端获取使用(默认file)
+ * @property {Array<String>} size-type original 原图,compressed 压缩图,默认二者都有(默认['original', 'compressed'])
+ * @property {Array<String>} source-type 选择图片的来源,album-从相册选图,camera-使用相机,默认二者都有(默认['album', 'camera'])
+ * @property {Boolean} preview-full-image 是否可以通过uni.previewImage预览已选择的图片(默认true)
+ * @property {Boolean} multiple 是否开启图片多选,部分安卓机型不支持(默认true)
+ * @property {Boolean} deletable 是否显示删除图片的按钮(默认true)
+ * @property {String Number} max-size 选择单个文件的最大大小,单位B(byte),默认不限制(默认Number.MAX_VALUE)
+ * @property {Array<Object>} file-list 默认显示的图片列表,数组元素为对象,必须提供url属性
+ * @property {Boolean} upload-text 选择图片按钮的提示文字(默认“选择图片”)
+ * @property {Boolean} auto-upload 选择完图片是否自动上传,见上方说明(默认true)
+ * @property {Boolean} show-tips 特殊情况下是否自动提示toast,见上方说明(默认true)
+ * @property {Boolean} show-upload-list 是否显示组件内部的图片预览(默认true)
+ * @event {Function} on-oversize 图片大小超出最大允许大小
+ * @event {Function} on-preview 全屏预览图片时触发
+ * @event {Function} on-remove 移除图片时触发
+ * @event {Function} on-success 图片上传成功时触发
+ * @event {Function} on-change 图片上传后,无论成功或者失败都会触发
+ * @event {Function} on-error 图片上传失败时触发
+ * @event {Function} on-progress 图片上传过程中的进度变化过程触发
+ * @event {Function} on-uploaded 所有图片上传完毕触发
+ * @event {Function} on-choose-complete 每次选择图片后触发,只是让外部可以得知每次选择后,内部的文件列表
+ * @example <u-upload :action="action" :file-list="fileList" ></u-upload>
+ */
+export default {
+ name: 'u-upload',
+ props: {
+ //是否显示组件自带的图片预览功能
+ showUploadList: {
+ type: Boolean,
+ default: true
+ },
+ // 后端地址
+ action: {
+ type: String,
+ default: ''
+ },
+ // 最大上传数量
+ maxCount: {
+ type: [String, Number],
+ default: 52
+ },
+ // 是否显示进度条
+ showProgress: {
+ type: Boolean,
+ default: true
+ },
+ // 是否启用
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ // 预览上传的图片时的裁剪模式,和image组件mode属性一致
+ imageMode: {
+ type: String,
+ default: 'aspectFill'
+ },
+ // 头部信息
+ header: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 额外携带的参数
+ formData: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ // 上传的文件字段名
+ name: {
+ type: String,
+ default: 'file'
+ },
+ // 所选的图片的尺寸, 可选值为original compressed
+ sizeType: {
+ type: Array,
+ default() {
+ return ['original', 'compressed'];
+ }
+ },
+ sourceType: {
+ type: Array,
+ default() {
+ return ['album', 'camera'];
+ }
+ },
+ // 是否在点击预览图后展示全屏图片预览
+ previewFullImage: {
+ type: Boolean,
+ default: true
+ },
+ // 是否开启图片多选,部分安卓机型不支持
+ multiple: {
+ type: Boolean,
+ default: true
+ },
+ // 是否展示删除按钮
+ deletable: {
+ type: Boolean,
+ default: true
+ },
+ // 文件大小限制,单位为byte
+ maxSize: {
+ type: [String, Number],
+ default: Number.MAX_VALUE
+ },
+ // 显示已上传的文件列表
+ fileList: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ // 上传区域的提示文字
+ uploadText: {
+ type: String,
+ default: '选择图片'
+ },
+ // 是否自动上传
+ autoUpload: {
+ type: Boolean,
+ default: true
+ },
+ // 是否显示toast消息提示
+ showTips: {
+ type: Boolean,
+ default: true
+ },
+ // 是否通过slot自定义传入选择图标的按钮
+ customBtn: {
+ type: Boolean,
+ default: false
+ },
+ // 内部预览图片区域和选择图片按钮的区域宽度
+ width: {
+ type: [String, Number],
+ default: 200
+ },
+ // 内部预览图片区域和选择图片按钮的区域高度
+ height: {
+ type: [String, Number],
+ default: 200
+ },
+ // 右上角关闭按钮的背景颜色
+ delBgColor: {
+ type: String,
+ default: '#fa3534'
+ },
+ // 右上角关闭按钮的叉号图标的颜色
+ delColor: {
+ type: String,
+ default: '#ffffff'
+ },
+ // 右上角删除图标名称,只能为uView内置图标
+ delIcon: {
+ type: String,
+ default: 'close'
+ },
+ // 如果上传后的返回值为json字符串,是否自动转json
+ toJson: {
+ type: Boolean,
+ default: true
+ },
+ // 上传前的钩子,每个文件上传前都会执行
+ beforeUpload: {
+ type: Function,
+ default: null
+ },
+ // 移除文件前的钩子
+ beforeRemove: {
+ type: Function,
+ default: null
+ },
+ // 允许上传的图片后缀
+ limitType:{
+ type: Array,
+ default() {
+ return ['png', 'jpg', 'jpeg', 'webp', 'gif'];
+ }
+ },
+ // 在各个回调事件中的最后一个参数返回,用于区别是哪一个组件的事件
+ index: {
+ type: [Number, String],
+ default: ''
+ }
+ },
+ mounted() {},
+ data() {
+ return {
+ lists: [],
+ isInCount: true,
+ uploading: false
+ };
+ },
+ watch: {
+ fileList: {
+ immediate: true,
+ handler(val) {
+ val.map(value => {
+ // 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList
+ // 时,会触发watch,导致重新把原来的图片再次添加到this.lists
+ // 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
+ let tmp = this.lists.some(val => {
+ return val.url == value.url;
+ })
+ // 如果内部没有这个图片(tmp为false),则添加到内部
+ !tmp && this.lists.push({ url: value.url, error: false, progress: 100 });
+ });
+ }
+ },
+ // 监听lists的变化,发出事件
+ lists(n) {
+ this.$emit('on-list-change', n, this.index);
+ }
+ },
+ methods: {
+ // 清除列表
+ clear() {
+ this.lists = [];
+ },
+ // 重新上传队列中上传失败的所有文件
+ reUpload() {
+ this.uploadFile();
+ },
+ // 选择图片
+ selectFile() {
+ if (this.disabled) return;
+ const { name = '', maxCount, multiple, maxSize, sizeType, lists, camera, compressed, maxDuration, sourceType } = this;
+ let chooseFile = null;
+ const newMaxCount = maxCount - lists.length;
+ // 设置为只选择图片的时候使用 chooseImage 来实现
+ chooseFile = new Promise((resolve, reject) => {
+ uni.chooseImage({
+ count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
+ sourceType: sourceType,
+ sizeType,
+ success: resolve,
+ fail: reject
+ });
+ });
+ chooseFile
+ .then(res => {
+ let file = null;
+ let listOldLength = this.lists.length;
+ res.tempFiles.map((val, index) => {
+ // 检查文件后缀是否允许,如果不在this.limitType内,就会返回false
+ if(!this.checkFileExt(val)) return ;
+
+ // 如果是非多选,index大于等于1或者超出最大限制数量时,不处理
+ if (!multiple && index >= 1) return;
+ if (val.size > maxSize) {
+ this.$emit('on-oversize', val, this.lists, this.index);
+ this.showToast('超出允许的文件大小');
+ } else {
+ if (maxCount <= lists.length) {
+ this.$emit('on-exceed', val, this.lists, this.index);
+ this.showToast('超出最大允许的文件个数');
+ return;
+ }
+ lists.push({
+ url: val.path,
+ progress: 0,
+ error: false,
+ file: val
+ });
+ }
+ });
+ // 每次图片选择完,抛出一个事件,并将当前内部选择的图片数组抛出去
+ this.$emit('on-choose-complete', this.lists, this.index);
+ if (this.autoUpload) this.uploadFile(listOldLength);
+ })
+ .catch(error => {
+ this.$emit('on-choose-fail', error);
+ });
+ },
+ // 提示用户消息
+ showToast(message, force = false) {
+ if (this.showTips || force) {
+ uni.showToast({
+ title: message,
+ icon: 'none'
+ });
+ }
+ },
+ // 该方法供用户通过ref调用,手动上传
+ upload() {
+ this.uploadFile();
+ },
+ // 对失败的图片重新上传
+ retry(index) {
+ this.lists[index].progress = 0;
+ this.lists[index].error = false;
+ this.lists[index].response = null;
+ uni.showLoading({
+ title: '重新上传'
+ });
+ this.uploadFile(index);
+ },
+ // 上传图片
+ async uploadFile(index = 0) {
+ if (this.disabled) return;
+ if (this.uploading) return;
+ // 全部上传完成
+ if (index >= this.lists.length) {
+ this.$emit('on-uploaded', this.lists, this.index);
+ return;
+ }
+ // 检查是否是已上传或者正在上传中
+ if (this.lists[index].progress == 100) {
+ if (this.autoUpload == false) this.uploadFile(index + 1);
+ return;
+ }
+ // 执行before-upload钩子
+ if(this.beforeUpload && typeof(this.beforeUpload) === 'function') {
+ // 执行回调,同时传入索引和文件列表当作参数
+ // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this
+ // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文
+ // 因为upload组件可能会被嵌套在其他组件内,比如u-form,这时this.$parent其实为u-form的this,
+ // 非页面的this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
+ // 明白意思即可,无需纠结this.$u.$parent.call(this)的细节
+ let beforeResponse = this.beforeUpload.bind(this.$u.$parent.call(this))(index, this.lists);
+ // 判断是否返回了promise
+ if (!!beforeResponse && typeof beforeResponse.then === 'function') {
+ await beforeResponse.then(res => {
+ // promise返回成功,不进行动作,继续上传
+ }).catch(err => {
+ // 进入catch回调的话,继续下一张
+ return this.uploadFile(index + 1);
+ })
+ } else if(beforeResponse === false) {
+ // 如果返回false,继续下一张图片的上传
+ return this.uploadFile(index + 1);
+ } else {
+ // 此处为返回"true"的情形,这里不写代码,就跳过此处,继续执行当前的上传逻辑
+ }
+ }
+ // 检查上传地址
+ if (!this.action) {
+ this.showToast('请配置上传地址', true);
+ return;
+ }
+ this.lists[index].error = false;
+ this.uploading = true;
+ // 创建上传对象
+ const task = uni.uploadFile({
+ url: this.action,
+ filePath: this.lists[index].url,
+ name: this.name,
+ formData: this.formData,
+ header: this.header,
+ success: res => {
+ // 判断是否json字符串,将其转为json格式
+ let data = this.toJson && this.$u.test.jsonString(res.data) ? JSON.parse(res.data) : res.data;
+ if (![200, 201, 204].includes(res.statusCode)) {
+ this.uploadError(index, data);
+ } else {
+ // 上传成功
+ this.lists[index].response = data;
+ this.lists[index].progress = 100;
+ this.lists[index].error = false;
+ this.$emit('on-success', data, index, this.lists, this.index);
+ }
+ },
+ fail: e => {
+ this.uploadError(index, e);
+ },
+ complete: res => {
+ uni.hideLoading();
+ this.uploading = false;
+ this.uploadFile(index + 1);
+ this.$emit('on-change', res, index, this.lists, this.index);
+ }
+ });
+ task.onProgressUpdate(res => {
+ if (res.progress > 0) {
+ this.lists[index].progress = res.progress;
+ this.$emit('on-progress', res, index, this.lists, this.index);
+ }
+ });
+ },
+ // 上传失败
+ uploadError(index, err) {
+ this.lists[index].progress = 0;
+ this.lists[index].error = true;
+ this.lists[index].response = null;
+ this.$emit('on-error', err, index, this.lists, this.index);
+ this.showToast('上传失败,请重试');
+ },
+ // 删除一个图片
+ deleteItem(index) {
+ uni.showModal({
+ title: '提示',
+ content: '您确定要删除此项吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ // 先检查是否有定义before-remove移除前钩子
+ // 执行before-remove钩子
+ if(this.beforeRemove && typeof(this.beforeRemove) === 'function') {
+ // 此处钩子执行 原理同before-remove参数,见上方注释
+ let beforeResponse = this.beforeRemove.bind(this.$u.$parent.call(this))(index, this.lists);
+ // 判断是否返回了promise
+ if (!!beforeResponse && typeof beforeResponse.then === 'function') {
+ await beforeResponse.then(res => {
+ // promise返回成功,不进行动作,继续上传
+ this.handlerDeleteItem(index);
+ }).catch(err => {
+ // 如果进入promise的reject,终止删除操作
+ this.showToast('已终止移除');
+ })
+ } else if(beforeResponse === false) {
+ // 返回false,终止删除
+ this.showToast('已终止移除');
+ } else {
+ // 如果返回true,执行删除操作
+ this.handlerDeleteItem(index);
+ }
+ } else {
+ // 如果不存在before-remove钩子,
+ this.handlerDeleteItem(index);
+ }
+ }
+ }
+ });
+ },
+ // 执行移除图片的动作,上方代码只是判断是否可以移除
+ handlerDeleteItem(index) {
+ // 如果文件正在上传中,终止上传任务,进度在0 < progress < 100则意味着正在上传
+ if (this.lists[index].process < 100 && this.lists[index].process > 0) {
+ typeof this.lists[index].uploadTask != 'undefined' && this.lists[index].uploadTask.abort();
+ }
+ this.lists.splice(index, 1);
+ this.$forceUpdate();
+ this.$emit('on-remove', index, this.lists, this.index);
+ this.showToast('移除成功');
+ },
+ // 用户通过ref手动的形式,移除一张图片
+ remove(index) {
+ // 判断索引的合法范围
+ if (index >= 0 && index < this.lists.length) {
+ this.lists.splice(index, 1);
+ this.$emit('on-list-change', this.lists, this.index);
+ }
+ },
+ // 预览图片
+ doPreviewImage(url, index) {
+ if (!this.previewFullImage) return;
+ const images = this.lists.map(item => item.url || item.path);
+ uni.previewImage({
+ urls: images,
+ current: url,
+ success: () => {
+ this.$emit('on-preview', url, this.lists, this.index);
+ },
+ fail: () => {
+ uni.showToast({
+ title: '预览图片失败',
+ icon: 'none'
+ });
+ }
+ });
+ },
+ // 判断文件后缀是否允许
+ checkFileExt(file) {
+ // 检查是否在允许的后缀中
+ let noArrowExt = false;
+ // 获取后缀名
+ let fileExt = '';
+ const reg = /.+\./;
+ // 如果是H5,需要从name中判断
+ // #ifdef H5
+ fileExt = file.name.replace(reg, "").toLowerCase();
+ // #endif
+ // 非H5,需要从path中读取后缀
+ // #ifndef H5
+ fileExt = file.path.replace(reg, "").toLowerCase();
+ // #endif
+ // 使用数组的some方法,只要符合limitType中的一个,就返回true
+ noArrowExt = this.limitType.some(ext => {
+ // 转为小写
+ return ext.toLowerCase() === fileExt;
+ })
+ if(!noArrowExt) this.showToast(`不允许选择${fileExt}格式的文件`);
+ return noArrowExt;
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import '../../libs/css/style.components.scss';
+
+.u-upload {
+ @include vue-flex;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.u-list-item {
+ width: 200rpx;
+ height: 200rpx;
+ overflow: hidden;
+ margin: 10rpx;
+ background: rgb(244, 245, 246);
+ position: relative;
+ border-radius: 10rpx;
+ /* #ifndef APP-NVUE */
+ display: flex;
+ /* #endif */
+ align-items: center;
+ justify-content: center;
+}
+
+.u-preview-wrap {
+ border: 1px solid rgb(235, 236, 238);
+}
+
+.u-add-wrap {
+ flex-direction: column;
+ color: $u-content-color;
+ font-size: 26rpx;
+}
+
+.u-add-tips {
+ margin-top: 20rpx;
+ line-height: 40rpx;
+}
+
+.u-add-wrap__hover {
+ background-color: rgb(235, 236, 238);
+}
+
+.u-preview-image {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: 10rpx;
+}
+
+.u-delete-icon {
+ position: absolute;
+ top: 10rpx;
+ right: 10rpx;
+ z-index: 10;
+ background-color: $u-type-error;
+ border-radius: 100rpx;
+ width: 44rpx;
+ height: 44rpx;
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.u-icon {
+ @include vue-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.u-progress {
+ position: absolute;
+ bottom: 10rpx;
+ left: 8rpx;
+ right: 8rpx;
+ z-index: 9;
+ width: auto;
+}
+
+.u-error-btn {
+ color: #ffffff;
+ background-color: $u-type-error;
+ font-size: 20rpx;
+ padding: 4px 0;
+ text-align: center;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 9;
+ line-height: 1;
+}
+</style>
diff --git a/uview-ui/components/u-verification-code/u-verification-code.vue b/uview-ui/components/u-verification-code/u-verification-code.vue
new file mode 100644
index 0000000..b3079f4
--- /dev/null
+++ b/uview-ui/components/u-verification-code/u-verification-code.vue
@@ -0,0 +1,164 @@
+<template>
+ <view class="u-code-wrap">
+ <!-- 此组件功能由js完成,无需写html逻辑 -->
+ </view>
+</template>
+
+<script>
+ /**
+ * verificationCode 验证码输入框
+ * @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件 不提供界面显示,只提供提示语,由用户将提示语嵌入到具体的场景
+ * @tutorial https://www.uviewui.com/components/verificationCode.html
+ * @property {Number String} seconds 倒计时所需的秒数(默认60)
+ * @property {String} start-text 开始前的提示语,见官网说明(默认获取验证码)
+ * @property {String} change-text 倒计时期间的提示语,必须带有字母"x",见官网说明(默认X秒重新获取)
+ * @property {String} end-text 倒计结束的提示语,见官网说明(默认重新获取)
+ * @property {Boolean} keep-running 是否在H5刷新或各端返回再进入时继续倒计时(默认false)
+ * @event {Function} change 倒计时期间,每秒触发一次
+ * @event {Function} start 开始倒计时触发
+ * @event {Function} end 结束倒计时触发
+ * @example <u-verification-code :seconds="seconds" @end="end" @start="start" ref="uCode"
+ */
+ export default {
+ name: "u-verification-code",
+ props: {
+ // 倒计时总秒数
+ seconds: {
+ type: [String, Number],
+ default: 60
+ },
+ // 尚未开始时提示
+ startText: {
+ type: String,
+ default: '获取验证码'
+ },
+ // 正在倒计时中的提示
+ changeText: {
+ type: String,
+ default: 'X秒重新获取'
+ },
+ // 倒计时结束时的提示
+ endText: {
+ type: String,
+ default: '重新获取'
+ },
+ // 是否在H5刷新或各端返回再进入时继续倒计时
+ keepRunning: {
+ type: Boolean,
+ default: false
+ },
+ // 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
+ uniqueKey: {
+ type: String,
+ default: ''
+ }
+ },
+ data() {
+ return {
+ secNum: this.seconds,
+ timer: null,
+ canGetCode: true, // 是否可以执行验证码操作
+ }
+ },
+ mounted() {
+ this.checkKeepRunning();
+ },
+ watch: {
+ seconds: {
+ immediate: true,
+ handler(n) {
+ this.secNum = n;
+ }
+ }
+ },
+ methods: {
+ checkKeepRunning() {
+ // 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
+ let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$uCountDownTimestamp'));
+ if(!lastTimestamp) return this.changeEvent(this.startText);
+ // 当前秒的时间戳
+ let nowTimestamp = Math.floor((+ new Date()) / 1000);
+ // 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
+ if(this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
+ // 剩余尚未执行完的倒计秒数
+ this.secNum = lastTimestamp - nowTimestamp;
+ // 清除本地保存的变量
+ uni.removeStorageSync(this.uniqueKey + '_$uCountDownTimestamp');
+ // 开始倒计时
+ this.start();
+ } else {
+ // 如果不存在需要继续上一次的倒计时,执行正常的逻辑
+ this.changeEvent(this.startText);
+ }
+ },
+ // 开始倒计时
+ start() {
+ // 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
+ if(this.timer) {
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+ this.$emit('start');
+ this.canGetCode = false;
+ // 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
+ this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
+ this.setTimeToStorage();
+ this.timer = setInterval(() => {
+ if (--this.secNum) {
+ // 用当前倒计时的秒数替换提示字符串中的"x"字母
+ this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
+ } else {
+ clearInterval(this.timer);
+ this.timer = null;
+ this.changeEvent(this.endText);
+ this.secNum = this.seconds;
+ this.$emit('end');
+ this.canGetCode = true;
+ }
+ }, 1000);
+ },
+ // 重置,可以让用户再次获取验证码
+ reset() {
+ this.canGetCode = true;
+ clearInterval(this.timer);
+ this.secNum = this.seconds;
+ this.changeEvent(this.endText);
+ },
+ changeEvent(text) {
+ this.$emit('change', text);
+ },
+ // 保存时间戳,为了防止倒计时尚未结束,H5刷新或者各端的右上角返回上一页再进来
+ setTimeToStorage() {
+ if(!this.keepRunning || !this.timer) return;
+ // 记录当前的时间戳,为了下次进入页面,如果还在倒计时内的话,继续倒计时
+ // 倒计时尚未结束,结果大于0;倒计时已经开始,就会小于初始值,如果等于初始值,说明没有开始倒计时,无需处理
+ if(this.secNum > 0 && this.secNum <= this.seconds) {
+ // 获取当前时间戳(+ new Date()为特殊写法),除以1000变成秒,再去除小数部分
+ let nowTimestamp = Math.floor((+ new Date()) / 1000);
+ // 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
+ uni.setStorage({
+ key: this.uniqueKey + '_$uCountDownTimestamp',
+ data: nowTimestamp + Number(this.secNum)
+ })
+ }
+ }
+ },
+ // 组件销毁的时候,清除定时器,否则定时器会继续存在,系统不会自动清除
+ beforeDestroy() {
+ this.setTimeToStorage();
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "../../libs/css/style.components.scss";
+
+ .u-code-wrap {
+ width: 0;
+ height: 0;
+ position: fixed;
+ z-index: -1;
+ }
+</style>
diff --git a/uview-ui/components/u-waterfall/u-waterfall.vue b/uview-ui/components/u-waterfall/u-waterfall.vue
new file mode 100644
index 0000000..31964b7
--- /dev/null
+++ b/uview-ui/components/u-waterfall/u-waterfall.vue
@@ -0,0 +1,176 @@
+<template>
+ <view class="u-waterfall">
+ <view id="u-left-column" class="u-column"><slot name="left" :leftList="leftList"></slot></view>
+ <view id="u-right-column" class="u-column"><slot name="right" :rightList="rightList"></slot></view>
+ </view>
+</template>
+
+<script>
+/**
+ * waterfall 瀑布流
+ * @description 这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实现了真正的 组件化,搭配LazyLoad 懒加载和loadMore 加载更多组件,让您开箱即用,眼前一亮。
+ * @tutorial https://www.uviewui.com/components/waterfall.html
+ * @property {Array} flow-list 用于渲染的数据
+ * @property {String Number} add-time 单条数据添加到队列的时间间隔,单位ms,见上方注意事项说明(默认200)
+ * @example <u-waterfall :flowList="flowList"></u-waterfall>
+ */
+export default {
+ name: "u-waterfall",
+ props: {
+ value: {
+ // 瀑布流数据
+ type: Array,
+ required: true,
+ default: function() {
+ return [];
+ }
+ },
+ // 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好
+ // 单位ms
+ addTime: {
+ type: [Number, String],
+ default: 200
+ },
+ // id值,用于清除某一条数据时,根据此idKey名称找到并移除,如数据为{idx: 22, name: 'lisa'}
+ // 那么该把idKey设置为idx
+ idKey: {
+ type: String,
+ default: 'id'
+ }
+ },
+ data() {
+ return {
+ leftList: [],
+ rightList: [],
+ tempList: [],
+ children: []
+ }
+ },
+ watch: {
+ copyFlowList(nVal, oVal) {
+ // 取差值,即这一次数组变化新增的部分
+ let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
+ // 拼接上原有数据
+ this.tempList = this.tempList.concat(this.cloneData(nVal.slice(startIndex)));
+ this.splitData();
+ }
+ },
+ mounted() {
+ this.tempList = this.cloneData(this.copyFlowList);
+ this.splitData();
+ },
+ computed: {
+ // 破坏flowList变量的引用,否则watch的结果新旧值是一样的
+ copyFlowList() {
+ return this.cloneData(this.value);
+ }
+ },
+ methods: {
+ async splitData() {
+ if (!this.tempList.length) return;
+ let leftRect = await this.$uGetRect('#u-left-column');
+ let rightRect = await this.$uGetRect('#u-right-column');
+ // 如果左边小于或等于右边,就添加到左边,否则添加到右边
+ let item = this.tempList[0];
+ // 解决多次快速上拉后,可能数据会乱的问题,因为经过上面的两个await节点查询阻塞一定时间,加上后面的定时器干扰
+ // 数组可能变成[],导致此item值可能为undefined
+ if(!item) return ;
+ if (leftRect.height < rightRect.height) {
+ this.leftList.push(item);
+ } else if (leftRect.height > rightRect.height) {
+ this.rightList.push(item);
+ } else {
+ // 这里是为了保证第一和第二张添加时,左右都能有内容
+ // 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边
+ if (this.leftList.length <= this.rightList.length) {
+ this.leftList.push(item);
+ } else {
+ this.rightList.push(item);
+ }
+ }
+ // 移除临时列表的第一项
+ this.tempList.splice(0, 1);
+ // 如果临时数组还有数据,继续循环
+ if (this.tempList.length) {
+ setTimeout(() => {
+ this.splitData();
+ }, this.addTime)
+ }
+ },
+ // 复制而不是引用对象和数组
+ cloneData(data) {
+ return JSON.parse(JSON.stringify(data));
+ },
+ // 清空数据列表
+ clear() {
+ this.leftList = [];
+ this.rightList = [];
+ // 同时清除父组件列表中的数据
+ this.$emit('input', []);
+ this.tempList = [];
+ },
+ // 清除某一条指定的数据,根据id实现
+ remove(id) {
+ // 如果findIndex找不到合适的条件,就会返回-1
+ let index = -1;
+ index = this.leftList.findIndex(val => val[this.idKey] == id);
+ if(index != -1) {
+ // 如果index不等于-1,说明已经找到了要找的id,根据index索引删除这一条数据
+ this.leftList.splice(index, 1);
+ } else {
+ // 同理于上方面的方法
+ index = this.rightList.findIndex(val => val[this.idKey] == id);
+ if(index != -1) this.rightList.splice(index, 1);
+ }
+ // 同时清除父组件的数据中的对应id的条目
+ index = this.value.findIndex(val => val[this.idKey] == id);
+ if(index != -1) this.$emit('input', this.value.splice(index, 1));
+ },
+ // 修改某条数据的某个属性
+ modify(id, key, value) {
+ // 如果findIndex找不到合适的条件,就会返回-1
+ let index = -1;
+ index = this.leftList.findIndex(val => val[this.idKey] == id);
+ if(index != -1) {
+ // 如果index不等于-1,说明已经找到了要找的id,修改对应key的值
+ this.leftList[index][key] = value;
+ } else {
+ // 同理于上方面的方法
+ index = this.rightList.findIndex(val => val[this.idKey] == id);
+ if(index != -1) this.rightList[index][key] = value;
+ }
+ // 修改父组件的数据中的对应id的条目
+ index = this.value.findIndex(val => val[this.idKey] == id);
+ if(index != -1) {
+ // 首先复制一份value的数据
+ let data = this.cloneData(this.value);
+ // 修改对应索引的key属性的值为value
+ data[index][key] = value;
+ // 修改父组件通过v-model绑定的变量的值
+ this.$emit('input', data);
+ }
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-waterfall {
+ @include vue-flex;
+ flex-direction: row;
+ align-items: flex-start;
+}
+
+.u-column {
+ @include vue-flex;
+ flex: 1;
+ flex-direction: column;
+ height: auto;
+}
+
+.u-image {
+ width: 100%;
+}
+</style>
diff --git a/uview-ui/iconfont.css b/uview-ui/iconfont.css
new file mode 100644
index 0000000..836c718
--- /dev/null
+++ b/uview-ui/iconfont.css
@@ -0,0 +1,910 @@
+/* #ifdef APP-PLUS */
+@font-face {
+ font-family: "uicon-iconfont";
+ font-weight: normal;
+ font-style: normal;
+ font-display: auto;
+ src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAGQYAAsAAAAAw2gAAGPEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCdAAqCv3SB/XABNgIkA4ZoC4M2AAQgBYRtB5cNG+OfdYacxwEA9eYzEqHbAaXC+ZFZWS8oKTr7/09LKmNsu7DdEEVL04JkStgZyOKZ/ILQ2JzQooY+O2mlDm88cwprtIUJRYoVp8q1MEe1Ow/WIUjUNfnNH9HJV5m92kW8dnj/3pEhB8aSgR+4kj24yOevPzf/ix2t5Ij79FHJAsd5EFJ2EoByUZPNHtWZ1VUw8TCKNsPzc+v9/WVQI8dGjhpsRMkSRg/YRimMGiDhCBkIyggFLMAzAAsVA/BOjAJUrBMVFTk9PQUPK0D0ro1REM/4bck0GjGLMBKwOzEqMDrm/+DNvwEAhTsoAAkoYGpB7e4LMInq4Z7d2/lSHmrkiVCowgFOGC/4BjLdjqZBMK9fkWmEJpgKgx8EK9nAPEPOk30pNCLq0BlSKNLexDrvFnL/EBcKlB/2YPqlzm92rMBKbxWxIXacD0TdMTTllTdfbtWnkEtVgBUaVkW6e6oqLSvgq84luVauJUGaMQg240CPmETe+8e/tZabTzHZvWtZycoqHH//BQBFFJvYg6cSCAPK0P/lViVvbmTWIzDi0N0rLjdoVkyOXJIrXJMg1hJIpYJpuwL9q2rensSOdFq7rfav29kR3LEgWD1R08h27tjxMauRWqk1yAqQxIEnxwk/k9Xvp4mNJ5uJLShAeP9ebatUY0naCgW0UITh59azOqqoboMT35k+wU5MV6BC/qWqJ5UnA9vtHQlfnOYXH0EGqAuWT9o/uCI/R6qH5JFb21r+/3mKC2j3fzlyPaQSlCkmTghdBueFPOIgIuD/pjV7uSVUtRJH3r1Qi793yiERbvfPn1kmfye50j+ht+xRakKdXWpRyFaEW2p3vgqDx5kmUV5C/ceN8zDBBaE/Jw+cu0rV+oKmNmRq88X0i/W3q7b+l373+kzPkBYGICUCkGiCVIIo+wTKgQBlF0E6BXIjpfPl9IoRIB0I0nsFUA6kfAHcKG6W9/JrN1/Mz5Ce9/jc+327ryVoKss14vz/aVtEFGJxKA6zw9isAcvQsULlmCICMkSMr7y8H5v/3H7HKsRIsIE7JNR0v68Ola+NidPXgYK7d4y5/kTrx1YiH3ACHY721PpPEwB52aopNP+2724kpHhSAITxv1FDjHzpgDzZMsCCoCevzA4ZdNuGwWaub2JsOr5/+GfTAhbIQEXxzM4jWww363cwcBfD37CDtobhdX241wMioCayVW4+bY0NiYeueRB+9rkNjZPR3SzDmPy+DZuAipEDJ95srvX1+/VHZ73km/Ct/p2utNSVpqu82hqCBAuZNq+8ATmVfJVWWHeYbwS/04tPTM/uwnx5/WrZyVW5K5oTo3NVqPm14m2BMjTemmu9rddzuzLfe3tb7nMPuIeaD4waumN34v8dT1ljigt5jIEn4Cl4Bp6D5ra9AG3NcPfedF1NNsxLUJuXzqNjHXsFxnkN7h/femLnVG/A/n11YXjrmqOdt2C8CXIrtGdHJTfBu2Cm98D7IK87oI0PwYGTB7d/BKbooLKD+cRp72PwCfgUPJjtMzDN5ocTVRGvs6Za+Bx8Ab4ErbS26yswyKFJmhmlgAJ7RxhpsHpyne6mux7O9NRLF7310VdL/fQ3QBlk9lgsrTBPIVaoHNDQMdRZoDhTZtCYLJhDgOkttEgOhiVZjcuWr1i5qiSi1jWlrF23fsPGoarqPdJQmPaAzQefgK8sZ8+dH+jCxUuXr1y9dv3GzVu37zTWRAX+AgQKEixEKFdcbtx5AINyGjgWYHT9vvZfIDCqgylAED4WAqOAYHwChMSnQCh8BoTG50AYfAGExVaEw53C40sgAnYgIs4mEt4TGe+LgrOIij2IhtOIjiOJga+ATLA2kCnWATLD7TLH47LACcTEE7LE10BWOJGs8Q2QDT6QLZ6UHe4QC0/JHveIjTOIg2+BHPCgHPGAnLAukDNGALlgPSBX3Ccuzic37Eju+A7IAyeRJ04mL4wE4uF48sZc+eBu8bESkADfA/niByA/nEP++BEoAD8BBWI0UBB+BgrG9hSCX4BC8ZAW4WmF4WEJcZdE+BVIjNNJgp1JipWBZHhG4RgDFIFdSI6dKBK/AUXhd6Bo/AEUgw8Vi/MoDn8CxeNMUuA2JeAjJeIUSsIqQErsSirsRmrMoWRsTSn4CygVfwOl4R+gdGxLi7EdLcG9ysC/QJmPNw0eURZOpWxsSTk4lnIxDigPx5EW9yv/nFXgqND5XUthfaAiHEXFeFYl2JNKsRctw96kw3Mqwz5Ujn2pArtTJfaj5difqnAA6bENVeNAqsFBtAIHUy2WBarDeKCVWA5oFaYC1WNJIAMuoAZcSI2YANSEpYCaMRFoNVYEWoNJQC2YDNSKhYDacBGtxcW0DgsDrcciQBuwKNBGDAdqxxJAHVgcqBOLAW3CYKAfMARoM86lLbiEtuJS2oZhQD24TNtxuXZgKFAvrtAeXKk+XKX9uFoHcI0O4lodwtJA/RgLNIDrNIjrNYRlgI7iBh3DjTqDm3QWN2sYt2gUR9AvWBXoBR7VOzxmPozNgflIbAHMx82tPbwZJFOzvJzMNDmb6fIyM8Q3M5VgDlereV4PzAv63byo53AICxLwkgUqvGyBel5RqXlVOvOays3rWmHeUK15Uw3mLa0289Ri3labeUdrzbv6yczSFTNb/8MKwIAcGwADKmwIDOTORsozG0trNlG+2VQVZjNVmtVUY1bXCrOG1pk1tcEfawH4D2DQdYP/N20SmD0zvuG/kuhhSdB/fz0IEhYaHmrTyNGHgpHDaUQyEvylEiPhpvAsgAdcUqDhBAzjoxDBamWWHEXLRUk3zQIxJnRqcWaNC1AmhIpAAVyaA7hpHlAPTAEsEAikPkuF4ArAbE4NKENRV7oFAztaGpkyLioJfbF3cbQNo6FblBgH+xgUe1gRDVZjE0h+jmFKOA1ZH2aGqUo1CNuTLdrewl6g5gToj+dRS0ckZ5JyNwz5Vguh2Wa0tKjj/kJ0Pi8Q8yPlTocrnq4hEa3FCDocKYsubQ9jkix6OMlKQVSKzZhMfyUP+hh8LpsQPaxNgRhujI5YpMtinZ4414eSNeBbw1Ls6Gp2amgIjjunapxZgSPKLKeXY1BBiz3kxFjZLCmGrd20fav4lvWoCFiF0i7H/rBPPxcbTXmpffcEi0en9a4TrZ3b29250myHaYrEbXJ2IQIbKp61FYJT8MxSGdedJsFuVe2162qscnZbu93dHb9dtt/tHxOSmhwU4liXKB6sThZdbqZB68SUGFIUHO9hC4V931S2mW42m7B+S/EEgYKUJasluMCKgWG0syNq01mLLImeKX+CQedh0gE8PQ1oajBrg1UqguHfLBI4fLvEHTNqQ01rZq/1J39onmem5XFG2PmFXDN/f7C8Zl/Cq6X+CZJlshonJDsrE/AIu0EMC9sGlTQsLrgq4vVMLdh5NKgO4rC/QGaKWGIacOw8l5RuOgcchkMH1+90IOa/2N+azrACjLEvwNZsit0UF7BcoRWCbK67FLt24V0TPbgcxG39QNk1uUNKGPRZcS7Y7J5ktZljwx4ATLywmxph7hHqvPNfk+GdpPwQNMgQwXQYO54MZiiwuRQE2xAwOQgOAqGgE/RQl5+FfF7eDYfm2jFIhuuoz9XThdADbICBfGs1rTkfbCtCEhxC5FEhFdA8I68xxB3fDFU9JZjRqUMNKcPlXD7pCm4sIH8q20pngJRErVfT2Iahf+8X8Lvg3AOBsOtwuevJxeXm2SYvAbmlbDkExXPQNDIWTadUAEa98rqioP2RNAsLylBYAMEHqJgBVgaLpgzMHbjbBA2L39wEpEXjzCY7s00W1LgT1EwRxSjjxoJ/oFoKjHPON5aDfedhXl8dmckO1uIN10j1HFmyxd2SFOnC0Vh9kVKwrAGJr0OuGlYpYquJrxYtQ2mlzzGVcVCL8swKGkTQ64kagF8j100W718Q8VopCopjK6C4i689URK20A+IJnQuzXMmR52pWYXM9Hpi04bbbujXXkyI4rNVaAWkKSDXORDJu/7z0pirFs1kEmQzXpT6cfjEUGba5thBeu5/cVtb3kINXO93sNeGlXdWDqW8Hfe6osCwCoqrx2W+Y7uOkVA5lLKlGFBqiITEw/FVPIzO4oLVG5FIN0RNBuV1nGh7JMPZTXV5Ho4HjtjKUErsFtxU6QAwTFvFtdCrDy/vjtdR1yFyq7L59XcVnfG+Rx8fNugzG5n4hSR8dfVxQtOPLXnV3U7typyHRy8KvUrEizAGooABJbOhIKbfJpjGMVh3UtTP7zGK1rIRZfTb3Lsw1r2mC4I6QtKc6cFxOj0gJi8doJz3ht3QfkJJ1wL/kAGhczPEyF41Y2VGn1I5pc51d/6ovdWl/R++PzjbHq1PH8agTYWXvDKFjYlQtx/giou9Kijc3D51Ry9CZgqZoq2SRhVnwZlZRRRgBmkvlgBUl9aIk4EYz0Ld31USbuBrAuX2cHRLqLkvaB/EQt/dhAhuSvI+lWsRSEvUWC1eFNI9VBWo8ByVBbcPhCMpx9csfloGoIYWdabz1qC15pKm5GcSYKDyBZPDbdrU6okbWL/G04cmkqLK7na4JW3mTtSQ1lp4KzldOg7Q+7J3YEJwv/wuuru5bNkSevbx0X4pjyaLxzTIFpb2bTClTaYAK5VDU4gwIQ0oaMJgscXpMtSQPWSJnFlqSYHUQjHGOGRKUH8O36cNr9+SoNKjs5XxSJ5Ky+n2FS3j8cepyIBkSzCnH/K07s6pmXizamV/7UUOgEJDBqRBqHOygXBIIVHwVooRWC7qBIzPMuxeDuU5bMWvt3V8Ap51RNVMI+ghOGnGhok7t75QDbfX+hlVr7KXA93sSUUvdVq8g4hMktX8uiXrkdWyjmawkwnROjZ/yWSORHT1kZOeOU918lDEm08fmk5fQovOZw48n6lB0JwiejPCVQHNy+Yi3nStRNdRYsk3/KCdyzDOBPdT3RcSEWTnMhc1KtuAJeNCExCwxbvSEd+EWSLpqAokpBRDybILw0GWJ5WICLUxnrU3v70ZJFQ6snWeJKejBykXDXRK7poBRMxK96reuvm9SPI/uFVG+LeyL1wIiFdJFAobVU4sITsEjSkVvZSt9hFPFAmCMGfPWo+WrkRm1j/ICLuKrhjPMQCAxQTKRWMjcRloQoArUYtA9LrztibHkNO7kgft3xgnNF1DGixWcOCk/e6DuVeL37stucRVKA/8tjsTiaXlZu7soX8nyS/8SmuVRmdrVllccLKGEN7vqCrtcczv14jfmVyQykAf3ig1GTo1M8FzhXwRSOzZwpns4LloG9+SyHQgpVxK5LcGVeV6pUQuJDV6UqZP5MkoIO0/JAKD/mzgNHlCvngeFWPrYIiGTcW9SEAwPFJGZ6TF+fgrVBZjsLkB8oTbAUODXA/7t+eKQiDtdeVpWOCq06nj9NZEmQio7UC7ockeAm2JYyQQaaNj8MbMoZqyT4S40BUhJ5uwQyw3OepOW1Q2rITt1Hg3eCuYEDuDTlIiEoYSMSjSVHju3rK8Uj2/kPfI72reEEn5D77TsyRz46rb4Fwc49qev2NsROWSHfmmHx37briuXDtZPWbFaIaLrcdcUJnH1U2G8dkk24tVhjReHe3rCwhsStxxo6p1qZ5LW+u1kvD+DTsrFg4DdfyQCrTDuzdHOz6DNoWyqXOoncD8KLT+C8pNiqE0DdeyTpqd6z2CJn5jBu8mzsXQA4U0spwOkrV6VaYE3+8guIKg0kAD1yJoh6/vRtCpKHpfFxKlhjI5PlM5Nk6lDuQUPIkNdEWaXk/i9tdWDppsyTsM9t29y+sA7BtYHER5q2gbOYvRtmZjbLgnghKmHeSXKhrKGznO7v1Eg7jmvq4svo1Wl0/E20tH9qGyx5eeVJtr275eqrjRPVcplxx1P7Cq5W7s0FP/lsU8hM3qRNNylTBTwnbYbc0a5+ldB5M8UxzyV0VQ2r2Wg4zfxR3GBMGon5T36dCTMsX4GiqF/2wXk3OhUJR6gtdB19zBwOF5L7zh/8rK8dSB8rJGolYqjTme+17uQDY7tl/rLMiR+mJwqEUbS09a55yo2r4QRFC27tgamxxwCXrHW2OjKwwyZhwJVkQHojOMhCbV+OtExBzqWz144bIAwPXJWSDl9V/AT3gt/FvV9DLpa4kkcYPK75GNRA1aDiHjMMPATTRifViXHA909in81q1XvX+wfHiN61JXFECEqzYQEfv/HDFYFFItyUjz4zKqm7ovloZwWPTQH71LMd61qsNBMTe7JemSwyHp4larXRNCn7NMfu4U+NORlgAJjug7eX/XAHRQQxoCfDGwzf5gri3/qvYmu7pPq5YEDVaz+2trZMgC7pVtVTd2HRgFfPM2kY25Ll6SOc3Q2RIFUZaVzcL54q+Ozo4NM1XwLvi2osPGiWJUn9QSdlnqyZgbcO9yM4yiNIMijGdQ2zBUHF4UV6om7EWCKS5wS/J3xb8d+SONy9jOLvu2JoP60VwkMQN07ZQ5qqpMcd+CE4QRPn+dX0mvvNZkpVdfQfhPev1V0Z36wWachYYQ3eRmvw0y5MasO0b1iibPof8wcWiEbOXhB4XAP4S+B49q+gJmXzNGTQqCDxbw5WD66y/fHrPfyuT7YW7l8KdnE4Ps4t73zz+61KxOMOujxFj2aTTFe7gnp7kgABgVsD10CvwGqmAErcCqtHZWG+BT5s+IIwhUDu4iJAp4v4qLHTwU5tngUJrx4C5XWBI23qzM6zIhlfuU7P1CPqfrZ+QDVmmC4NoKNzL1OD2aSWC06s0Wn0fqZSMduphiyNC+okatppSEAPWk8qD+oix8EYiCJd+LNRAbStUos1rq14goRTgeVh4i0l4+RWmMVWQEJEhBtL4II0We6UBxmCHgYSS+LBCQU8pQbV3TwVaX+wVBsQ+CD091vUEfaANTl4fgzGu/c4rlFhh5y2Q07snSbzpu5QJgNLSolAGsz6U/0ZOhppppp0fLAwFMVBlmnVJFptBgpmVKGECEzg3aOPJmH1hIpGl91Lks8E+gcjD64gSTrluWWAARj6UXHhQnDNuB7keTt0mgXKCeVVsHBa0uFyMaKifSUUCyd020gBEpAb6cmV5IqOJ6xtw4G2jPFbVgdh94xis61hMVglUA7TV5Les9yNoiyN47XnFo5mqwv2Lglp5uzMELnNQ8kG3j/b3t+IjFV9cFIGsHsutjg6YbFMqPW13VdIxED5cwOv8Em0DAIUcRoon26OQP923iA49DobDctXYKxcR3AKUJsEnfYIiAn4NKPVZ25AZ2olE50nWtLWP/kn+rSQF84pbKtRCV+d0BLBrgJWuQ4Rh168LgfjctiRyqQ1nj+noGt/yUwhg5HkeEy4dwIc7Cvlm6ytQZ8L0D7/xRjz0whoJnHH5CH3tndWVoqNwmaLzQysMQvA+24yGzYD4ZwCbfT+thJ8klKI0fJlDw1RwxKDKWLUZCNoPssMf0o2Ws2PfiDG3cvgcILQ38kCGuiVAMWNZtfhAopddem+UJQj4OntsYGkIChGZlSC/o/UnkTV3yEDKDJBvAqAyZDcg7JPlmB3z/NuQx0bF3Ifcg98jZltCjGDAGpPw4QEwRwfgSJYvjatCyzG8y1NlMxL4o5HikxKOlh1VYlTzj9mnkl9RBc4ahQtI0wyMFXYJMc0Pge/jcwBPdRCLc+aJU3CWaqstAufCIeomrsJ1AFGY6/mwHPahHVh/xmfX2SZhV6gYEJhinHPjs/DwX2d77BhWFhvFvVr4jSuh3oin6ljQRfvjP+b/SlEj5odhpCCi4ehNhzBhLdLnKEP7BjR+Zhd/Y2SFIcV1rgKJwye1srRKZ5bHOxzNG2hgGxC+/0+P80WKyfY+qQZdbpRXue1R2KxSl2i00ZKA6kHU43MWiqyeAPwoAVbMwHnjk+CI3aPO5jrmHJGp++vAeWjEqU/aSkkip4n42UurvLMWqP+J+riFu6uxlpQlxxlpQGH9ZjptOKfaG0P9VeAyeGC+iqds18Q30QM2KhCXhHrokaLjPkmX8OKlSFU1D81hxS/d3AKcw3Ap0SgT6j9kX6AoW0VZCUSnE4w+jhJSm5m5EMFCP4V/I8RHzC0F+INjYCIVklYlSuUqNclnUOgtEmcoeWhwgldjKqhRP+plqNmICWyZufBov1/ZAsZQGuZP+nhwDvPJMeX8cwuo6oJfX6hV2FD9941s1rBQ6n7DAdI15y6+X74vQHtP5ytb3r8nJtZmaC5EcaBSLaANCXkwDKznaqFDKRwdl7b/Pu6So1X090akA1oTr0bEENqZmibeYBhvSUtw2gilHjQyl2Q/cuv6S4630xlYF8z9rkB+ZTDEvphEaVKZmMiwayg4SIHlhApIxEVX4q1ESoY0xg7pnKHauYTLwYkOFumLLuB/Iu8D5SIa+wZToNxJPGONdZEoLIv1xLjIJNo4K0wOHhjVjcmxHSsnOjO44yPwj5lpLOwJpINT8kWjT4WNwePOXADWeUepOyYP9ByhwoN7FZsU2vYcAGo3sJjEbT06dnVOKBwADocztZ50ekLFu25iQ5Ey6luygQRVUSxkZPZCg0hgd0l7xc+zFjiS+I5iWDIKxL7EIhwrcS5BLGgwiGJcPxj4e5h42pMjDLM8WQ5Te9YVf2TORuKL1oBck8gYY9kPWfPh55ynVii+ZI6T8vOnXgUQWJAINiPj1rkcDLdj7xI8xVSJI/NNdT6bR+QZO/q6sRMc7x+CifRr9ksSc57WoDOisla8Sm+VicLG9W/Wjn2SSQInxS52bIq7igDSTqCiS6g6VHv9GSh+Lb9KFgt3EbcE5lf6pSRWuDNsnzVFrsLoectCnXeOq4X3Wtd37AxxkO2o6QBGAhR09CkBMpESSRyN0OsDQBsIWCXWU5qDWewgqIxXQDp7q5uc6oYaeCF6zpjBCUZKGSLikTk1DZNb3f2khif0PTQCePvgV5Ap88EtMcUnEsBjxRbl4VX78/181nbbsAnR9pO7l1ns+4dY09vyk6xNJ8uOKcyT8X3j38KQ3OMgMhBqudT8NtadUCaoOwAiFAmttJC2uOHkMFtcGzl2JFqHtf7iaR6Ee1CBYFfz4TmjoWh1NwhNxWnKAdyozJ3DJvXD0O5jvA/UbJ7O2zR7j/Ma8zXWelB8Hxu9VnIEZ8K3Qp7FU0K03UoNmpzm2V9ewkctSvh8tvztZHP1WcN9gTxJMBBXiiieN5HX0qAX3WdJmM+Cg+LXLLHUMM9J4NZU0EKDQ5y3ZSXaKnUwHeVGVcW+O6GuWtgWa68FueXHPdCv1btld9de9DVs237UXFSFPu7C2uY2a5BZpyXzPt+HE/PDojq2sfzO5V6+zitZovjspcwG10LYGLRyDsUXeFKi3MbWuv1jnV1mTymNokNXj5kyegqNFKpKiAH2bwMAB+jQLxqlREBxqBU4rQuZO7Nw3IsBTeyICjSb2xEpzCKXOuH9doTUNshIBYRghJGAQprcbSgwtnlWe1jEiDCSUW7pbG/4lNn6P9a9b2B+ROjE61602C3dJuEmRBMAmafG96cuBzIpBn8bcs5OHfJulnFHMDqImCr8FPE019EJolMQNWebj+MZgdaooJdzqmaYUAxj8EVvi4gte1c/Pv0BmhKSZeipETqYs0wgMutcyaWGzQcNoCoU0I4zxFoTcm/dmQXdCSIOJGWzxZSV8PjSjyUnaC8qWLmSJG4Rrg5K/v3gz4kHcDkl5eHvGMDncEPfowxkgQqQT5mJ/PE27QqW1cQlV2Fg5L7h8VwqMyUIgZJS9nxfNewC06r/osk+IKyHWbu2QEc0ix2rrUW/m2ClM92zwr67lWnsuOEjI2RPNKgLrK9gIobDYqVy/rKxMn98GQTE/vv6tTo88CuGgHf0dlTVnXmwN+tijuS1roWz7DLDkRm3HOZxzM52Vc2nizruHB4UWrp0ZOwDgEu0h/skNdMNDwAx12D+iIWCajOMqiQYOwJNJhmAnBcO9wKkZQBWKPr+1bM5cOYHENjJ22vnLstPaVCU0g7lPud7tFppO5waQFjnIpfszDqTOuSTivW5XkerIsnjSvaGjIitzG892JwZ3cgO6i8c81IBKRWncjRQluGbU024NcCuNUqXf5gWbskkW28kBD971BIf2baAQbAJ5SjmXJqvLg48Ojg4gw8UbbsDOnfTgMw8rt8JmrjRpbeXyCoBWbe/7gBdPk243O1n1bNRaYwQ8y5GcMNYtBBL8FO/9T4Y7nXJebV/NIp4I+52EjYDu0B6l4gMPvKaq+LhSuMUdxE35PjcwYumtF0mKqNyHpjR4uglKPRtvex4WWLGMvJkqC6j48dwwjyWAxsGtiBLMEW3OOiWbKpZuVqTy27tLYK02PZluf9ZmJmDR3F2c4EjQVKwm75MPbusDCmQm3+JIN8OZqN238yGmXxqt2zvX+uMfHWQCSXNvSIMg2qnlU2htZUhlD6DuC4Q2cSGl6eOaT7Xj0cD2XdgHt5/7PGH4j8HFE73l/JZ9miWbCWm8//5Hnrd03uczmEBhI5O9/f27WdLYMMXGlvUbOToh11ztPEsX7zDLTQz7XO0H7+ygAm2xwzomNvZQQ5EgPXfbmD7+yZOfjR+UV8kWINsavhmQ1qMvbClbh57CRndTbytt/t+IlUM2cxsPPBrw83rbYUIveu0shyQDbG37gEOgv/NUZB7SrdcNOiIz/vTx4zP/i8+OqiKV01kK39MSzxiz/74i4ByvAwlB4LQM96HxCa2tJ2Z7P9y742U3IKkc3JyHDolnzESo9pSEqfOAbgMYPEq+sVD8goApBR5iZ0Th/0rQ+Qo1KhI9XzWQmhG6YnYJwBt4gtvZX35E/AbsTJHWAssmANx4d5Xlm8xN1Oxx+sLOq8sxlBgoPgvxUzDKB5+jKJV4nr8LCxaX6N7DpJ7h1MnITu+rLh5sas1ZDVppROoChQ5qt/Hm5sW1XXAypIkk2TCykwqBn9wWYXIGXau7W9ZVwu2scKr0o7Hg1a09J8+jVJBwFNn2OyucEj9xMXjT6WZezTSwCafUbTTd3eFgiFmVp+5FAU04C5BqkjAj2hYfuSG2C4WsQCHdQbNzcONmiGDe2twRmcbcVzlPOz2dvavXsFmBBFeBiDhmt7K2qiAKw8RoEJkh5f+V7NpApcnTYxo7Crs00VRPIx8i6V0gS52b1mne6MdttBzpvGZt72dkoMM6jByHgkDoBMIjj4Z5Zm6bsfOJfWOAbH5h/oqz8M54SQVoec3oIrBY+4qRfAJtZWIuFKTquOcAZby3OmKSTaKXOVvq9/ydsQP0nXBwpuSuAFupbqX/WLHUB0qjAyLZ+3pnbFJTSvtAMypOJ6nEElyeYDwlxg+CjU7fDVP6UuoPjczP6D1oOkVQVV5Z+nkepPSpr6Dn2/XtCE1msNbJSw3XyNsdqapYfZ4vy9VKgcB6xBXZTqQAivsJ54wxQJM7AF37VIPoUG9eU2rYQKui0A9zMaHShvtQ3m1TZUmfDPRoi3E988P9DmqjwV99YIg1NAMpHVJSLTe/Wp3dx6bajzhJ73ogv5IbLRDB9BhWRhYcRZGv3JYJDZyVSQNltW43IxhA11edZyGx7mm3fFdYxlR28lkgdRfM+5krv+JkWTUZ5bPzT+fMzUpr5pTK5PwapRXTeY/Q/8SPV/ZVrr4srVAreTIBbZdOrtKNiyEvvB+nDtkOfGm6zp+Exdfqoc5PI3k82P8i9VXhqm6V0XHMDRXVD1Ah/Mb+J/Q+qr2sjbqFvTq9ubph3Lt7qgpxw8wKPRi634f1obUcLKtmojKN87Bf50JkTFTaHJJ2EH8KDP4QlYHWc3o/YUPU2tlbLPjynfqo2tXMxdak1elHslskjmEkcQpRKbRlpdsnq9nTv7/MhttLe9VNOo/3b3u7XhvFYosW7f5zq/POMv8lTeLGL1RhroJoCYuw8DYXZ9a8hWwlH4OGW6WHB1+0PVKrgoZ/zAMjL0kFL2Y5n4izhSr5Iymmt8Hoqc7rZ5Tbob25k02c7b52ekb4PuEGv6xLK5bpQCqLkleLY+jqARs5k4LZN+LSBXssJ1usPp6RIEhIORUb9MdwA9xX2xpoOygT85EpSjkIBlFTl/s2P+cXPo33ihjerxjDfHR4Jy9fu9WQZ3ycya1spDKvpZ9wRLveYw1tFTzFMyzKYOogdg/v7Dwn2p84aI+Cb8g99hCqeTvo3k5PvDI8r3aTIiVXp5f2GUZS2+NBY9PU6nxTU9eotMgEhxlZ5PjA63QmoxikRzf41DSVFk9fSmmehDlHVWbTb2LGP5gRBTT2v0aEAWgSe9eh+SMaZ+eIsDF7NWdV6kqKoqajB7l4Lh0n2tqJx2RhXJktGpwVe7nNFq7aWJG1TAgEAoCjnrjAbas5Be8myuMRoPeFUhvpjc8pT9ux1lvqMb091AUsR3QeZNElBVzA+c2Zoe0ErjJlQqTQ+UDVo7aNIdIH54RtD+SgpjY4xpcJFo57Jnw+WDrUUAoNT7X8Djp9Jm+wCCHoDou0AJ5sjTncGxpshtfeEQhbL7SZeZUYT3ZfUkwSxHG5NAEGZQJNSIu7X9edFp8MoypP63hmS+WK7hMjD2JnK+QpKop9K+vNnWVoGNJrZuTNBOljPj9qj1Y84j0jAwuZYRjsGlpskyg+4DFbQrV0YIW0n9h7EQKucYF1FqjI1EKVDaBc7W2+mxopdX6QggduaFp11N2ek0uLImtCzfN/oyKGo//jLb4yZ3L4GDxsIkaklZNawo7uPizPiQqZunpon9N9BQ8QQPvVyQUXKYvSF1tNddo0b+2sz36gII1HakljwleeKESuApdIZvh7Si+vTlIynk9hJ7s91KqNTFaaaJ0VKsNAR+xkFJLMQ5bT6Og4rrNp19Rtrc4Z5ZnuWLbayzdZwJ3RqpXG7OEP0XJdTUCwHMrMr9TZuj87Xu3FjPJgeqT/nmijEZM/VnHS7W7Mi0rGvQZkNJmai+k8ExO7VnFuwyDpLqtamIdtiXxlmdIi00knRnaUtD9jbVKdV9qVOuKO0vItsodgR5wE7tz8lERgZXAWw4ov9LwsYee8h9Qmw8agNWL/K+9dQaXICLvqeQlUJTKKXFOE26e35d2oAKcDhynz4ZGb8v42CzZ/uEExKjPtyOsDfitDG/RLKcaN02KoyG44hg/K6hPthImDmpZAAZzA7XNE5hDKpFiuTK3pz1cD522bnaC0Kn6NytNzO+ZnpQ+teWCYWwNHq2dJiqlWqRrYJS1XXrIqt5FqlE6x+Bt+zVAU3EVz3x0CKA6XgN60oz/NTuA6QguEU3Y55pOune6iiINsj1Gz4QzZnMX3i8638sCvlpyAJ0+5HXPn3Fa2gqim7z1p9a+ZW4+0Ifgem+94lP5jLC7N40cdLHBONWWKfa6bZ3HekdhIQuHeHRT6JQIemMa06RoNKb5NFaTG+QGlulwI0bpIdEBKm51cFBvApVFkL+t/nzuqgUlo+RYkGoSHTUsr78N+AqJyqpmNXxsVe3se2z6nxjUclUGLz7N08URhKOXiPiNZvdCIsN6IwN3t6HJRJ+ZddcZcpfw/Z7+e39h4Hrk8m2TP4sU/mFaadJpUmf6wCjfLnsSv2m5a5Says0rHQ2uXrR1f1rhMkMiK1etjWQr7IUOFbi0rlq04yo5PWa6aqTazjz8akgzvmpraCRNlFN7VV/IcYHQ8hpybQwZ7TAG2Ixl+3fDNmzDYbgXoIz8g/7djLuZwfRqiLO0oBeS11RatK0gZqOYj3pSGODUmgrSi1aJ6LWkfcYWegD1dUihYG1U/9M1Eu2aoXt0+RDYlDx1cOLuD8pxQbt67d2ir1kS7bQgEl78wMcEeoq18l7AVIbWVnnVca3vErGhEMylma3fn9DTk5GmxtvIrL0xNwPGbLRlZtLpOKA9Rvm1beWMRHSEK5X3djyxaRguj26mb0dLLXJEPReflRTcW6mVQNG8JBH5+SvZ9+huFmm3nt7AG19t7utRN2IY4fRpeS9TQ5NeSVgS2Sw5u24qtofgtwBQxhfI7AGSGu0ya5pRvqOJO6Vr0SYyjA08AQnweopDQTgiFIreGtZIbvPciUZTrBT6Tg1QVlU+SzprOSknZzDMDVclSUo+BAVYtawBcowws1C4MULQUWar65YKUJaO+pKpYSspQi8gEK1WZeWzcgJ3KbiDum/RjsXExCAnc/oB3Vz2+dGyQSLTmhSimzavNZ8w+U/NpJvnUz0MjxGriyFCoJXESmpr6Bn6cXTi3czvP2gY9Y7aU7HSMMG82T6CJ+p2hntwb2gu6O6FQVE7uxEOIQlG6krcdJiMax/rGjPkBYYHUR1ogWI0ELQfeRMrbI7ZH3tq9cdp7I+NxXzF38d8yua+lHxImOFyKSdXGHDduBuJKZ9I33JkzFTUS+zrRkvUI4CcYEx2PINpqHmbcITGzy5LydrcNh7vf0A6Fqnw7TDriOnAwI0zl08HoiLo1iIPrQtW+3ubxHXgAMxapvNNMkVcaU1fGYlpJrZjyzMRAKE56nXz8UJFbNEWaVjzwb0A3ogW6zFf9lDFK/6tMnQAdj+HrRrp9Y4A2H4px48gHHwGalhZPywPR/23ljHG2/hcN8mi5N+xjIa0WisV9wLl92/uniwcz6wQLHSnPop5/PfL9h0dCl3o/4hOzJ0S/cSfgOwW/eRPm87yDgEy3ok2CSZoVvQbeFOcJ9Ez8BokYI3sUQ5wftgOTY+yLdwEQrbkXghK6Z/v0NLxz2N0oJTdgET2+2xBd8ERggt0bTmk4InjUOaz18UAKfwlZE0ted4017LEMrPAsHdak/Gvs8IiCFa/aI9fsVpC/xq9KDGm32aTlHzUyulU10Ya+FiluOS/W3SGWHi8JTqOksPhinGVyjPix7ZfMPZurc/7FQw3AqFb4Hi4cknrFrQUJyE1PLh+EFSWa0J26dHDVJkTYwCtBdFyd2AWUV8iq3WPMAUT0n8ZHLRzmjsDbGH4EwiUgBMOur7HP1RwWbissVHdfhbQalHLsyROWd335Ku3tieMbeP9JPjXBXSjpkWfLGYbg8Z863zQvz0t42OaF5h8fsJ3Xa5eX/x1p4VyAaFuL0CLy167NjwCirc2tq0VkyWXnWKggH8SB2IKCWCgOyi+Aeq7z80F6U23VGibHqqu2vM2q7UBnN7Zrz1aCVq+7rDjMNSVLEIN4mjWNhDpEopOPCt8OIBCDsnpWY2DxZgKV+A0Pg8gF2PIbgSaelQc2deICBqUw/B5BY73jAfgVfNahIrC5I5wACO+IRuJ17IdI30y793zAgcDJWdwx+DurjzcPH8Mt9Iv6F3C74Xme08Xao5PYBuT0EespGu+ILes7vBvHGqH0k481ZXiIktxFpTriArGPCIi6Uve33iaCEbkj1EuljZIELAFJ6UoCTEByVRFLgYgqG8/cEhD5EfJqC4ipX5xBQ4sFzaI1Qs7PXBqfMmqawFQAC4V/79qf4ANJfy7vUwEfiZNhUQD2MglqWGSFutx2g0Oiujy/qOAYFrlgbmCfN+oipCXQpk2IEkTpps4Sgg7HUClUsmlTySLfFB9Ber4gLYXvmwiVlEDBJuDFKkLH7EkgK9va2p7USHXxrCXRxqSTk1UmN5LiyliDgSxIk42ZkrQB/LLaALWUFxmWLQmKc91K+G7+nZAe+MXgc8MXscC4wg9X872rodycVD2bzmWlrGIWRh6kYMmnu+OVzH2XZ6nVdH+2rWoSCspWRf27hMuF3IL9924hMBuatFXb+0MF1IpPDE4ERuxbnn+w1aOkxDt2UF/mixVHFJnuAksLwhLzF6WwN8B+gE8P8VqPkeru6wSYDoxAl81qHcDwz0AdcDfvPq8bvoBU4TxDkL2QXl02supoUTG+CeF/YivwHb346D83uAqUtjO616w3jB2GKSrChNmHkalolBpV4c434vytEq0TnXRQgwWlHLe3g4sTm0udKH5RGyt2JWzCUcotCtv0+BmTGXxnbkWx+l552nS6Qz/28zVilb5jOtEb0rWfgZvuP+5/wJLtyrYE/3PxwMqey8bzu4ZtdyQOjDxb7XUY/2cGP/1IrpFiEeJ4fQztH+j00f5R9qNd+xAKOR0pED7Jp/pvBLW+3pU+agU0TFFPDoPYiGxXb9/lFkLo7tLVEidvt3CH/WB4Uk3+u0AUbUjpDRUGggFKOKsLWFX7iJAPHKPDLcug4bvJNgNAI7YBPn84pYmY222rBtQkm3kRzKim86G0mhXpEPcBDpyW/KPox2bTdERNDZTeQUBZA0qD0mtq0kNdpc4uEfGGKtP1k0ppBwlj/DbyY0rrLnj/l83lWUb0eecL+Ci4g9o7HbZ93uVvl6fB9dp/XO4Ghx5/yoi643QHFw1bUTumPXcA/6x9mKi7V0Ji7r7XOanZWNIHkw4EI/q289a0Z1cWx0zNu/zjfDeE/IVnSLkB7wD4T5iVHXZ0kqDB5umEBhVWQ5zdkx4WWSMJDA6UkWgkIcme7ATbJVv9Tra3opFnSfZjwrl9fJZMs4KjWYzqTegYzYpGEkRHdlmyDd0x8svYxfTBtkVG8snvk5NClpCBC8sDAlGtJWQBb6qYzZJI6ClF+hCLptDvrrMWPFKoM6Z/z6aY8o3G/Z9qDAenv/LCI1qv+eq1CUmY9N4Hpo9704Aws+MjPDWUaUe2sYbwrIzeu+bfqclgPwZ2icXmAN/nU8CQPzNkyM4wg03HzhgcO35kHWB67NnB3NYb3ukrh2oEFwypgcNBNT4B8mvxl2i1LrXUiyxqq7lMkS78IJRKhD9QpWCALCQXpHYwG5x+M/fPqtj14GGPlcCqn0YrpYdG639ne95011bQSAenMEQfy27Ft0m3pQ+zKz+/zbItFq0LbO2cOkB4iqhorwz7l2NAhqneL7KfXt4iBR77DjNMb/KShSPciUN/TPnc7f8bAiGfK3+wqA2vFWdWPIOLgQdanhHdt4ZXL3wcYeXbHa7xwVna4DF12E5vN4KWqsPXhLzE9dv6+2FCooAkCLfISW7bXyaXoxMw2mGR93EHvC86GNL1K26aJSBHzfqZn4GwmI7tpvRT+ynd3BujyP+/IM8hR798GQX2vwcA4Zh3kohgzGSISrpJog6ZjAFinxvmkCa8LMVQFlNGoo4xxsK/Qj6GVkx0a/Rozq0Rf0k5VywN+yomsx/7iqXu0qZAyUQJwsqq8sqS5wN82e66j5jfMR95RovfLYwjQMd4XryhCGxltNJ96jBGLyOmLrkuzUmdD7UFsbV5ykPsd1rmJYP9dM/STHbaz0NcI0uL/BwZU2bxQ98tyMyL10FIzf0s8JfQWhrgLKFvqxC++bFxLRSu6SMzVPZRs3xZ5pwsnCUxJfrKqv/nAClIT4ekmBmvGYyfS3ZMTJaLpqPArUbGzgRmjCAU5wniDuIJC8WaZ+XzdrIbzh3OPFhUr1B8cgfulIQEociuLyyIvtkFvKgHRShKSKC0vgD+px7+Y0vY4ffa99Vmm92IjPM80S7UBZsjdWSUX7GNjZuihllpDPKZp9L1UDxRWv27OigIcXScpqXRHXXceOkAkd1YBvO46In1W0k/XvqRtPXmOvD49xGWnjVSXz/I1rKHt012e4qgHi9hOfWuwZltLoCTxmi57VUFRIiRp4VNuHrfXIfwno3x8WnWc/Kpkvh40HshGqUT9gYpto4YuoFx74Je+KO1y1rQU15SQkwXC7gHpqnSfYryoqhd2DTCSnt/LkvN4/qziKtQysa2LepgZ0dut96aHevKAd60x9fSoRitNkbxgWdmTDqioru7AixCEYzpGja62EnqxPZfshjNng5ldDMW56WbdLEWs7pMTHdb+UrmcN9GMXWR8SdtKGYXU38mvMbuHK4FFgzUkx6LPTzTPWgdj3RPEFK+2VGqeKiQOg5AH/q2AX+M5QVpg9RY/cQR3ARO8fSNULCypgPJwgJRXLJZQeOhAvOCMcgXA/wuLTwsKsQMjhG4W6aF14m5njUcLDDNF8flm+WvO5ZiOoH/8NB+l2nWsvwZ/d+mpjZqwIJtk6HKSijY5A6wHTeBj0BvK+Hk/JB8oGEG6t0175Q4pXpADYT4iQHVVcOolINMDRQByTMz5RBRQvDWIjI1eut/HzCoDL+7p29ilNd5lZJKDlQeUDRyaheWIOpqoSUU6uocFuqEN1+7RG2ArycsCd0bumQ8oeFP8z0LTHMgEgHfhTlzYQ8IhOyqZ6tBHq2Dpn2xFXApnU1ivuv19jgJ7OftldQlte5RHqSjeWsnqWAbISE/o9Yt2p20h6Y16ChtnenQbwLZL0lyzsK58qS/FOfLGErAoJTkeDtsQEdn7xzFv8yzVggVYNmdUoLQB8yekhOIvqMfvTs0kP8FzAOPpfGfuM64fLfnz7kOz+c57I8ucy7z3pzvhicuH+1Wh2SGeZSa6ZRKnWmpe1hmCJRYXLz8tXAACbdfuNCOIEo0W0cOCMGyAFoTobXGuoFkXecuMhcyQ5aTJZ70aZlsmu4paHu8EjJF5nXu1iTrhprWJgLN+Pw5W2Qlw03gSTitBSY/I/zS9QckIQYI1/R4zx/juBkRvI/H+FZGzHYtjoSfwMmsROyBQY0GPF+L2pd6NIOwxj0JM+kqCveuR7QpQQS/MSe/wT8CWO/b0xiw3Pmagc3Yh6/5BSG7FNzkHLjcOSNX7BjdEBuNm8GZG27i4iBNZu8CbGZ1qcmZ4gDnpqCcDBffIClu5m5rJMsr/HNOVLMg+XVcyC9NmIDlLvVFLtgbOHPcCJZy5/LHLNLSYw6YO6NntgUh/ZPgJGWxLgkZBy3NQ8fipjU6fhPnECkWisWRyPRYdbWcqTtjpoJhEghOqKoxucm7xBsxIeBm8J/xMziC6TDv0tg0q7FRSqOi5hhBjLmylSoB5l5oyG2EfBPHZMTEwZ/ibJIjbr+HHDSZgk3NOKefjfTm+N6BXry5NMK7aYKhAd703FbzlTrisXJjlmneyupq2lzFt9nUxcp48oGKzdW/9NIOtha3prmSzT2KPVDf+lYjKUX1dpgutiH0/efLTWRz8eOZk4HxwaBqNc1UIYrbO4ohorRCFKt4RljNoXGAD+ww7AGgBkoVV839zQEdL6lUlb0qvKXJ7GVOSZL9kclIxGylmcSseRJGJgdK93e3oaqvF7KAfWr1DZMa4D+FqLhMMz7VRLLf5JRQwjs1fpJ9jl4OUf6S/Wdp1hIbWr9+uYhuPwnq/z1M5AGVjAOHjvh8Qg0aGV8/RiNupYlE1KXUsKFtJdLGWD1h3QskGmlLikhbCnSLdLQL3WE9SyOhIPn7GtS8YqliHlXzPkie273YbKnZBwj4id9s9LPKe7MTA6Aeb0YaADU0BX57HsbxK43KZsF6iyqNt/M4twFnOSQSU6liMSTHPk6Pd02Rtha6VaFbrXxuijD9MVbuWDAX1vK4q9LQ0eSsWDTZGtI6uYh6a4dhCF9xJmyJbfR7V7nzQG8/3O3cDcsR9M0RzRGCFUA00vXUA1S9QxWkqrx/uPJ3cx6yMGL66XqHfOgq5fvsEroe6rcwWvSDKgc9uPrpO+UqlG+zrV9nNO+HFOwXrtL0DvpHeLqe3o826g7Rv5L0KwtqmUAA+ZhehbQLQuYrao9O8Yj6iiQ8wO4zJVeTTfdDlD7hm+Evy37gWe64hLr6eVfu+qwa+0tzL4VXOFey2wBndhIoxFW+uUNGAxO+E3NriRQmhVSbS5SR64Zy+fVD8+GfndiWnVuNBpqWmJjomBYLqkWrRtM2ZMRw8zF4bg55/AIuc9wWGBuEYQP8Y5y1yzhJ/huLHzC5T+3+hOJF67n5rr/EqZTKjsHG7szkbRrNiAEzHBkdE9OiAejqrKzWcnHZ9et984gfCUgChZBXSwRMQKifAY8kGIm5glUhQAPg86lSM8smJ5LZFA2ZaTOz5jBtGLKvzAlt0bRDvDlMN8bb6+Dx8i9ohZ9IgcZX1AY3gCDDuhzLeLS/CK0gZB8/aBhtITVhH4UhtPixHoIQICKPWD7k/12ZAC3nfvX57j9UTlzuDwTlmwqytv1KxtRKMV0acpayIDuKlqJpxQCfzmLlpD05GsteSaaTokk0szRMF8Ye04rhalB7faeuzBerPm3+go7zFcah8TlrA1cFrVqRg3+s/YTouC99n0qfT7JJzfWgW43TzST6uw3Qnv/yr+W9ft+k2aCc+54LEpXf/IayiXn+gK9bcqJD7S3TnBKofUkLBtP8saKfSeNe559NPTvvNU66Wmy5n74d0XMACBAtRYV7ny5Hi5duA3lsLe2nhXnKT7Q8dh5tjKaVf1CmyPOfhJ/myULK/MIUTc1WgSkyjSwk0SaFkzQSIEgo0w7DDk+MqZ2cGx2dR/7HmCacBhxn1qHHtDZweDh69ybk2f1vuJIDYmGMXLA41SIt8dmrcRIhGgq3jghPbf9X+e7dTUwnbRURkapRdPfaTXpKGhYr901PwwBjo5DA9+P7+7fFDbuC1otjtnkDeXYka6VSLIyQeqtSzVPD7j0YJRLCIZGlo3mV/hLpqTKJTLLeslUu9clShY8fX8C4XjGx0qry8gl2N3s0MytK7u93bfwmp5UDWLD7CeTCR+RJ5ImFhRMwu7KwgDwBVGE91dU9QpWxABzSuWics5yvRkZHyX92znLKdNUdBIWIjrEL1C/0aghAegc9nVVNFwAz7IHzMZEnFz6eQOLrv3TAY2OIDmOhA+bMhbZlPrUaTZ2PwIy8RacxszPN8v+VMpS4faM0RHKh94m+1BL1HBgDhiIkH8I5loZpudEoTIF3VH0p98ck2Kjmg+3tYmvqfecsU5Zplm7r99WxJASYV1ifcpSiTqH4ilMavtMpZ59pyqnL+hr1lMbH+ZQTX3NKwc9JSB9RU1785CmFD/oUUiqcS+qod6azj14oRZ5C+zwGMxAULiTxuEJMIyZMw/al4GOahVnNIc3ZVU9Bs/w/vcwVDEVa0RTNIofNwvroFPy+MF3H5K3jpbx0YLzn5kbXO7TQ3YAbvcVBT18zFPIINEBFIAEkBFAAPbdvkgJMFQ8G1ANQNU0J6HoIqAGkH+3nEvYFmQftM98bbB68t952EKBHBweqXQ0CPx3D/Nb088S6G9Zr/rpRMcxqR6xOWI7UpjKdft7drbG6ubbQwulXzG9fYVfEP1D3f7f3XQ9mgoC9RyZ+Ls9EGm58Zb/W5AMni0Ic5ZoTMzWsdMLq5uuP/93+7/rOLtou4HN4Y122tIEid24irpySjkxDLqbMsQWGIvd+mu0O52RIN8ktvpf9km2Az3G7rCQct8i9h07Sh+pJE9AsvafI3SBgz3nVotJQtXmHU2xSzMEZd+PpBF2ALgFVKrPLybWbA+Gu7DnbuiWNrLMtT0QChTPLFDeXAZg9972k02Mu4fIMlzpUuEtY9Qi41KHGXsKpOi4Z8nCXMHrSi35El4hziVqkw8wxCPg8NidEb0Y97CDODM793cd8QzR1NyWW6b8ulisGZ4gdqIdvXKft9jniFHEOBF9sY77ZLETPyZCKnUrrnlhPWT9Zt8LS9Q2COLcdgm+tgyA63sJ8wHb41CmRaZjeJeAGdfTWrT//FKCCsD3Y4xjpx8tbt4cp9HNfnTdi5adPD9h+PTDol/p699eOzvT8jz5UKOf48d9+A189CJtf13DxTjZktli7G4dPtiZXe12EHcvepi+2XJxe8QbheJGXn9w2fNLd6L5NQ3zSu2UwjXXBn9Ky+ryVcrOqX6J9OYq2wwA/u9GBtH3gqycas2Y58Ow36UsslzAOFxYktzIerQMWLG6Fb/U6NppxwrFALE8RAjKf/Jk0dcxpyZ7SPISi/H5xXnJpivSZzKf07ktmR+0KutikRsHC24lQ2ZEI1K4xKHFGNnTgRPff61yds33/tpeaYznhZpzm23uB/544JpTs0875JGYuUy+anU1elClhfuJ4twM1a+JNkXLUz92CWbSrvhxUAPlfb4si2C7x9x9KLAsW2QRGV0XGYMesGZ2WnQz6qiDU0Tcf9clWqrZsDYBWIrbtgBoo1rLEQj42NmK84EbutXjbXwAE+Asmq1hdzt2MVYxu566OdNQo9Y+87u61kKy7rm6bGJzs7laH0UdN56df1tXWpqXU1AQHGr+drfj+vZVgqAVGoSei20nT3qnL/Y6HgvV9xmHQYXrz0s3TzKOZ7wqW+/MksC1sxwzyhvDWk+XeTVXet6dZYyXdgd2HtwlzvS4eW7AXO8fuoT0+0DXwHhOOg0h7t4swRaVaVtrdNeMw4jDz5ODBcNnAwC2nAadpD2Sq+ln6yoIsVmRkmz0+Qps14bgd5g9FRFIszd8r1ezlBGtySX9k2+AWYRLALF0qZ9ZFfUmuRKrF3ORAJSsqMlDby8sdZ0D9PbDGEelg7ZOkqQzCMsN8Hn96bXyX6ma2bA5tZjBDzy0zm3RbGqMaeyrrVZMOpE2cScM01oCdNrxZ4yOUstdvOX4MINgbNXrvFkOlWDaPNh2kyjvavWEDVe+gp9Jy2vdCT9Cm/y5MWbN7ABiLs7DaRG1SXmLecVeyXEQVEHhRFmCg+lANzvsBWLDbzmX1if87EFs5OhYQnpD61DnJ4HpQB0Jhv/xHOeXnj5pwn0D5+5lNu8+YSe+KGfdpdEml2gGiIoXsKO2mtVmCyTrtrspuW/5R+Z8/rLIyUYmsxq1EKhMrPh6YZveAQob9HJ7JGFV8OWN7JuRRRre2783laqH1tbPUjDMZatNr1kK1iWWa8839FvvRVfCSa4YpK8OYQUmlpUkQ9a8XBeGBRtkCw1ViJysslLEc93pTlMbAvOVx8VRE4rJi22+z82TUJ7H4E6r/hyz5qvup6dvdRc67dzkWH/x2a6n7VWt7fyo8RVtvMKQHIT118MUbEJywb/JQhcMvYgx/Ch63vtWDN1XLA1KgaF+m8j37nYUy0i/F0FPnfzyrm3EKlepW0Tp7Lb81Bf6xf15ZPP7X+MkW0qvuzfzI/gzypNuH1maz6XbMhJ3a0CvLLYDF8iujxat267qYt5ld6+wiOIg1utu6NQ4EP22o7w8wgH/wlRw/woqbFD5lt/O2NItc6G06zL2t3D4Q9opLnHPRaMFPt2xvs1i3rY/cFmDQlcPJJLY1m/yWDBqSfY5wxO3HAXcCBOls+/rKXavFcN/jR32wCI8eBYG+R4+diatdnfv6ZlwmXHidexAr+vpWIGoRjSI8BFoLCIsA1NMubBpG6ZkB3c+KKTNr5dKKzzvmOkrKxIPmm+2Bv8LcxvlwL8cLcRz++BF5Anlc6HFAuBw+Dp9w0zMd0JudN5+b+eleaFK57qvsK8+/x28bhYPQ1i5sD5ykdovtMehiMU4J/CNJAsDOfZ473HxQ7CFSU6rfSoOz/rFa5pFpgROJfM6T7YhCsFAoZFCXhocGicbSWEvMavxyDhx1DHDvcaQKCgSo+vCCLH5EiUkaDXmcgXCcRTEEpU9UapIsNjmDFflQIirV+IhEqRssUEZpcMuDrMV4FTNhiWsiiMXUmCcRoW1DFK94W7UgJbS/MfEqmnPIh1/Lio0V3LZqkOBouwWN3Fgg2CG4M4p2j963lx3PdGZHyUEExiQfeCZ5tCrcXzhmbWVsa1y9c3VTV5NOs7QgJQaw4EXn4C9XVphzEAwguUdwxblIYh5Rsljib/FECKKWeDKPQSQlLh2EHn33g5WVSaIOefSon0KJJpZW+1b9OvpeHGRWZ/tPOw7/d+ezMvYwLYrVSEiHkiSICATtpstrQQRKgxD+0YpVQbGIKiqLfklgUhYFNQKeG97qw/2RjaAijwVpYK+0ulq2Lw3UVV3LCSGW244tAbuk0wlJYc9tq107Xx3bgWDUMHkVhYJkACBX5IEIX6sjK3giqgq9m9i0Ad3EQIL/ikqiMdX0NtMtOYxlKBQGi+SbVVY0xm1JPimkmZjBVjvK8+39vmIQEkLvJlqY4Q7G3tfXwZUORE17Gc1VUCXnkF+/TgLp/NfJwXxlzt+RX78qLn07daanjbSqabVpWfOmpxQW9s/zv8fFpaR+W5+5dqBt7HXzGmEpWDXJOmhLgXXC0e+x8clpk0s2GtRbY1Zm+NZn/5TCwf554WNs90XXZawbaJ18cAcvWwkmLaXmzqPfYuNT0sb4tlQHsNl9kSs9A1XPippcSTkeL1Bn2ER/CHRdlEGvR0XZrbx7nCKIz1BH2wR+0Hk1dlF3L2WtnMT7X8p55LdvI8KeTwj6+A3tEZGrOboFj+ITYFcFFakgKJBUhStM4KPwW4Y0OSd97w7vPY4x3YUgyOBwZfpZO8+S0mGZExbeZYo+sXf4ki9Yv6gGlcL9Od3te8lZOxO7RO6sIZ69jr69eaCbTeJSTCjXS74HR//MRaXUuF33xyP+RVKLkRVt26oRekQ/VVAVxVTkvwi8/3XwdENna7A2YdVSILOm5njt8QUVB4TJaqPMEIzSza7cBM6iHu2GUumi4MSActweJS4KGb2xvIxYjt2TRJbkhvLUiEm9wwSYerW4dOqMkhkWTlXfAKFnzqwPBW7mK9bYJe+5f+xyivslMDUesTgUBPfpo4I2jOh/Kc5zb9c4pWR3FHs0STySSzYNFXs1JCxe1BDVLl0Iqfsf21ZBrym4evbflJ7x//KvAeFeh6gton2prl60Ufj//5GjRAlBP5L/oPxxBSrqS0cm4n0ynggX1XheH/Gv5kKQIA6FxbrkZT34YgqJdleWqoGr70I45433roakHZ/X5bq8Nl1lsAOuFrIG7q4Lz56/2oMX9HZeT7YUymmpjzeiQSKuIMlPWxWahE7C5if5h08gCEeAabz9YIm1kB0RZVe9zg50mWz6uzGP3g8iofjA7M2OsdPBK+QJYHR+XWBUn8aFudhf0JtZ4+sJDNiEgL+n6i+JkIl0koiUFf4jL/WvGL77Qgqh1iSaOWOR+NSUvzNvpy/Lb42ag31rbGalZogm6/ufelcvP7YY4+uEWLmfaig44Xsib9HcucigNxO6IwlHSq6sfTn2uzHz848W6W89IxHO75bZuARsikpPMmugZUEyWt3Kv4BXIvrbjTs/Lt73nfZb4hmbs4m/2fuX8LrNeOJZKgDz6uPOhinde7f3vC9uX8Ld1/m7pKgESSnJ3udnkUIXR9drarbfpQzzolPQ6cOFi4hCwXTYUupSR4nFQOeQl4ansdBoGP+favRy1f52iJfGigLRKKxO7hxNp6alOOMAQBRXW1TzGD635ol0qM/c2Uyfcci9ABVnGhnV8cyvPGsdldgCQtjbTKvGNu/O/LBdaqNr7dQxKA7Fr0BfypUq2/g8t/jUu0oEJEnI/pbHpCOc02UXX1a6iMwSft24gX4oGHEEMRizUAOjbNTuoUFeIts0Ak0AaU6ILGO9UdsGdzeDnpE84EAyuEV4iGvjVFg1MtcBRqj4dlSW0EIILkEL9iJns/s6iGGfkcjrhMAdkOxJDrMMs/po+bEzkGS0JEj7YSov7p5mzTNVS+KdzaQ/ecV6QUpDiQZSgVWp1PBeQiJUrIVimdoOmQL3lElNgoqXQBFQeHFGEhQHLctAhKPdJOqTuFv0WzjSN/q3tbgZ+sxcnDn6HBh0oX4yzz25shZqhc5sh6uAWFLyiJVdFni/0IY628FBlPR0+KdTlU4qp6tO9+4qGClccSLaASScs3u1bqPQoZ7ViBxEbk+zHqHDrknhmStMFmWWZJI4TPYYNZGZtIfIZ/oMscgpAsR/+Yh8Yj4pKiYhBJ595/cOltWtJ/VlQfNyHcFsPbLahGbbEH7C9kQg+iSDyIs8a3s2/AcIit61li094fOjpMrUUQmyN/BkY9+34X8CUwEu5Iemjrb7Xw7zi7eetU+MKa0kMc23yO1WR8LpYL2nV7rzavi3JAQHUVtV+47zLukaZwuSwSez4jiP449a32nSlV7O2mF3XyNVr2kQsTtle2OGyq6H/U8vb5qEj+aQG/8stRvxCHfqAIWKk3YIaKe7NSJtC6w+joEZSd/5MdbmUSZxJfst9oWn3E+AyiE59qDtXcSFFEby+8CgtHgRC1++J9rx5Acr2ckdJ2k3Wisb6/Ci37QVYsuo8qiOColn9OLQEvO3v4KccbYrs5JJTJYeQb79Cz5LlLOOIPm3nWISQO4NikVREVlAmZ/7Wvd99UUk9eSfB3R7nAoQtV1pJBwQeZT+fOIu8uPZw4/a7RuOIDZmeSES4RoV1ISY6ol79I5TcB5EDXmSPb2QXkD/8IY9UZ5Q8lKwBLXl3qSfi5nFP1tehfhVnvHMulrCLLlqCQRwGm+Zv9lFM88YT0+0J+H/8KgHCB5oi8QJRr9PQSl3oTFF9B8+JmtPSoWZaJ0pppQjO8MuFW+0F8dUfVG+X2DgcfmdJZqbMy9MifT5x+J3jqb3HLPZG8T2Sw/ay0obnUa2SHf1ZhFi/lOJvqbkoY6E91IVsz/t7EXysSPDMRF96F9x8dlSojRN6tPJ2KmRchR2D9FFcBrYyWKJpe+RTEK0WUxNSXlClCOr0PQH7xfp0cR/GL+yRGgSWo8qRuej8S72kgdKCwWxjpXIYJeaRSZGH5hVKslk52ZZoa1qQGVzr5fv9+MN8Bv7JybmAljWuqeU/qCSk5HgvYw0HhPzpPofJ9N2ClKqSZYCQfkvLKSU0m7q9E+1Q1XYPxD0TxhloFBJb0WMu3NiRUEJzJOxJE05iB9DVLPxfqhAs0dHvlv1cm4WosQxJzkuYTDcSuMaZTcxiNhRokgAnd6/QHxIY+oX8PCPfK+dfv415j6ThHxFwkVY+T0RYRUfv9ZCjIi0ER4alNlo2ONV8YnTjgMOt+MTpEucQDA998QaXQRTG19GS2e1LL/xAuum4huoPaSY9M3czdZPuWlRVE9rvJSoDtIG5QWpcNZShu1nh8+2js52xk8Na6AufoWVU2GzlzvoSnjauw+xDFHbaMvRcziDds6HTGcSDjl/Gl7kanHNjZkMbx2VGib0j5PNunZNBpWW6yP8xwr20fba2gJ8MjAJ/pZpjulJblmMYDlE0fZuKwbbCosLeznaXgozJqazU8/E4Y4UOD6Z0R/J7+t5SUa0BRcJZ3e/upw2WdpNN6eaMroBC44YQwKAHKMAQLAdl6YY523STj2W73wv4UQR6fk7U2f6t35Gn5mFbXXuMiHHJz94kRl+68eQPIxcIsOzB56YgHuIGgSENxnp16zVNvvJ61jbJmpYJl3OrdisTH3rDl5XBBR0GN/OUE3tdnVUyB9nkKCA0yJ9F1mYAKdf7EVM3GK7k8Clt+Bu+aQnbEidEbLcVzO6ES+wge6D+v6x4U0ZfBaZeZv/QHK+ZMOk+9071AuSV4LbSFmvbjndGhi4IIYqMe00IJFLYhjAnq10HZjd6mcQNAiwWbm5Wdi+xuC3ZRZaN/JXx2g10KTNL5PbX8orLR3hOVPr758I8dz0vH9S8alpk2mBxvqJLdUh1b85wFivhioqoDalrihXI4iScLMKdX4FU0vMyxfkqxlTC5T1UESGJhxSLzIyIXkWVUl2XEL1g9KAjOKYSVZSNz8BH2dnPwJ8OCfAx1btDlB9DTVQxDyNpPBV9pmdnpv8m4N8aj2dSkOQh8DsrE/OIg/xlEJn5P3IN4Eh9Hlf8jvQ1QRHNQX2we8KrAJ3w5Mn4DVAObgb5ieRERhr7jIkqrJzb3VrDCgP8qogcLRY5K6Fu1euEneRu6DwUVT/gVP8oqVSUrvP0o/yYKf1hgcU9IzHzBMz33N6g/XOB7bxXGBE74enp+H9RArBdvxqSBaNwjfdA9ceSFfWqUhqyDrAosLIE0bzwHsukrvf2t4xIQNjlEHYOLf3GcM8kBprtVgY8tTCBHPBHVmYtehnAO7J33feME/ObjwTcI1VSTtOXc649mxAh6KhaSgd/8NMeN/58H1PqlWh7QfkhdUKhdZNW9VAq59nJ2ayE+YZ5UPG5ieGLwgvWfqMeA4hnaXAS0D64/VP4Az46fXzlgeU7TqKhdqCottOebCVPOqpW+VZNtKiAeatAsf0AjUVtJpB5g3LJFL5T1cEVW6LOTDXT4T1HIYwoeeegoCpI7VBkf2qPHAMfv8BeRQ+9uHDMWQbdHer5wp0YlOWU8bOjIzf/l////XMOX5k/ZGdSq9LLf32cW7svA9T+BOXp0SCE6gm4F/e2WmvCSQQ5NZyoL2mU2hEvoKNwnmhEX6FNFoFYbDzWMwrjO6aaxVRPuyaDlMf1LiLNB5Z2eirRXJvN57Q1dvbRcB3g+DsSlFstJYbGA+kLv89evRfthYPArXc2Gi3vEC/ZDsgiEtNjJEtT0rcvdxk+e7E0VqMLDVREAfskv0CJxDV0Wbm/VBSWakS6l0SuHu3x3uV0PZZCCWZ90ebIavAH2bMXkdOzZJpZJJJop07gMHoTMNDa3QadN4ANT1IXujcQbSmqyvO06ALoZQn6UAQHWthcWvo7NPiQOANuxe6/ecfAnTgq9Id18inBm0n9xXdUL795Rgthsa0i0NafFtKW3JrSiswfnqYUE8k+7eh+vHlIP+gy3kiF4gZgYO0cGO05V42OR470YmIIkWS4IYJC3I8fVOMZSNDTcNbhEMu3svvRflF3lO3sQhnQmqTxhjLTWxSvMVTZnh0OKJzwmKxYO+Ntmw0UUM45muuqI0rgxYvBjKiHJNB4dwpCBLd2d7/vtpd7HwwGahBQ45V3M/J1+IxtSFbDWmATFi3snlTcEoDmGj0K/JIve+R4lc80dAApXT6Zz0U7wM5niBwyuuzmdoJ5I17HYvfil4Ydd6xZ3nhzUhRXa9X8n1eBxCiGq4Q6kR9S7ALa8C9tZ4rp5XL4TDiOvV6s2bvu6YW4Usq/mqQtad6bkJGF2VEgyhQnIGMdlXellqw3XLdbSiSnqACDfhfC4pygv3jl2EUWU7Z5Sr7BjVobYSb4qweBco5Gon2Edl+uuRelEViS/o8033sVxTFaAixLLHccd1OqwiXLBkHFNbbJSV9+iQRO3bv8M78j+gBb+NKxvnP66z3dCTnu+9NPRDBcagFYrj/zse9VmVP4oirI65UldJPJy/pjCuzDswGx5KE8a13LSMb4gELFpw/3/cdDm3ue9rs9YcQG5eF9o5j/bW85meBIv2yrKmrgJFRDmKj/71FEvuNyV1kNvU2XVJOhx9JeAsbDWIzywsYtFCrMnRgR4vXgafQFuT5L1/AyRzW4n88VodlNFMxE7emj6Z2OLICvlTCXkeiYzayPYU1TFlldFSvwwoJpxiNCIct47/ulqY02wkDi0zUzdpERBa3hIkxvVNuT4x8WddxnDnpehx5w3tZnHe4k8OiIbgaQvRaZpXvFbY+clAu/9BQ3fChHMiBd6L9N5ks1x/I/d9X8arei10Oeumu5szXAZYBjmnA2Ppgyw0beON8QuQX1A2FhYIcJrCQCau9rECHlbWJVdak66SjBvE8M+4zOvhb8GHRx7ErBLXb4QvnkO2003ivoRu2N8ZwXndNZPuFc+0fXp1+j61FFLe3FweL6Ag67IrTHxaRG2uwFKdPWQHFVtWvtrln1dgDNQ0IKO/09Dygq9iG8PQwaXNu5Xl4wHtxoofnjUJvMM8CUoO5+SaFcfGFJvnc4FQMFKvVLhnntSHgNceOtyDWII4fD0eMbhYIKBvhcIY/HXJOmDF1PmQ3uAh6aTMm72rEjyzm9RoFN2GZfk7gJwAB3qfbry08C4o9PsuI5jBgcaBJIELC4OBHR3SwvpBD7l5baDf/6PkR1Ml1RGe2exURbo5/lu/sZZuFa5uD757fzdsXrnnuuxdMjma88fN7kxbkEJDqb2Ybxv9jvDan+FPSoEJ6Vd91vxtUvW4E+HfsNidP7lbjBf+KedWetu621YE8MewEeq/+vu/b0xEt6pb7LZR2mJGajgZ880M8dHJ6qLfn1yDwmcnqZqB7kPfPCYXHIdP4Wy9NUT3EEzEW+xHtC5v2//eFuP/+cmQMJh8+AcMRGGTUJf1+EEJraIWzYZTy28MnLXTmM0p59PB2msXoxx+QZ/a/sfmppxX3bO7x4/ZN1RIoNRWIgUQlCcR7JUAsPRXub7ei0JjaaEEAPZb6xHHCaYZCnXMadXpCpd52GjmfZAfNKVZ9Wzn1Ll1qoq0cQ9FwYtlxbBAtdSzqivCaw8BMBkJTX6+BiFJGaBgRmMJv/OPH54+g9U1ifWKkDkVvYGsDpDHUZ6KHnRcghOqIpvsRbqeHDEOn3SLuX+2r53bf0kPwzHLA8pnAw/K+W/x37KsI1BmcAXcGFTGg5NJbqAPUFvp9cLuswQpS7DY5vbnmBRnSW/TfHuN/pccFjFNuXIbeoYXBBVxGi4OewXVr2BJTgMYujWZdSLFdcvvKtnskeosEXFcjTVeq6Eu+qwchPd2N2wIGBieAQOm4u1eV351Q2CXjB6538pt+8r7K+FGW0nuvMCLberVjobosnB5tO6XczM+/2dOPVlWNsrvZEgDCbpoIuMl64muBMJySwfDlpkOuMP7PH7Hkhvn7z49Nr+2/Kv6PU+uHKKMC1IbzDvF4wJtmeQNelneWgP4AfAehBwDRttmfKeMXWwkqjRa/v6KvlqA0qL9e7LhO0QgHkM3IAS7dGB4Fxnoh/vKBzXOU8PH0BcwRysSN4VSzBmS+h3RIYgbmHNh92aEh2FgFHMdt45NfPc+QzZx42S7u+HzPHa6wUJhXovcfwLBqprtqB+rrhx/kmEe+/UeqQsn4amxu6iK3bbGRu+gboMLmrgKqUiEk8sBCc7thYDTZoyi5f3BBiFRxaAJNQWl6fVqwUMcAEUtE2fq9y2ipaRQVBedH8CyalvoD/dzNU7rYJv3xFNCYR9qtzzFgfNe+br8WOazfcXN3e+YwuYcOkG3VjYBfo25c4lLhWu1a4SIZN6y6yxkLK9pRiw6WRII4U9olMS5GHcRV4w6iJL3gtLfqHNQxOjjV5L5jP5zS4EhiK59Kd3gsCwVRwqbzZ6cCrG/84pP3JMwH3+EqXL5ft5ufT5VSEQd19GPf1iGsTTDYD9tqbV+DpaCfJWX1gPcYQV9jXDNeYNJkSaCfu7UZeTyXKJM5Kj7v95RTx7adFzaBKAmf2NeVrq6fDozm2TJIRgFve6TlFE2xxUax1S4ZE8u3UHJUca5ptACpF2WSW6nNC9eG52krT37myC675DZRZD050lb+DtSXBmnb/tgkywk+pJYDnCDe7sUkBnnInJUO3sJuFvU7LmOTQhfwB14sj7MX2Y/h8UZ5ebQskjUX6xFFUD+5ruQ/+TLIHQevZRVhbh23tJNNQ6+Y5tas+VoNjfJNMGuKDMx4cmtXA/YB24+Ku2qZdO8BHJD2rvaSxt/1E6r3c5cnZc88z0GrIFcJJ4GyKV5UnjEk7vFKfwsmufbwtoWZx9POTTKL5MtcSt4bYErIwSTE132kTybxbnbkRZ/3EI6TFMOC9ER02QiGMXEORLBhQeAgc6Q7QvnpnmnMygBXW07yRzEmTAcEnFKbqtc8lg2chHiu/5Pfl/rOutLrOMCVPRU66TZpu9CcHnngXBFblDLXHHWg+0p4XfQmYCAlCNLoFDiebubEjhsv9ej0MJxSwiREhAseHjNwg/i4gwN+0jnazisISAG9z3IgHHMsDD7cdJIvxDfdsR3P1s6iwzLZdz2N9jDwMO2KDy0Q7HSlFY6ECOfD3JkOloDUOkubJog37g66+MsbMenqid3syX1B4RNmDtAbmvmRVf1j2rkTQqbEDyU+68sS/RTqzIZTvugF+jl6uTZ23OlVG23l5M5hJNQ0mYl82yJMY6iAzpPMuCSqv22TiATxbX/R4nMCbW9NphE60LzL6BJvP+4yThA/R+oLCrmzZIg/Q/n+7n04JNXuNnXRjfrwkhQ5ZmpB0MQqAQ70uIpnxmygIA/09gE8c9LGMWbc5xE5xJEiaU6FZJCe2lzPRKNLjRIi1kdgKG3D/pvXgUiiUzzOxXX0DdH2Nsp/+Me2qce4uY0+IWK0ae5ANY0O94H6zSIUL7fj+m7j4ZekpofyJkmqhNiSHrR9NoxBcKklxJsR7yeR1dFD1ysfVbZRtuKvYVn6jQl2Pa1u4/1qD2QkiHdLcRWV9PXxjKQt8+sGSDomSomslSIOGPqIA/R5PtYImrcaC3Y0iGlMH3P1itsQka2VCWv2OtBAfKBfMD0E8VyEj3bzY8Dob+6OvpD/mzAUwDd1HPiKBDzz8kduCCA1fmybvXKohuz/E7Xt/NqLQ3wwQ/GJ6MjbD9Do8Mj+K0bH8wE4mkBmaP/YBVywiwD4mnkfeGYTGUDe3UnARwxaUNT3bdOUSL2KAEvg2M/AsPEjLAcn67r7ORAI/Jywwvy83LR6fT4MUw0BZUcBqlsx8CFUl3wEsgs+jOo9B+l+56OY+c9HowbYx1MHBpNDAqcwolpIQAsxbL+gi7VUsXU6bfqD/Sg0blfiZ/7HmLomvKQy0fWfaBgibOZ7f12aQhKr4WNwbCjFgov9gtej6PK3p/QWrY1H5vpwggZoIYbtFr6ssrX01tcp9sD/YD8KDcZOly/7H2PqJx5eHp9T+D75lmqnuhjneX9dUE0hjWWr58gHqoZSLDi92i94PQqH6m9PuytLaflH+ZH6qYGvfbvcNibL/0LBFGXZ2nkmeVe3x+vzA4gw+ZUMfVGhU1UqbazzQRjFSZrlRVnVTdv1wzjNy7rtx3ndz/v9AAjBP1aHn/mLxLONifSzNllxvCBKb2KWp2q68Twxettxf3b0AxSEUZykWV6UVd20XT+M07ys236c1/283w+AEIygGE6QFM2wHC+Ikqy8soeqG8/i3R3bcT0/CKM4SbO8KKu6abt+GKd5+YOw7u72h+PpfLne8AQiiUyh0ugMJovN4fL4AqFI7Pml25LJtbSv7tJB/tBMwRuzudgO1ByO07HlupMvSmhRD/15p/Xy3EgK4ySTuOEMGwFlnmxMMQiWMqEVQcaeaaMJkGXLsC7RmyVxI7Z6MM1fTrQoy++ic8ieSqsOSV/d8CmXp100JloyZFxpCH8cCFF9tPrMeIutGbpuo/tkB4J3Wl6oZ5jlk+ZsTNCcGr39RuF9xv7h1nxmweX15K+vJLegUOlwrvAZpqJ9aMKNd9OSb0O8UwvCKXhWWTKu+6c4Xjc79AOLJibjVAsaLlxwPNufYt4Re7a3FFttkQ2GsFyK6WbGWnMkFEBjBFBjm7AurDwns6iciBeuZOLjrlWpFu/gQ5vLXLxETJ/2LucldX+WyrJkeb5I48Nh8+ibQwqTj8ioOX1Sw4e6pLSiHJJIR7GdGrTnl93FqqwZSvFCOc6jMR6N5dRztiZcBXI4EOWu5pTdETV8r4xYPcYOqZ0M1Iz6PB8yFWBZcrN216hjIKir58345V60wkPuUoEinHFzwO7eDs2JsCdpqjIpZJ3zAr9r1TVRWbjEECDR7iMFSxH1k8bLJxVEHZgyUgCZBj6JAsu4Fawee5s5HT6ZfECmdIqKegsoOglC/0zJQAAFo07pPAWouhaIFniwH2U7ErVzgYyBSKpwE/LGq2t3rEQDYlJfXeokwGbtRWMagrd5S3FMmtEvfPV0RttXVhP00QgyNZWmULkKPDVwgXrMGUa6Nqdh0qBCyjYstQkmtAAjAXvZgIZJBi7b7v2FhBKtExYJ5a4Hu+d1oxRq/iK2eki0oPeEj1OWneh3JryGGVrlvdqOpOqOwGz6+CWn3u/T/a3IJ3FBevI1zIBdukX8BL4ds1y7rUtSRbnjDP1etwvSN1HUdbC5r8ddUqyiwiFBmxNv9RpmfXt10aJDAFX5oH5CNdHabugdixB1rUtLYdnmKXwcTy3yTOLZGQJzetKitZgpIPjoLzMF1Ton0NRLyhu6dNyiQUuG6GMlWO60RaOWzTX67usKiuFECGN5oxXp5rRsZAG14Eyuzsqi0lcsIXbhZXfE6EcNZIbQMe0oYAQgasNMBz3b7BUkHTFTg0RHoQhMlFZGGU/ejdeMfwpLflT1HFiEd7znbVfdav94mdP3O1MIyQDLftKTl4cVRG0qHVMl62E/A27D/FIprv6AhPMnZyCtkyiY2+6pcPhsG04nYIZDR726wQ2tPPykY/qi72XWgLJd/QA7GNW5ClDzf93Ax5/xDwF6LH+Ojcb7g0HTgZkhDLg1su2qLt5SbLB98Sv0n7jS8XkU1BIX6/wZHi1U+twvu9VQ3N3+DwAAAA==') format('woff2');
+}
+
+/* #endif */
+
+/* 支付宝,百度,头条小程序目前读取大的本地字体文件,导致无法显示图标,故用在线加载的方式-2020-05-12 */
+/* #ifndef APP-PLUS */
+@font-face {
+ font-family: "uicon-iconfont";
+ src: url('//at.alicdn.com/t/font_1529455_k4s6di1d1.eot?t=1596960292384');
+ /* IE9 */
+ src: url('//at.alicdn.com/t/font_1529455_k4s6di1d1.eot?t=1596960292384#iefix') format('embedded-opentype'),
+ /* IE6-IE8 */
+ url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'),
+ url('//at.alicdn.com/t/font_1529455_k4s6di1d1.woff?t=1596960292384') format('woff'),
+ url('//at.alicdn.com/t/font_1529455_k4s6di1d1.ttf?t=1596960292384') format('truetype'),
+ /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
+ url('//at.alicdn.com/t/font_1529455_k4s6di1d1.svg?t=1596960292384#iconfont') format('svg');
+}
+
+/* #endif */
+
+.u-iconfont {
+ position: relative;
+ display: flex;
+ font: normal normal normal 14px/1 "uicon-iconfont";
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.u-iconfont::before {
+ display: flex;
+ align-items: center;
+}
+
+.uicon-en:before {
+ content: "\e70a";
+}
+
+.uicon-zh:before {
+ content: "\e692";
+}
+
+.uicon-level:before {
+ content: "\e693";
+}
+
+.uicon-woman:before {
+ content: "\e69c";
+}
+
+.uicon-man:before {
+ content: "\e697";
+}
+
+.uicon-column-line:before {
+ content: "\e68e";
+}
+
+.uicon-empty-page:before {
+ content: "\e627";
+}
+
+.uicon-empty-data:before {
+ content: "\e62f";
+}
+
+.uicon-empty-car:before {
+ content: "\e602";
+}
+
+.uicon-empty-order:before {
+ content: "\e639";
+}
+
+.uicon-empty-address:before {
+ content: "\e646";
+}
+
+.uicon-empty-message:before {
+ content: "\e6a9";
+}
+
+.uicon-empty-search:before {
+ content: "\e664";
+}
+
+.uicon-empty-favor:before {
+ content: "\e67c";
+}
+
+.uicon-empty-coupon:before {
+ content: "\e682";
+}
+
+.uicon-empty-history:before {
+ content: "\e684";
+}
+
+.uicon-empty-permission:before {
+ content: "\e686";
+}
+
+.uicon-empty-news:before {
+ content: "\e687";
+}
+
+.uicon-empty-wifi:before {
+ content: "\e688";
+}
+
+.uicon-empty-list:before {
+ content: "\e68b";
+}
+
+.uicon-arrow-left-double:before {
+ content: "\e68c";
+}
+
+.uicon-arrow-right-double:before {
+ content: "\e68d";
+}
+
+.uicon-red-packet:before {
+ content: "\e691";
+}
+
+.uicon-red-packet-fill:before {
+ content: "\e690";
+}
+
+.uicon-order:before {
+ content: "\e68f";
+}
+
+.uicon-nav-back-arrow:before {
+ content: "\e67f";
+}
+
+.uicon-nav-back:before {
+ content: "\e683";
+}
+
+.uicon-checkbox-mark:before {
+ content: "\e6a8";
+}
+
+.uicon-arrow-up-fill:before {
+ content: "\e6b0";
+}
+
+.uicon-arrow-down-fill:before {
+ content: "\e600";
+}
+
+.uicon-backspace:before {
+ content: "\e67b";
+}
+
+.uicon-android-circle-fill:before {
+ content: "\e67e";
+}
+
+.uicon-android-fill:before {
+ content: "\e67d";
+}
+
+.uicon-question:before {
+ content: "\e715";
+}
+
+.uicon-pause:before {
+ content: "\e8fa";
+}
+
+.uicon-close:before {
+ content: "\e685";
+}
+
+.uicon-volume-up:before {
+ content: "\e633";
+}
+
+.uicon-volume-off:before {
+ content: "\e644";
+}
+
+.uicon-info:before {
+ content: "\e653";
+}
+
+.uicon-error:before {
+ content: "\e6d3";
+}
+
+.uicon-lock-opened-fill:before {
+ content: "\e974";
+}
+
+.uicon-lock-fill:before {
+ content: "\e979";
+}
+
+.uicon-lock:before {
+ content: "\e97a";
+}
+
+.uicon-photo-fill:before {
+ content: "\e98b";
+}
+
+.uicon-photo:before {
+ content: "\e98d";
+}
+
+.uicon-account-fill:before {
+ content: "\e614";
+}
+
+.uicon-minus-people-fill:before {
+ content: "\e615";
+}
+
+.uicon-plus-people-fill:before {
+ content: "\e626";
+}
+
+.uicon-account:before {
+ content: "\e628";
+}
+
+.uicon-thumb-down-fill:before {
+ content: "\e726";
+}
+
+.uicon-thumb-down:before {
+ content: "\e727";
+}
+
+.uicon-thumb-up-fill:before {
+ content: "\e72f";
+}
+
+.uicon-thumb-up:before {
+ content: "\e733";
+}
+
+.uicon-person-delete-fill:before {
+ content: "\e66a";
+}
+
+.uicon-cut:before {
+ content: "\e948";
+}
+
+.uicon-fingerprint:before {
+ content: "\e955";
+}
+
+.uicon-home-fill:before {
+ content: "\e964";
+}
+
+.uicon-home:before {
+ content: "\e965";
+}
+
+.uicon-hourglass-half-fill:before {
+ content: "\e966";
+}
+
+.uicon-hourglass:before {
+ content: "\e967";
+}
+
+.uicon-lock-open:before {
+ content: "\e973";
+}
+
+.uicon-integral-fill:before {
+ content: "\e703";
+}
+
+.uicon-integral:before {
+ content: "\e704";
+}
+
+.uicon-coupon:before {
+ content: "\e8ae";
+}
+
+.uicon-coupon-fill:before {
+ content: "\e8c4";
+}
+
+.uicon-kefu-ermai:before {
+ content: "\e656";
+}
+
+.uicon-scan:before {
+ content: "\e662";
+}
+
+.uicon-rmb:before {
+ content: "\e608";
+}
+
+.uicon-rmb-circle-fill:before {
+ content: "\e657";
+}
+
+.uicon-rmb-circle:before {
+ content: "\e677";
+}
+
+.uicon-gift:before {
+ content: "\e65b";
+}
+
+.uicon-gift-fill:before {
+ content: "\e65c";
+}
+
+.uicon-bookmark-fill:before {
+ content: "\e63b";
+}
+
+.uicon-zhuanfa:before {
+ content: "\e60b";
+}
+
+.uicon-eye-off-outline:before {
+ content: "\e62b";
+}
+
+.uicon-eye-off:before {
+ content: "\e648";
+}
+
+.uicon-pause-circle:before {
+ content: "\e643";
+}
+
+.uicon-play-circle:before {
+ content: "\e647";
+}
+
+.uicon-pause-circle-fill:before {
+ content: "\e654";
+}
+
+.uicon-play-circle-fill:before {
+ content: "\e655";
+}
+
+.uicon-grid:before {
+ content: "\e673";
+}
+
+.uicon-play-right:before {
+ content: "\e610";
+}
+
+.uicon-play-left:before {
+ content: "\e66d";
+}
+
+.uicon-calendar:before {
+ content: "\e66e";
+}
+
+.uicon-rewind-right:before {
+ content: "\e66f";
+}
+
+.uicon-rewind-left:before {
+ content: "\e671";
+}
+
+.uicon-skip-forward-right:before {
+ content: "\e672";
+}
+
+.uicon-skip-back-left:before {
+ content: "\e674";
+}
+
+.uicon-play-left-fill:before {
+ content: "\e675";
+}
+
+.uicon-play-right-fill:before {
+ content: "\e676";
+}
+
+.uicon-grid-fill:before {
+ content: "\e678";
+}
+
+.uicon-rewind-left-fill:before {
+ content: "\e679";
+}
+
+.uicon-rewind-right-fill:before {
+ content: "\e67a";
+}
+
+.uicon-pushpin:before {
+ content: "\e7e3";
+}
+
+.uicon-star:before {
+ content: "\e65f";
+}
+
+.uicon-star-fill:before {
+ content: "\e669";
+}
+
+.uicon-server-fill:before {
+ content: "\e751";
+}
+
+.uicon-server-man:before {
+ content: "\e6bc";
+}
+
+.uicon-edit-pen:before {
+ content: "\e612";
+}
+
+.uicon-edit-pen-fill:before {
+ content: "\e66b";
+}
+
+.uicon-wifi:before {
+ content: "\e667";
+}
+
+.uicon-wifi-off:before {
+ content: "\e668";
+}
+
+.uicon-file-text:before {
+ content: "\e663";
+}
+
+.uicon-file-text-fill:before {
+ content: "\e665";
+}
+
+.uicon-more-dot-fill:before {
+ content: "\e630";
+}
+
+.uicon-minus:before {
+ content: "\e618";
+}
+
+.uicon-minus-circle:before {
+ content: "\e61b";
+}
+
+.uicon-plus:before {
+ content: "\e62d";
+}
+
+.uicon-plus-circle:before {
+ content: "\e62e";
+}
+
+.uicon-minus-circle-fill:before {
+ content: "\e652";
+}
+
+.uicon-plus-circle-fill:before {
+ content: "\e661";
+}
+
+.uicon-email:before {
+ content: "\e611";
+}
+
+.uicon-email-fill:before {
+ content: "\e642";
+}
+
+.uicon-phone:before {
+ content: "\e622";
+}
+
+.uicon-phone-fill:before {
+ content: "\e64f";
+}
+
+.uicon-clock:before {
+ content: "\e60f";
+}
+
+.uicon-car:before {
+ content: "\e60c";
+}
+
+.uicon-car-fill:before {
+ content: "\e636";
+}
+
+.uicon-warning:before {
+ content: "\e694";
+}
+
+.uicon-warning-fill:before {
+ content: "\e64d";
+}
+
+.uicon-search:before {
+ content: "\e62a";
+}
+
+.uicon-baidu-circle-fill:before {
+ content: "\e680";
+}
+
+.uicon-baidu:before {
+ content: "\e681";
+}
+
+.uicon-facebook:before {
+ content: "\e689";
+}
+
+.uicon-facebook-circle-fill:before {
+ content: "\e68a";
+}
+
+.uicon-qzone:before {
+ content: "\e695";
+}
+
+.uicon-qzone-circle-fill:before {
+ content: "\e696";
+}
+
+.uicon-moments-circel-fill:before {
+ content: "\e69a";
+}
+
+.uicon-moments:before {
+ content: "\e69b";
+}
+
+.uicon-qq-circle-fill:before {
+ content: "\e6a0";
+}
+
+.uicon-qq-fill:before {
+ content: "\e6a1";
+}
+
+.uicon-weibo:before {
+ content: "\e6a4";
+}
+
+.uicon-weibo-circle-fill:before {
+ content: "\e6a5";
+}
+
+.uicon-taobao:before {
+ content: "\e6a6";
+}
+
+.uicon-taobao-circle-fill:before {
+ content: "\e6a7";
+}
+
+.uicon-twitter:before {
+ content: "\e6aa";
+}
+
+.uicon-twitter-circle-fill:before {
+ content: "\e6ab";
+}
+
+.uicon-weixin-circle-fill:before {
+ content: "\e6b1";
+}
+
+.uicon-weixin-fill:before {
+ content: "\e6b2";
+}
+
+.uicon-zhifubao-circle-fill:before {
+ content: "\e6b8";
+}
+
+.uicon-zhifubao:before {
+ content: "\e6b9";
+}
+
+.uicon-zhihu:before {
+ content: "\e6ba";
+}
+
+.uicon-zhihu-circle-fill:before {
+ content: "\e709";
+}
+
+.uicon-list:before {
+ content: "\e650";
+}
+
+.uicon-list-dot:before {
+ content: "\e616";
+}
+
+.uicon-setting:before {
+ content: "\e61f";
+}
+
+.uicon-bell:before {
+ content: "\e609";
+}
+
+.uicon-bell-fill:before {
+ content: "\e640";
+}
+
+.uicon-attach:before {
+ content: "\e632";
+}
+
+.uicon-shopping-cart:before {
+ content: "\e621";
+}
+
+.uicon-shopping-cart-fill:before {
+ content: "\e65d";
+}
+
+.uicon-tags:before {
+ content: "\e629";
+}
+
+.uicon-share:before {
+ content: "\e631";
+}
+
+.uicon-question-circle-fill:before {
+ content: "\e666";
+}
+
+.uicon-question-circle:before {
+ content: "\e625";
+}
+
+.uicon-error-circle:before {
+ content: "\e624";
+}
+
+.uicon-checkmark-circle:before {
+ content: "\e63d";
+}
+
+.uicon-close-circle:before {
+ content: "\e63f";
+}
+
+.uicon-info-circle:before {
+ content: "\e660";
+}
+
+.uicon-md-person-add:before {
+ content: "\e6e4";
+}
+
+.uicon-md-person-fill:before {
+ content: "\e6ea";
+}
+
+.uicon-bag-fill:before {
+ content: "\e617";
+}
+
+.uicon-bag:before {
+ content: "\e619";
+}
+
+.uicon-chat-fill:before {
+ content: "\e61e";
+}
+
+.uicon-chat:before {
+ content: "\e620";
+}
+
+.uicon-more-circle:before {
+ content: "\e63e";
+}
+
+.uicon-more-circle-fill:before {
+ content: "\e645";
+}
+
+.uicon-volume:before {
+ content: "\e66c";
+}
+
+.uicon-volume-fill:before {
+ content: "\e670";
+}
+
+.uicon-reload:before {
+ content: "\e788";
+}
+
+.uicon-camera:before {
+ content: "\e7d7";
+}
+
+.uicon-heart:before {
+ content: "\e7df";
+}
+
+.uicon-heart-fill:before {
+ content: "\e851";
+}
+
+.uicon-minus-square-fill:before {
+ content: "\e855";
+}
+
+.uicon-plus-square-fill:before {
+ content: "\e856";
+}
+
+.uicon-pushpin-fill:before {
+ content: "\e86e";
+}
+
+.uicon-camera-fill:before {
+ content: "\e870";
+}
+
+.uicon-setting-fill:before {
+ content: "\e872";
+}
+
+.uicon-google:before {
+ content: "\e87a";
+}
+
+.uicon-ie:before {
+ content: "\e87b";
+}
+
+.uicon-apple-fill:before {
+ content: "\e881";
+}
+
+.uicon-chrome-circle-fill:before {
+ content: "\e885";
+}
+
+.uicon-github-circle-fill:before {
+ content: "\e887";
+}
+
+.uicon-IE-circle-fill:before {
+ content: "\e889";
+}
+
+.uicon-google-circle-fill:before {
+ content: "\e88a";
+}
+
+.uicon-arrow-down:before {
+ content: "\e60d";
+}
+
+.uicon-arrow-left:before {
+ content: "\e60e";
+}
+
+.uicon-map:before {
+ content: "\e61d";
+}
+
+.uicon-man-add-fill:before {
+ content: "\e64c";
+}
+
+.uicon-tags-fill:before {
+ content: "\e651";
+}
+
+.uicon-arrow-leftward:before {
+ content: "\e601";
+}
+
+.uicon-arrow-rightward:before {
+ content: "\e603";
+}
+
+.uicon-arrow-downward:before {
+ content: "\e604";
+}
+
+.uicon-arrow-right:before {
+ content: "\e605";
+}
+
+.uicon-arrow-up:before {
+ content: "\e606";
+}
+
+.uicon-arrow-upward:before {
+ content: "\e607";
+}
+
+.uicon-bookmark:before {
+ content: "\e60a";
+}
+
+.uicon-eye:before {
+ content: "\e613";
+}
+
+.uicon-man-delete:before {
+ content: "\e61a";
+}
+
+.uicon-man-add:before {
+ content: "\e61c";
+}
+
+.uicon-trash:before {
+ content: "\e623";
+}
+
+.uicon-error-circle-fill:before {
+ content: "\e62c";
+}
+
+.uicon-calendar-fill:before {
+ content: "\e634";
+}
+
+.uicon-checkmark-circle-fill:before {
+ content: "\e635";
+}
+
+.uicon-close-circle-fill:before {
+ content: "\e637";
+}
+
+.uicon-clock-fill:before {
+ content: "\e638";
+}
+
+.uicon-checkmark:before {
+ content: "\e63a";
+}
+
+.uicon-download:before {
+ content: "\e63c";
+}
+
+.uicon-eye-fill:before {
+ content: "\e641";
+}
+
+.uicon-mic-off:before {
+ content: "\e649";
+}
+
+.uicon-mic:before {
+ content: "\e64a";
+}
+
+.uicon-info-circle-fill:before {
+ content: "\e64b";
+}
+
+.uicon-map-fill:before {
+ content: "\e64e";
+}
+
+.uicon-trash-fill:before {
+ content: "\e658";
+}
+
+.uicon-volume-off-fill:before {
+ content: "\e659";
+}
+
+.uicon-volume-up-fill:before {
+ content: "\e65a";
+}
+
+.uicon-share-fill:before {
+ content: "\e65e";
+}
diff --git a/uview-ui/index.js b/uview-ui/index.js
new file mode 100644
index 0000000..ddda341
--- /dev/null
+++ b/uview-ui/index.js
@@ -0,0 +1,137 @@
+// 引入全局mixin
+import mixin from './libs/mixin/mixin.js'
+// 引入关于是否mixin集成小程序分享的配置
+// import wxshare from './libs/mixin/mpShare.js'
+// 全局挂载引入http相关请求拦截插件
+import http from './libs/request'
+
+function wranning(str) {
+ // 开发环境进行信息输出,主要是一些报错信息
+ // 这个环境的来由是在程序编写时候,点击hx编辑器运行调试代码的时候,详见:
+ // https://uniapp.dcloud.io/frame?id=%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83%e5%92%8c%e7%94%9f%e4%ba%a7%e7%8e%af%e5%a2%83
+ if (process.env.NODE_ENV === 'development') {
+ console.warn(str)
+ }
+}
+
+// 尝试判断在根目录的/store中是否有$u.mixin.js,此文件uView默认为需要挂在到全局的vuex的state变量
+// HX2.6.11版本,放到try中,控制台依然会警告,暂时不用此方式,
+// let vuexStore = {};
+// try {
+// vuexStore = require("@/store/$u.mixin.js");
+// } catch (e) {
+// //TODO handle the exception
+// }
+
+// post类型对象参数转为get类型url参数
+import queryParams from './libs/function/queryParams.js'
+// 路由封装
+import route from './libs/function/route.js'
+// 时间格式化
+import timeFormat from './libs/function/timeFormat.js'
+// 时间戳格式化,返回多久之前
+import timeFrom from './libs/function/timeFrom.js'
+// 颜色渐变相关,colorGradient-颜色渐变,hexToRgb-十六进制颜色转rgb颜色,rgbToHex-rgb转十六进制
+import colorGradient from './libs/function/colorGradient.js'
+// 生成全局唯一guid字符串
+import guid from './libs/function/guid.js'
+// 主题相关颜色,info|success|warning|primary|default|error,此颜色已在uview.scss中定义,但是为js中也能使用,故也定义一份
+import color from './libs/function/color.js'
+// 根据type获取图标名称
+import type2icon from './libs/function/type2icon.js'
+// 打乱数组的顺序
+import randomArray from './libs/function/randomArray.js'
+// 对象和数组的深度克隆
+import deepClone from './libs/function/deepClone.js'
+// 对象深度拷贝
+import deepMerge from './libs/function/deepMerge.js'
+// 添加单位
+import addUnit from './libs/function/addUnit.js'
+
+// 规则检验
+import test from './libs/function/test.js'
+// 随机数
+import random from './libs/function/random.js'
+// 去除空格
+import trim from './libs/function/trim.js'
+// toast提示,对uni.showToast的封装
+import toast from './libs/function/toast.js'
+// 获取父组件参数
+import getParent from './libs/function/getParent.js'
+// 获取整个父组件
+import $parent from './libs/function/$parent.js'
+// 获取sys()和os()工具方法
+// 获取设备信息,挂载到$u的sys()(system的缩写)属性中,
+// 同时把安卓和ios平台的名称"ios"和"android"挂到$u.os()中,方便取用
+import {sys, os} from './libs/function/sys.js'
+// 防抖方法
+import debounce from './libs/function/debounce.js'
+// 节流方法
+import throttle from './libs/function/throttle.js'
+
+
+// 配置信息
+import config from './libs/config/config.js'
+// 各个需要fixed的地方的z-index配置文件
+import zIndex from './libs/config/zIndex.js'
+
+const $u = {
+ queryParams: queryParams,
+ route: route,
+ timeFormat: timeFormat,
+ date: timeFormat, // 另名date
+ timeFrom,
+ colorGradient: colorGradient.colorGradient,
+ guid,
+ color,
+ sys,
+ os,
+ type2icon,
+ randomArray,
+ wranning,
+ get: http.get,
+ post: http.post,
+ put: http.put,
+ 'delete': http.delete,
+ hexToRgb: colorGradient.hexToRgb,
+ rgbToHex: colorGradient.rgbToHex,
+ test,
+ random,
+ deepClone,
+ deepMerge,
+ getParent,
+ $parent,
+ addUnit,
+ trim,
+ type: ['primary', 'success', 'error', 'warning', 'info'],
+ http,
+ toast,
+ config, // uView配置信息相关,比如版本号
+ zIndex,
+ debounce,
+ throttle,
+}
+
+const install = Vue => {
+ Vue.mixin(mixin)
+ if (Vue.prototype.openShare) {
+ Vue.mixin(mpShare);
+ }
+ // Vue.mixin(vuexStore);
+ // 时间格式化,同时两个名称,date和timeFormat
+ Vue.filter('timeFormat', (timestamp, format) => {
+ return timeFormat(timestamp, format)
+ })
+ Vue.filter('date', (timestamp, format) => {
+ return timeFormat(timestamp, format)
+ })
+ // 将多久以前的方法,注入到全局过滤器
+ Vue.filter('timeFrom', (timestamp, format) => {
+ return timeFrom(timestamp, format)
+ })
+ Vue.prototype.$u = $u
+}
+
+export default {
+ install
+}
\ No newline at end of file
diff --git a/uview-ui/index.scss b/uview-ui/index.scss
new file mode 100644
index 0000000..84daa72
--- /dev/null
+++ b/uview-ui/index.scss
@@ -0,0 +1,23 @@
+// 引入公共基础类
+@import "./libs/css/common.scss";
+@import "./libs/css/color.scss";
+
+// 非nvue的样式
+/* #ifndef APP-NVUE */
+@import "./libs/css/style.vue.scss";
+/* #endif */
+
+// nvue的特有样式
+/* #ifdef APP-NVUE */
+@import "./libs/css/style.nvue.scss";
+/* #endif */
+
+// 小程序特有的样式
+/* #ifdef MP */
+@import "./libs/css/style.mp.scss";
+/* #endif */
+
+// H5特有的样式
+/* #ifdef H5 */
+@import "./libs/css/style.h5.scss";
+/* #endif */
\ No newline at end of file
diff --git a/uview-ui/libs/config/config.js b/uview-ui/libs/config/config.js
new file mode 100644
index 0000000..22bb74e
--- /dev/null
+++ b/uview-ui/libs/config/config.js
@@ -0,0 +1,15 @@
+// 此版本发布于2020-10-31
+let version = '1.7.8';
+
+export default {
+ v: version,
+ version: version,
+ // 主题名称
+ type: [
+ 'primary',
+ 'success',
+ 'info',
+ 'error',
+ 'warning'
+ ]
+}
\ No newline at end of file
diff --git a/uview-ui/libs/config/zIndex.js b/uview-ui/libs/config/zIndex.js
new file mode 100644
index 0000000..d60e5bd
--- /dev/null
+++ b/uview-ui/libs/config/zIndex.js
@@ -0,0 +1,20 @@
+// uniapp在H5中各API的z-index值如下:
+/**
+ * actionsheet: 999
+ * modal: 999
+ * navigate: 998
+ * tabbar: 998
+ * toast: 999
+ */
+
+export default {
+ toast: 10090,
+ noNetwork: 10080,
+ // popup包含popup,actionsheet,keyboard,picker的值
+ popup: 10075,
+ mask: 10070,
+ navbar: 980,
+ topTips: 975,
+ sticky: 970,
+ indexListSticky: 965,
+}
\ No newline at end of file
diff --git a/uview-ui/libs/css/color.scss b/uview-ui/libs/css/color.scss
new file mode 100644
index 0000000..279bc40
--- /dev/null
+++ b/uview-ui/libs/css/color.scss
@@ -0,0 +1,155 @@
+.u-type-primary-light {
+ color: $u-type-primary-light;
+}
+
+.u-type-warning-light {
+ color: $u-type-warning-light;
+}
+
+.u-type-success-light {
+ color: $u-type-success-light;
+}
+
+.u-type-error-light {
+ color: $u-type-error-light;
+}
+
+.u-type-info-light {
+ color: $u-type-info-light;
+}
+
+.u-type-primary-light-bg {
+ background-color: $u-type-primary-light;
+}
+
+.u-type-warning-light-bg {
+ background-color: $u-type-warning-light;
+}
+
+.u-type-success-light-bg {
+ background-color: $u-type-success-light;
+}
+
+.u-type-error-light-bg {
+ background-color: $u-type-error-light;
+}
+
+.u-type-info-light-bg {
+ background-color: $u-type-info-light;
+}
+
+.u-type-primary-dark {
+ color: $u-type-primary-dark;
+}
+
+.u-type-warning-dark {
+ color: $u-type-warning-dark;
+}
+
+.u-type-success-dark {
+ color: $u-type-success-dark;
+}
+
+.u-type-error-dark {
+ color: $u-type-error-dark;
+}
+
+.u-type-info-dark {
+ color: $u-type-info-dark;
+}
+
+.u-type-primary-dark-bg {
+ background-color: $u-type-primary-dark;
+}
+
+.u-type-warning-dark-bg {
+ background-color: $u-type-warning-dark;
+}
+
+.u-type-success-dark-bg {
+ background-color: $u-type-success-dark;
+}
+
+.u-type-error-dark-bg {
+ background-color: $u-type-error-dark;
+}
+
+.u-type-info-dark-bg {
+ background-color: $u-type-info-dark;
+}
+
+.u-type-primary-disabled {
+ color: $u-type-primary-disabled;
+}
+
+.u-type-warning-disabled {
+ color: $u-type-warning-disabled;
+}
+
+.u-type-success-disabled {
+ color: $u-type-success-disabled;
+}
+
+.u-type-error-disabled {
+ color: $u-type-error-disabled;
+}
+
+.u-type-info-disabled {
+ color: $u-type-info-disabled;
+}
+
+.u-type-primary {
+ color: $u-type-primary;
+}
+
+.u-type-warning {
+ color: $u-type-warning;
+}
+
+.u-type-success {
+ color: $u-type-success;
+}
+
+.u-type-error {
+ color: $u-type-error;
+}
+
+.u-type-info {
+ color: $u-type-info;
+}
+
+.u-type-primary-bg {
+ background-color: $u-type-primary;
+}
+
+.u-type-warning-bg {
+ background-color: $u-type-warning;
+}
+
+.u-type-success-bg {
+ background-color: $u-type-success;
+}
+
+.u-type-error-bg {
+ background-color: $u-type-error;
+}
+
+.u-type-info-bg {
+ background-color: $u-type-info;
+}
+
+.u-main-color {
+ color: $u-main-color;
+}
+
+.u-content-color {
+ color: $u-content-color;
+}
+
+.u-tips-color {
+ color: $u-tips-color;
+}
+
+.u-light-color {
+ color: $u-light-color;
+}
diff --git a/uview-ui/libs/css/common.scss b/uview-ui/libs/css/common.scss
new file mode 100644
index 0000000..29eb7f4
--- /dev/null
+++ b/uview-ui/libs/css/common.scss
@@ -0,0 +1,176 @@
+.u-relative,
+.u-rela {
+ position: relative;
+}
+
+.u-absolute,
+.u-abso {
+ position: absolute;
+}
+
+// nvue不能用标签命名样式,不能放在微信组件中,否则微信开发工具会报警告,无法使用标签名当做选择器
+/* #ifndef APP-NVUE */
+image {
+ display: inline-block;
+}
+
+// 在weex,也即nvue中,所有元素默认为border-box
+view,
+text {
+ box-sizing: border-box;
+}
+/* #endif */
+
+.u-font-xs {
+ font-size: 22rpx;
+}
+
+.u-font-sm {
+ font-size: 26rpx;
+}
+
+.u-font-md {
+ font-size: 28rpx;
+}
+
+.u-font-lg {
+ font-size: 30rpx;
+}
+
+.u-font-xl {
+ font-size: 34rpx;
+}
+
+.u-flex {
+ /* #ifndef APP-NVUE */
+ display: flex;
+ /* #endif */
+ flex-direction: row;
+ align-items: center;
+}
+
+.u-flex-wrap {
+ flex-wrap: wrap;
+}
+
+.u-flex-nowrap {
+ flex-wrap: nowrap;
+}
+
+.u-col-center {
+ align-items: center;
+}
+
+.u-col-top {
+ align-items: flex-start;
+}
+
+.u-col-bottom {
+ align-items: flex-end;
+}
+
+.u-row-center {
+ justify-content: center;
+}
+
+.u-row-left {
+ justify-content: flex-start;
+}
+
+.u-row-right {
+ justify-content: flex-end;
+}
+
+.u-row-between {
+ justify-content: space-between;
+}
+
+.u-row-around {
+ justify-content: space-around;
+}
+
+.u-text-left {
+ text-align: left;
+}
+
+.u-text-center {
+ text-align: center;
+}
+
+.u-text-right {
+ text-align: right;
+}
+
+.u-flex-col {
+ /* #ifndef APP-NVUE */
+ display: flex;
+ /* #endif */
+ flex-direction: column;
+}
+
+// 定义flex等分
+@for $i from 0 through 12 {
+ .u-flex-#{$i} {
+ flex: $i;
+ }
+}
+
+// 定义字体(px)单位,小于20都为px单位字体
+@for $i from 9 to 20 {
+ .u-font-#{$i} {
+ font-size: $i + px;
+ }
+}
+
+// 定义字体(rpx)单位,大于或等于20的都为rpx单位字体
+@for $i from 20 through 40 {
+ .u-font-#{$i} {
+ font-size: $i + rpx;
+ }
+}
+
+// 定义内外边距,历遍1-80
+@for $i from 0 through 80 {
+ // 只要双数和能被5除尽的数
+ @if $i % 2 == 0 or $i % 5 == 0 {
+ // 得出:u-margin-30或者u-m-30
+ .u-margin-#{$i}, .u-m-#{$i} {
+ margin: $i + rpx!important;
+ }
+
+ // 得出:u-padding-30或者u-p-30
+ .u-padding-#{$i}, .u-p-#{$i} {
+ padding: $i + rpx!important;
+ }
+
+ @each $short, $long in l left, t top, r right, b bottom {
+ // 缩写版,结果如: u-m-l-30
+ // 定义外边距
+ .u-m-#{$short}-#{$i} {
+ margin-#{$long}: $i + rpx!important;
+ }
+
+ // 定义内边距
+ .u-p-#{$short}-#{$i} {
+ padding-#{$long}: $i + rpx!important;
+ }
+
+ // 完整版,结果如:u-margin-left-30
+ // 定义外边距
+ .u-margin-#{$long}-#{$i} {
+ margin-#{$long}: $i + rpx!important;
+ }
+
+ // 定义内边距
+ .u-padding-#{$long}-#{$i} {
+ padding-#{$long}: $i + rpx!important;
+ }
+ }
+ }
+}
+
+// 重置nvue的默认关于flex的样式
+.u-reset-nvue {
+ flex-direction: row;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/uview-ui/libs/css/style.components.scss b/uview-ui/libs/css/style.components.scss
new file mode 100644
index 0000000..6835876
--- /dev/null
+++ b/uview-ui/libs/css/style.components.scss
@@ -0,0 +1,7 @@
+// 定义混入指令,用于在非nvue环境下的flex定义,因为nvue没有display属性,会报错
+@mixin vue-flex($direction: row) {
+ /* #ifndef APP-NVUE */
+ display: flex;
+ flex-direction: $direction;
+ /* #endif */
+}
\ No newline at end of file
diff --git a/uview-ui/libs/css/style.h5.scss b/uview-ui/libs/css/style.h5.scss
new file mode 100644
index 0000000..62839eb
--- /dev/null
+++ b/uview-ui/libs/css/style.h5.scss
@@ -0,0 +1,8 @@
+/* H5的时候,隐藏滚动条 */
+::-webkit-scrollbar {
+ display: none;
+ width: 0 !important;
+ height: 0 !important;
+ -webkit-appearance: none;
+ background: transparent;
+}
diff --git a/uview-ui/libs/css/style.mp.scss b/uview-ui/libs/css/style.mp.scss
new file mode 100644
index 0000000..ca1d3e0
--- /dev/null
+++ b/uview-ui/libs/css/style.mp.scss
@@ -0,0 +1,72 @@
+/* start--微信小程序编译后页面有组件名的元素,特别处理--start */
+/* #ifdef MP-WEIXIN || MP-QQ */
+u-td, u-th {
+ flex: 1;
+ align-self: stretch;
+}
+
+.u-td {
+ height: 100%;
+}
+
+u-icon {
+ display: inline-flex;
+ align-items: center;
+}
+
+// 各家小程序宫格组件外层设置为100%,避免受到父元素display: flex;的影响
+u-grid {
+ width: 100%;
+ flex: 0 0 100%;
+}
+
+// 避免小程序线条组件因为父组件display: flex;而失效
+u-line {
+ flex: 1;
+}
+
+u-switch {
+ display: inline-flex;
+ align-items: center;
+}
+
+u-dropdown {
+ flex: 1;
+}
+/* #endif */
+/* end-微信小程序编译后页面有组件名的元素,特别处理--end */
+
+
+/* #ifdef MP-QQ || MP-TOUTIAO */
+// 需要做这一切额外的兼容,都是因为TX的无能
+u-icon {
+ line-height: 0;
+}
+/* #endif */
+
+/* start--头条小程序编译后页面有组件名的元素,特别处理--start */
+// 由于头条小程序不支持直接组件名形式写样式,目前只能在写组件的时候给组件加上对应的类名
+/* #ifdef MP-TOUTIAO */
+.u-td, .u-th, .u-tr {
+ flex: 1;
+ align-self: stretch;
+}
+
+.u-row, .u-col {
+ flex: 1;
+ align-self: stretch;
+}
+
+// 避免小程序线条组件因为父组件display: flex;而失效
+.u-line {
+ flex: 1;
+}
+
+.u-dropdown {
+ flex: 1;
+}
+/* #endif */
+/* end-头条小程序编译后页面有组件名的元素,特别处理--end */
+
+
+
diff --git a/uview-ui/libs/css/style.nvue.scss b/uview-ui/libs/css/style.nvue.scss
new file mode 100644
index 0000000..4a6192a
--- /dev/null
+++ b/uview-ui/libs/css/style.nvue.scss
@@ -0,0 +1,3 @@
+.nvue {
+ font-size: 24rpx;
+}
\ No newline at end of file
diff --git a/uview-ui/libs/css/style.vue.scss b/uview-ui/libs/css/style.vue.scss
new file mode 100644
index 0000000..1ab3707
--- /dev/null
+++ b/uview-ui/libs/css/style.vue.scss
@@ -0,0 +1,175 @@
+page {
+ color: $u-main-color;
+ font-size: 28rpx;
+}
+
+/* start--去除webkit的默认样式--start */
+.u-fix-ios-appearance {
+ -webkit-appearance:none;
+}
+/* end--去除webkit的默认样式--end */
+
+/* start--icon图标外层套一个view,让其达到更好的垂直居中的效果--start */
+.u-icon-wrap {
+ display: flex;
+ align-items: center;
+}
+/* end-icon图标外层套一个view,让其达到更好的垂直居中的效果--end */
+
+/* start--iPhoneX底部安全区定义--start */
+.safe-area-inset-bottom {
+ padding-bottom: 0;
+ padding-bottom: constant(safe-area-inset-bottom);
+ padding-bottom: env(safe-area-inset-bottom);
+}
+/* end-iPhoneX底部安全区定义--end */
+
+/* start--各种hover点击反馈相关的类名-start */
+.u-hover-class {
+ // background-color: #f7f8f9!important;
+ opacity: 0.6;
+}
+
+.u-cell-hover {
+ background-color: #f7f8f9!important;
+}
+/* end--各种hover点击反馈相关的类名--end */
+
+/* start--文本行数限制--start */
+.u-line-1 {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.u-line-2 {
+ -webkit-line-clamp: 2;
+}
+
+.u-line-3 {
+ -webkit-line-clamp: 3;
+}
+
+.u-line-4 {
+ -webkit-line-clamp: 4;
+}
+
+.u-line-5 {
+ -webkit-line-clamp: 5;
+}
+
+.u-line-2, .u-line-3, .u-line-4, .u-line-5 {
+ overflow: hidden;
+ word-break: break-all;
+ text-overflow: ellipsis;
+ display: -webkit-box; // 弹性伸缩盒
+ -webkit-box-orient: vertical; // 设置伸缩盒子元素排列方式
+}
+
+/* end--文本行数限制--end */
+
+
+/* start--Retina 屏幕下的 1px 边框--start */
+.u-border,
+.u-border-bottom,
+.u-border-left,
+.u-border-right,
+.u-border-top,
+.u-border-top-bottom {
+ position: relative
+}
+
+.u-border-bottom:after,
+.u-border-left:after,
+.u-border-right:after,
+.u-border-top-bottom:after,
+.u-border-top:after,
+.u-border:after {
+ /* #ifndef APP-NVUE */
+ content: ' ';
+ /* #endif */
+ position: absolute;
+ left: 0;
+ top: 0;
+ pointer-events: none;
+ box-sizing: border-box;
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
+ // 多加0.1%,能解决有时候边框缺失的问题
+ width: 199.8%;
+ height: 199.7%;
+ transform: scale(0.5, 0.5);
+ border: 0 solid $u-border-color;
+ z-index: 2;
+}
+
+.u-border-top:after {
+ border-top-width: 1px
+}
+
+.u-border-left:after {
+ border-left-width: 1px
+}
+
+.u-border-right:after {
+ border-right-width: 1px
+}
+
+.u-border-bottom:after {
+ border-bottom-width: 1px
+}
+
+.u-border-top-bottom:after {
+ border-width: 1px 0
+}
+
+.u-border:after {
+ border-width: 1px
+}
+/* end--Retina 屏幕下的 1px 边框--end */
+
+
+/* start--clearfix--start */
+.u-clearfix:after,
+.clearfix:after {
+ /* #ifndef APP-NVUE */
+ content: '';
+ /* #endif */
+ display: table;
+ clear: both
+}
+/* end--clearfix--end */
+
+/* start--高斯模糊tabbar底部处理--start */
+.u-blur-effect-inset {
+ width: 750rpx;
+ height: var(--window-bottom);
+ background-color: #FFFFFF;
+}
+/* end--高斯模糊tabbar底部处理--end */
+
+/* start--提升H5端uni.toast()的层级,避免被uView的modal等遮盖--start */
+/* #ifdef H5 */
+uni-toast {
+ z-index: 10090;
+}
+uni-toast .uni-toast {
+ z-index: 10090;
+}
+/* #endif */
+/* end--提升H5端uni.toast()的层级,避免被uView的modal等遮盖--end */
+
+/* start--去除button的所有默认样式--start */
+.u-reset-button {
+ padding: 0;
+ font-size: inherit;
+ line-height: inherit;
+ background-color: transparent;
+ color: inherit;
+}
+
+.u-reset-button::after {
+ border: none;
+}
+/* end--去除button的所有默认样式--end */
+
diff --git a/uview-ui/libs/function/$parent.js b/uview-ui/libs/function/$parent.js
new file mode 100644
index 0000000..80515c4
--- /dev/null
+++ b/uview-ui/libs/function/$parent.js
@@ -0,0 +1,18 @@
+// 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
+// this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
+// 这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name
+// 值(默认为undefined),就是查找最顶层的$parent
+export default function $parent(name = undefined) {
+ let parent = this.$parent;
+ // 通过while历遍,这里主要是为了H5需要多层解析的问题
+ while (parent) {
+ // 父组件
+ if (parent.$options && parent.$options.name !== name) {
+ // 如果组件的name不相等,继续上一级寻找
+ parent = parent.$parent;
+ } else {
+ return parent;
+ }
+ }
+ return false;
+}
\ No newline at end of file
diff --git a/uview-ui/libs/function/addUnit.js b/uview-ui/libs/function/addUnit.js
new file mode 100644
index 0000000..247fae2
--- /dev/null
+++ b/uview-ui/libs/function/addUnit.js
@@ -0,0 +1,8 @@
+import validation from './test.js';
+
+// 添加单位,如果有rpx,%,px等单位结尾或者值为auto,直接返回,否则加上rpx单位结尾
+export default function addUnit(value = 'auto', unit = 'rpx') {
+ value = String(value);
+ // 用uView内置验证规则中的number判断是否为数值
+ return validation.number(value) ? `${value}${unit}` : value;
+}
\ No newline at end of file
diff --git a/uview-ui/libs/function/bem.js b/uview-ui/libs/function/bem.js
new file mode 100644
index 0000000..05d1a36
--- /dev/null
+++ b/uview-ui/libs/function/bem.js
@@ -0,0 +1,5 @@
+function bem(name, conf) {
+
+}
+
+module.exports.bem = bem;
diff --git a/uview-ui/libs/function/color.js b/uview-ui/libs/function/color.js
new file mode 100644
index 0000000..dafb8c1
--- /dev/null
+++ b/uview-ui/libs/function/color.js
@@ -0,0 +1,37 @@
+// 为了让用户能够自定义主题,会逐步弃用此文件,各颜色通过css提供
+// 为了给某些特殊场景使用和向后兼容,无需删除此文件(2020-06-20)
+let color = {
+ primary: "#2979ff",
+ primaryDark: "#2b85e4",
+ primaryDisabled: "#a0cfff",
+ primaryLight: "#ecf5ff",
+ bgColor: "#f3f4f6",
+
+ info: "#909399",
+ infoDark: "#82848a",
+ infoDisabled: "#c8c9cc",
+ infoLight: "#f4f4f5",
+
+ warning: "#ff9900",
+ warningDark: "#f29100",
+ warningDisabled: "#fcbd71",
+ warningLight: "#fdf6ec",
+
+ error: "#fa3534",
+ errorDark: "#dd6161",
+ errorDisabled: "#fab6b6",
+ errorLight: "#fef0f0",
+
+ success: "#19be6b",
+ successDark: "#18b566",
+ successDisabled: "#71d5a1",
+ successLight: "#dbf1e1",
+
+ mainColor: "#303133",
+ contentColor: "#606266",
+ tipsColor: "#909399",
+ lightColor: "#c0c4cc",
+ borderColor: "#e4e7ed"
+}
+
+export default color;
\ No newline at end of file
diff --git a/uview-ui/libs/function/colorGradient.js b/uview-ui/libs/function/colorGradient.js
new file mode 100644
index 0000000..7157513
--- /dev/null
+++ b/uview-ui/libs/function/colorGradient.js
@@ -0,0 +1,100 @@
+/**
+ * 求两个颜色之间的渐变值
+ * @param {string} startColor 开始的颜色
+ * @param {string} endColor 结束的颜色
+ * @param {number} step 颜色等分的份额
+ * */
+function colorGradient(startColor = 'rgb(0, 0, 0)', endColor = 'rgb(255, 255, 255)', step = 10) {
+ let startRGB = hexToRgb(startColor, false); //转换为rgb数组模式
+ let startR = startRGB[0];
+ let startG = startRGB[1];
+ let startB = startRGB[2];
+
+ let endRGB = hexToRgb(endColor, false);
+ let endR = endRGB[0];
+ let endG = endRGB[1];
+ let endB = endRGB[2];
+
+ let sR = (endR - startR) / step; //总差值
+ let sG = (endG - startG) / step;
+ let sB = (endB - startB) / step;
+ let colorArr = [];
+ for (let i = 0; i < step; i++) {
+ //计算每一步的hex值
+ let hex = rgbToHex('rgb(' + Math.round((sR * i + startR)) + ',' + Math.round((sG * i + startG)) + ',' + Math.round((sB *
+ i + startB)) + ')');
+ colorArr.push(hex);
+ }
+ return colorArr;
+}
+
+// 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
+function hexToRgb(sColor, str = true) {
+ let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+ sColor = sColor.toLowerCase();
+ if (sColor && reg.test(sColor)) {
+ if (sColor.length === 4) {
+ let sColorNew = "#";
+ for (let i = 1; i < 4; i += 1) {
+ sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
+ }
+ sColor = sColorNew;
+ }
+ //处理六位的颜色值
+ let sColorChange = [];
+ for (let i = 1; i < 7; i += 2) {
+ sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));
+ }
+ if(!str) {
+ return sColorChange;
+ } else {
+ return `rgb(${sColorChange[0]},${sColorChange[1]},${sColorChange[2]})`;
+ }
+ } else if (/^(rgb|RGB)/.test(sColor)) {
+ let arr = sColor.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(",")
+ return arr.map(val => Number(val));
+ } else {
+ return sColor;
+ }
+};
+
+// 将rgb表示方式转换为hex表示方式
+function rgbToHex(rgb) {
+ let _this = rgb;
+ let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+ if (/^(rgb|RGB)/.test(_this)) {
+ let aColor = _this.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(",");
+ let strHex = "#";
+ for (let i = 0; i < aColor.length; i++) {
+ let hex = Number(aColor[i]).toString(16);
+ hex = String(hex).length == 1 ? 0 + '' + hex : hex; // 保证每个rgb的值为2位
+ if (hex === "0") {
+ hex += hex;
+ }
+ strHex += hex;
+ }
+ if (strHex.length !== 7) {
+ strHex = _this;
+ }
+ return strHex;
+ } else if (reg.test(_this)) {
+ let aNum = _this.replace(/#/, "").split("");
+ if (aNum.length === 6) {
+ return _this;
+ } else if (aNum.length === 3) {
+ let numHex = "#";
+ for (let i = 0; i < aNum.length; i += 1) {
+ numHex += (aNum[i] + aNum[i]);
+ }
+ return numHex;
+ }
+ } else {
+ return _this;
+ }
+}
+
+export default {
+ colorGradient,
+ hexToRgb,
+ rgbToHex
+}
\ No newline at end of file
diff --git a/uview-ui/libs/function/debounce.js b/uview-ui/libs/function/debounce.js
new file mode 100644
index 0000000..4f1027b
--- /dev/null
+++ b/uview-ui/libs/function/debounce.js
@@ -0,0 +1,29 @@
+let timeout = null;
+
+/**
+ * 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数
+ *
+ * @param {Function} func 要执行的回调函数
+ * @param {Number} wait 延时的时间
+ * @param {Boolean} immediate 是否立即执行
+ * @return null
+ */
+function debounce(func, wait = 500, immediate = false) {
+ // 清除定时器
+ if (timeout !== null) clearTimeout(timeout);
+ // 立即执行,此类情况一般用不到
+ if (immediate) {
+ var callNow = !timeout;
+ timeout = setTimeout(function() {
+ timeout = null;
+ }, wait);
+ if (callNow) typeof func === 'function' && func();
+ } else {
+ // 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法
+ timeout = setTimeout(function() {
+ typeof func === 'function' && func();
+ }, wait);
+ }
+}
+
+export default debounce
diff --git a/uview-ui/libs/function/deepClone.js b/uview-ui/libs/function/deepClone.js
new file mode 100644
index 0000000..3db999a
--- /dev/null
+++ b/uview-ui/libs/function/deepClone.js
@@ -0,0 +1,23 @@
+// 判断arr是否为一个数组,返回一个bool值
+function isArray (arr) {
+ return Object.prototype.toString.call(arr) === '[object Array]';
+}
+
+// 深度克隆
+function deepClone (obj) {
+ // 对常见的“非”值,直接返回原来值
+ if([null, undefined, NaN, false].includes(obj)) return obj;
+ if(typeof obj !== "object" && typeof obj !== 'function') {
+ //原始类型直接返回
+ return obj;
+ }
+ var o = isArray(obj) ? [] : {};
+ for(let i in obj) {
+ if(obj.hasOwnProperty(i)){
+ o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
+ }
+ }
+ return o;
+}
+
+export default deepClone;
diff --git a/uview-ui/libs/function/deepMerge.js b/uview-ui/libs/function/deepMerge.js
new file mode 100644
index 0000000..81d2d18
--- /dev/null
+++ b/uview-ui/libs/function/deepMerge.js
@@ -0,0 +1,30 @@
+import deepClone from "./deepClone";
+
+// JS对象深度合并
+function deepMerge(target = {}, source = {}) {
+ target = deepClone(target);
+ if (typeof target !== 'object' || typeof source !== 'object') return false;
+ for (var prop in source) {
+ if (!source.hasOwnProperty(prop)) continue;
+ if (prop in target) {
+ if (typeof target[prop] !== 'object') {
+ target[prop] = source[prop];
+ } else {
+ if (typeof source[prop] !== 'object') {
+ target[prop] = source[prop];
+ } else {
+ if (target[prop].concat && source[prop].concat) {
+ target[prop] = target[prop].concat(source[prop]);
+ } else {
+ target[prop] = deepMerge(target[prop], source[prop]);
+ }
+ }
+ }
+ } else {
+ target[prop] = source[prop];
+ }
+ }
+ return target;
+}
+
+export default deepMerge;
\ No newline at end of file
diff --git a/uview-ui/libs/function/getParent.js b/uview-ui/libs/function/getParent.js
new file mode 100644
index 0000000..9cb45c4
--- /dev/null
+++ b/uview-ui/libs/function/getParent.js
@@ -0,0 +1,47 @@
+// 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
+// this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
+export default function getParent(name, keys) {
+ let parent = this.$parent;
+ // 通过while历遍,这里主要是为了H5需要多层解析的问题
+ while (parent) {
+ // 父组件
+ if (parent.$options.name !== name) {
+ // 如果组件的name不相等,继续上一级寻找
+ parent = parent.$parent;
+ } else {
+ let data = {};
+ // 判断keys是否数组,如果传过来的是一个数组,那么直接使用数组元素值当做键值去父组件寻找
+ if(Array.isArray(keys)) {
+ keys.map(val => {
+ data[val] = parent[val] ? parent[val] : '';
+ })
+ } else {
+ // 历遍传过来的对象参数
+ for(let i in keys) {
+ // 如果子组件有此值则用,无此值则用父组件的值
+ // 判断是否空数组,如果是,则用父组件的值,否则用子组件的值
+ if(Array.isArray(keys[i])) {
+ if(keys[i].length) {
+ data[i] = keys[i];
+ } else {
+ data[i] = parent[i];
+ }
+ } else if(keys[i].constructor === Object) {
+ // 判断是否对象,如果是对象,且有属性,那么使用子组件的值,否则使用父组件的值
+ if(Object.keys(keys[i]).length) {
+ data[i] = keys[i];
+ } else {
+ data[i] = parent[i];
+ }
+ } else {
+ // 只要子组件有传值,即使是false值,也是“传值”了,也需要覆盖父组件的同名参数
+ data[i] = (keys[i] || keys[i] === false) ? keys[i] : parent[i];
+ }
+ }
+ }
+ return data;
+ }
+ }
+
+ return {};
+}
\ No newline at end of file
diff --git a/uview-ui/libs/function/guid.js b/uview-ui/libs/function/guid.js
new file mode 100644
index 0000000..8497664
--- /dev/null
+++ b/uview-ui/libs/function/guid.js
@@ -0,0 +1,41 @@
+/**
+ * 本算法来源于简书开源代码,详见:https://www.jianshu.com/p/fdbf293d0a85
+ * 全局唯一标识符(uuid,Globally Unique Identifier),也称作 uuid(Universally Unique IDentifier)
+ * 一般用于多个组件之间,给它一个唯一的标识符,或者v-for循环的时候,如果使用数组的index可能会导致更新列表出现问题
+ * 最可能的情况是左滑删除item或者对某条信息流"不喜欢"并去掉它的时候,会导致组件内的数据可能出现错乱
+ * v-for的时候,推荐使用后端返回的id而不是循环的index
+ * @param {Number} len uuid的长度
+ * @param {Boolean} firstU 将返回的首字母置为"u"
+ * @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
+ */
+function guid(len = 32, firstU = true, radix = null) {
+ let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
+ let uuid = [];
+ radix = radix || chars.length;
+
+ if (len) {
+ // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
+ for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
+ } else {
+ let r;
+ // rfc4122标准要求返回的uuid中,某些位为固定的字符
+ uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
+ uuid[14] = '4';
+
+ for (let i = 0; i < 36; i++) {
+ if (!uuid[i]) {
+ r = 0 | Math.random() * 16;
+ uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
+ }
+ }
+ }
+ // 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
+ if (firstU) {
+ uuid.shift();
+ return 'u' + uuid.join('');
+ } else {
+ return uuid.join('');
+ }
+}
+
+export default guid;
diff --git a/uview-ui/libs/function/md5.js b/uview-ui/libs/function/md5.js
new file mode 100644
index 0000000..8d541a1
--- /dev/null
+++ b/uview-ui/libs/function/md5.js
@@ -0,0 +1,385 @@
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
+function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
+function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
+function hex_hmac_md5(k, d)
+ { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function b64_hmac_md5(k, d)
+ { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function any_hmac_md5(k, d, e)
+ { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function md5_vm_test()
+{
+ return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
+}
+
+/*
+ * Calculate the MD5 of a raw string
+ */
+function rstr_md5(s)
+{
+ return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+}
+
+/*
+ * Calculate the HMAC-MD5, of a key and some data (raw strings)
+ */
+function rstr_hmac_md5(key, data)
+{
+ var bkey = rstr2binl(key);
+ if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
+
+ var ipad = Array(16), opad = Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+ return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
+}
+
+/*
+ * Convert a raw string to a hex string
+ */
+function rstr2hex(input)
+{
+ try { hexcase } catch(e) { hexcase=0; }
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var output = "";
+ var x;
+ for(var i = 0; i < input.length; i++)
+ {
+ x = input.charCodeAt(i);
+ output += hex_tab.charAt((x >>> 4) & 0x0F)
+ + hex_tab.charAt( x & 0x0F);
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to a base-64 string
+ */
+function rstr2b64(input)
+{
+ try { b64pad } catch(e) { b64pad=''; }
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var output = "";
+ var len = input.length;
+ for(var i = 0; i < len; i += 3)
+ {
+ var triplet = (input.charCodeAt(i) << 16)
+ | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
+ | (i + 2 < len ? input.charCodeAt(i+2) : 0);
+ for(var j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > input.length * 8) output += b64pad;
+ else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
+ }
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to an arbitrary string encoding
+ */
+function rstr2any(input, encoding)
+{
+ var divisor = encoding.length;
+ var i, j, q, x, quotient;
+
+ /* Convert to an array of 16-bit big-endian values, forming the dividend */
+ var dividend = Array(Math.ceil(input.length / 2));
+ for(i = 0; i < dividend.length; i++)
+ {
+ dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
+ }
+
+ /*
+ * Repeatedly perform a long division. The binary array forms the dividend,
+ * the length of the encoding is the divisor. Once computed, the quotient
+ * forms the dividend for the next step. All remainders are stored for later
+ * use.
+ */
+ var full_length = Math.ceil(input.length * 8 /
+ (Math.log(encoding.length) / Math.log(2)));
+ var remainders = Array(full_length);
+ for(j = 0; j < full_length; j++)
+ {
+ quotient = Array();
+ x = 0;
+ for(i = 0; i < dividend.length; i++)
+ {
+ x = (x << 16) + dividend[i];
+ q = Math.floor(x / divisor);
+ x -= q * divisor;
+ if(quotient.length > 0 || q > 0)
+ quotient[quotient.length] = q;
+ }
+ remainders[j] = x;
+ dividend = quotient;
+ }
+
+ /* Convert the remainders to the output string */
+ var output = "";
+ for(i = remainders.length - 1; i >= 0; i--)
+ output += encoding.charAt(remainders[i]);
+
+ return output;
+}
+
+/*
+ * Encode a string as utf-8.
+ * For efficiency, this assumes the input is valid utf-16.
+ */
+function str2rstr_utf8(input)
+{
+ var output = "";
+ var i = -1;
+ var x, y;
+
+ while(++i < input.length)
+ {
+ /* Decode utf-16 surrogate pairs */
+ x = input.charCodeAt(i);
+ y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
+ if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
+ {
+ x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
+ i++;
+ }
+
+ /* Encode output as utf-8 */
+ if(x <= 0x7F)
+ output += String.fromCharCode(x);
+ else if(x <= 0x7FF)
+ output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
+ 0x80 | ( x & 0x3F));
+ else if(x <= 0xFFFF)
+ output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
+ 0x80 | ((x >>> 6 ) & 0x3F),
+ 0x80 | ( x & 0x3F));
+ else if(x <= 0x1FFFFF)
+ output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
+ 0x80 | ((x >>> 12) & 0x3F),
+ 0x80 | ((x >>> 6 ) & 0x3F),
+ 0x80 | ( x & 0x3F));
+ }
+ return output;
+}
+
+/*
+ * Encode a string as utf-16
+ */
+function str2rstr_utf16le(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length; i++)
+ output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
+ (input.charCodeAt(i) >>> 8) & 0xFF);
+ return output;
+}
+
+function str2rstr_utf16be(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length; i++)
+ output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
+ input.charCodeAt(i) & 0xFF);
+ return output;
+}
+
+/*
+ * Convert a raw string to an array of little-endian words
+ * Characters >255 have their high-byte silently ignored.
+ */
+function rstr2binl(input)
+{
+ var output = Array(input.length >> 2);
+ for(var i = 0; i < output.length; i++)
+ output[i] = 0;
+ for(var i = 0; i < input.length * 8; i += 8)
+ output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
+ return output;
+}
+
+/*
+ * Convert an array of little-endian words to a string
+ */
+function binl2rstr(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length * 32; i += 8)
+ output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
+ return output;
+}
+
+/*
+ * Calculate the MD5 of an array of little-endian words, and a bit length.
+ */
+function binl_md5(x, len)
+{
+ /* append padding */
+ x[len >> 5] |= 0x80 << ((len) % 32);
+ x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+
+ for(var i = 0; i < x.length; i += 16)
+ {
+ var olda = a;
+ var oldb = b;
+ var oldc = c;
+ var oldd = d;
+
+ a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+ d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+ c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
+ b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+ a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+ d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
+ c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+ b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+ a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
+ d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+ c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+ b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+ a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
+ d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+ c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+ b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
+
+ a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+ d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+ c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
+ b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+ a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+ d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
+ c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+ b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+ a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
+ d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+ c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+ b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
+ a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+ d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+ c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
+ b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+ a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+ d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+ c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
+ b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+ a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+ d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
+ c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+ b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+ a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
+ d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+ c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+ b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
+ a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+ d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+ c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
+ b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+ a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+ d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
+ c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+ b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+ a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
+ d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+ c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+ b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+ a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
+ d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+ c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+ b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
+ a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+ d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+ c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
+ b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ }
+ return Array(a, b, c, d);
+}
+
+/*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+function md5_cmn(q, a, b, x, s, t)
+{
+ return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
+}
+function md5_ff(a, b, c, d, x, s, t)
+{
+ return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+}
+function md5_gg(a, b, c, d, x, s, t)
+{
+ return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+}
+function md5_hh(a, b, c, d, x, s, t)
+{
+ return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+}
+function md5_ii(a, b, c, d, x, s, t)
+{
+ return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function bit_rol(num, cnt)
+{
+ return (num << cnt) | (num >>> (32 - cnt));
+}
+
+module.exports = {
+ md5 : function(str){
+ return hex_md5(str);
+ }
+}
\ No newline at end of file
diff --git a/uview-ui/libs/function/queryParams.js b/uview-ui/libs/function/queryParams.js
new file mode 100644
index 0000000..81c7e5e
--- /dev/null
+++ b/uview-ui/libs/function/queryParams.js
@@ -0,0 +1,58 @@
+/**
+ * 对象转url参数
+ * @param {*} data,对象
+ * @param {*} isPrefix,是否自动加上"?"
+ */
+function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
+ let prefix = isPrefix ? '?' : ''
+ let _result = []
+ if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets';
+ for (let key in data) {
+ let value = data[key]
+ // 去掉为空的参数
+ if (['', undefined, null].indexOf(value) >= 0) {
+ continue;
+ }
+ // 如果值为数组,另行处理
+ if (value.constructor === Array) {
+ // e.g. {ids: [1, 2, 3]}
+ switch (arrayFormat) {
+ case 'indices':
+ // 结果: ids[0]=1&ids[1]=2&ids[2]=3
+ for (let i = 0; i < value.length; i++) {
+ _result.push(key + '[' + i + ']=' + value[i])
+ }
+ break;
+ case 'brackets':
+ // 结果: ids[]=1&ids[]=2&ids[]=3
+ value.forEach(_value => {
+ _result.push(key + '[]=' + _value)
+ })
+ break;
+ case 'repeat':
+ // 结果: ids=1&ids=2&ids=3
+ value.forEach(_value => {
+ _result.push(key + '=' + _value)
+ })
+ break;
+ case 'comma':
+ // 结果: ids=1,2,3
+ let commaStr = "";
+ value.forEach(_value => {
+ commaStr += (commaStr ? "," : "") + _value;
+ })
+ _result.push(key + '=' + commaStr)
+ break;
+ default:
+ value.forEach(_value => {
+ _result.push(key + '[]=' + _value)
+ })
+ }
+ } else {
+ _result.push(key + '=' + value)
+ }
+ }
+ return _result.length ? prefix + _result.join('&') : ''
+}
+
+export default queryParams;
diff --git a/uview-ui/libs/function/random.js b/uview-ui/libs/function/random.js
new file mode 100644
index 0000000..e155279
--- /dev/null
+++ b/uview-ui/libs/function/random.js
@@ -0,0 +1,10 @@
+function random(min, max) {
+ if (min >= 0 && max > 0 && max >= min) {
+ let gab = max - min + 1;
+ return Math.floor(Math.random() * gab + min);
+ } else {
+ return 0;
+ }
+}
+
+export default random;
diff --git a/uview-ui/libs/function/randomArray.js b/uview-ui/libs/function/randomArray.js
new file mode 100644
index 0000000..590a048
--- /dev/null
+++ b/uview-ui/libs/function/randomArray.js
@@ -0,0 +1,7 @@
+// 打乱数组
+function randomArray(array = []) {
+ // 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0
+ return array.sort(() => Math.random() - 0.5);
+}
+
+export default randomArray
diff --git a/uview-ui/libs/function/route.js b/uview-ui/libs/function/route.js
new file mode 100644
index 0000000..1e39057
--- /dev/null
+++ b/uview-ui/libs/function/route.js
@@ -0,0 +1,85 @@
+import queryParams from '../../libs/function/queryParams.js';
+/**
+ * 路由跳转
+ * 注意:本方法没有对跳转的回调函数进行封装
+ */
+function route(options = {}, params = false) {
+ let config = {
+ type: 'navigateTo',
+ url: '',
+ delta: 1, // navigateBack页面后退时,回退的层数
+ params: {}, // 传递的参数
+ animationType: 'pop-in', // 窗口动画,只在APP有效
+ animationDuration: 300, // 窗口动画持续时间,单位毫秒,只在APP有效
+ };
+ config = Object.assign(config, options);
+ // 如果url没有"/"开头,添加上,因为uni的路由跳转需要"/"开头
+ if (config.url[0] != '/') config.url = '/' + config.url;
+ // 判断是否有传递显式的参数,Object.keys转为数组并判断长度,switchTab类型时不能携带参数
+ if (Object.keys(config.params).length && config.type != 'switchTab') {
+ // 判断用户传递的url中,是否带有参数
+ // 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
+ // 如果有url中有get参数,转换后无需带上"?"
+ let query = '';
+ if (/.*\/.*\?.*=.*/.test(config.url)) {
+ // object对象转为get类型的参数
+ query = queryParams(config.params, false);
+ // 因为已有get参数,所以后面拼接的参数需要带上"&"隔开
+ config.url += "&" + query;
+ } else {
+ query = queryParams(config.params);
+ config.url += query;
+ }
+ }
+ // 简写形式,把url和参数拼接起来
+ if (typeof options === 'string' && typeof params == 'object') {
+ let query = '';
+ if (/.*\/.*\?.*=.*/.test(options)) {
+ // object对象转为get类型的参数
+ query = queryParams(params, false);
+ // 因为已有get参数,所以后面拼接的参数需要带上"&"隔开
+ options += "&" + query;
+ } else {
+ query = queryParams(params);
+ options += query;
+ }
+ }
+ // 判断是否一个字符串,如果是,直接跳转(简写法)
+ // 如果是中情形,默认第二个参数为对象形式的参数
+ if (typeof options === 'string') {
+ if (options[0] != '/') options = '/' + options;
+ return uni.navigateTo({
+ url: options
+ });
+ }
+ // navigateTo类型的跳转
+ if (config.type == 'navigateTo' || config.type == 'to') {
+ return uni.navigateTo({
+ url: config.url,
+ animationType: config.animationType,
+ animationDuration: config.animationDuration,
+ });
+ }
+ if (config.type == 'redirectTo' || config.type == 'redirect') {
+ return uni.redirectTo({
+ url: config.url,
+ });
+ }
+ if (config.type == 'switchTab' || config.type == 'tab') {
+ return uni.switchTab({
+ url: config.url,
+ });
+ }
+ if (config.type == 'reLaunch') {
+ return uni.reLaunch({
+ url: config.url
+ });
+ }
+ if (config.type == 'navigateBack' || config.type == 'back') {
+ return uni.navigateBack({
+ delta: parseInt(config.delta ? config.delta : this.delta)
+ });
+ }
+}
+
+export default route;
diff --git a/uview-ui/libs/function/sys.js b/uview-ui/libs/function/sys.js
new file mode 100644
index 0000000..00f6a28
--- /dev/null
+++ b/uview-ui/libs/function/sys.js
@@ -0,0 +1,9 @@
+export function os() {
+ return uni.getSystemInfoSync().platform;
+};
+
+export function sys() {
+ return uni.getSystemInfoSync();
+}
+
+
diff --git a/uview-ui/libs/function/test.js b/uview-ui/libs/function/test.js
new file mode 100644
index 0000000..b8418a6
--- /dev/null
+++ b/uview-ui/libs/function/test.js
@@ -0,0 +1,232 @@
+/**
+ * 验证电子邮箱格式
+ */
+function email(value) {
+ return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value);
+}
+
+/**
+ * 验证手机格式
+ */
+function mobile(value) {
+ return /^1[23456789]\d{9}$/.test(value)
+}
+
+/**
+ * 验证URL格式
+ */
+function url(value) {
+ return /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w-.\/?%&=]*)?/.test(value)
+}
+
+/**
+ * 验证日期格式
+ */
+function date(value) {
+ return !/Invalid|NaN/.test(new Date(value).toString())
+}
+
+/**
+ * 验证ISO类型的日期格式
+ */
+function dateISO(value) {
+ return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
+}
+
+/**
+ * 验证十进制数字
+ */
+function number(value) {
+ return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
+}
+
+/**
+ * 验证整数
+ */
+function digits(value) {
+ return /^\d+$/.test(value)
+}
+
+/**
+ * 验证身份证号码
+ */
+function idCard(value) {
+ return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
+ value)
+}
+
+/**
+ * 是否车牌号
+ */
+function carNo(value) {
+ // 新能源车牌
+ const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
+ // 旧车牌
+ const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
+ if (value.length === 7) {
+ return creg.test(value);
+ } else if (value.length === 8) {
+ return xreg.test(value);
+ } else {
+ return false;
+ }
+}
+
+/**
+ * 金额,只允许2位小数
+ */
+function amount(value) {
+ //金额,只允许保留两位小数
+ return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
+}
+
+/**
+ * 中文
+ */
+function chinese(value) {
+ let reg = /^[\u4e00-\u9fa5]+$/gi;
+ return reg.test(value);
+}
+
+/**
+ * 只能输入字母
+ */
+function letter(value) {
+ return /^[a-zA-Z]*$/.test(value);
+}
+
+/**
+ * 只能是字母或者数字
+ */
+function enOrNum(value) {
+ //英文或者数字
+ let reg = /^[0-9a-zA-Z]*$/g;
+ return reg.test(value);
+}
+
+/**
+ * 验证是否包含某个值
+ */
+function contains(value, param) {
+ return value.indexOf(param) >= 0
+}
+
+/**
+ * 验证一个值范围[min, max]
+ */
+function range(value, param) {
+ return value >= param[0] && value <= param[1]
+}
+
+/**
+ * 验证一个长度范围[min, max]
+ */
+function rangeLength(value, param) {
+ return value.length >= param[0] && value.length <= param[1]
+}
+
+/**
+ * 是否固定电话
+ */
+function landline(value) {
+ let reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
+ return reg.test(value);
+}
+
+/**
+ * 判断是否为空
+ */
+function empty(value) {
+ switch (typeof value) {
+ case 'undefined':
+ return true;
+ case 'string':
+ if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
+ break;
+ case 'boolean':
+ if (!value) return true;
+ break;
+ case 'number':
+ if (0 === value || isNaN(value)) return true;
+ break;
+ case 'object':
+ if (null === value || value.length === 0) return true;
+ for (var i in value) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+/**
+ * 是否json字符串
+ */
+function jsonString(value) {
+ if (typeof value == 'string') {
+ try {
+ var obj = JSON.parse(value);
+ if (typeof obj == 'object' && obj) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+ return false;
+}
+
+
+/**
+ * 是否数组
+ */
+function array(value) {
+ if (typeof Array.isArray === "function") {
+ return Array.isArray(value);
+ } else {
+ return Object.prototype.toString.call(value) === "[object Array]";
+ }
+}
+
+/**
+ * 是否对象
+ */
+function object(value) {
+ return Object.prototype.toString.call(value) === '[object Object]';
+}
+
+/**
+ * 是否短信验证码
+ */
+function code(value, len = 6) {
+ return new RegExp(`^\\d{${len}}$`).test(value);
+}
+
+
+export default {
+ email,
+ mobile,
+ url,
+ date,
+ dateISO,
+ number,
+ digits,
+ idCard,
+ carNo,
+ amount,
+ chinese,
+ letter,
+ enOrNum,
+ contains,
+ range,
+ rangeLength,
+ empty,
+ isEmpty: empty,
+ jsonString,
+ landline,
+ object,
+ array,
+ code
+}
diff --git a/uview-ui/libs/function/throttle.js b/uview-ui/libs/function/throttle.js
new file mode 100644
index 0000000..ad830b2
--- /dev/null
+++ b/uview-ui/libs/function/throttle.js
@@ -0,0 +1,32 @@
+let timer, flag;
+/**
+ * 节流原理:在一定时间内,只能触发一次
+ *
+ * @param {Function} func 要执行的回调函数
+ * @param {Number} wait 延时的时间
+ * @param {Boolean} immediate 是否立即执行
+ * @return null
+ */
+function throttle(func, wait = 500, immediate = true) {
+ if (immediate) {
+ if (!flag) {
+ flag = true;
+ // 如果是立即执行,则在wait毫秒内开始时执行
+ typeof func === 'function' && func();
+ timer = setTimeout(() => {
+ flag = false;
+ }, wait);
+ }
+ } else {
+ if (!flag) {
+ flag = true
+ // 如果是非立即执行,则在wait毫秒内的结束处执行
+ timer = setTimeout(() => {
+ flag = false
+ typeof func === 'function' && func();
+ }, wait);
+ }
+
+ }
+};
+export default throttle
diff --git a/uview-ui/libs/function/timeFormat.js b/uview-ui/libs/function/timeFormat.js
new file mode 100644
index 0000000..1238010
--- /dev/null
+++ b/uview-ui/libs/function/timeFormat.js
@@ -0,0 +1,52 @@
+// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序
+// 所以这里做一个兼容polyfill的兼容处理
+if (!String.prototype.padStart) {
+ // 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
+ String.prototype.padStart = function(maxLength, fillString = ' ') {
+ if (Object.prototype.toString.call(fillString) !== "[object String]") throw new TypeError(
+ 'fillString must be String')
+ let str = this
+ // 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
+ if (str.length >= maxLength) return String(str)
+
+ let fillLength = maxLength - str.length,
+ times = Math.ceil(fillLength / fillString.length)
+ while (times >>= 1) {
+ fillString += fillString
+ if (times === 1) {
+ fillString += fillString
+ }
+ }
+ return fillString.slice(0, fillLength) + str;
+ }
+}
+
+function timeFormat(timestamp = null, fmt = 'yyyy-mm-dd') {
+ // 其他更多是格式化有如下:
+ // yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合
+ timestamp = parseInt(timestamp);
+ // 如果为null,则格式化当前时间
+ if (!timestamp) timestamp = Number(new Date());
+ // 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位)
+ if (timestamp.toString().length == 10) timestamp *= 1000;
+ let date = new Date(timestamp);
+ let ret;
+ let opt = {
+ "y+": date.getFullYear().toString(), // 年
+ "m+": (date.getMonth() + 1).toString(), // 月
+ "d+": date.getDate().toString(), // 日
+ "h+": date.getHours().toString(), // 时
+ "M+": date.getMinutes().toString(), // 分
+ "s+": date.getSeconds().toString() // 秒
+ // 有其他格式化字符需求可以继续添加,必须转化成字符串
+ };
+ for (let k in opt) {
+ ret = new RegExp("(" + k + ")").exec(fmt);
+ if (ret) {
+ fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
+ };
+ };
+ return fmt;
+}
+
+export default timeFormat
diff --git a/uview-ui/libs/function/timeFrom.js b/uview-ui/libs/function/timeFrom.js
new file mode 100644
index 0000000..52d858e
--- /dev/null
+++ b/uview-ui/libs/function/timeFrom.js
@@ -0,0 +1,46 @@
+import timeFormat from '../../libs/function/timeFormat.js';
+
+/**
+ * 时间戳转为多久之前
+ * @param String timestamp 时间戳
+ * @param String | Boolean format 如果为时间格式字符串,超出一定时间范围,返回固定的时间格式;
+ * 如果为布尔值false,无论什么时间,都返回多久以前的格式
+ */
+function timeFrom(timestamp = null, format = 'yyyy-mm-dd') {
+ if (timestamp == null) timestamp = Number(new Date());
+ timestamp = parseInt(timestamp);
+ // 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位)
+ if (timestamp.toString().length == 10) timestamp *= 1000;
+ var timer = (new Date()).getTime() - timestamp;
+ timer = parseInt(timer / 1000);
+ // 如果小于5分钟,则返回"刚刚",其他以此类推
+ let tips = '';
+ switch (true) {
+ case timer < 300:
+ tips = '刚刚';
+ break;
+ case timer >= 300 && timer < 3600:
+ tips = parseInt(timer / 60) + '分钟前';
+ break;
+ case timer >= 3600 && timer < 86400:
+ tips = parseInt(timer / 3600) + '小时前';
+ break;
+ case timer >= 86400 && timer < 2592000:
+ tips = parseInt(timer / 86400) + '天前';
+ break;
+ default:
+ // 如果format为false,则无论什么时间戳,都显示xx之前
+ if(format === false) {
+ if(timer >= 2592000 && timer < 365 * 86400) {
+ tips = parseInt(timer / (86400 * 30)) + '个月前';
+ } else {
+ tips = parseInt(timer / (86400 * 365)) + '年前';
+ }
+ } else {
+ tips = timeFormat(timestamp, format);
+ }
+ }
+ return tips;
+}
+
+export default timeFrom;
diff --git a/uview-ui/libs/function/toast.js b/uview-ui/libs/function/toast.js
new file mode 100644
index 0000000..91afa73
--- /dev/null
+++ b/uview-ui/libs/function/toast.js
@@ -0,0 +1,9 @@
+function toast(title, duration = 1500) {
+ uni.showToast({
+ title: title,
+ icon: 'none',
+ duration: duration
+ })
+}
+
+export default toast
diff --git a/uview-ui/libs/function/trim.js b/uview-ui/libs/function/trim.js
new file mode 100644
index 0000000..72adc37
--- /dev/null
+++ b/uview-ui/libs/function/trim.js
@@ -0,0 +1,15 @@
+function trim(str, pos = 'both') {
+ if (pos == 'both') {
+ return str.replace(/^\s+|\s+$/g, "");
+ } else if (pos == "left") {
+ return str.replace(/^\s*/, '');
+ } else if (pos == 'right') {
+ return str.replace(/(\s*$)/g, "");
+ } else if (pos == 'all') {
+ return str.replace(/\s+/g, "");
+ } else {
+ return str;
+ }
+}
+
+export default trim
diff --git a/uview-ui/libs/function/type2icon.js b/uview-ui/libs/function/type2icon.js
new file mode 100644
index 0000000..23cb40e
--- /dev/null
+++ b/uview-ui/libs/function/type2icon.js
@@ -0,0 +1,35 @@
+/**
+ * 根据主题type值,获取对应的图标
+ * @param String type 主题名称,primary|info|error|warning|success
+ * @param String fill 是否使用fill填充实体的图标
+ */
+function type2icon(type = 'success', fill = false) {
+ // 如果非预置值,默认为success
+ if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success';
+ let iconName = '';
+ // 目前(2019-12-12),info和primary使用同一个图标
+ switch (type) {
+ case 'primary':
+ iconName = 'info-circle';
+ break;
+ case 'info':
+ iconName = 'info-circle';
+ break;
+ case 'error':
+ iconName = 'close-circle';
+ break;
+ case 'warning':
+ iconName = 'error-circle';
+ break;
+ case 'success':
+ iconName = 'checkmark-circle';
+ break;
+ default:
+ iconName = 'checkmark-circle';
+ }
+ // 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的
+ if (fill) iconName += '-fill';
+ return iconName;
+}
+
+export default type2icon
diff --git a/uview-ui/libs/mixin/mixin.js b/uview-ui/libs/mixin/mixin.js
new file mode 100644
index 0000000..43742f4
--- /dev/null
+++ b/uview-ui/libs/mixin/mixin.js
@@ -0,0 +1,50 @@
+module.exports = {
+ data() {
+ return {}
+ },
+ onLoad() {
+ // getRect挂载到$u上,因为这方法需要使用in(this),所以无法把它独立成一个单独的文件导出
+ this.$u.getRect = this.$uGetRect
+ },
+ methods: {
+ // 查询节点信息
+ // 目前此方法在支付宝小程序中无法获取组件跟接点的尺寸,为支付宝的bug(2020-07-21)
+ // 解决办法为在组件根部再套一个没有任何作用的view元素
+ $uGetRect(selector, all) {
+ return new Promise(resolve => {
+ uni.createSelectorQuery().
+ in(this)[all ? 'selectAll' : 'select'](selector)
+ .boundingClientRect(rect => {
+ if (all && Array.isArray(rect) && rect.length) {
+ resolve(rect)
+ }
+ if (!all && rect) {
+ resolve(rect)
+ }
+ })
+ .exec()
+ })
+ },
+ getParentData(parentName = '') {
+ // 避免在created中去定义parent变量
+ if(!this.parent) this.parent = false;
+ // 这里的本质原理是,通过获取父组件实例(也即u-radio-group的this)
+ // 将父组件this中对应的参数,赋值给本组件(u-radio的this)的parentData对象中对应的属性
+ // 之所以需要这么做,是因为所有端中,头条小程序不支持通过this.parent.xxx去监听父组件参数的变化
+ this.parent = this.$u.$parent.call(this, parentName);
+ if(this.parent) {
+ // 历遍parentData中的属性,将parent中的同名属性赋值给parentData
+ Object.keys(this.parentData).map(key => {
+ this.parentData[key] = this.parent[key];
+ });
+ }
+ },
+ // 阻止事件冒泡
+ preventEvent(e) {
+ e && e.stopPropagation && e.stopPropagation()
+ }
+ },
+ onReachBottom() {
+ uni.$emit('uOnReachBottom')
+ }
+}
diff --git a/uview-ui/libs/mixin/mpShare.js b/uview-ui/libs/mixin/mpShare.js
new file mode 100644
index 0000000..057d369
--- /dev/null
+++ b/uview-ui/libs/mixin/mpShare.js
@@ -0,0 +1,18 @@
+module.exports = {
+ onLoad() {
+ // 设置默认的转发参数
+ this.$u.mpShare = {
+ title: '', // 默认为小程序名称
+ path: '', // 默认为当前页面路径
+ imageUrl: '' // 默认为当前页面的截图
+ }
+ },
+ onShareAppMessage() {
+ return this.$u.mpShare
+ },
+ // #ifdef MP-WEIXIN
+ onShareTimeline() {
+ return this.$u.mpShare
+ }
+ // #endif
+}
diff --git a/uview-ui/libs/request/index.js b/uview-ui/libs/request/index.js
new file mode 100644
index 0000000..ced1744
--- /dev/null
+++ b/uview-ui/libs/request/index.js
@@ -0,0 +1,169 @@
+import deepMerge from "../function/deepMerge";
+import validate from "../function/test";
+class Request {
+ // 设置全局默认配置
+ setConfig(customConfig) {
+ // 深度合并对象,否则会造成对象深层属性丢失
+ this.config = deepMerge(this.config, customConfig);
+ }
+
+ // 主要请求部分
+ request(options = {}) {
+ // 检查请求拦截
+ if (this.interceptor.request && typeof this.interceptor.request === 'function') {
+ let tmpConfig = {};
+ let interceptorRequest = this.interceptor.request(options);
+ if (interceptorRequest === false) {
+ // 返回一个处于pending状态中的Promise,来取消原promise,避免进入then()回调
+ return new Promise(()=>{});
+ }
+ this.options = interceptorRequest;
+ }
+ options.dataType = options.dataType || this.config.dataType;
+ options.responseType = options.responseType || this.config.responseType;
+ options.url = options.url || '';
+ options.params = options.params || {};
+ options.header = Object.assign(this.config.header, options.header);
+ options.method = options.method || this.config.method;
+
+ return new Promise((resolve, reject) => {
+ options.complete = (response) => {
+ // 请求返回后,隐藏loading(如果请求返回快的话,可能会没有loading)
+ uni.hideLoading();
+ // 清除定时器,如果请求回来了,就无需loading
+ clearTimeout(this.config.timer);
+ this.config.timer = null;
+ // 判断用户对拦截返回数据的要求,如果originalData为true,返回所有的数据(response)到拦截器,否则只返回response.data
+ if(this.config.originalData) {
+ // 判断是否存在拦截器
+ if (this.interceptor.response && typeof this.interceptor.response === 'function') {
+ let resInterceptors = this.interceptor.response(response);
+ // 如果拦截器不返回false,就将拦截器返回的内容给this.$u.post的then回调
+ if (resInterceptors !== false) {
+ resolve(resInterceptors);
+ } else {
+ // 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
+ reject(response);
+ }
+ } else {
+ // 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
+ resolve(response);
+ }
+ } else {
+ if (response.statusCode == 200) {
+ if (this.interceptor.response && typeof this.interceptor.response === 'function') {
+ let resInterceptors = this.interceptor.response(response.data);
+ if (resInterceptors !== false) {
+ resolve(resInterceptors);
+ } else {
+ reject(response.data);
+ }
+ } else {
+ // 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
+ resolve(response.data);
+ }
+ } else {
+ // 不返回原始数据的情况下,服务器状态码不为200,modal弹框提示
+ // if(response.errMsg) {
+ // uni.showModal({
+ // title: response.errMsg
+ // });
+ // }
+ reject(response)
+ }
+ }
+ }
+
+ // 判断用户传递的URL是否/开头,如果不是,加上/,这里使用了uView的test.js验证库的url()方法
+ options.url = validate.url(options.url) ? options.url : (this.config.baseUrl + (options.url.indexOf('/') == 0 ?
+ options.url : '/' + options.url));
+
+ // 是否显示loading
+ // 加一个是否已有timer定时器的判断,否则有两个同时请求的时候,后者会清除前者的定时器id
+ // 而没有清除前者的定时器,导致前者超时,一直显示loading
+ if(this.config.showLoading && !this.config.timer) {
+ this.config.timer = setTimeout(() => {
+ uni.showLoading({
+ title: this.config.loadingText,
+ mask: this.config.loadingMask
+ })
+ this.config.timer = null;
+ }, this.config.loadingTime);
+ }
+ uni.request(options);
+ })
+ // .catch(res => {
+ // // 如果返回reject(),不让其进入this.$u.post().then().catch()后面的catct()
+ // // 因为很多人都会忘了写后面的catch(),导致报错捕获不到catch
+ // return new Promise(()=>{});
+ // })
+ }
+
+ constructor() {
+ this.config = {
+ baseUrl: '', // 请求的根域名
+ // 默认的请求头
+ header: {},
+ method: 'POST',
+ // 设置为json,返回后uni.request会对数据进行一次JSON.parse
+ dataType: 'json',
+ // 此参数无需处理,因为5+和支付宝小程序不支持,默认为text即可
+ responseType: 'text',
+ showLoading: true, // 是否显示请求中的loading
+ loadingText: '请求中...',
+ loadingTime: 800, // 在此时间内,请求还没回来的话,就显示加载中动画,单位ms
+ timer: null, // 定时器
+ originalData: false, // 是否在拦截器中返回服务端的原始数据,见文档说明
+ loadingMask: true, // 展示loading的时候,是否给一个透明的蒙层,防止触摸穿透
+ }
+
+ // 拦截器
+ this.interceptor = {
+ // 请求前的拦截
+ request: null,
+ // 请求后的拦截
+ response: null
+ }
+
+ // get请求
+ this.get = (url, data = {}, header = {}) => {
+ return this.request({
+ method: 'GET',
+ url,
+ header,
+ data
+ })
+ }
+
+ // post请求
+ this.post = (url, data = {}, header = {}) => {
+ return this.request({
+ url,
+ method: 'POST',
+ header,
+ data
+ })
+ }
+
+ // put请求,不支持支付宝小程序(HX2.6.15)
+ this.put = (url, data = {}, header = {}) => {
+ return this.request({
+ url,
+ method: 'PUT',
+ header,
+ data
+ })
+ }
+
+ // delete请求,不支持支付宝和头条小程序(HX2.6.15)
+ this.delete = (url, data = {}, header = {}) => {
+ return this.request({
+ url,
+ method: 'DELETE',
+ header,
+ data
+ })
+ }
+ }
+}
+export default new Request
diff --git a/uview-ui/libs/store/index.js b/uview-ui/libs/store/index.js
new file mode 100644
index 0000000..a5927b3
--- /dev/null
+++ b/uview-ui/libs/store/index.js
@@ -0,0 +1,19 @@
+// 暂时不用vuex模块方式实现,将该方法直接放入到/store/index.js中
+const module = {
+ actions: {
+ $uStore({rootState}, params) {
+ let nameArr = params.name.split('.');
+ if(nameArr.length >= 2) {
+ let obj = rootState[nameArr[0]];
+ for(let i = 1; i < nameArr.length - 1; i ++) {
+ obj = obj[nameArr[i]];
+ }
+ obj[nameArr[nameArr.length - 1]] = params.value;
+ } else {
+ rootState[params.name] = params.value;
+ }
+ }
+ }
+}
+
+export default module
\ No newline at end of file
diff --git a/uview-ui/libs/util/area.js b/uview-ui/libs/util/area.js
new file mode 100644
index 0000000..0d602e9
--- /dev/null
+++ b/uview-ui/libs/util/area.js
@@ -0,0 +1 @@
+var areaData=[[[{"label":"东城区","value":"110101"},{"label":"西城区","value":"110102"},{"label":"朝阳区","value":"110105"},{"label":"丰台区","value":"110106"},{"label":"石景山区","value":"110107"},{"label":"海淀区","value":"110108"},{"label":"门头沟区","value":"110109"},{"label":"房山区","value":"110111"},{"label":"通州区","value":"110112"},{"label":"顺义区","value":"110113"},{"label":"昌平区","value":"110114"},{"label":"大兴区","value":"110115"},{"label":"怀柔区","value":"110116"},{"label":"平谷区","value":"110117"},{"label":"密云区","value":"110118"},{"label":"延庆区","value":"110119"}]],[[{"label":"和平区","value":"120101"},{"label":"河东区","value":"120102"},{"label":"河西区","value":"120103"},{"label":"南开区","value":"120104"},{"label":"河北区","value":"120105"},{"label":"红桥区","value":"120106"},{"label":"东丽区","value":"120110"},{"label":"西青区","value":"120111"},{"label":"津南区","value":"120112"},{"label":"北辰区","value":"120113"},{"label":"武清区","value":"120114"},{"label":"宝坻区","value":"120115"},{"label":"滨海新区","value":"120116"},{"label":"宁河区","value":"120117"},{"label":"静海区","value":"120118"},{"label":"蓟州区","value":"120119"}]],[[{"label":"长安区","value":"130102"},{"label":"桥西区","value":"130104"},{"label":"新华区","value":"130105"},{"label":"井陉矿区","value":"130107"},{"label":"裕华区","value":"130108"},{"label":"藁城区","value":"130109"},{"label":"鹿泉区","value":"130110"},{"label":"栾城区","value":"130111"},{"label":"井陉县","value":"130121"},{"label":"正定县","value":"130123"},{"label":"行唐县","value":"130125"},{"label":"灵寿县","value":"130126"},{"label":"高邑县","value":"130127"},{"label":"深泽县","value":"130128"},{"label":"赞皇县","value":"130129"},{"label":"无极县","value":"130130"},{"label":"平山县","value":"130131"},{"label":"元氏县","value":"130132"},{"label":"赵县","value":"130133"},{"label":"石家庄高新技术产业开发区","value":"130171"},{"label":"石家庄循环化工园区","value":"130172"},{"label":"辛集市","value":"130181"},{"label":"晋州市","value":"130183"},{"label":"新乐市","value":"130184"}],[{"label":"路南区","value":"130202"},{"label":"路北区","value":"130203"},{"label":"古冶区","value":"130204"},{"label":"开平区","value":"130205"},{"label":"丰南区","value":"130207"},{"label":"丰润区","value":"130208"},{"label":"曹妃甸区","value":"130209"},{"label":"滦县","value":"130223"},{"label":"滦南县","value":"130224"},{"label":"乐亭县","value":"130225"},{"label":"迁西县","value":"130227"},{"label":"玉田县","value":"130229"},{"label":"唐山市芦台经济技术开发区","value":"130271"},{"label":"唐山市汉沽管理区","value":"130272"},{"label":"唐山高新技术产业开发区","value":"130273"},{"label":"河北唐山海港经济开发区","value":"130274"},{"label":"遵化市","value":"130281"},{"label":"迁安市","value":"130283"}],[{"label":"海港区","value":"130302"},{"label":"山海关区","value":"130303"},{"label":"北戴河区","value":"130304"},{"label":"抚宁区","value":"130306"},{"label":"青龙满族自治县","value":"130321"},{"label":"昌黎县","value":"130322"},{"label":"卢龙县","value":"130324"},{"label":"秦皇岛市经济技术开发区","value":"130371"},{"label":"北戴河新区","value":"130372"}],[{"label":"邯山区","value":"130402"},{"label":"丛台区","value":"130403"},{"label":"复兴区","value":"130404"},{"label":"峰峰矿区","value":"130406"},{"label":"肥乡区","value":"130407"},{"label":"永年区","value":"130408"},{"label":"临漳县","value":"130423"},{"label":"成安县","value":"130424"},{"label":"大名县","value":"130425"},{"label":"涉县","value":"130426"},{"label":"磁县","value":"130427"},{"label":"邱县","value":"130430"},{"label":"鸡泽县","value":"130431"},{"label":"广平县","value":"130432"},{"label":"馆陶县","value":"130433"},{"label":"魏县","value":"130434"},{"label":"曲周县","value":"130435"},{"label":"邯郸经济技术开发区","value":"130471"},{"label":"邯郸冀南新区","value":"130473"},{"label":"武安市","value":"130481"}],[{"label":"桥东区","value":"130502"},{"label":"桥西区","value":"130503"},{"label":"邢台县","value":"130521"},{"label":"临城县","value":"130522"},{"label":"内丘县","value":"130523"},{"label":"柏乡县","value":"130524"},{"label":"隆尧县","value":"130525"},{"label":"任县","value":"130526"},{"label":"南和县","value":"130527"},{"label":"宁晋县","value":"130528"},{"label":"巨鹿县","value":"130529"},{"label":"新河县","value":"130530"},{"label":"广宗县","value":"130531"},{"label":"平乡县","value":"130532"},{"label":"威县","value":"130533"},{"label":"清河县","value":"130534"},{"label":"临西县","value":"130535"},{"label":"河北邢台经济开发区","value":"130571"},{"label":"南宫市","value":"130581"},{"label":"沙河市","value":"130582"}],[{"label":"竞秀区","value":"130602"},{"label":"莲池区","value":"130606"},{"label":"满城区","value":"130607"},{"label":"清苑区","value":"130608"},{"label":"徐水区","value":"130609"},{"label":"涞水县","value":"130623"},{"label":"阜平县","value":"130624"},{"label":"定兴县","value":"130626"},{"label":"唐县","value":"130627"},{"label":"高阳县","value":"130628"},{"label":"容城县","value":"130629"},{"label":"涞源县","value":"130630"},{"label":"望都县","value":"130631"},{"label":"安新县","value":"130632"},{"label":"易县","value":"130633"},{"label":"曲阳县","value":"130634"},{"label":"蠡县","value":"130635"},{"label":"顺平县","value":"130636"},{"label":"博野县","value":"130637"},{"label":"雄县","value":"130638"},{"label":"保定高新技术产业开发区","value":"130671"},{"label":"保定白沟新城","value":"130672"},{"label":"涿州市","value":"130681"},{"label":"定州市","value":"130682"},{"label":"安国市","value":"130683"},{"label":"高碑店市","value":"130684"}],[{"label":"桥东区","value":"130702"},{"label":"桥西区","value":"130703"},{"label":"宣化区","value":"130705"},{"label":"下花园区","value":"130706"},{"label":"万全区","value":"130708"},{"label":"崇礼区","value":"130709"},{"label":"张北县","value":"130722"},{"label":"康保县","value":"130723"},{"label":"沽源县","value":"130724"},{"label":"尚义县","value":"130725"},{"label":"蔚县","value":"130726"},{"label":"阳原县","value":"130727"},{"label":"怀安县","value":"130728"},{"label":"怀来县","value":"130730"},{"label":"涿鹿县","value":"130731"},{"label":"赤城县","value":"130732"},{"label":"张家口市高新技术产业开发区","value":"130771"},{"label":"张家口市察北管理区","value":"130772"},{"label":"张家口市塞北管理区","value":"130773"}],[{"label":"双桥区","value":"130802"},{"label":"双滦区","value":"130803"},{"label":"鹰手营子矿区","value":"130804"},{"label":"承德县","value":"130821"},{"label":"兴隆县","value":"130822"},{"label":"滦平县","value":"130824"},{"label":"隆化县","value":"130825"},{"label":"丰宁满族自治县","value":"130826"},{"label":"宽城满族自治县","value":"130827"},{"label":"围场满族蒙古族自治县","value":"130828"},{"label":"承德高新技术产业开发区","value":"130871"},{"label":"平泉市","value":"130881"}],[{"label":"新华区","value":"130902"},{"label":"运河区","value":"130903"},{"label":"沧县","value":"130921"},{"label":"青县","value":"130922"},{"label":"东光县","value":"130923"},{"label":"海兴县","value":"130924"},{"label":"盐山县","value":"130925"},{"label":"肃宁县","value":"130926"},{"label":"南皮县","value":"130927"},{"label":"吴桥县","value":"130928"},{"label":"献县","value":"130929"},{"label":"孟村回族自治县","value":"130930"},{"label":"河北沧州经济开发区","value":"130971"},{"label":"沧州高新技术产业开发区","value":"130972"},{"label":"沧州渤海新区","value":"130973"},{"label":"泊头市","value":"130981"},{"label":"任丘市","value":"130982"},{"label":"黄骅市","value":"130983"},{"label":"河间市","value":"130984"}],[{"label":"安次区","value":"131002"},{"label":"广阳区","value":"131003"},{"label":"固安县","value":"131022"},{"label":"永清县","value":"131023"},{"label":"香河县","value":"131024"},{"label":"大城县","value":"131025"},{"label":"文安县","value":"131026"},{"label":"大厂回族自治县","value":"131028"},{"label":"廊坊经济技术开发区","value":"131071"},{"label":"霸州市","value":"131081"},{"label":"三河市","value":"131082"}],[{"label":"桃城区","value":"131102"},{"label":"冀州区","value":"131103"},{"label":"枣强县","value":"131121"},{"label":"武邑县","value":"131122"},{"label":"武强县","value":"131123"},{"label":"饶阳县","value":"131124"},{"label":"安平县","value":"131125"},{"label":"故城县","value":"131126"},{"label":"景县","value":"131127"},{"label":"阜城县","value":"131128"},{"label":"河北衡水经济开发区","value":"131171"},{"label":"衡水滨湖新区","value":"131172"},{"label":"深州市","value":"131182"}]],[[{"label":"小店区","value":"140105"},{"label":"迎泽区","value":"140106"},{"label":"杏花岭区","value":"140107"},{"label":"尖草坪区","value":"140108"},{"label":"万柏林区","value":"140109"},{"label":"晋源区","value":"140110"},{"label":"清徐县","value":"140121"},{"label":"阳曲县","value":"140122"},{"label":"娄烦县","value":"140123"},{"label":"山西转型综合改革示范区","value":"140171"},{"label":"古交市","value":"140181"}],[{"label":"城区","value":"140202"},{"label":"矿区","value":"140203"},{"label":"南郊区","value":"140211"},{"label":"新荣区","value":"140212"},{"label":"阳高县","value":"140221"},{"label":"天镇县","value":"140222"},{"label":"广灵县","value":"140223"},{"label":"灵丘县","value":"140224"},{"label":"浑源县","value":"140225"},{"label":"左云县","value":"140226"},{"label":"大同县","value":"140227"},{"label":"山西大同经济开发区","value":"140271"}],[{"label":"城区","value":"140302"},{"label":"矿区","value":"140303"},{"label":"郊区","value":"140311"},{"label":"平定县","value":"140321"},{"label":"盂县","value":"140322"},{"label":"山西阳泉经济开发区","value":"140371"}],[{"label":"城区","value":"140402"},{"label":"郊区","value":"140411"},{"label":"长治县","value":"140421"},{"label":"襄垣县","value":"140423"},{"label":"屯留县","value":"140424"},{"label":"平顺县","value":"140425"},{"label":"黎城县","value":"140426"},{"label":"壶关县","value":"140427"},{"label":"长子县","value":"140428"},{"label":"武乡县","value":"140429"},{"label":"沁县","value":"140430"},{"label":"沁源县","value":"140431"},{"label":"山西长治高新技术产业园区","value":"140471"},{"label":"潞城市","value":"140481"}],[{"label":"城区","value":"140502"},{"label":"沁水县","value":"140521"},{"label":"阳城县","value":"140522"},{"label":"陵川县","value":"140524"},{"label":"泽州县","value":"140525"},{"label":"高平市","value":"140581"}],[{"label":"朔城区","value":"140602"},{"label":"平鲁区","value":"140603"},{"label":"山阴县","value":"140621"},{"label":"应县","value":"140622"},{"label":"右玉县","value":"140623"},{"label":"怀仁县","value":"140624"},{"label":"山西朔州经济开发区","value":"140671"}],[{"label":"榆次区","value":"140702"},{"label":"榆社县","value":"140721"},{"label":"左权县","value":"140722"},{"label":"和顺县","value":"140723"},{"label":"昔阳县","value":"140724"},{"label":"寿阳县","value":"140725"},{"label":"太谷县","value":"140726"},{"label":"祁县","value":"140727"},{"label":"平遥县","value":"140728"},{"label":"灵石县","value":"140729"},{"label":"介休市","value":"140781"}],[{"label":"盐湖区","value":"140802"},{"label":"临猗县","value":"140821"},{"label":"万荣县","value":"140822"},{"label":"闻喜县","value":"140823"},{"label":"稷山县","value":"140824"},{"label":"新绛县","value":"140825"},{"label":"绛县","value":"140826"},{"label":"垣曲县","value":"140827"},{"label":"夏县","value":"140828"},{"label":"平陆县","value":"140829"},{"label":"芮城县","value":"140830"},{"label":"永济市","value":"140881"},{"label":"河津市","value":"140882"}],[{"label":"忻府区","value":"140902"},{"label":"定襄县","value":"140921"},{"label":"五台县","value":"140922"},{"label":"代县","value":"140923"},{"label":"繁峙县","value":"140924"},{"label":"宁武县","value":"140925"},{"label":"静乐县","value":"140926"},{"label":"神池县","value":"140927"},{"label":"五寨县","value":"140928"},{"label":"岢岚县","value":"140929"},{"label":"河曲县","value":"140930"},{"label":"保德县","value":"140931"},{"label":"偏关县","value":"140932"},{"label":"五台山风景名胜区","value":"140971"},{"label":"原平市","value":"140981"}],[{"label":"尧都区","value":"141002"},{"label":"曲沃县","value":"141021"},{"label":"翼城县","value":"141022"},{"label":"襄汾县","value":"141023"},{"label":"洪洞县","value":"141024"},{"label":"古县","value":"141025"},{"label":"安泽县","value":"141026"},{"label":"浮山县","value":"141027"},{"label":"吉县","value":"141028"},{"label":"乡宁县","value":"141029"},{"label":"大宁县","value":"141030"},{"label":"隰县","value":"141031"},{"label":"永和县","value":"141032"},{"label":"蒲县","value":"141033"},{"label":"汾西县","value":"141034"},{"label":"侯马市","value":"141081"},{"label":"霍州市","value":"141082"}],[{"label":"离石区","value":"141102"},{"label":"文水县","value":"141121"},{"label":"交城县","value":"141122"},{"label":"兴县","value":"141123"},{"label":"临县","value":"141124"},{"label":"柳林县","value":"141125"},{"label":"石楼县","value":"141126"},{"label":"岚县","value":"141127"},{"label":"方山县","value":"141128"},{"label":"中阳县","value":"141129"},{"label":"交口县","value":"141130"},{"label":"孝义市","value":"141181"},{"label":"汾阳市","value":"141182"}]],[[{"label":"新城区","value":"150102"},{"label":"回民区","value":"150103"},{"label":"玉泉区","value":"150104"},{"label":"赛罕区","value":"150105"},{"label":"土默特左旗","value":"150121"},{"label":"托克托县","value":"150122"},{"label":"和林格尔县","value":"150123"},{"label":"清水河县","value":"150124"},{"label":"武川县","value":"150125"},{"label":"呼和浩特金海工业园区","value":"150171"},{"label":"呼和浩特经济技术开发区","value":"150172"}],[{"label":"东河区","value":"150202"},{"label":"昆都仑区","value":"150203"},{"label":"青山区","value":"150204"},{"label":"石拐区","value":"150205"},{"label":"白云鄂博矿区","value":"150206"},{"label":"九原区","value":"150207"},{"label":"土默特右旗","value":"150221"},{"label":"固阳县","value":"150222"},{"label":"达尔罕茂明安联合旗","value":"150223"},{"label":"包头稀土高新技术产业开发区","value":"150271"}],[{"label":"海勃湾区","value":"150302"},{"label":"海南区","value":"150303"},{"label":"乌达区","value":"150304"}],[{"label":"红山区","value":"150402"},{"label":"元宝山区","value":"150403"},{"label":"松山区","value":"150404"},{"label":"阿鲁科尔沁旗","value":"150421"},{"label":"巴林左旗","value":"150422"},{"label":"巴林右旗","value":"150423"},{"label":"林西县","value":"150424"},{"label":"克什克腾旗","value":"150425"},{"label":"翁牛特旗","value":"150426"},{"label":"喀喇沁旗","value":"150428"},{"label":"宁城县","value":"150429"},{"label":"敖汉旗","value":"150430"}],[{"label":"科尔沁区","value":"150502"},{"label":"科尔沁左翼中旗","value":"150521"},{"label":"科尔沁左翼后旗","value":"150522"},{"label":"开鲁县","value":"150523"},{"label":"库伦旗","value":"150524"},{"label":"奈曼旗","value":"150525"},{"label":"扎鲁特旗","value":"150526"},{"label":"通辽经济技术开发区","value":"150571"},{"label":"霍林郭勒市","value":"150581"}],[{"label":"东胜区","value":"150602"},{"label":"康巴什区","value":"150603"},{"label":"达拉特旗","value":"150621"},{"label":"准格尔旗","value":"150622"},{"label":"鄂托克前旗","value":"150623"},{"label":"鄂托克旗","value":"150624"},{"label":"杭锦旗","value":"150625"},{"label":"乌审旗","value":"150626"},{"label":"伊金霍洛旗","value":"150627"}],[{"label":"海拉尔区","value":"150702"},{"label":"扎赉诺尔区","value":"150703"},{"label":"阿荣旗","value":"150721"},{"label":"莫力达瓦达斡尔族自治旗","value":"150722"},{"label":"鄂伦春自治旗","value":"150723"},{"label":"鄂温克族自治旗","value":"150724"},{"label":"陈巴尔虎旗","value":"150725"},{"label":"新巴尔虎左旗","value":"150726"},{"label":"新巴尔虎右旗","value":"150727"},{"label":"满洲里市","value":"150781"},{"label":"牙克石市","value":"150782"},{"label":"扎兰屯市","value":"150783"},{"label":"额尔古纳市","value":"150784"},{"label":"根河市","value":"150785"}],[{"label":"临河区","value":"150802"},{"label":"五原县","value":"150821"},{"label":"磴口县","value":"150822"},{"label":"乌拉特前旗","value":"150823"},{"label":"乌拉特中旗","value":"150824"},{"label":"乌拉特后旗","value":"150825"},{"label":"杭锦后旗","value":"150826"}],[{"label":"集宁区","value":"150902"},{"label":"卓资县","value":"150921"},{"label":"化德县","value":"150922"},{"label":"商都县","value":"150923"},{"label":"兴和县","value":"150924"},{"label":"凉城县","value":"150925"},{"label":"察哈尔右翼前旗","value":"150926"},{"label":"察哈尔右翼中旗","value":"150927"},{"label":"察哈尔右翼后旗","value":"150928"},{"label":"四子王旗","value":"150929"},{"label":"丰镇市","value":"150981"}],[{"label":"乌兰浩特市","value":"152201"},{"label":"阿尔山市","value":"152202"},{"label":"科尔沁右翼前旗","value":"152221"},{"label":"科尔沁右翼中旗","value":"152222"},{"label":"扎赉特旗","value":"152223"},{"label":"突泉县","value":"152224"}],[{"label":"二连浩特市","value":"152501"},{"label":"锡林浩特市","value":"152502"},{"label":"阿巴嘎旗","value":"152522"},{"label":"苏尼特左旗","value":"152523"},{"label":"苏尼特右旗","value":"152524"},{"label":"东乌珠穆沁旗","value":"152525"},{"label":"西乌珠穆沁旗","value":"152526"},{"label":"太仆寺旗","value":"152527"},{"label":"镶黄旗","value":"152528"},{"label":"正镶白旗","value":"152529"},{"label":"正蓝旗","value":"152530"},{"label":"多伦县","value":"152531"},{"label":"乌拉盖管委会","value":"152571"}],[{"label":"阿拉善左旗","value":"152921"},{"label":"阿拉善右旗","value":"152922"},{"label":"额济纳旗","value":"152923"},{"label":"内蒙古阿拉善经济开发区","value":"152971"}]],[[{"label":"和平区","value":"210102"},{"label":"沈河区","value":"210103"},{"label":"大东区","value":"210104"},{"label":"皇姑区","value":"210105"},{"label":"铁西区","value":"210106"},{"label":"苏家屯区","value":"210111"},{"label":"浑南区","value":"210112"},{"label":"沈北新区","value":"210113"},{"label":"于洪区","value":"210114"},{"label":"辽中区","value":"210115"},{"label":"康平县","value":"210123"},{"label":"法库县","value":"210124"},{"label":"新民市","value":"210181"}],[{"label":"中山区","value":"210202"},{"label":"西岗区","value":"210203"},{"label":"沙河口区","value":"210204"},{"label":"甘井子区","value":"210211"},{"label":"旅顺口区","value":"210212"},{"label":"金州区","value":"210213"},{"label":"普兰店区","value":"210214"},{"label":"长海县","value":"210224"},{"label":"瓦房店市","value":"210281"},{"label":"庄河市","value":"210283"}],[{"label":"铁东区","value":"210302"},{"label":"铁西区","value":"210303"},{"label":"立山区","value":"210304"},{"label":"千山区","value":"210311"},{"label":"台安县","value":"210321"},{"label":"岫岩满族自治县","value":"210323"},{"label":"海城市","value":"210381"}],[{"label":"新抚区","value":"210402"},{"label":"东洲区","value":"210403"},{"label":"望花区","value":"210404"},{"label":"顺城区","value":"210411"},{"label":"抚顺县","value":"210421"},{"label":"新宾满族自治县","value":"210422"},{"label":"清原满族自治县","value":"210423"}],[{"label":"平山区","value":"210502"},{"label":"溪湖区","value":"210503"},{"label":"明山区","value":"210504"},{"label":"南芬区","value":"210505"},{"label":"本溪满族自治县","value":"210521"},{"label":"桓仁满族自治县","value":"210522"}],[{"label":"元宝区","value":"210602"},{"label":"振兴区","value":"210603"},{"label":"振安区","value":"210604"},{"label":"宽甸满族自治县","value":"210624"},{"label":"东港市","value":"210681"},{"label":"凤城市","value":"210682"}],[{"label":"古塔区","value":"210702"},{"label":"凌河区","value":"210703"},{"label":"太和区","value":"210711"},{"label":"黑山县","value":"210726"},{"label":"义县","value":"210727"},{"label":"凌海市","value":"210781"},{"label":"北镇市","value":"210782"}],[{"label":"站前区","value":"210802"},{"label":"西市区","value":"210803"},{"label":"鲅鱼圈区","value":"210804"},{"label":"老边区","value":"210811"},{"label":"盖州市","value":"210881"},{"label":"大石桥市","value":"210882"}],[{"label":"海州区","value":"210902"},{"label":"新邱区","value":"210903"},{"label":"太平区","value":"210904"},{"label":"清河门区","value":"210905"},{"label":"细河区","value":"210911"},{"label":"阜新蒙古族自治县","value":"210921"},{"label":"彰武县","value":"210922"}],[{"label":"白塔区","value":"211002"},{"label":"文圣区","value":"211003"},{"label":"宏伟区","value":"211004"},{"label":"弓长岭区","value":"211005"},{"label":"太子河区","value":"211011"},{"label":"辽阳县","value":"211021"},{"label":"灯塔市","value":"211081"}],[{"label":"双台子区","value":"211102"},{"label":"兴隆台区","value":"211103"},{"label":"大洼区","value":"211104"},{"label":"盘山县","value":"211122"}],[{"label":"银州区","value":"211202"},{"label":"清河区","value":"211204"},{"label":"铁岭县","value":"211221"},{"label":"西丰县","value":"211223"},{"label":"昌图县","value":"211224"},{"label":"调兵山市","value":"211281"},{"label":"开原市","value":"211282"}],[{"label":"双塔区","value":"211302"},{"label":"龙城区","value":"211303"},{"label":"朝阳县","value":"211321"},{"label":"建平县","value":"211322"},{"label":"喀喇沁左翼蒙古族自治县","value":"211324"},{"label":"北票市","value":"211381"},{"label":"凌源市","value":"211382"}],[{"label":"连山区","value":"211402"},{"label":"龙港区","value":"211403"},{"label":"南票区","value":"211404"},{"label":"绥中县","value":"211421"},{"label":"建昌县","value":"211422"},{"label":"兴城市","value":"211481"}]],[[{"label":"南关区","value":"220102"},{"label":"宽城区","value":"220103"},{"label":"朝阳区","value":"220104"},{"label":"二道区","value":"220105"},{"label":"绿园区","value":"220106"},{"label":"双阳区","value":"220112"},{"label":"九台区","value":"220113"},{"label":"农安县","value":"220122"},{"label":"长春经济技术开发区","value":"220171"},{"label":"长春净月高新技术产业开发区","value":"220172"},{"label":"长春高新技术产业开发区","value":"220173"},{"label":"长春汽车经济技术开发区","value":"220174"},{"label":"榆树市","value":"220182"},{"label":"德惠市","value":"220183"}],[{"label":"昌邑区","value":"220202"},{"label":"龙潭区","value":"220203"},{"label":"船营区","value":"220204"},{"label":"丰满区","value":"220211"},{"label":"永吉县","value":"220221"},{"label":"吉林经济开发区","value":"220271"},{"label":"吉林高新技术产业开发区","value":"220272"},{"label":"吉林中国新加坡食品区","value":"220273"},{"label":"蛟河市","value":"220281"},{"label":"桦甸市","value":"220282"},{"label":"舒兰市","value":"220283"},{"label":"磐石市","value":"220284"}],[{"label":"铁西区","value":"220302"},{"label":"铁东区","value":"220303"},{"label":"梨树县","value":"220322"},{"label":"伊通满族自治县","value":"220323"},{"label":"公主岭市","value":"220381"},{"label":"双辽市","value":"220382"}],[{"label":"龙山区","value":"220402"},{"label":"西安区","value":"220403"},{"label":"东丰县","value":"220421"},{"label":"东辽县","value":"220422"}],[{"label":"东昌区","value":"220502"},{"label":"二道江区","value":"220503"},{"label":"通化县","value":"220521"},{"label":"辉南县","value":"220523"},{"label":"柳河县","value":"220524"},{"label":"梅河口市","value":"220581"},{"label":"集安市","value":"220582"}],[{"label":"浑江区","value":"220602"},{"label":"江源区","value":"220605"},{"label":"抚松县","value":"220621"},{"label":"靖宇县","value":"220622"},{"label":"长白朝鲜族自治县","value":"220623"},{"label":"临江市","value":"220681"}],[{"label":"宁江区","value":"220702"},{"label":"前郭尔罗斯蒙古族自治县","value":"220721"},{"label":"长岭县","value":"220722"},{"label":"乾安县","value":"220723"},{"label":"吉林松原经济开发区","value":"220771"},{"label":"扶余市","value":"220781"}],[{"label":"洮北区","value":"220802"},{"label":"镇赉县","value":"220821"},{"label":"通榆县","value":"220822"},{"label":"吉林白城经济开发区","value":"220871"},{"label":"洮南市","value":"220881"},{"label":"大安市","value":"220882"}],[{"label":"延吉市","value":"222401"},{"label":"图们市","value":"222402"},{"label":"敦化市","value":"222403"},{"label":"珲春市","value":"222404"},{"label":"龙井市","value":"222405"},{"label":"和龙市","value":"222406"},{"label":"汪清县","value":"222424"},{"label":"安图县","value":"222426"}]],[[{"label":"道里区","value":"230102"},{"label":"南岗区","value":"230103"},{"label":"道外区","value":"230104"},{"label":"平房区","value":"230108"},{"label":"松北区","value":"230109"},{"label":"香坊区","value":"230110"},{"label":"呼兰区","value":"230111"},{"label":"阿城区","value":"230112"},{"label":"双城区","value":"230113"},{"label":"依兰县","value":"230123"},{"label":"方正县","value":"230124"},{"label":"宾县","value":"230125"},{"label":"巴彦县","value":"230126"},{"label":"木兰县","value":"230127"},{"label":"通河县","value":"230128"},{"label":"延寿县","value":"230129"},{"label":"尚志市","value":"230183"},{"label":"五常市","value":"230184"}],[{"label":"龙沙区","value":"230202"},{"label":"建华区","value":"230203"},{"label":"铁锋区","value":"230204"},{"label":"昂昂溪区","value":"230205"},{"label":"富拉尔基区","value":"230206"},{"label":"碾子山区","value":"230207"},{"label":"梅里斯达斡尔族区","value":"230208"},{"label":"龙江县","value":"230221"},{"label":"依安县","value":"230223"},{"label":"泰来县","value":"230224"},{"label":"甘南县","value":"230225"},{"label":"富裕县","value":"230227"},{"label":"克山县","value":"230229"},{"label":"克东县","value":"230230"},{"label":"拜泉县","value":"230231"},{"label":"讷河市","value":"230281"}],[{"label":"鸡冠区","value":"230302"},{"label":"恒山区","value":"230303"},{"label":"滴道区","value":"230304"},{"label":"梨树区","value":"230305"},{"label":"城子河区","value":"230306"},{"label":"麻山区","value":"230307"},{"label":"鸡东县","value":"230321"},{"label":"虎林市","value":"230381"},{"label":"密山市","value":"230382"}],[{"label":"向阳区","value":"230402"},{"label":"工农区","value":"230403"},{"label":"南山区","value":"230404"},{"label":"兴安区","value":"230405"},{"label":"东山区","value":"230406"},{"label":"兴山区","value":"230407"},{"label":"萝北县","value":"230421"},{"label":"绥滨县","value":"230422"}],[{"label":"尖山区","value":"230502"},{"label":"岭东区","value":"230503"},{"label":"四方台区","value":"230505"},{"label":"宝山区","value":"230506"},{"label":"集贤县","value":"230521"},{"label":"友谊县","value":"230522"},{"label":"宝清县","value":"230523"},{"label":"饶河县","value":"230524"}],[{"label":"萨尔图区","value":"230602"},{"label":"龙凤区","value":"230603"},{"label":"让胡路区","value":"230604"},{"label":"红岗区","value":"230605"},{"label":"大同区","value":"230606"},{"label":"肇州县","value":"230621"},{"label":"肇源县","value":"230622"},{"label":"林甸县","value":"230623"},{"label":"杜尔伯特蒙古族自治县","value":"230624"},{"label":"大庆高新技术产业开发区","value":"230671"}],[{"label":"伊春区","value":"230702"},{"label":"南岔区","value":"230703"},{"label":"友好区","value":"230704"},{"label":"西林区","value":"230705"},{"label":"翠峦区","value":"230706"},{"label":"新青区","value":"230707"},{"label":"美溪区","value":"230708"},{"label":"金山屯区","value":"230709"},{"label":"五营区","value":"230710"},{"label":"乌马河区","value":"230711"},{"label":"汤旺河区","value":"230712"},{"label":"带岭区","value":"230713"},{"label":"乌伊岭区","value":"230714"},{"label":"红星区","value":"230715"},{"label":"上甘岭区","value":"230716"},{"label":"嘉荫县","value":"230722"},{"label":"铁力市","value":"230781"}],[{"label":"向阳区","value":"230803"},{"label":"前进区","value":"230804"},{"label":"东风区","value":"230805"},{"label":"郊区","value":"230811"},{"label":"桦南县","value":"230822"},{"label":"桦川县","value":"230826"},{"label":"汤原县","value":"230828"},{"label":"同江市","value":"230881"},{"label":"富锦市","value":"230882"},{"label":"抚远市","value":"230883"}],[{"label":"新兴区","value":"230902"},{"label":"桃山区","value":"230903"},{"label":"茄子河区","value":"230904"},{"label":"勃利县","value":"230921"}],[{"label":"东安区","value":"231002"},{"label":"阳明区","value":"231003"},{"label":"爱民区","value":"231004"},{"label":"西安区","value":"231005"},{"label":"林口县","value":"231025"},{"label":"牡丹江经济技术开发区","value":"231071"},{"label":"绥芬河市","value":"231081"},{"label":"海林市","value":"231083"},{"label":"宁安市","value":"231084"},{"label":"穆棱市","value":"231085"},{"label":"东宁市","value":"231086"}],[{"label":"爱辉区","value":"231102"},{"label":"嫩江县","value":"231121"},{"label":"逊克县","value":"231123"},{"label":"孙吴县","value":"231124"},{"label":"北安市","value":"231181"},{"label":"五大连池市","value":"231182"}],[{"label":"北林区","value":"231202"},{"label":"望奎县","value":"231221"},{"label":"兰西县","value":"231222"},{"label":"青冈县","value":"231223"},{"label":"庆安县","value":"231224"},{"label":"明水县","value":"231225"},{"label":"绥棱县","value":"231226"},{"label":"安达市","value":"231281"},{"label":"肇东市","value":"231282"},{"label":"海伦市","value":"231283"}],[{"label":"加格达奇区","value":"232701"},{"label":"松岭区","value":"232702"},{"label":"新林区","value":"232703"},{"label":"呼中区","value":"232704"},{"label":"呼玛县","value":"232721"},{"label":"塔河县","value":"232722"},{"label":"漠河县","value":"232723"}]],[[{"label":"黄浦区","value":"310101"},{"label":"徐汇区","value":"310104"},{"label":"长宁区","value":"310105"},{"label":"静安区","value":"310106"},{"label":"普陀区","value":"310107"},{"label":"虹口区","value":"310109"},{"label":"杨浦区","value":"310110"},{"label":"闵行区","value":"310112"},{"label":"宝山区","value":"310113"},{"label":"嘉定区","value":"310114"},{"label":"浦东新区","value":"310115"},{"label":"金山区","value":"310116"},{"label":"松江区","value":"310117"},{"label":"青浦区","value":"310118"},{"label":"奉贤区","value":"310120"},{"label":"崇明区","value":"310151"}]],[[{"label":"玄武区","value":"320102"},{"label":"秦淮区","value":"320104"},{"label":"建邺区","value":"320105"},{"label":"鼓楼区","value":"320106"},{"label":"浦口区","value":"320111"},{"label":"栖霞区","value":"320113"},{"label":"雨花台区","value":"320114"},{"label":"江宁区","value":"320115"},{"label":"六合区","value":"320116"},{"label":"溧水区","value":"320117"},{"label":"高淳区","value":"320118"}],[{"label":"锡山区","value":"320205"},{"label":"惠山区","value":"320206"},{"label":"滨湖区","value":"320211"},{"label":"梁溪区","value":"320213"},{"label":"新吴区","value":"320214"},{"label":"江阴市","value":"320281"},{"label":"宜兴市","value":"320282"}],[{"label":"鼓楼区","value":"320302"},{"label":"云龙区","value":"320303"},{"label":"贾汪区","value":"320305"},{"label":"泉山区","value":"320311"},{"label":"铜山区","value":"320312"},{"label":"丰县","value":"320321"},{"label":"沛县","value":"320322"},{"label":"睢宁县","value":"320324"},{"label":"徐州经济技术开发区","value":"320371"},{"label":"新沂市","value":"320381"},{"label":"邳州市","value":"320382"}],[{"label":"天宁区","value":"320402"},{"label":"钟楼区","value":"320404"},{"label":"新北区","value":"320411"},{"label":"武进区","value":"320412"},{"label":"金坛区","value":"320413"},{"label":"溧阳市","value":"320481"}],[{"label":"虎丘区","value":"320505"},{"label":"吴中区","value":"320506"},{"label":"相城区","value":"320507"},{"label":"姑苏区","value":"320508"},{"label":"吴江区","value":"320509"},{"label":"苏州工业园区","value":"320571"},{"label":"常熟市","value":"320581"},{"label":"张家港市","value":"320582"},{"label":"昆山市","value":"320583"},{"label":"太仓市","value":"320585"}],[{"label":"崇川区","value":"320602"},{"label":"港闸区","value":"320611"},{"label":"通州区","value":"320612"},{"label":"海安县","value":"320621"},{"label":"如东县","value":"320623"},{"label":"南通经济技术开发区","value":"320671"},{"label":"启东市","value":"320681"},{"label":"如皋市","value":"320682"},{"label":"海门市","value":"320684"}],[{"label":"连云区","value":"320703"},{"label":"海州区","value":"320706"},{"label":"赣榆区","value":"320707"},{"label":"东海县","value":"320722"},{"label":"灌云县","value":"320723"},{"label":"灌南县","value":"320724"},{"label":"连云港经济技术开发区","value":"320771"},{"label":"连云港高新技术产业开发区","value":"320772"}],[{"label":"淮安区","value":"320803"},{"label":"淮阴区","value":"320804"},{"label":"清江浦区","value":"320812"},{"label":"洪泽区","value":"320813"},{"label":"涟水县","value":"320826"},{"label":"盱眙县","value":"320830"},{"label":"金湖县","value":"320831"},{"label":"淮安经济技术开发区","value":"320871"}],[{"label":"亭湖区","value":"320902"},{"label":"盐都区","value":"320903"},{"label":"大丰区","value":"320904"},{"label":"响水县","value":"320921"},{"label":"滨海县","value":"320922"},{"label":"阜宁县","value":"320923"},{"label":"射阳县","value":"320924"},{"label":"建湖县","value":"320925"},{"label":"盐城经济技术开发区","value":"320971"},{"label":"东台市","value":"320981"}],[{"label":"广陵区","value":"321002"},{"label":"邗江区","value":"321003"},{"label":"江都区","value":"321012"},{"label":"宝应县","value":"321023"},{"label":"扬州经济技术开发区","value":"321071"},{"label":"仪征市","value":"321081"},{"label":"高邮市","value":"321084"}],[{"label":"京口区","value":"321102"},{"label":"润州区","value":"321111"},{"label":"丹徒区","value":"321112"},{"label":"镇江新区","value":"321171"},{"label":"丹阳市","value":"321181"},{"label":"扬中市","value":"321182"},{"label":"句容市","value":"321183"}],[{"label":"海陵区","value":"321202"},{"label":"高港区","value":"321203"},{"label":"姜堰区","value":"321204"},{"label":"泰州医药高新技术产业开发区","value":"321271"},{"label":"兴化市","value":"321281"},{"label":"靖江市","value":"321282"},{"label":"泰兴市","value":"321283"}],[{"label":"宿城区","value":"321302"},{"label":"宿豫区","value":"321311"},{"label":"沭阳县","value":"321322"},{"label":"泗阳县","value":"321323"},{"label":"泗洪县","value":"321324"},{"label":"宿迁经济技术开发区","value":"321371"}]],[[{"label":"上城区","value":"330102"},{"label":"下城区","value":"330103"},{"label":"江干区","value":"330104"},{"label":"拱墅区","value":"330105"},{"label":"西湖区","value":"330106"},{"label":"滨江区","value":"330108"},{"label":"萧山区","value":"330109"},{"label":"余杭区","value":"330110"},{"label":"富阳区","value":"330111"},{"label":"临安区","value":"330112"},{"label":"桐庐县","value":"330122"},{"label":"淳安县","value":"330127"},{"label":"建德市","value":"330182"}],[{"label":"海曙区","value":"330203"},{"label":"江北区","value":"330205"},{"label":"北仑区","value":"330206"},{"label":"镇海区","value":"330211"},{"label":"鄞州区","value":"330212"},{"label":"奉化区","value":"330213"},{"label":"象山县","value":"330225"},{"label":"宁海县","value":"330226"},{"label":"余姚市","value":"330281"},{"label":"慈溪市","value":"330282"}],[{"label":"鹿城区","value":"330302"},{"label":"龙湾区","value":"330303"},{"label":"瓯海区","value":"330304"},{"label":"洞头区","value":"330305"},{"label":"永嘉县","value":"330324"},{"label":"平阳县","value":"330326"},{"label":"苍南县","value":"330327"},{"label":"文成县","value":"330328"},{"label":"泰顺县","value":"330329"},{"label":"温州经济技术开发区","value":"330371"},{"label":"瑞安市","value":"330381"},{"label":"乐清市","value":"330382"}],[{"label":"南湖区","value":"330402"},{"label":"秀洲区","value":"330411"},{"label":"嘉善县","value":"330421"},{"label":"海盐县","value":"330424"},{"label":"海宁市","value":"330481"},{"label":"平湖市","value":"330482"},{"label":"桐乡市","value":"330483"}],[{"label":"吴兴区","value":"330502"},{"label":"南浔区","value":"330503"},{"label":"德清县","value":"330521"},{"label":"长兴县","value":"330522"},{"label":"安吉县","value":"330523"}],[{"label":"越城区","value":"330602"},{"label":"柯桥区","value":"330603"},{"label":"上虞区","value":"330604"},{"label":"新昌县","value":"330624"},{"label":"诸暨市","value":"330681"},{"label":"嵊州市","value":"330683"}],[{"label":"婺城区","value":"330702"},{"label":"金东区","value":"330703"},{"label":"武义县","value":"330723"},{"label":"浦江县","value":"330726"},{"label":"磐安县","value":"330727"},{"label":"兰溪市","value":"330781"},{"label":"义乌市","value":"330782"},{"label":"东阳市","value":"330783"},{"label":"永康市","value":"330784"}],[{"label":"柯城区","value":"330802"},{"label":"衢江区","value":"330803"},{"label":"常山县","value":"330822"},{"label":"开化县","value":"330824"},{"label":"龙游县","value":"330825"},{"label":"江山市","value":"330881"}],[{"label":"定海区","value":"330902"},{"label":"普陀区","value":"330903"},{"label":"岱山县","value":"330921"},{"label":"嵊泗县","value":"330922"}],[{"label":"椒江区","value":"331002"},{"label":"黄岩区","value":"331003"},{"label":"路桥区","value":"331004"},{"label":"三门县","value":"331022"},{"label":"天台县","value":"331023"},{"label":"仙居县","value":"331024"},{"label":"温岭市","value":"331081"},{"label":"临海市","value":"331082"},{"label":"玉环市","value":"331083"}],[{"label":"莲都区","value":"331102"},{"label":"青田县","value":"331121"},{"label":"缙云县","value":"331122"},{"label":"遂昌县","value":"331123"},{"label":"松阳县","value":"331124"},{"label":"云和县","value":"331125"},{"label":"庆元县","value":"331126"},{"label":"景宁畲族自治县","value":"331127"},{"label":"龙泉市","value":"331181"}]],[[{"label":"瑶海区","value":"340102"},{"label":"庐阳区","value":"340103"},{"label":"蜀山区","value":"340104"},{"label":"包河区","value":"340111"},{"label":"长丰县","value":"340121"},{"label":"肥东县","value":"340122"},{"label":"肥西县","value":"340123"},{"label":"庐江县","value":"340124"},{"label":"合肥高新技术产业开发区","value":"340171"},{"label":"合肥经济技术开发区","value":"340172"},{"label":"合肥新站高新技术产业开发区","value":"340173"},{"label":"巢湖市","value":"340181"}],[{"label":"镜湖区","value":"340202"},{"label":"弋江区","value":"340203"},{"label":"鸠江区","value":"340207"},{"label":"三山区","value":"340208"},{"label":"芜湖县","value":"340221"},{"label":"繁昌县","value":"340222"},{"label":"南陵县","value":"340223"},{"label":"无为县","value":"340225"},{"label":"芜湖经济技术开发区","value":"340271"},{"label":"安徽芜湖长江大桥经济开发区","value":"340272"}],[{"label":"龙子湖区","value":"340302"},{"label":"蚌山区","value":"340303"},{"label":"禹会区","value":"340304"},{"label":"淮上区","value":"340311"},{"label":"怀远县","value":"340321"},{"label":"五河县","value":"340322"},{"label":"固镇县","value":"340323"},{"label":"蚌埠市高新技术开发区","value":"340371"},{"label":"蚌埠市经济开发区","value":"340372"}],[{"label":"大通区","value":"340402"},{"label":"田家庵区","value":"340403"},{"label":"谢家集区","value":"340404"},{"label":"八公山区","value":"340405"},{"label":"潘集区","value":"340406"},{"label":"凤台县","value":"340421"},{"label":"寿县","value":"340422"}],[{"label":"花山区","value":"340503"},{"label":"雨山区","value":"340504"},{"label":"博望区","value":"340506"},{"label":"当涂县","value":"340521"},{"label":"含山县","value":"340522"},{"label":"和县","value":"340523"}],[{"label":"杜集区","value":"340602"},{"label":"相山区","value":"340603"},{"label":"烈山区","value":"340604"},{"label":"濉溪县","value":"340621"}],[{"label":"铜官区","value":"340705"},{"label":"义安区","value":"340706"},{"label":"郊区","value":"340711"},{"label":"枞阳县","value":"340722"}],[{"label":"迎江区","value":"340802"},{"label":"大观区","value":"340803"},{"label":"宜秀区","value":"340811"},{"label":"怀宁县","value":"340822"},{"label":"潜山县","value":"340824"},{"label":"太湖县","value":"340825"},{"label":"宿松县","value":"340826"},{"label":"望江县","value":"340827"},{"label":"岳西县","value":"340828"},{"label":"安徽安庆经济开发区","value":"340871"},{"label":"桐城市","value":"340881"}],[{"label":"屯溪区","value":"341002"},{"label":"黄山区","value":"341003"},{"label":"徽州区","value":"341004"},{"label":"歙县","value":"341021"},{"label":"休宁县","value":"341022"},{"label":"黟县","value":"341023"},{"label":"祁门县","value":"341024"}],[{"label":"琅琊区","value":"341102"},{"label":"南谯区","value":"341103"},{"label":"来安县","value":"341122"},{"label":"全椒县","value":"341124"},{"label":"定远县","value":"341125"},{"label":"凤阳县","value":"341126"},{"label":"苏滁现代产业园","value":"341171"},{"label":"滁州经济技术开发区","value":"341172"},{"label":"天长市","value":"341181"},{"label":"明光市","value":"341182"}],[{"label":"颍州区","value":"341202"},{"label":"颍东区","value":"341203"},{"label":"颍泉区","value":"341204"},{"label":"临泉县","value":"341221"},{"label":"太和县","value":"341222"},{"label":"阜南县","value":"341225"},{"label":"颍上县","value":"341226"},{"label":"阜阳合肥现代产业园区","value":"341271"},{"label":"阜阳经济技术开发区","value":"341272"},{"label":"界首市","value":"341282"}],[{"label":"埇桥区","value":"341302"},{"label":"砀山县","value":"341321"},{"label":"萧县","value":"341322"},{"label":"灵璧县","value":"341323"},{"label":"泗县","value":"341324"},{"label":"宿州马鞍山现代产业园区","value":"341371"},{"label":"宿州经济技术开发区","value":"341372"}],[{"label":"金安区","value":"341502"},{"label":"裕安区","value":"341503"},{"label":"叶集区","value":"341504"},{"label":"霍邱县","value":"341522"},{"label":"舒城县","value":"341523"},{"label":"金寨县","value":"341524"},{"label":"霍山县","value":"341525"}],[{"label":"谯城区","value":"341602"},{"label":"涡阳县","value":"341621"},{"label":"蒙城县","value":"341622"},{"label":"利辛县","value":"341623"}],[{"label":"贵池区","value":"341702"},{"label":"东至县","value":"341721"},{"label":"石台县","value":"341722"},{"label":"青阳县","value":"341723"}],[{"label":"宣州区","value":"341802"},{"label":"郎溪县","value":"341821"},{"label":"广德县","value":"341822"},{"label":"泾县","value":"341823"},{"label":"绩溪县","value":"341824"},{"label":"旌德县","value":"341825"},{"label":"宣城市经济开发区","value":"341871"},{"label":"宁国市","value":"341881"}]],[[{"label":"鼓楼区","value":"350102"},{"label":"台江区","value":"350103"},{"label":"仓山区","value":"350104"},{"label":"马尾区","value":"350105"},{"label":"晋安区","value":"350111"},{"label":"闽侯县","value":"350121"},{"label":"连江县","value":"350122"},{"label":"罗源县","value":"350123"},{"label":"闽清县","value":"350124"},{"label":"永泰县","value":"350125"},{"label":"平潭县","value":"350128"},{"label":"福清市","value":"350181"},{"label":"长乐市","value":"350182"}],[{"label":"思明区","value":"350203"},{"label":"海沧区","value":"350205"},{"label":"湖里区","value":"350206"},{"label":"集美区","value":"350211"},{"label":"同安区","value":"350212"},{"label":"翔安区","value":"350213"}],[{"label":"城厢区","value":"350302"},{"label":"涵江区","value":"350303"},{"label":"荔城区","value":"350304"},{"label":"秀屿区","value":"350305"},{"label":"仙游县","value":"350322"}],[{"label":"梅列区","value":"350402"},{"label":"三元区","value":"350403"},{"label":"明溪县","value":"350421"},{"label":"清流县","value":"350423"},{"label":"宁化县","value":"350424"},{"label":"大田县","value":"350425"},{"label":"尤溪县","value":"350426"},{"label":"沙县","value":"350427"},{"label":"将乐县","value":"350428"},{"label":"泰宁县","value":"350429"},{"label":"建宁县","value":"350430"},{"label":"永安市","value":"350481"}],[{"label":"鲤城区","value":"350502"},{"label":"丰泽区","value":"350503"},{"label":"洛江区","value":"350504"},{"label":"泉港区","value":"350505"},{"label":"惠安县","value":"350521"},{"label":"安溪县","value":"350524"},{"label":"永春县","value":"350525"},{"label":"德化县","value":"350526"},{"label":"金门县","value":"350527"},{"label":"石狮市","value":"350581"},{"label":"晋江市","value":"350582"},{"label":"南安市","value":"350583"}],[{"label":"芗城区","value":"350602"},{"label":"龙文区","value":"350603"},{"label":"云霄县","value":"350622"},{"label":"漳浦县","value":"350623"},{"label":"诏安县","value":"350624"},{"label":"长泰县","value":"350625"},{"label":"东山县","value":"350626"},{"label":"南靖县","value":"350627"},{"label":"平和县","value":"350628"},{"label":"华安县","value":"350629"},{"label":"龙海市","value":"350681"}],[{"label":"延平区","value":"350702"},{"label":"建阳区","value":"350703"},{"label":"顺昌县","value":"350721"},{"label":"浦城县","value":"350722"},{"label":"光泽县","value":"350723"},{"label":"松溪县","value":"350724"},{"label":"政和县","value":"350725"},{"label":"邵武市","value":"350781"},{"label":"武夷山市","value":"350782"},{"label":"建瓯市","value":"350783"}],[{"label":"新罗区","value":"350802"},{"label":"永定区","value":"350803"},{"label":"长汀县","value":"350821"},{"label":"上杭县","value":"350823"},{"label":"武平县","value":"350824"},{"label":"连城县","value":"350825"},{"label":"漳平市","value":"350881"}],[{"label":"蕉城区","value":"350902"},{"label":"霞浦县","value":"350921"},{"label":"古田县","value":"350922"},{"label":"屏南县","value":"350923"},{"label":"寿宁县","value":"350924"},{"label":"周宁县","value":"350925"},{"label":"柘荣县","value":"350926"},{"label":"福安市","value":"350981"},{"label":"福鼎市","value":"350982"}]],[[{"label":"东湖区","value":"360102"},{"label":"西湖区","value":"360103"},{"label":"青云谱区","value":"360104"},{"label":"湾里区","value":"360105"},{"label":"青山湖区","value":"360111"},{"label":"新建区","value":"360112"},{"label":"南昌县","value":"360121"},{"label":"安义县","value":"360123"},{"label":"进贤县","value":"360124"}],[{"label":"昌江区","value":"360202"},{"label":"珠山区","value":"360203"},{"label":"浮梁县","value":"360222"},{"label":"乐平市","value":"360281"}],[{"label":"安源区","value":"360302"},{"label":"湘东区","value":"360313"},{"label":"莲花县","value":"360321"},{"label":"上栗县","value":"360322"},{"label":"芦溪县","value":"360323"}],[{"label":"濂溪区","value":"360402"},{"label":"浔阳区","value":"360403"},{"label":"柴桑区","value":"360404"},{"label":"武宁县","value":"360423"},{"label":"修水县","value":"360424"},{"label":"永修县","value":"360425"},{"label":"德安县","value":"360426"},{"label":"都昌县","value":"360428"},{"label":"湖口县","value":"360429"},{"label":"彭泽县","value":"360430"},{"label":"瑞昌市","value":"360481"},{"label":"共青城市","value":"360482"},{"label":"庐山市","value":"360483"}],[{"label":"渝水区","value":"360502"},{"label":"分宜县","value":"360521"}],[{"label":"月湖区","value":"360602"},{"label":"余江县","value":"360622"},{"label":"贵溪市","value":"360681"}],[{"label":"章贡区","value":"360702"},{"label":"南康区","value":"360703"},{"label":"赣县区","value":"360704"},{"label":"信丰县","value":"360722"},{"label":"大余县","value":"360723"},{"label":"上犹县","value":"360724"},{"label":"崇义县","value":"360725"},{"label":"安远县","value":"360726"},{"label":"龙南县","value":"360727"},{"label":"定南县","value":"360728"},{"label":"全南县","value":"360729"},{"label":"宁都县","value":"360730"},{"label":"于都县","value":"360731"},{"label":"兴国县","value":"360732"},{"label":"会昌县","value":"360733"},{"label":"寻乌县","value":"360734"},{"label":"石城县","value":"360735"},{"label":"瑞金市","value":"360781"}],[{"label":"吉州区","value":"360802"},{"label":"青原区","value":"360803"},{"label":"吉安县","value":"360821"},{"label":"吉水县","value":"360822"},{"label":"峡江县","value":"360823"},{"label":"新干县","value":"360824"},{"label":"永丰县","value":"360825"},{"label":"泰和县","value":"360826"},{"label":"遂川县","value":"360827"},{"label":"万安县","value":"360828"},{"label":"安福县","value":"360829"},{"label":"永新县","value":"360830"},{"label":"井冈山市","value":"360881"}],[{"label":"袁州区","value":"360902"},{"label":"奉新县","value":"360921"},{"label":"万载县","value":"360922"},{"label":"上高县","value":"360923"},{"label":"宜丰县","value":"360924"},{"label":"靖安县","value":"360925"},{"label":"铜鼓县","value":"360926"},{"label":"丰城市","value":"360981"},{"label":"樟树市","value":"360982"},{"label":"高安市","value":"360983"}],[{"label":"临川区","value":"361002"},{"label":"东乡区","value":"361003"},{"label":"南城县","value":"361021"},{"label":"黎川县","value":"361022"},{"label":"南丰县","value":"361023"},{"label":"崇仁县","value":"361024"},{"label":"乐安县","value":"361025"},{"label":"宜黄县","value":"361026"},{"label":"金溪县","value":"361027"},{"label":"资溪县","value":"361028"},{"label":"广昌县","value":"361030"}],[{"label":"信州区","value":"361102"},{"label":"广丰区","value":"361103"},{"label":"上饶县","value":"361121"},{"label":"玉山县","value":"361123"},{"label":"铅山县","value":"361124"},{"label":"横峰县","value":"361125"},{"label":"弋阳县","value":"361126"},{"label":"余干县","value":"361127"},{"label":"鄱阳县","value":"361128"},{"label":"万年县","value":"361129"},{"label":"婺源县","value":"361130"},{"label":"德兴市","value":"361181"}]],[[{"label":"历下区","value":"370102"},{"label":"市中区","value":"370103"},{"label":"槐荫区","value":"370104"},{"label":"天桥区","value":"370105"},{"label":"历城区","value":"370112"},{"label":"长清区","value":"370113"},{"label":"章丘区","value":"370114"},{"label":"平阴县","value":"370124"},{"label":"济阳县","value":"370125"},{"label":"商河县","value":"370126"},{"label":"济南高新技术产业开发区","value":"370171"}],[{"label":"市南区","value":"370202"},{"label":"市北区","value":"370203"},{"label":"黄岛区","value":"370211"},{"label":"崂山区","value":"370212"},{"label":"李沧区","value":"370213"},{"label":"城阳区","value":"370214"},{"label":"即墨区","value":"370215"},{"label":"青岛高新技术产业开发区","value":"370271"},{"label":"胶州市","value":"370281"},{"label":"平度市","value":"370283"},{"label":"莱西市","value":"370285"}],[{"label":"淄川区","value":"370302"},{"label":"张店区","value":"370303"},{"label":"博山区","value":"370304"},{"label":"临淄区","value":"370305"},{"label":"周村区","value":"370306"},{"label":"桓台县","value":"370321"},{"label":"高青县","value":"370322"},{"label":"沂源县","value":"370323"}],[{"label":"市中区","value":"370402"},{"label":"薛城区","value":"370403"},{"label":"峄城区","value":"370404"},{"label":"台儿庄区","value":"370405"},{"label":"山亭区","value":"370406"},{"label":"滕州市","value":"370481"}],[{"label":"东营区","value":"370502"},{"label":"河口区","value":"370503"},{"label":"垦利区","value":"370505"},{"label":"利津县","value":"370522"},{"label":"广饶县","value":"370523"},{"label":"东营经济技术开发区","value":"370571"},{"label":"东营港经济开发区","value":"370572"}],[{"label":"芝罘区","value":"370602"},{"label":"福山区","value":"370611"},{"label":"牟平区","value":"370612"},{"label":"莱山区","value":"370613"},{"label":"长岛县","value":"370634"},{"label":"烟台高新技术产业开发区","value":"370671"},{"label":"烟台经济技术开发区","value":"370672"},{"label":"龙口市","value":"370681"},{"label":"莱阳市","value":"370682"},{"label":"莱州市","value":"370683"},{"label":"蓬莱市","value":"370684"},{"label":"招远市","value":"370685"},{"label":"栖霞市","value":"370686"},{"label":"海阳市","value":"370687"}],[{"label":"潍城区","value":"370702"},{"label":"寒亭区","value":"370703"},{"label":"坊子区","value":"370704"},{"label":"奎文区","value":"370705"},{"label":"临朐县","value":"370724"},{"label":"昌乐县","value":"370725"},{"label":"潍坊滨海经济技术开发区","value":"370772"},{"label":"青州市","value":"370781"},{"label":"诸城市","value":"370782"},{"label":"寿光市","value":"370783"},{"label":"安丘市","value":"370784"},{"label":"高密市","value":"370785"},{"label":"昌邑市","value":"370786"}],[{"label":"任城区","value":"370811"},{"label":"兖州区","value":"370812"},{"label":"微山县","value":"370826"},{"label":"鱼台县","value":"370827"},{"label":"金乡县","value":"370828"},{"label":"嘉祥县","value":"370829"},{"label":"汶上县","value":"370830"},{"label":"泗水县","value":"370831"},{"label":"梁山县","value":"370832"},{"label":"济宁高新技术产业开发区","value":"370871"},{"label":"曲阜市","value":"370881"},{"label":"邹城市","value":"370883"}],[{"label":"泰山区","value":"370902"},{"label":"岱岳区","value":"370911"},{"label":"宁阳县","value":"370921"},{"label":"东平县","value":"370923"},{"label":"新泰市","value":"370982"},{"label":"肥城市","value":"370983"}],[{"label":"环翠区","value":"371002"},{"label":"文登区","value":"371003"},{"label":"威海火炬高技术产业开发区","value":"371071"},{"label":"威海经济技术开发区","value":"371072"},{"label":"威海临港经济技术开发区","value":"371073"},{"label":"荣成市","value":"371082"},{"label":"乳山市","value":"371083"}],[{"label":"东港区","value":"371102"},{"label":"岚山区","value":"371103"},{"label":"五莲县","value":"371121"},{"label":"莒县","value":"371122"},{"label":"日照经济技术开发区","value":"371171"},{"label":"日照国际海洋城","value":"371172"}],[{"label":"莱城区","value":"371202"},{"label":"钢城区","value":"371203"}],[{"label":"兰山区","value":"371302"},{"label":"罗庄区","value":"371311"},{"label":"河东区","value":"371312"},{"label":"沂南县","value":"371321"},{"label":"郯城县","value":"371322"},{"label":"沂水县","value":"371323"},{"label":"兰陵县","value":"371324"},{"label":"费县","value":"371325"},{"label":"平邑县","value":"371326"},{"label":"莒南县","value":"371327"},{"label":"蒙阴县","value":"371328"},{"label":"临沭县","value":"371329"},{"label":"临沂高新技术产业开发区","value":"371371"},{"label":"临沂经济技术开发区","value":"371372"},{"label":"临沂临港经济开发区","value":"371373"}],[{"label":"德城区","value":"371402"},{"label":"陵城区","value":"371403"},{"label":"宁津县","value":"371422"},{"label":"庆云县","value":"371423"},{"label":"临邑县","value":"371424"},{"label":"齐河县","value":"371425"},{"label":"平原县","value":"371426"},{"label":"夏津县","value":"371427"},{"label":"武城县","value":"371428"},{"label":"德州经济技术开发区","value":"371471"},{"label":"德州运河经济开发区","value":"371472"},{"label":"乐陵市","value":"371481"},{"label":"禹城市","value":"371482"}],[{"label":"东昌府区","value":"371502"},{"label":"阳谷县","value":"371521"},{"label":"莘县","value":"371522"},{"label":"茌平县","value":"371523"},{"label":"东阿县","value":"371524"},{"label":"冠县","value":"371525"},{"label":"高唐县","value":"371526"},{"label":"临清市","value":"371581"}],[{"label":"滨城区","value":"371602"},{"label":"沾化区","value":"371603"},{"label":"惠民县","value":"371621"},{"label":"阳信县","value":"371622"},{"label":"无棣县","value":"371623"},{"label":"博兴县","value":"371625"},{"label":"邹平县","value":"371626"}],[{"label":"牡丹区","value":"371702"},{"label":"定陶区","value":"371703"},{"label":"曹县","value":"371721"},{"label":"单县","value":"371722"},{"label":"成武县","value":"371723"},{"label":"巨野县","value":"371724"},{"label":"郓城县","value":"371725"},{"label":"鄄城县","value":"371726"},{"label":"东明县","value":"371728"},{"label":"菏泽经济技术开发区","value":"371771"},{"label":"菏泽高新技术开发区","value":"371772"}]],[[{"label":"中原区","value":"410102"},{"label":"二七区","value":"410103"},{"label":"管城回族区","value":"410104"},{"label":"金水区","value":"410105"},{"label":"上街区","value":"410106"},{"label":"惠济区","value":"410108"},{"label":"中牟县","value":"410122"},{"label":"郑州经济技术开发区","value":"410171"},{"label":"郑州高新技术产业开发区","value":"410172"},{"label":"郑州航空港经济综合实验区","value":"410173"},{"label":"巩义市","value":"410181"},{"label":"荥阳市","value":"410182"},{"label":"新密市","value":"410183"},{"label":"新郑市","value":"410184"},{"label":"登封市","value":"410185"}],[{"label":"龙亭区","value":"410202"},{"label":"顺河回族区","value":"410203"},{"label":"鼓楼区","value":"410204"},{"label":"禹王台区","value":"410205"},{"label":"祥符区","value":"410212"},{"label":"杞县","value":"410221"},{"label":"通许县","value":"410222"},{"label":"尉氏县","value":"410223"},{"label":"兰考县","value":"410225"}],[{"label":"老城区","value":"410302"},{"label":"西工区","value":"410303"},{"label":"瀍河回族区","value":"410304"},{"label":"涧西区","value":"410305"},{"label":"吉利区","value":"410306"},{"label":"洛龙区","value":"410311"},{"label":"孟津县","value":"410322"},{"label":"新安县","value":"410323"},{"label":"栾川县","value":"410324"},{"label":"嵩县","value":"410325"},{"label":"汝阳县","value":"410326"},{"label":"宜阳县","value":"410327"},{"label":"洛宁县","value":"410328"},{"label":"伊川县","value":"410329"},{"label":"洛阳高新技术产业开发区","value":"410371"},{"label":"偃师市","value":"410381"}],[{"label":"新华区","value":"410402"},{"label":"卫东区","value":"410403"},{"label":"石龙区","value":"410404"},{"label":"湛河区","value":"410411"},{"label":"宝丰县","value":"410421"},{"label":"叶县","value":"410422"},{"label":"鲁山县","value":"410423"},{"label":"郏县","value":"410425"},{"label":"平顶山高新技术产业开发区","value":"410471"},{"label":"平顶山市新城区","value":"410472"},{"label":"舞钢市","value":"410481"},{"label":"汝州市","value":"410482"}],[{"label":"文峰区","value":"410502"},{"label":"北关区","value":"410503"},{"label":"殷都区","value":"410505"},{"label":"龙安区","value":"410506"},{"label":"安阳县","value":"410522"},{"label":"汤阴县","value":"410523"},{"label":"滑县","value":"410526"},{"label":"内黄县","value":"410527"},{"label":"安阳高新技术产业开发区","value":"410571"},{"label":"林州市","value":"410581"}],[{"label":"鹤山区","value":"410602"},{"label":"山城区","value":"410603"},{"label":"淇滨区","value":"410611"},{"label":"浚县","value":"410621"},{"label":"淇县","value":"410622"},{"label":"鹤壁经济技术开发区","value":"410671"}],[{"label":"红旗区","value":"410702"},{"label":"卫滨区","value":"410703"},{"label":"凤泉区","value":"410704"},{"label":"牧野区","value":"410711"},{"label":"新乡县","value":"410721"},{"label":"获嘉县","value":"410724"},{"label":"原阳县","value":"410725"},{"label":"延津县","value":"410726"},{"label":"封丘县","value":"410727"},{"label":"长垣县","value":"410728"},{"label":"新乡高新技术产业开发区","value":"410771"},{"label":"新乡经济技术开发区","value":"410772"},{"label":"新乡市平原城乡一体化示范区","value":"410773"},{"label":"卫辉市","value":"410781"},{"label":"辉县市","value":"410782"}],[{"label":"解放区","value":"410802"},{"label":"中站区","value":"410803"},{"label":"马村区","value":"410804"},{"label":"山阳区","value":"410811"},{"label":"修武县","value":"410821"},{"label":"博爱县","value":"410822"},{"label":"武陟县","value":"410823"},{"label":"温县","value":"410825"},{"label":"焦作城乡一体化示范区","value":"410871"},{"label":"沁阳市","value":"410882"},{"label":"孟州市","value":"410883"}],[{"label":"华龙区","value":"410902"},{"label":"清丰县","value":"410922"},{"label":"南乐县","value":"410923"},{"label":"范县","value":"410926"},{"label":"台前县","value":"410927"},{"label":"濮阳县","value":"410928"},{"label":"河南濮阳工业园区","value":"410971"},{"label":"濮阳经济技术开发区","value":"410972"}],[{"label":"魏都区","value":"411002"},{"label":"建安区","value":"411003"},{"label":"鄢陵县","value":"411024"},{"label":"襄城县","value":"411025"},{"label":"许昌经济技术开发区","value":"411071"},{"label":"禹州市","value":"411081"},{"label":"长葛市","value":"411082"}],[{"label":"源汇区","value":"411102"},{"label":"郾城区","value":"411103"},{"label":"召陵区","value":"411104"},{"label":"舞阳县","value":"411121"},{"label":"临颍县","value":"411122"},{"label":"漯河经济技术开发区","value":"411171"}],[{"label":"湖滨区","value":"411202"},{"label":"陕州区","value":"411203"},{"label":"渑池县","value":"411221"},{"label":"卢氏县","value":"411224"},{"label":"河南三门峡经济开发区","value":"411271"},{"label":"义马市","value":"411281"},{"label":"灵宝市","value":"411282"}],[{"label":"宛城区","value":"411302"},{"label":"卧龙区","value":"411303"},{"label":"南召县","value":"411321"},{"label":"方城县","value":"411322"},{"label":"西峡县","value":"411323"},{"label":"镇平县","value":"411324"},{"label":"内乡县","value":"411325"},{"label":"淅川县","value":"411326"},{"label":"社旗县","value":"411327"},{"label":"唐河县","value":"411328"},{"label":"新野县","value":"411329"},{"label":"桐柏县","value":"411330"},{"label":"南阳高新技术产业开发区","value":"411371"},{"label":"南阳市城乡一体化示范区","value":"411372"},{"label":"邓州市","value":"411381"}],[{"label":"梁园区","value":"411402"},{"label":"睢阳区","value":"411403"},{"label":"民权县","value":"411421"},{"label":"睢县","value":"411422"},{"label":"宁陵县","value":"411423"},{"label":"柘城县","value":"411424"},{"label":"虞城县","value":"411425"},{"label":"夏邑县","value":"411426"},{"label":"豫东综合物流产业聚集区","value":"411471"},{"label":"河南商丘经济开发区","value":"411472"},{"label":"永城市","value":"411481"}],[{"label":"浉河区","value":"411502"},{"label":"平桥区","value":"411503"},{"label":"罗山县","value":"411521"},{"label":"光山县","value":"411522"},{"label":"新县","value":"411523"},{"label":"商城县","value":"411524"},{"label":"固始县","value":"411525"},{"label":"潢川县","value":"411526"},{"label":"淮滨县","value":"411527"},{"label":"息县","value":"411528"},{"label":"信阳高新技术产业开发区","value":"411571"}],[{"label":"川汇区","value":"411602"},{"label":"扶沟县","value":"411621"},{"label":"西华县","value":"411622"},{"label":"商水县","value":"411623"},{"label":"沈丘县","value":"411624"},{"label":"郸城县","value":"411625"},{"label":"淮阳县","value":"411626"},{"label":"太康县","value":"411627"},{"label":"鹿邑县","value":"411628"},{"label":"河南周口经济开发区","value":"411671"},{"label":"项城市","value":"411681"}],[{"label":"驿城区","value":"411702"},{"label":"西平县","value":"411721"},{"label":"上蔡县","value":"411722"},{"label":"平舆县","value":"411723"},{"label":"正阳县","value":"411724"},{"label":"确山县","value":"411725"},{"label":"泌阳县","value":"411726"},{"label":"汝南县","value":"411727"},{"label":"遂平县","value":"411728"},{"label":"新蔡县","value":"411729"},{"label":"河南驻马店经济开发区","value":"411771"}],[{"label":"济源市","value":"419001"}]],[[{"label":"江岸区","value":"420102"},{"label":"江汉区","value":"420103"},{"label":"硚口区","value":"420104"},{"label":"汉阳区","value":"420105"},{"label":"武昌区","value":"420106"},{"label":"青山区","value":"420107"},{"label":"洪山区","value":"420111"},{"label":"东西湖区","value":"420112"},{"label":"汉南区","value":"420113"},{"label":"蔡甸区","value":"420114"},{"label":"江夏区","value":"420115"},{"label":"黄陂区","value":"420116"},{"label":"新洲区","value":"420117"}],[{"label":"黄石港区","value":"420202"},{"label":"西塞山区","value":"420203"},{"label":"下陆区","value":"420204"},{"label":"铁山区","value":"420205"},{"label":"阳新县","value":"420222"},{"label":"大冶市","value":"420281"}],[{"label":"茅箭区","value":"420302"},{"label":"张湾区","value":"420303"},{"label":"郧阳区","value":"420304"},{"label":"郧西县","value":"420322"},{"label":"竹山县","value":"420323"},{"label":"竹溪县","value":"420324"},{"label":"房县","value":"420325"},{"label":"丹江口市","value":"420381"}],[{"label":"西陵区","value":"420502"},{"label":"伍家岗区","value":"420503"},{"label":"点军区","value":"420504"},{"label":"猇亭区","value":"420505"},{"label":"夷陵区","value":"420506"},{"label":"远安县","value":"420525"},{"label":"兴山县","value":"420526"},{"label":"秭归县","value":"420527"},{"label":"长阳土家族自治县","value":"420528"},{"label":"五峰土家族自治县","value":"420529"},{"label":"宜都市","value":"420581"},{"label":"当阳市","value":"420582"},{"label":"枝江市","value":"420583"}],[{"label":"襄城区","value":"420602"},{"label":"樊城区","value":"420606"},{"label":"襄州区","value":"420607"},{"label":"南漳县","value":"420624"},{"label":"谷城县","value":"420625"},{"label":"保康县","value":"420626"},{"label":"老河口市","value":"420682"},{"label":"枣阳市","value":"420683"},{"label":"宜城市","value":"420684"}],[{"label":"梁子湖区","value":"420702"},{"label":"华容区","value":"420703"},{"label":"鄂城区","value":"420704"}],[{"label":"东宝区","value":"420802"},{"label":"掇刀区","value":"420804"},{"label":"京山县","value":"420821"},{"label":"沙洋县","value":"420822"},{"label":"钟祥市","value":"420881"}],[{"label":"孝南区","value":"420902"},{"label":"孝昌县","value":"420921"},{"label":"大悟县","value":"420922"},{"label":"云梦县","value":"420923"},{"label":"应城市","value":"420981"},{"label":"安陆市","value":"420982"},{"label":"汉川市","value":"420984"}],[{"label":"沙市区","value":"421002"},{"label":"荆州区","value":"421003"},{"label":"公安县","value":"421022"},{"label":"监利县","value":"421023"},{"label":"江陵县","value":"421024"},{"label":"荆州经济技术开发区","value":"421071"},{"label":"石首市","value":"421081"},{"label":"洪湖市","value":"421083"},{"label":"松滋市","value":"421087"}],[{"label":"黄州区","value":"421102"},{"label":"团风县","value":"421121"},{"label":"红安县","value":"421122"},{"label":"罗田县","value":"421123"},{"label":"英山县","value":"421124"},{"label":"浠水县","value":"421125"},{"label":"蕲春县","value":"421126"},{"label":"黄梅县","value":"421127"},{"label":"龙感湖管理区","value":"421171"},{"label":"麻城市","value":"421181"},{"label":"武穴市","value":"421182"}],[{"label":"咸安区","value":"421202"},{"label":"嘉鱼县","value":"421221"},{"label":"通城县","value":"421222"},{"label":"崇阳县","value":"421223"},{"label":"通山县","value":"421224"},{"label":"赤壁市","value":"421281"}],[{"label":"曾都区","value":"421303"},{"label":"随县","value":"421321"},{"label":"广水市","value":"421381"}],[{"label":"恩施市","value":"422801"},{"label":"利川市","value":"422802"},{"label":"建始县","value":"422822"},{"label":"巴东县","value":"422823"},{"label":"宣恩县","value":"422825"},{"label":"咸丰县","value":"422826"},{"label":"来凤县","value":"422827"},{"label":"鹤峰县","value":"422828"}],[{"label":"仙桃市","value":"429004"},{"label":"潜江市","value":"429005"},{"label":"天门市","value":"429006"},{"label":"神农架林区","value":"429021"}]],[[{"label":"芙蓉区","value":"430102"},{"label":"天心区","value":"430103"},{"label":"岳麓区","value":"430104"},{"label":"开福区","value":"430105"},{"label":"雨花区","value":"430111"},{"label":"望城区","value":"430112"},{"label":"长沙县","value":"430121"},{"label":"浏阳市","value":"430181"},{"label":"宁乡市","value":"430182"}],[{"label":"荷塘区","value":"430202"},{"label":"芦淞区","value":"430203"},{"label":"石峰区","value":"430204"},{"label":"天元区","value":"430211"},{"label":"株洲县","value":"430221"},{"label":"攸县","value":"430223"},{"label":"茶陵县","value":"430224"},{"label":"炎陵县","value":"430225"},{"label":"云龙示范区","value":"430271"},{"label":"醴陵市","value":"430281"}],[{"label":"雨湖区","value":"430302"},{"label":"岳塘区","value":"430304"},{"label":"湘潭县","value":"430321"},{"label":"湖南湘潭高新技术产业园区","value":"430371"},{"label":"湘潭昭山示范区","value":"430372"},{"label":"湘潭九华示范区","value":"430373"},{"label":"湘乡市","value":"430381"},{"label":"韶山市","value":"430382"}],[{"label":"珠晖区","value":"430405"},{"label":"雁峰区","value":"430406"},{"label":"石鼓区","value":"430407"},{"label":"蒸湘区","value":"430408"},{"label":"南岳区","value":"430412"},{"label":"衡阳县","value":"430421"},{"label":"衡南县","value":"430422"},{"label":"衡山县","value":"430423"},{"label":"衡东县","value":"430424"},{"label":"祁东县","value":"430426"},{"label":"衡阳综合保税区","value":"430471"},{"label":"湖南衡阳高新技术产业园区","value":"430472"},{"label":"湖南衡阳松木经济开发区","value":"430473"},{"label":"耒阳市","value":"430481"},{"label":"常宁市","value":"430482"}],[{"label":"双清区","value":"430502"},{"label":"大祥区","value":"430503"},{"label":"北塔区","value":"430511"},{"label":"邵东县","value":"430521"},{"label":"新邵县","value":"430522"},{"label":"邵阳县","value":"430523"},{"label":"隆回县","value":"430524"},{"label":"洞口县","value":"430525"},{"label":"绥宁县","value":"430527"},{"label":"新宁县","value":"430528"},{"label":"城步苗族自治县","value":"430529"},{"label":"武冈市","value":"430581"}],[{"label":"岳阳楼区","value":"430602"},{"label":"云溪区","value":"430603"},{"label":"君山区","value":"430611"},{"label":"岳阳县","value":"430621"},{"label":"华容县","value":"430623"},{"label":"湘阴县","value":"430624"},{"label":"平江县","value":"430626"},{"label":"岳阳市屈原管理区","value":"430671"},{"label":"汨罗市","value":"430681"},{"label":"临湘市","value":"430682"}],[{"label":"武陵区","value":"430702"},{"label":"鼎城区","value":"430703"},{"label":"安乡县","value":"430721"},{"label":"汉寿县","value":"430722"},{"label":"澧县","value":"430723"},{"label":"临澧县","value":"430724"},{"label":"桃源县","value":"430725"},{"label":"石门县","value":"430726"},{"label":"常德市西洞庭管理区","value":"430771"},{"label":"津市市","value":"430781"}],[{"label":"永定区","value":"430802"},{"label":"武陵源区","value":"430811"},{"label":"慈利县","value":"430821"},{"label":"桑植县","value":"430822"}],[{"label":"资阳区","value":"430902"},{"label":"赫山区","value":"430903"},{"label":"南县","value":"430921"},{"label":"桃江县","value":"430922"},{"label":"安化县","value":"430923"},{"label":"益阳市大通湖管理区","value":"430971"},{"label":"湖南益阳高新技术产业园区","value":"430972"},{"label":"沅江市","value":"430981"}],[{"label":"北湖区","value":"431002"},{"label":"苏仙区","value":"431003"},{"label":"桂阳县","value":"431021"},{"label":"宜章县","value":"431022"},{"label":"永兴县","value":"431023"},{"label":"嘉禾县","value":"431024"},{"label":"临武县","value":"431025"},{"label":"汝城县","value":"431026"},{"label":"桂东县","value":"431027"},{"label":"安仁县","value":"431028"},{"label":"资兴市","value":"431081"}],[{"label":"零陵区","value":"431102"},{"label":"冷水滩区","value":"431103"},{"label":"祁阳县","value":"431121"},{"label":"东安县","value":"431122"},{"label":"双牌县","value":"431123"},{"label":"道县","value":"431124"},{"label":"江永县","value":"431125"},{"label":"宁远县","value":"431126"},{"label":"蓝山县","value":"431127"},{"label":"新田县","value":"431128"},{"label":"江华瑶族自治县","value":"431129"},{"label":"永州经济技术开发区","value":"431171"},{"label":"永州市金洞管理区","value":"431172"},{"label":"永州市回龙圩管理区","value":"431173"}],[{"label":"鹤城区","value":"431202"},{"label":"中方县","value":"431221"},{"label":"沅陵县","value":"431222"},{"label":"辰溪县","value":"431223"},{"label":"溆浦县","value":"431224"},{"label":"会同县","value":"431225"},{"label":"麻阳苗族自治县","value":"431226"},{"label":"新晃侗族自治县","value":"431227"},{"label":"芷江侗族自治县","value":"431228"},{"label":"靖州苗族侗族自治县","value":"431229"},{"label":"通道侗族自治县","value":"431230"},{"label":"怀化市洪江管理区","value":"431271"},{"label":"洪江市","value":"431281"}],[{"label":"娄星区","value":"431302"},{"label":"双峰县","value":"431321"},{"label":"新化县","value":"431322"},{"label":"冷水江市","value":"431381"},{"label":"涟源市","value":"431382"}],[{"label":"吉首市","value":"433101"},{"label":"泸溪县","value":"433122"},{"label":"凤凰县","value":"433123"},{"label":"花垣县","value":"433124"},{"label":"保靖县","value":"433125"},{"label":"古丈县","value":"433126"},{"label":"永顺县","value":"433127"},{"label":"龙山县","value":"433130"},{"label":"湖南吉首经济开发区","value":"433172"},{"label":"湖南永顺经济开发区","value":"433173"}]],[[{"label":"荔湾区","value":"440103"},{"label":"越秀区","value":"440104"},{"label":"海珠区","value":"440105"},{"label":"天河区","value":"440106"},{"label":"白云区","value":"440111"},{"label":"黄埔区","value":"440112"},{"label":"番禺区","value":"440113"},{"label":"花都区","value":"440114"},{"label":"南沙区","value":"440115"},{"label":"从化区","value":"440117"},{"label":"增城区","value":"440118"}],[{"label":"武江区","value":"440203"},{"label":"浈江区","value":"440204"},{"label":"曲江区","value":"440205"},{"label":"始兴县","value":"440222"},{"label":"仁化县","value":"440224"},{"label":"翁源县","value":"440229"},{"label":"乳源瑶族自治县","value":"440232"},{"label":"新丰县","value":"440233"},{"label":"乐昌市","value":"440281"},{"label":"南雄市","value":"440282"}],[{"label":"罗湖区","value":"440303"},{"label":"福田区","value":"440304"},{"label":"南山区","value":"440305"},{"label":"宝安区","value":"440306"},{"label":"龙岗区","value":"440307"},{"label":"盐田区","value":"440308"},{"label":"龙华区","value":"440309"},{"label":"坪山区","value":"440310"}],[{"label":"香洲区","value":"440402"},{"label":"斗门区","value":"440403"},{"label":"金湾区","value":"440404"}],[{"label":"龙湖区","value":"440507"},{"label":"金平区","value":"440511"},{"label":"濠江区","value":"440512"},{"label":"潮阳区","value":"440513"},{"label":"潮南区","value":"440514"},{"label":"澄海区","value":"440515"},{"label":"南澳县","value":"440523"}],[{"label":"禅城区","value":"440604"},{"label":"南海区","value":"440605"},{"label":"顺德区","value":"440606"},{"label":"三水区","value":"440607"},{"label":"高明区","value":"440608"}],[{"label":"蓬江区","value":"440703"},{"label":"江海区","value":"440704"},{"label":"新会区","value":"440705"},{"label":"台山市","value":"440781"},{"label":"开平市","value":"440783"},{"label":"鹤山市","value":"440784"},{"label":"恩平市","value":"440785"}],[{"label":"赤坎区","value":"440802"},{"label":"霞山区","value":"440803"},{"label":"坡头区","value":"440804"},{"label":"麻章区","value":"440811"},{"label":"遂溪县","value":"440823"},{"label":"徐闻县","value":"440825"},{"label":"廉江市","value":"440881"},{"label":"雷州市","value":"440882"},{"label":"吴川市","value":"440883"}],[{"label":"茂南区","value":"440902"},{"label":"电白区","value":"440904"},{"label":"高州市","value":"440981"},{"label":"化州市","value":"440982"},{"label":"信宜市","value":"440983"}],[{"label":"端州区","value":"441202"},{"label":"鼎湖区","value":"441203"},{"label":"高要区","value":"441204"},{"label":"广宁县","value":"441223"},{"label":"怀集县","value":"441224"},{"label":"封开县","value":"441225"},{"label":"德庆县","value":"441226"},{"label":"四会市","value":"441284"}],[{"label":"惠城区","value":"441302"},{"label":"惠阳区","value":"441303"},{"label":"博罗县","value":"441322"},{"label":"惠东县","value":"441323"},{"label":"龙门县","value":"441324"}],[{"label":"梅江区","value":"441402"},{"label":"梅县区","value":"441403"},{"label":"大埔县","value":"441422"},{"label":"丰顺县","value":"441423"},{"label":"五华县","value":"441424"},{"label":"平远县","value":"441426"},{"label":"蕉岭县","value":"441427"},{"label":"兴宁市","value":"441481"}],[{"label":"城区","value":"441502"},{"label":"海丰县","value":"441521"},{"label":"陆河县","value":"441523"},{"label":"陆丰市","value":"441581"}],[{"label":"源城区","value":"441602"},{"label":"紫金县","value":"441621"},{"label":"龙川县","value":"441622"},{"label":"连平县","value":"441623"},{"label":"和平县","value":"441624"},{"label":"东源县","value":"441625"}],[{"label":"江城区","value":"441702"},{"label":"阳东区","value":"441704"},{"label":"阳西县","value":"441721"},{"label":"阳春市","value":"441781"}],[{"label":"清城区","value":"441802"},{"label":"清新区","value":"441803"},{"label":"佛冈县","value":"441821"},{"label":"阳山县","value":"441823"},{"label":"连山壮族瑶族自治县","value":"441825"},{"label":"连南瑶族自治县","value":"441826"},{"label":"英德市","value":"441881"},{"label":"连州市","value":"441882"}],[{"label":"东莞市","value":"441900"}],[{"label":"中山市","value":"442000"}],[{"label":"湘桥区","value":"445102"},{"label":"潮安区","value":"445103"},{"label":"饶平县","value":"445122"}],[{"label":"榕城区","value":"445202"},{"label":"揭东区","value":"445203"},{"label":"揭西县","value":"445222"},{"label":"惠来县","value":"445224"},{"label":"普宁市","value":"445281"}],[{"label":"云城区","value":"445302"},{"label":"云安区","value":"445303"},{"label":"新兴县","value":"445321"},{"label":"郁南县","value":"445322"},{"label":"罗定市","value":"445381"}]],[[{"label":"兴宁区","value":"450102"},{"label":"青秀区","value":"450103"},{"label":"江南区","value":"450105"},{"label":"西乡塘区","value":"450107"},{"label":"良庆区","value":"450108"},{"label":"邕宁区","value":"450109"},{"label":"武鸣区","value":"450110"},{"label":"隆安县","value":"450123"},{"label":"马山县","value":"450124"},{"label":"上林县","value":"450125"},{"label":"宾阳县","value":"450126"},{"label":"横县","value":"450127"}],[{"label":"城中区","value":"450202"},{"label":"鱼峰区","value":"450203"},{"label":"柳南区","value":"450204"},{"label":"柳北区","value":"450205"},{"label":"柳江区","value":"450206"},{"label":"柳城县","value":"450222"},{"label":"鹿寨县","value":"450223"},{"label":"融安县","value":"450224"},{"label":"融水苗族自治县","value":"450225"},{"label":"三江侗族自治县","value":"450226"}],[{"label":"秀峰区","value":"450302"},{"label":"叠彩区","value":"450303"},{"label":"象山区","value":"450304"},{"label":"七星区","value":"450305"},{"label":"雁山区","value":"450311"},{"label":"临桂区","value":"450312"},{"label":"阳朔县","value":"450321"},{"label":"灵川县","value":"450323"},{"label":"全州县","value":"450324"},{"label":"兴安县","value":"450325"},{"label":"永福县","value":"450326"},{"label":"灌阳县","value":"450327"},{"label":"龙胜各族自治县","value":"450328"},{"label":"资源县","value":"450329"},{"label":"平乐县","value":"450330"},{"label":"荔浦县","value":"450331"},{"label":"恭城瑶族自治县","value":"450332"}],[{"label":"万秀区","value":"450403"},{"label":"长洲区","value":"450405"},{"label":"龙圩区","value":"450406"},{"label":"苍梧县","value":"450421"},{"label":"藤县","value":"450422"},{"label":"蒙山县","value":"450423"},{"label":"岑溪市","value":"450481"}],[{"label":"海城区","value":"450502"},{"label":"银海区","value":"450503"},{"label":"铁山港区","value":"450512"},{"label":"合浦县","value":"450521"}],[{"label":"港口区","value":"450602"},{"label":"防城区","value":"450603"},{"label":"上思县","value":"450621"},{"label":"东兴市","value":"450681"}],[{"label":"钦南区","value":"450702"},{"label":"钦北区","value":"450703"},{"label":"灵山县","value":"450721"},{"label":"浦北县","value":"450722"}],[{"label":"港北区","value":"450802"},{"label":"港南区","value":"450803"},{"label":"覃塘区","value":"450804"},{"label":"平南县","value":"450821"},{"label":"桂平市","value":"450881"}],[{"label":"玉州区","value":"450902"},{"label":"福绵区","value":"450903"},{"label":"容县","value":"450921"},{"label":"陆川县","value":"450922"},{"label":"博白县","value":"450923"},{"label":"兴业县","value":"450924"},{"label":"北流市","value":"450981"}],[{"label":"右江区","value":"451002"},{"label":"田阳县","value":"451021"},{"label":"田东县","value":"451022"},{"label":"平果县","value":"451023"},{"label":"德保县","value":"451024"},{"label":"那坡县","value":"451026"},{"label":"凌云县","value":"451027"},{"label":"乐业县","value":"451028"},{"label":"田林县","value":"451029"},{"label":"西林县","value":"451030"},{"label":"隆林各族自治县","value":"451031"},{"label":"靖西市","value":"451081"}],[{"label":"八步区","value":"451102"},{"label":"平桂区","value":"451103"},{"label":"昭平县","value":"451121"},{"label":"钟山县","value":"451122"},{"label":"富川瑶族自治县","value":"451123"}],[{"label":"金城江区","value":"451202"},{"label":"宜州区","value":"451203"},{"label":"南丹县","value":"451221"},{"label":"天峨县","value":"451222"},{"label":"凤山县","value":"451223"},{"label":"东兰县","value":"451224"},{"label":"罗城仫佬族自治县","value":"451225"},{"label":"环江毛南族自治县","value":"451226"},{"label":"巴马瑶族自治县","value":"451227"},{"label":"都安瑶族自治县","value":"451228"},{"label":"大化瑶族自治县","value":"451229"}],[{"label":"兴宾区","value":"451302"},{"label":"忻城县","value":"451321"},{"label":"象州县","value":"451322"},{"label":"武宣县","value":"451323"},{"label":"金秀瑶族自治县","value":"451324"},{"label":"合山市","value":"451381"}],[{"label":"江州区","value":"451402"},{"label":"扶绥县","value":"451421"},{"label":"宁明县","value":"451422"},{"label":"龙州县","value":"451423"},{"label":"大新县","value":"451424"},{"label":"天等县","value":"451425"},{"label":"凭祥市","value":"451481"}]],[[{"label":"秀英区","value":"460105"},{"label":"龙华区","value":"460106"},{"label":"琼山区","value":"460107"},{"label":"美兰区","value":"460108"}],[{"label":"海棠区","value":"460202"},{"label":"吉阳区","value":"460203"},{"label":"天涯区","value":"460204"},{"label":"崖州区","value":"460205"}],[{"label":"西沙群岛","value":"460321"},{"label":"南沙群岛","value":"460322"},{"label":"中沙群岛的岛礁及其海域","value":"460323"}],[{"label":"儋州市","value":"460400"}],[{"label":"五指山市","value":"469001"},{"label":"琼海市","value":"469002"},{"label":"文昌市","value":"469005"},{"label":"万宁市","value":"469006"},{"label":"东方市","value":"469007"},{"label":"定安县","value":"469021"},{"label":"屯昌县","value":"469022"},{"label":"澄迈县","value":"469023"},{"label":"临高县","value":"469024"},{"label":"白沙黎族自治县","value":"469025"},{"label":"昌江黎族自治县","value":"469026"},{"label":"乐东黎族自治县","value":"469027"},{"label":"陵水黎族自治县","value":"469028"},{"label":"保亭黎族苗族自治县","value":"469029"},{"label":"琼中黎族苗族自治县","value":"469030"}]],[[{"label":"万州区","value":"500101"},{"label":"涪陵区","value":"500102"},{"label":"渝中区","value":"500103"},{"label":"大渡口区","value":"500104"},{"label":"江北区","value":"500105"},{"label":"沙坪坝区","value":"500106"},{"label":"九龙坡区","value":"500107"},{"label":"南岸区","value":"500108"},{"label":"北碚区","value":"500109"},{"label":"綦江区","value":"500110"},{"label":"大足区","value":"500111"},{"label":"渝北区","value":"500112"},{"label":"巴南区","value":"500113"},{"label":"黔江区","value":"500114"},{"label":"长寿区","value":"500115"},{"label":"江津区","value":"500116"},{"label":"合川区","value":"500117"},{"label":"永川区","value":"500118"},{"label":"南川区","value":"500119"},{"label":"璧山区","value":"500120"},{"label":"铜梁区","value":"500151"},{"label":"潼南区","value":"500152"},{"label":"荣昌区","value":"500153"},{"label":"开州区","value":"500154"},{"label":"梁平区","value":"500155"},{"label":"武隆区","value":"500156"}],[{"label":"城口县","value":"500229"},{"label":"丰都县","value":"500230"},{"label":"垫江县","value":"500231"},{"label":"忠县","value":"500233"},{"label":"云阳县","value":"500235"},{"label":"奉节县","value":"500236"},{"label":"巫山县","value":"500237"},{"label":"巫溪县","value":"500238"},{"label":"石柱土家族自治县","value":"500240"},{"label":"秀山土家族苗族自治县","value":"500241"},{"label":"酉阳土家族苗族自治县","value":"500242"},{"label":"彭水苗族土家族自治县","value":"500243"}]],[[{"label":"锦江区","value":"510104"},{"label":"青羊区","value":"510105"},{"label":"金牛区","value":"510106"},{"label":"武侯区","value":"510107"},{"label":"成华区","value":"510108"},{"label":"龙泉驿区","value":"510112"},{"label":"青白江区","value":"510113"},{"label":"新都区","value":"510114"},{"label":"温江区","value":"510115"},{"label":"双流区","value":"510116"},{"label":"郫都区","value":"510117"},{"label":"金堂县","value":"510121"},{"label":"大邑县","value":"510129"},{"label":"蒲江县","value":"510131"},{"label":"新津县","value":"510132"},{"label":"都江堰市","value":"510181"},{"label":"彭州市","value":"510182"},{"label":"邛崃市","value":"510183"},{"label":"崇州市","value":"510184"},{"label":"简阳市","value":"510185"}],[{"label":"自流井区","value":"510302"},{"label":"贡井区","value":"510303"},{"label":"大安区","value":"510304"},{"label":"沿滩区","value":"510311"},{"label":"荣县","value":"510321"},{"label":"富顺县","value":"510322"}],[{"label":"东区","value":"510402"},{"label":"西区","value":"510403"},{"label":"仁和区","value":"510411"},{"label":"米易县","value":"510421"},{"label":"盐边县","value":"510422"}],[{"label":"江阳区","value":"510502"},{"label":"纳溪区","value":"510503"},{"label":"龙马潭区","value":"510504"},{"label":"泸县","value":"510521"},{"label":"合江县","value":"510522"},{"label":"叙永县","value":"510524"},{"label":"古蔺县","value":"510525"}],[{"label":"旌阳区","value":"510603"},{"label":"罗江区","value":"510604"},{"label":"中江县","value":"510623"},{"label":"广汉市","value":"510681"},{"label":"什邡市","value":"510682"},{"label":"绵竹市","value":"510683"}],[{"label":"涪城区","value":"510703"},{"label":"游仙区","value":"510704"},{"label":"安州区","value":"510705"},{"label":"三台县","value":"510722"},{"label":"盐亭县","value":"510723"},{"label":"梓潼县","value":"510725"},{"label":"北川羌族自治县","value":"510726"},{"label":"平武县","value":"510727"},{"label":"江油市","value":"510781"}],[{"label":"利州区","value":"510802"},{"label":"昭化区","value":"510811"},{"label":"朝天区","value":"510812"},{"label":"旺苍县","value":"510821"},{"label":"青川县","value":"510822"},{"label":"剑阁县","value":"510823"},{"label":"苍溪县","value":"510824"}],[{"label":"船山区","value":"510903"},{"label":"安居区","value":"510904"},{"label":"蓬溪县","value":"510921"},{"label":"射洪县","value":"510922"},{"label":"大英县","value":"510923"}],[{"label":"市中区","value":"511002"},{"label":"东兴区","value":"511011"},{"label":"威远县","value":"511024"},{"label":"资中县","value":"511025"},{"label":"内江经济开发区","value":"511071"},{"label":"隆昌市","value":"511083"}],[{"label":"市中区","value":"511102"},{"label":"沙湾区","value":"511111"},{"label":"五通桥区","value":"511112"},{"label":"金口河区","value":"511113"},{"label":"犍为县","value":"511123"},{"label":"井研县","value":"511124"},{"label":"夹江县","value":"511126"},{"label":"沐川县","value":"511129"},{"label":"峨边彝族自治县","value":"511132"},{"label":"马边彝族自治县","value":"511133"},{"label":"峨眉山市","value":"511181"}],[{"label":"顺庆区","value":"511302"},{"label":"高坪区","value":"511303"},{"label":"嘉陵区","value":"511304"},{"label":"南部县","value":"511321"},{"label":"营山县","value":"511322"},{"label":"蓬安县","value":"511323"},{"label":"仪陇县","value":"511324"},{"label":"西充县","value":"511325"},{"label":"阆中市","value":"511381"}],[{"label":"东坡区","value":"511402"},{"label":"彭山区","value":"511403"},{"label":"仁寿县","value":"511421"},{"label":"洪雅县","value":"511423"},{"label":"丹棱县","value":"511424"},{"label":"青神县","value":"511425"}],[{"label":"翠屏区","value":"511502"},{"label":"南溪区","value":"511503"},{"label":"宜宾县","value":"511521"},{"label":"江安县","value":"511523"},{"label":"长宁县","value":"511524"},{"label":"高县","value":"511525"},{"label":"珙县","value":"511526"},{"label":"筠连县","value":"511527"},{"label":"兴文县","value":"511528"},{"label":"屏山县","value":"511529"}],[{"label":"广安区","value":"511602"},{"label":"前锋区","value":"511603"},{"label":"岳池县","value":"511621"},{"label":"武胜县","value":"511622"},{"label":"邻水县","value":"511623"},{"label":"华蓥市","value":"511681"}],[{"label":"通川区","value":"511702"},{"label":"达川区","value":"511703"},{"label":"宣汉县","value":"511722"},{"label":"开江县","value":"511723"},{"label":"大竹县","value":"511724"},{"label":"渠县","value":"511725"},{"label":"达州经济开发区","value":"511771"},{"label":"万源市","value":"511781"}],[{"label":"雨城区","value":"511802"},{"label":"名山区","value":"511803"},{"label":"荥经县","value":"511822"},{"label":"汉源县","value":"511823"},{"label":"石棉县","value":"511824"},{"label":"天全县","value":"511825"},{"label":"芦山县","value":"511826"},{"label":"宝兴县","value":"511827"}],[{"label":"巴州区","value":"511902"},{"label":"恩阳区","value":"511903"},{"label":"通江县","value":"511921"},{"label":"南江县","value":"511922"},{"label":"平昌县","value":"511923"},{"label":"巴中经济开发区","value":"511971"}],[{"label":"雁江区","value":"512002"},{"label":"安岳县","value":"512021"},{"label":"乐至县","value":"512022"}],[{"label":"马尔康市","value":"513201"},{"label":"汶川县","value":"513221"},{"label":"理县","value":"513222"},{"label":"茂县","value":"513223"},{"label":"松潘县","value":"513224"},{"label":"九寨沟县","value":"513225"},{"label":"金川县","value":"513226"},{"label":"小金县","value":"513227"},{"label":"黑水县","value":"513228"},{"label":"壤塘县","value":"513230"},{"label":"阿坝县","value":"513231"},{"label":"若尔盖县","value":"513232"},{"label":"红原县","value":"513233"}],[{"label":"康定市","value":"513301"},{"label":"泸定县","value":"513322"},{"label":"丹巴县","value":"513323"},{"label":"九龙县","value":"513324"},{"label":"雅江县","value":"513325"},{"label":"道孚县","value":"513326"},{"label":"炉霍县","value":"513327"},{"label":"甘孜县","value":"513328"},{"label":"新龙县","value":"513329"},{"label":"德格县","value":"513330"},{"label":"白玉县","value":"513331"},{"label":"石渠县","value":"513332"},{"label":"色达县","value":"513333"},{"label":"理塘县","value":"513334"},{"label":"巴塘县","value":"513335"},{"label":"乡城县","value":"513336"},{"label":"稻城县","value":"513337"},{"label":"得荣县","value":"513338"}],[{"label":"西昌市","value":"513401"},{"label":"木里藏族自治县","value":"513422"},{"label":"盐源县","value":"513423"},{"label":"德昌县","value":"513424"},{"label":"会理县","value":"513425"},{"label":"会东县","value":"513426"},{"label":"宁南县","value":"513427"},{"label":"普格县","value":"513428"},{"label":"布拖县","value":"513429"},{"label":"金阳县","value":"513430"},{"label":"昭觉县","value":"513431"},{"label":"喜德县","value":"513432"},{"label":"冕宁县","value":"513433"},{"label":"越西县","value":"513434"},{"label":"甘洛县","value":"513435"},{"label":"美姑县","value":"513436"},{"label":"雷波县","value":"513437"}]],[[{"label":"南明区","value":"520102"},{"label":"云岩区","value":"520103"},{"label":"花溪区","value":"520111"},{"label":"乌当区","value":"520112"},{"label":"白云区","value":"520113"},{"label":"观山湖区","value":"520115"},{"label":"开阳县","value":"520121"},{"label":"息烽县","value":"520122"},{"label":"修文县","value":"520123"},{"label":"清镇市","value":"520181"}],[{"label":"钟山区","value":"520201"},{"label":"六枝特区","value":"520203"},{"label":"水城县","value":"520221"},{"label":"盘州市","value":"520281"}],[{"label":"红花岗区","value":"520302"},{"label":"汇川区","value":"520303"},{"label":"播州区","value":"520304"},{"label":"桐梓县","value":"520322"},{"label":"绥阳县","value":"520323"},{"label":"正安县","value":"520324"},{"label":"道真仡佬族苗族自治县","value":"520325"},{"label":"务川仡佬族苗族自治县","value":"520326"},{"label":"凤冈县","value":"520327"},{"label":"湄潭县","value":"520328"},{"label":"余庆县","value":"520329"},{"label":"习水县","value":"520330"},{"label":"赤水市","value":"520381"},{"label":"仁怀市","value":"520382"}],[{"label":"西秀区","value":"520402"},{"label":"平坝区","value":"520403"},{"label":"普定县","value":"520422"},{"label":"镇宁布依族苗族自治县","value":"520423"},{"label":"关岭布依族苗族自治县","value":"520424"},{"label":"紫云苗族布依族自治县","value":"520425"}],[{"label":"七星关区","value":"520502"},{"label":"大方县","value":"520521"},{"label":"黔西县","value":"520522"},{"label":"金沙县","value":"520523"},{"label":"织金县","value":"520524"},{"label":"纳雍县","value":"520525"},{"label":"威宁彝族回族苗族自治县","value":"520526"},{"label":"赫章县","value":"520527"}],[{"label":"碧江区","value":"520602"},{"label":"万山区","value":"520603"},{"label":"江口县","value":"520621"},{"label":"玉屏侗族自治县","value":"520622"},{"label":"石阡县","value":"520623"},{"label":"思南县","value":"520624"},{"label":"印江土家族苗族自治县","value":"520625"},{"label":"德江县","value":"520626"},{"label":"沿河土家族自治县","value":"520627"},{"label":"松桃苗族自治县","value":"520628"}],[{"label":"兴义市","value":"522301"},{"label":"兴仁县","value":"522322"},{"label":"普安县","value":"522323"},{"label":"晴隆县","value":"522324"},{"label":"贞丰县","value":"522325"},{"label":"望谟县","value":"522326"},{"label":"册亨县","value":"522327"},{"label":"安龙县","value":"522328"}],[{"label":"凯里市","value":"522601"},{"label":"黄平县","value":"522622"},{"label":"施秉县","value":"522623"},{"label":"三穗县","value":"522624"},{"label":"镇远县","value":"522625"},{"label":"岑巩县","value":"522626"},{"label":"天柱县","value":"522627"},{"label":"锦屏县","value":"522628"},{"label":"剑河县","value":"522629"},{"label":"台江县","value":"522630"},{"label":"黎平县","value":"522631"},{"label":"榕江县","value":"522632"},{"label":"从江县","value":"522633"},{"label":"雷山县","value":"522634"},{"label":"麻江县","value":"522635"},{"label":"丹寨县","value":"522636"}],[{"label":"都匀市","value":"522701"},{"label":"福泉市","value":"522702"},{"label":"荔波县","value":"522722"},{"label":"贵定县","value":"522723"},{"label":"瓮安县","value":"522725"},{"label":"独山县","value":"522726"},{"label":"平塘县","value":"522727"},{"label":"罗甸县","value":"522728"},{"label":"长顺县","value":"522729"},{"label":"龙里县","value":"522730"},{"label":"惠水县","value":"522731"},{"label":"三都水族自治县","value":"522732"}]],[[{"label":"五华区","value":"530102"},{"label":"盘龙区","value":"530103"},{"label":"官渡区","value":"530111"},{"label":"西山区","value":"530112"},{"label":"东川区","value":"530113"},{"label":"呈贡区","value":"530114"},{"label":"晋宁区","value":"530115"},{"label":"富民县","value":"530124"},{"label":"宜良县","value":"530125"},{"label":"石林彝族自治县","value":"530126"},{"label":"嵩明县","value":"530127"},{"label":"禄劝彝族苗族自治县","value":"530128"},{"label":"寻甸回族彝族自治县","value":"530129"},{"label":"安宁市","value":"530181"}],[{"label":"麒麟区","value":"530302"},{"label":"沾益区","value":"530303"},{"label":"马龙县","value":"530321"},{"label":"陆良县","value":"530322"},{"label":"师宗县","value":"530323"},{"label":"罗平县","value":"530324"},{"label":"富源县","value":"530325"},{"label":"会泽县","value":"530326"},{"label":"宣威市","value":"530381"}],[{"label":"红塔区","value":"530402"},{"label":"江川区","value":"530403"},{"label":"澄江县","value":"530422"},{"label":"通海县","value":"530423"},{"label":"华宁县","value":"530424"},{"label":"易门县","value":"530425"},{"label":"峨山彝族自治县","value":"530426"},{"label":"新平彝族傣族自治县","value":"530427"},{"label":"元江哈尼族彝族傣族自治县","value":"530428"}],[{"label":"隆阳区","value":"530502"},{"label":"施甸县","value":"530521"},{"label":"龙陵县","value":"530523"},{"label":"昌宁县","value":"530524"},{"label":"腾冲市","value":"530581"}],[{"label":"昭阳区","value":"530602"},{"label":"鲁甸县","value":"530621"},{"label":"巧家县","value":"530622"},{"label":"盐津县","value":"530623"},{"label":"大关县","value":"530624"},{"label":"永善县","value":"530625"},{"label":"绥江县","value":"530626"},{"label":"镇雄县","value":"530627"},{"label":"彝良县","value":"530628"},{"label":"威信县","value":"530629"},{"label":"水富县","value":"530630"}],[{"label":"古城区","value":"530702"},{"label":"玉龙纳西族自治县","value":"530721"},{"label":"永胜县","value":"530722"},{"label":"华坪县","value":"530723"},{"label":"宁蒗彝族自治县","value":"530724"}],[{"label":"思茅区","value":"530802"},{"label":"宁洱哈尼族彝族自治县","value":"530821"},{"label":"墨江哈尼族自治县","value":"530822"},{"label":"景东彝族自治县","value":"530823"},{"label":"景谷傣族彝族自治县","value":"530824"},{"label":"镇沅彝族哈尼族拉祜族自治县","value":"530825"},{"label":"江城哈尼族彝族自治县","value":"530826"},{"label":"孟连傣族拉祜族佤族自治县","value":"530827"},{"label":"澜沧拉祜族自治县","value":"530828"},{"label":"西盟佤族自治县","value":"530829"}],[{"label":"临翔区","value":"530902"},{"label":"凤庆县","value":"530921"},{"label":"云县","value":"530922"},{"label":"永德县","value":"530923"},{"label":"镇康县","value":"530924"},{"label":"双江拉祜族佤族布朗族傣族自治县","value":"530925"},{"label":"耿马傣族佤族自治县","value":"530926"},{"label":"沧源佤族自治县","value":"530927"}],[{"label":"楚雄市","value":"532301"},{"label":"双柏县","value":"532322"},{"label":"牟定县","value":"532323"},{"label":"南华县","value":"532324"},{"label":"姚安县","value":"532325"},{"label":"大姚县","value":"532326"},{"label":"永仁县","value":"532327"},{"label":"元谋县","value":"532328"},{"label":"武定县","value":"532329"},{"label":"禄丰县","value":"532331"}],[{"label":"个旧市","value":"532501"},{"label":"开远市","value":"532502"},{"label":"蒙自市","value":"532503"},{"label":"弥勒市","value":"532504"},{"label":"屏边苗族自治县","value":"532523"},{"label":"建水县","value":"532524"},{"label":"石屏县","value":"532525"},{"label":"泸西县","value":"532527"},{"label":"元阳县","value":"532528"},{"label":"红河县","value":"532529"},{"label":"金平苗族瑶族傣族自治县","value":"532530"},{"label":"绿春县","value":"532531"},{"label":"河口瑶族自治县","value":"532532"}],[{"label":"文山市","value":"532601"},{"label":"砚山县","value":"532622"},{"label":"西畴县","value":"532623"},{"label":"麻栗坡县","value":"532624"},{"label":"马关县","value":"532625"},{"label":"丘北县","value":"532626"},{"label":"广南县","value":"532627"},{"label":"富宁县","value":"532628"}],[{"label":"景洪市","value":"532801"},{"label":"勐海县","value":"532822"},{"label":"勐腊县","value":"532823"}],[{"label":"大理市","value":"532901"},{"label":"漾濞彝族自治县","value":"532922"},{"label":"祥云县","value":"532923"},{"label":"宾川县","value":"532924"},{"label":"弥渡县","value":"532925"},{"label":"南涧彝族自治县","value":"532926"},{"label":"巍山彝族回族自治县","value":"532927"},{"label":"永平县","value":"532928"},{"label":"云龙县","value":"532929"},{"label":"洱源县","value":"532930"},{"label":"剑川县","value":"532931"},{"label":"鹤庆县","value":"532932"}],[{"label":"瑞丽市","value":"533102"},{"label":"芒市","value":"533103"},{"label":"梁河县","value":"533122"},{"label":"盈江县","value":"533123"},{"label":"陇川县","value":"533124"}],[{"label":"泸水市","value":"533301"},{"label":"福贡县","value":"533323"},{"label":"贡山独龙族怒族自治县","value":"533324"},{"label":"兰坪白族普米族自治县","value":"533325"}],[{"label":"香格里拉市","value":"533401"},{"label":"德钦县","value":"533422"},{"label":"维西傈僳族自治县","value":"533423"}]],[[{"label":"城关区","value":"540102"},{"label":"堆龙德庆区","value":"540103"},{"label":"林周县","value":"540121"},{"label":"当雄县","value":"540122"},{"label":"尼木县","value":"540123"},{"label":"曲水县","value":"540124"},{"label":"达孜县","value":"540126"},{"label":"墨竹工卡县","value":"540127"},{"label":"格尔木藏青工业园区","value":"540171"},{"label":"拉萨经济技术开发区","value":"540172"},{"label":"西藏文化旅游创意园区","value":"540173"},{"label":"达孜工业园区","value":"540174"}],[{"label":"桑珠孜区","value":"540202"},{"label":"南木林县","value":"540221"},{"label":"江孜县","value":"540222"},{"label":"定日县","value":"540223"},{"label":"萨迦县","value":"540224"},{"label":"拉孜县","value":"540225"},{"label":"昂仁县","value":"540226"},{"label":"谢通门县","value":"540227"},{"label":"白朗县","value":"540228"},{"label":"仁布县","value":"540229"},{"label":"康马县","value":"540230"},{"label":"定结县","value":"540231"},{"label":"仲巴县","value":"540232"},{"label":"亚东县","value":"540233"},{"label":"吉隆县","value":"540234"},{"label":"聂拉木县","value":"540235"},{"label":"萨嘎县","value":"540236"},{"label":"岗巴县","value":"540237"}],[{"label":"卡若区","value":"540302"},{"label":"江达县","value":"540321"},{"label":"贡觉县","value":"540322"},{"label":"类乌齐县","value":"540323"},{"label":"丁青县","value":"540324"},{"label":"察雅县","value":"540325"},{"label":"八宿县","value":"540326"},{"label":"左贡县","value":"540327"},{"label":"芒康县","value":"540328"},{"label":"洛隆县","value":"540329"},{"label":"边坝县","value":"540330"}],[{"label":"巴宜区","value":"540402"},{"label":"工布江达县","value":"540421"},{"label":"米林县","value":"540422"},{"label":"墨脱县","value":"540423"},{"label":"波密县","value":"540424"},{"label":"察隅县","value":"540425"},{"label":"朗县","value":"540426"}],[{"label":"乃东区","value":"540502"},{"label":"扎囊县","value":"540521"},{"label":"贡嘎县","value":"540522"},{"label":"桑日县","value":"540523"},{"label":"琼结县","value":"540524"},{"label":"曲松县","value":"540525"},{"label":"措美县","value":"540526"},{"label":"洛扎县","value":"540527"},{"label":"加查县","value":"540528"},{"label":"隆子县","value":"540529"},{"label":"错那县","value":"540530"},{"label":"浪卡子县","value":"540531"}],[{"label":"那曲县","value":"542421"},{"label":"嘉黎县","value":"542422"},{"label":"比如县","value":"542423"},{"label":"聂荣县","value":"542424"},{"label":"安多县","value":"542425"},{"label":"申扎县","value":"542426"},{"label":"索县","value":"542427"},{"label":"班戈县","value":"542428"},{"label":"巴青县","value":"542429"},{"label":"尼玛县","value":"542430"},{"label":"双湖县","value":"542431"}],[{"label":"普兰县","value":"542521"},{"label":"札达县","value":"542522"},{"label":"噶尔县","value":"542523"},{"label":"日土县","value":"542524"},{"label":"革吉县","value":"542525"},{"label":"改则县","value":"542526"},{"label":"措勤县","value":"542527"}]],[[{"label":"新城区","value":"610102"},{"label":"碑林区","value":"610103"},{"label":"莲湖区","value":"610104"},{"label":"灞桥区","value":"610111"},{"label":"未央区","value":"610112"},{"label":"雁塔区","value":"610113"},{"label":"阎良区","value":"610114"},{"label":"临潼区","value":"610115"},{"label":"长安区","value":"610116"},{"label":"高陵区","value":"610117"},{"label":"鄠邑区","value":"610118"},{"label":"蓝田县","value":"610122"},{"label":"周至县","value":"610124"}],[{"label":"王益区","value":"610202"},{"label":"印台区","value":"610203"},{"label":"耀州区","value":"610204"},{"label":"宜君县","value":"610222"}],[{"label":"渭滨区","value":"610302"},{"label":"金台区","value":"610303"},{"label":"陈仓区","value":"610304"},{"label":"凤翔县","value":"610322"},{"label":"岐山县","value":"610323"},{"label":"扶风县","value":"610324"},{"label":"眉县","value":"610326"},{"label":"陇县","value":"610327"},{"label":"千阳县","value":"610328"},{"label":"麟游县","value":"610329"},{"label":"凤县","value":"610330"},{"label":"太白县","value":"610331"}],[{"label":"秦都区","value":"610402"},{"label":"杨陵区","value":"610403"},{"label":"渭城区","value":"610404"},{"label":"三原县","value":"610422"},{"label":"泾阳县","value":"610423"},{"label":"乾县","value":"610424"},{"label":"礼泉县","value":"610425"},{"label":"永寿县","value":"610426"},{"label":"彬县","value":"610427"},{"label":"长武县","value":"610428"},{"label":"旬邑县","value":"610429"},{"label":"淳化县","value":"610430"},{"label":"武功县","value":"610431"},{"label":"兴平市","value":"610481"}],[{"label":"临渭区","value":"610502"},{"label":"华州区","value":"610503"},{"label":"潼关县","value":"610522"},{"label":"大荔县","value":"610523"},{"label":"合阳县","value":"610524"},{"label":"澄城县","value":"610525"},{"label":"蒲城县","value":"610526"},{"label":"白水县","value":"610527"},{"label":"富平县","value":"610528"},{"label":"韩城市","value":"610581"},{"label":"华阴市","value":"610582"}],[{"label":"宝塔区","value":"610602"},{"label":"安塞区","value":"610603"},{"label":"延长县","value":"610621"},{"label":"延川县","value":"610622"},{"label":"子长县","value":"610623"},{"label":"志丹县","value":"610625"},{"label":"吴起县","value":"610626"},{"label":"甘泉县","value":"610627"},{"label":"富县","value":"610628"},{"label":"洛川县","value":"610629"},{"label":"宜川县","value":"610630"},{"label":"黄龙县","value":"610631"},{"label":"黄陵县","value":"610632"}],[{"label":"汉台区","value":"610702"},{"label":"南郑区","value":"610703"},{"label":"城固县","value":"610722"},{"label":"洋县","value":"610723"},{"label":"西乡县","value":"610724"},{"label":"勉县","value":"610725"},{"label":"宁强县","value":"610726"},{"label":"略阳县","value":"610727"},{"label":"镇巴县","value":"610728"},{"label":"留坝县","value":"610729"},{"label":"佛坪县","value":"610730"}],[{"label":"榆阳区","value":"610802"},{"label":"横山区","value":"610803"},{"label":"府谷县","value":"610822"},{"label":"靖边县","value":"610824"},{"label":"定边县","value":"610825"},{"label":"绥德县","value":"610826"},{"label":"米脂县","value":"610827"},{"label":"佳县","value":"610828"},{"label":"吴堡县","value":"610829"},{"label":"清涧县","value":"610830"},{"label":"子洲县","value":"610831"},{"label":"神木市","value":"610881"}],[{"label":"汉滨区","value":"610902"},{"label":"汉阴县","value":"610921"},{"label":"石泉县","value":"610922"},{"label":"宁陕县","value":"610923"},{"label":"紫阳县","value":"610924"},{"label":"岚皋县","value":"610925"},{"label":"平利县","value":"610926"},{"label":"镇坪县","value":"610927"},{"label":"旬阳县","value":"610928"},{"label":"白河县","value":"610929"}],[{"label":"商州区","value":"611002"},{"label":"洛南县","value":"611021"},{"label":"丹凤县","value":"611022"},{"label":"商南县","value":"611023"},{"label":"山阳县","value":"611024"},{"label":"镇安县","value":"611025"},{"label":"柞水县","value":"611026"}]],[[{"label":"城关区","value":"620102"},{"label":"七里河区","value":"620103"},{"label":"西固区","value":"620104"},{"label":"安宁区","value":"620105"},{"label":"红古区","value":"620111"},{"label":"永登县","value":"620121"},{"label":"皋兰县","value":"620122"},{"label":"榆中县","value":"620123"},{"label":"兰州新区","value":"620171"}],[{"label":"嘉峪关市","value":"620201"}],[{"label":"金川区","value":"620302"},{"label":"永昌县","value":"620321"}],[{"label":"白银区","value":"620402"},{"label":"平川区","value":"620403"},{"label":"靖远县","value":"620421"},{"label":"会宁县","value":"620422"},{"label":"景泰县","value":"620423"}],[{"label":"秦州区","value":"620502"},{"label":"麦积区","value":"620503"},{"label":"清水县","value":"620521"},{"label":"秦安县","value":"620522"},{"label":"甘谷县","value":"620523"},{"label":"武山县","value":"620524"},{"label":"张家川回族自治县","value":"620525"}],[{"label":"凉州区","value":"620602"},{"label":"民勤县","value":"620621"},{"label":"古浪县","value":"620622"},{"label":"天祝藏族自治县","value":"620623"}],[{"label":"甘州区","value":"620702"},{"label":"肃南裕固族自治县","value":"620721"},{"label":"民乐县","value":"620722"},{"label":"临泽县","value":"620723"},{"label":"高台县","value":"620724"},{"label":"山丹县","value":"620725"}],[{"label":"崆峒区","value":"620802"},{"label":"泾川县","value":"620821"},{"label":"灵台县","value":"620822"},{"label":"崇信县","value":"620823"},{"label":"华亭县","value":"620824"},{"label":"庄浪县","value":"620825"},{"label":"静宁县","value":"620826"},{"label":"平凉工业园区","value":"620871"}],[{"label":"肃州区","value":"620902"},{"label":"金塔县","value":"620921"},{"label":"瓜州县","value":"620922"},{"label":"肃北蒙古族自治县","value":"620923"},{"label":"阿克塞哈萨克族自治县","value":"620924"},{"label":"玉门市","value":"620981"},{"label":"敦煌市","value":"620982"}],[{"label":"西峰区","value":"621002"},{"label":"庆城县","value":"621021"},{"label":"环县","value":"621022"},{"label":"华池县","value":"621023"},{"label":"合水县","value":"621024"},{"label":"正宁县","value":"621025"},{"label":"宁县","value":"621026"},{"label":"镇原县","value":"621027"}],[{"label":"安定区","value":"621102"},{"label":"通渭县","value":"621121"},{"label":"陇西县","value":"621122"},{"label":"渭源县","value":"621123"},{"label":"临洮县","value":"621124"},{"label":"漳县","value":"621125"},{"label":"岷县","value":"621126"}],[{"label":"武都区","value":"621202"},{"label":"成县","value":"621221"},{"label":"文县","value":"621222"},{"label":"宕昌县","value":"621223"},{"label":"康县","value":"621224"},{"label":"西和县","value":"621225"},{"label":"礼县","value":"621226"},{"label":"徽县","value":"621227"},{"label":"两当县","value":"621228"}],[{"label":"临夏市","value":"622901"},{"label":"临夏县","value":"622921"},{"label":"康乐县","value":"622922"},{"label":"永靖县","value":"622923"},{"label":"广河县","value":"622924"},{"label":"和政县","value":"622925"},{"label":"东乡族自治县","value":"622926"},{"label":"积石山保安族东乡族撒拉族自治县","value":"622927"}],[{"label":"合作市","value":"623001"},{"label":"临潭县","value":"623021"},{"label":"卓尼县","value":"623022"},{"label":"舟曲县","value":"623023"},{"label":"迭部县","value":"623024"},{"label":"玛曲县","value":"623025"},{"label":"碌曲县","value":"623026"},{"label":"夏河县","value":"623027"}]],[[{"label":"城东区","value":"630102"},{"label":"城中区","value":"630103"},{"label":"城西区","value":"630104"},{"label":"城北区","value":"630105"},{"label":"大通回族土族自治县","value":"630121"},{"label":"湟中县","value":"630122"},{"label":"湟源县","value":"630123"}],[{"label":"乐都区","value":"630202"},{"label":"平安区","value":"630203"},{"label":"民和回族土族自治县","value":"630222"},{"label":"互助土族自治县","value":"630223"},{"label":"化隆回族自治县","value":"630224"},{"label":"循化撒拉族自治县","value":"630225"}],[{"label":"门源回族自治县","value":"632221"},{"label":"祁连县","value":"632222"},{"label":"海晏县","value":"632223"},{"label":"刚察县","value":"632224"}],[{"label":"同仁县","value":"632321"},{"label":"尖扎县","value":"632322"},{"label":"泽库县","value":"632323"},{"label":"河南蒙古族自治县","value":"632324"}],[{"label":"共和县","value":"632521"},{"label":"同德县","value":"632522"},{"label":"贵德县","value":"632523"},{"label":"兴海县","value":"632524"},{"label":"贵南县","value":"632525"}],[{"label":"玛沁县","value":"632621"},{"label":"班玛县","value":"632622"},{"label":"甘德县","value":"632623"},{"label":"达日县","value":"632624"},{"label":"久治县","value":"632625"},{"label":"玛多县","value":"632626"}],[{"label":"玉树市","value":"632701"},{"label":"杂多县","value":"632722"},{"label":"称多县","value":"632723"},{"label":"治多县","value":"632724"},{"label":"囊谦县","value":"632725"},{"label":"曲麻莱县","value":"632726"}],[{"label":"格尔木市","value":"632801"},{"label":"德令哈市","value":"632802"},{"label":"乌兰县","value":"632821"},{"label":"都兰县","value":"632822"},{"label":"天峻县","value":"632823"},{"label":"大柴旦行政委员会","value":"632857"},{"label":"冷湖行政委员会","value":"632858"},{"label":"茫崖行政委员会","value":"632859"}]],[[{"label":"兴庆区","value":"640104"},{"label":"西夏区","value":"640105"},{"label":"金凤区","value":"640106"},{"label":"永宁县","value":"640121"},{"label":"贺兰县","value":"640122"},{"label":"灵武市","value":"640181"}],[{"label":"大武口区","value":"640202"},{"label":"惠农区","value":"640205"},{"label":"平罗县","value":"640221"}],[{"label":"利通区","value":"640302"},{"label":"红寺堡区","value":"640303"},{"label":"盐池县","value":"640323"},{"label":"同心县","value":"640324"},{"label":"青铜峡市","value":"640381"}],[{"label":"原州区","value":"640402"},{"label":"西吉县","value":"640422"},{"label":"隆德县","value":"640423"},{"label":"泾源县","value":"640424"},{"label":"彭阳县","value":"640425"}],[{"label":"沙坡头区","value":"640502"},{"label":"中宁县","value":"640521"},{"label":"海原县","value":"640522"}]],[[{"label":"天山区","value":"650102"},{"label":"沙依巴克区","value":"650103"},{"label":"新市区","value":"650104"},{"label":"水磨沟区","value":"650105"},{"label":"头屯河区","value":"650106"},{"label":"达坂城区","value":"650107"},{"label":"米东区","value":"650109"},{"label":"乌鲁木齐县","value":"650121"},{"label":"乌鲁木齐经济技术开发区","value":"650171"},{"label":"乌鲁木齐高新技术产业开发区","value":"650172"}],[{"label":"独山子区","value":"650202"},{"label":"克拉玛依区","value":"650203"},{"label":"白碱滩区","value":"650204"},{"label":"乌尔禾区","value":"650205"}],[{"label":"高昌区","value":"650402"},{"label":"鄯善县","value":"650421"},{"label":"托克逊县","value":"650422"}],[{"label":"伊州区","value":"650502"},{"label":"巴里坤哈萨克自治县","value":"650521"},{"label":"伊吾县","value":"650522"}],[{"label":"昌吉市","value":"652301"},{"label":"阜康市","value":"652302"},{"label":"呼图壁县","value":"652323"},{"label":"玛纳斯县","value":"652324"},{"label":"奇台县","value":"652325"},{"label":"吉木萨尔县","value":"652327"},{"label":"木垒哈萨克自治县","value":"652328"}],[{"label":"博乐市","value":"652701"},{"label":"阿拉山口市","value":"652702"},{"label":"精河县","value":"652722"},{"label":"温泉县","value":"652723"}],[{"label":"库尔勒市","value":"652801"},{"label":"轮台县","value":"652822"},{"label":"尉犁县","value":"652823"},{"label":"若羌县","value":"652824"},{"label":"且末县","value":"652825"},{"label":"焉耆回族自治县","value":"652826"},{"label":"和静县","value":"652827"},{"label":"和硕县","value":"652828"},{"label":"博湖县","value":"652829"},{"label":"库尔勒经济技术开发区","value":"652871"}],[{"label":"阿克苏市","value":"652901"},{"label":"温宿县","value":"652922"},{"label":"库车县","value":"652923"},{"label":"沙雅县","value":"652924"},{"label":"新和县","value":"652925"},{"label":"拜城县","value":"652926"},{"label":"乌什县","value":"652927"},{"label":"阿瓦提县","value":"652928"},{"label":"柯坪县","value":"652929"}],[{"label":"阿图什市","value":"653001"},{"label":"阿克陶县","value":"653022"},{"label":"阿合奇县","value":"653023"},{"label":"乌恰县","value":"653024"}],[{"label":"喀什市","value":"653101"},{"label":"疏附县","value":"653121"},{"label":"疏勒县","value":"653122"},{"label":"英吉沙县","value":"653123"},{"label":"泽普县","value":"653124"},{"label":"莎车县","value":"653125"},{"label":"叶城县","value":"653126"},{"label":"麦盖提县","value":"653127"},{"label":"岳普湖县","value":"653128"},{"label":"伽师县","value":"653129"},{"label":"巴楚县","value":"653130"},{"label":"塔什库尔干塔吉克自治县","value":"653131"}],[{"label":"和田市","value":"653201"},{"label":"和田县","value":"653221"},{"label":"墨玉县","value":"653222"},{"label":"皮山县","value":"653223"},{"label":"洛浦县","value":"653224"},{"label":"策勒县","value":"653225"},{"label":"于田县","value":"653226"},{"label":"民丰县","value":"653227"}],[{"label":"伊宁市","value":"654002"},{"label":"奎屯市","value":"654003"},{"label":"霍尔果斯市","value":"654004"},{"label":"伊宁县","value":"654021"},{"label":"察布查尔锡伯自治县","value":"654022"},{"label":"霍城县","value":"654023"},{"label":"巩留县","value":"654024"},{"label":"新源县","value":"654025"},{"label":"昭苏县","value":"654026"},{"label":"特克斯县","value":"654027"},{"label":"尼勒克县","value":"654028"}],[{"label":"塔城市","value":"654201"},{"label":"乌苏市","value":"654202"},{"label":"额敏县","value":"654221"},{"label":"沙湾县","value":"654223"},{"label":"托里县","value":"654224"},{"label":"裕民县","value":"654225"},{"label":"和布克赛尔蒙古自治县","value":"654226"}],[{"label":"阿勒泰市","value":"654301"},{"label":"布尔津县","value":"654321"},{"label":"富蕴县","value":"654322"},{"label":"福海县","value":"654323"},{"label":"哈巴河县","value":"654324"},{"label":"青河县","value":"654325"},{"label":"吉木乃县","value":"654326"}],[{"label":"石河子市","value":"659001"},{"label":"阿拉尔市","value":"659002"},{"label":"图木舒克市","value":"659003"},{"label":"五家渠市","value":"659004"},{"label":"铁门关市","value":"659006"}]],[[{"label":"台北","value":"660101"}],[{"label":"高雄","value":"660201"}],[{"label":"基隆","value":"660301"}],[{"label":"台中","value":"660401"}],[{"label":"台南","value":"660501"}],[{"label":"新竹","value":"660601"}],[{"label":"嘉义","value":"660701"}],[{"label":"宜兰","value":"660801"}],[{"label":"桃园","value":"660901"}],[{"label":"苗栗","value":"661001"}],[{"label":"彰化","value":"661101"}],[{"label":"南投","value":"661201"}],[{"label":"云林","value":"661301"}],[{"label":"屏东","value":"661401"}],[{"label":"台东","value":"661501"}],[{"label":"花莲","value":"661601"}],[{"label":"澎湖","value":"661701"}]],[[{"label":"香港岛","value":"670101"}],[{"label":"九龙","value":"670201"}],[{"label":"新界","value":"670301"}]],[[{"label":"澳门半岛","value":"680101"}],[{"label":"氹仔岛","value":"680201"}],[{"label":"路环岛","value":"680301"}],[{"label":"路氹城","value":"680401"}]]];export default areaData;
\ No newline at end of file
diff --git a/uview-ui/libs/util/async-validator.js b/uview-ui/libs/util/async-validator.js
new file mode 100644
index 0000000..d7215b9
--- /dev/null
+++ b/uview-ui/libs/util/async-validator.js
@@ -0,0 +1,1356 @@
+function _extends() {
+ _extends = Object.assign || function(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ for (var key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ return target;
+ };
+
+ return _extends.apply(this, arguments);
+}
+
+/* eslint no-console:0 */
+var formatRegExp = /%[sdj%]/g;
+var warning = function warning() {}; // don't print warning message when in production env or node runtime
+
+if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production' && typeof window !==
+ 'undefined' && typeof document !== 'undefined') {
+ warning = function warning(type, errors) {
+ if (typeof console !== 'undefined' && console.warn) {
+ if (errors.every(function(e) {
+ return typeof e === 'string';
+ })) {
+ console.warn(type, errors);
+ }
+ }
+ };
+}
+
+function convertFieldsError(errors) {
+ if (!errors || !errors.length) return null;
+ var fields = {};
+ errors.forEach(function(error) {
+ var field = error.field;
+ fields[field] = fields[field] || [];
+ fields[field].push(error);
+ });
+ return fields;
+}
+
+function format() {
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ var i = 1;
+ var f = args[0];
+ var len = args.length;
+
+ if (typeof f === 'function') {
+ return f.apply(null, args.slice(1));
+ }
+
+ if (typeof f === 'string') {
+ var str = String(f).replace(formatRegExp, function(x) {
+ if (x === '%%') {
+ return '%';
+ }
+
+ if (i >= len) {
+ return x;
+ }
+
+ switch (x) {
+ case '%s':
+ return String(args[i++]);
+
+ case '%d':
+ return Number(args[i++]);
+
+ case '%j':
+ try {
+ return JSON.stringify(args[i++]);
+ } catch (_) {
+ return '[Circular]';
+ }
+
+ break;
+
+ default:
+ return x;
+ }
+ });
+
+ for (var arg = args[i]; i < len; arg = args[++i]) {
+ str += " " + arg;
+ }
+
+ return str;
+ }
+
+ return f;
+}
+
+function isNativeStringType(type) {
+ return type === 'string' || type === 'url' || type === 'hex' || type === 'email' || type === 'pattern';
+}
+
+function isEmptyValue(value, type) {
+ if (value === undefined || value === null) {
+ return true;
+ }
+
+ if (type === 'array' && Array.isArray(value) && !value.length) {
+ return true;
+ }
+
+ if (isNativeStringType(type) && typeof value === 'string' && !value) {
+ return true;
+ }
+
+ return false;
+}
+
+function asyncParallelArray(arr, func, callback) {
+ var results = [];
+ var total = 0;
+ var arrLength = arr.length;
+
+ function count(errors) {
+ results.push.apply(results, errors);
+ total++;
+
+ if (total === arrLength) {
+ callback(results);
+ }
+ }
+
+ arr.forEach(function(a) {
+ func(a, count);
+ });
+}
+
+function asyncSerialArray(arr, func, callback) {
+ var index = 0;
+ var arrLength = arr.length;
+
+ function next(errors) {
+ if (errors && errors.length) {
+ callback(errors);
+ return;
+ }
+
+ var original = index;
+ index = index + 1;
+
+ if (original < arrLength) {
+ func(arr[original], next);
+ } else {
+ callback([]);
+ }
+ }
+
+ next([]);
+}
+
+function flattenObjArr(objArr) {
+ var ret = [];
+ Object.keys(objArr).forEach(function(k) {
+ ret.push.apply(ret, objArr[k]);
+ });
+ return ret;
+}
+
+function asyncMap(objArr, option, func, callback) {
+ if (option.first) {
+ var _pending = new Promise(function(resolve, reject) {
+ var next = function next(errors) {
+ callback(errors);
+ return errors.length ? reject({
+ errors: errors,
+ fields: convertFieldsError(errors)
+ }) : resolve();
+ };
+
+ var flattenArr = flattenObjArr(objArr);
+ asyncSerialArray(flattenArr, func, next);
+ });
+
+ _pending["catch"](function(e) {
+ return e;
+ });
+
+ return _pending;
+ }
+
+ var firstFields = option.firstFields || [];
+
+ if (firstFields === true) {
+ firstFields = Object.keys(objArr);
+ }
+
+ var objArrKeys = Object.keys(objArr);
+ var objArrLength = objArrKeys.length;
+ var total = 0;
+ var results = [];
+ var pending = new Promise(function(resolve, reject) {
+ var next = function next(errors) {
+ results.push.apply(results, errors);
+ total++;
+
+ if (total === objArrLength) {
+ callback(results);
+ return results.length ? reject({
+ errors: results,
+ fields: convertFieldsError(results)
+ }) : resolve();
+ }
+ };
+
+ if (!objArrKeys.length) {
+ callback(results);
+ resolve();
+ }
+
+ objArrKeys.forEach(function(key) {
+ var arr = objArr[key];
+
+ if (firstFields.indexOf(key) !== -1) {
+ asyncSerialArray(arr, func, next);
+ } else {
+ asyncParallelArray(arr, func, next);
+ }
+ });
+ });
+ pending["catch"](function(e) {
+ return e;
+ });
+ return pending;
+}
+
+function complementError(rule) {
+ return function(oe) {
+ if (oe && oe.message) {
+ oe.field = oe.field || rule.fullField;
+ return oe;
+ }
+
+ return {
+ message: typeof oe === 'function' ? oe() : oe,
+ field: oe.field || rule.fullField
+ };
+ };
+}
+
+function deepMerge(target, source) {
+ if (source) {
+ for (var s in source) {
+ if (source.hasOwnProperty(s)) {
+ var value = source[s];
+
+ if (typeof value === 'object' && typeof target[s] === 'object') {
+ target[s] = _extends({}, target[s], {}, value);
+ } else {
+ target[s] = value;
+ }
+ }
+ }
+ }
+
+ return target;
+}
+
+/**
+ * Rule for validating required fields.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function required(rule, value, source, errors, options, type) {
+ if (rule.required && (!source.hasOwnProperty(rule.field) || isEmptyValue(value, type || rule.type))) {
+ errors.push(format(options.messages.required, rule.fullField));
+ }
+}
+
+/**
+ * Rule for validating whitespace.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function whitespace(rule, value, source, errors, options) {
+ if (/^\s+$/.test(value) || value === '') {
+ errors.push(format(options.messages.whitespace, rule.fullField));
+ }
+}
+
+/* eslint max-len:0 */
+
+var pattern = {
+ // http://emailregex.com/
+ email: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ url: new RegExp(
+ "^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$",
+ 'i'),
+ hex: /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i
+};
+var types = {
+ integer: function integer(value) {
+ return types.number(value) && parseInt(value, 10) === value;
+ },
+ "float": function float(value) {
+ return types.number(value) && !types.integer(value);
+ },
+ array: function array(value) {
+ return Array.isArray(value);
+ },
+ regexp: function regexp(value) {
+ if (value instanceof RegExp) {
+ return true;
+ }
+
+ try {
+ return !!new RegExp(value);
+ } catch (e) {
+ return false;
+ }
+ },
+ date: function date(value) {
+ return typeof value.getTime === 'function' && typeof value.getMonth === 'function' && typeof value.getYear ===
+ 'function';
+ },
+ number: function number(value) {
+ if (isNaN(value)) {
+ return false;
+ }
+
+ // 修改源码,将字符串数值先转为数值
+ return typeof +value === 'number';
+ },
+ object: function object(value) {
+ return typeof value === 'object' && !types.array(value);
+ },
+ method: function method(value) {
+ return typeof value === 'function';
+ },
+ email: function email(value) {
+ return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255;
+ },
+ url: function url(value) {
+ return typeof value === 'string' && !!value.match(pattern.url);
+ },
+ hex: function hex(value) {
+ return typeof value === 'string' && !!value.match(pattern.hex);
+ }
+};
+/**
+ * Rule for validating the type of a value.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function type(rule, value, source, errors, options) {
+ if (rule.required && value === undefined) {
+ required(rule, value, source, errors, options);
+ return;
+ }
+
+ var custom = ['integer', 'float', 'array', 'regexp', 'object', 'method', 'email', 'number', 'date', 'url', 'hex'];
+ var ruleType = rule.type;
+
+ if (custom.indexOf(ruleType) > -1) {
+ if (!types[ruleType](value)) {
+ errors.push(format(options.messages.types[ruleType], rule.fullField, rule.type));
+ } // straight typeof check
+
+ } else if (ruleType && typeof value !== rule.type) {
+ errors.push(format(options.messages.types[ruleType], rule.fullField, rule.type));
+ }
+}
+
+/**
+ * Rule for validating minimum and maximum allowed values.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function range(rule, value, source, errors, options) {
+ var len = typeof rule.len === 'number';
+ var min = typeof rule.min === 'number';
+ var max = typeof rule.max === 'number'; // 正则匹配码点范围从U+010000一直到U+10FFFF的文字(补充平面Supplementary Plane)
+
+ var spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
+ var val = value;
+ var key = null;
+ var num = typeof value === 'number';
+ var str = typeof value === 'string';
+ var arr = Array.isArray(value);
+
+ if (num) {
+ key = 'number';
+ } else if (str) {
+ key = 'string';
+ } else if (arr) {
+ key = 'array';
+ } // if the value is not of a supported type for range validation
+ // the validation rule rule should use the
+ // type property to also test for a particular type
+
+
+ if (!key) {
+ return false;
+ }
+
+ if (arr) {
+ val = value.length;
+ }
+
+ if (str) {
+ // 处理码点大于U+010000的文字length属性不准确的bug,如"𠮷𠮷𠮷".lenght !== 3
+ val = value.replace(spRegexp, '_').length;
+ }
+
+ if (len) {
+ if (val !== rule.len) {
+ errors.push(format(options.messages[key].len, rule.fullField, rule.len));
+ }
+ } else if (min && !max && val < rule.min) {
+ errors.push(format(options.messages[key].min, rule.fullField, rule.min));
+ } else if (max && !min && val > rule.max) {
+ errors.push(format(options.messages[key].max, rule.fullField, rule.max));
+ } else if (min && max && (val < rule.min || val > rule.max)) {
+ errors.push(format(options.messages[key].range, rule.fullField, rule.min, rule.max));
+ }
+}
+
+var ENUM = 'enum';
+/**
+ * Rule for validating a value exists in an enumerable list.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function enumerable(rule, value, source, errors, options) {
+ rule[ENUM] = Array.isArray(rule[ENUM]) ? rule[ENUM] : [];
+
+ if (rule[ENUM].indexOf(value) === -1) {
+ errors.push(format(options.messages[ENUM], rule.fullField, rule[ENUM].join(', ')));
+ }
+}
+
+/**
+ * Rule for validating a regular expression pattern.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param source The source object being validated.
+ * @param errors An array of errors that this rule may add
+ * validation errors to.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function pattern$1(rule, value, source, errors, options) {
+ if (rule.pattern) {
+ if (rule.pattern instanceof RegExp) {
+ // if a RegExp instance is passed, reset `lastIndex` in case its `global`
+ // flag is accidentally set to `true`, which in a validation scenario
+ // is not necessary and the result might be misleading
+ rule.pattern.lastIndex = 0;
+
+ if (!rule.pattern.test(value)) {
+ errors.push(format(options.messages.pattern.mismatch, rule.fullField, value, rule.pattern));
+ }
+ } else if (typeof rule.pattern === 'string') {
+ var _pattern = new RegExp(rule.pattern);
+
+ if (!_pattern.test(value)) {
+ errors.push(format(options.messages.pattern.mismatch, rule.fullField, value, rule.pattern));
+ }
+ }
+ }
+}
+
+var rules = {
+ required: required,
+ whitespace: whitespace,
+ type: type,
+ range: range,
+ "enum": enumerable,
+ pattern: pattern$1
+};
+
+/**
+ * Performs validation for string types.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function string(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value, 'string') && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options, 'string');
+
+ if (!isEmptyValue(value, 'string')) {
+ rules.type(rule, value, source, errors, options);
+ rules.range(rule, value, source, errors, options);
+ rules.pattern(rule, value, source, errors, options);
+
+ if (rule.whitespace === true) {
+ rules.whitespace(rule, value, source, errors, options);
+ }
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a function.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function method(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a number.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function number(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (value === '') {
+ value = undefined;
+ }
+
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ rules.range(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a boolean.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function _boolean(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates the regular expression type.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function regexp(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (!isEmptyValue(value)) {
+ rules.type(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a number is an integer.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function integer(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ rules.range(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a number is a floating point number.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function floatFn(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ rules.range(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates an array.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function array(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value, 'array') && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options, 'array');
+
+ if (!isEmptyValue(value, 'array')) {
+ rules.type(rule, value, source, errors, options);
+ rules.range(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates an object.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function object(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules.type(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+var ENUM$1 = 'enum';
+/**
+ * Validates an enumerable list.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function enumerable$1(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (value !== undefined) {
+ rules[ENUM$1](rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Validates a regular expression pattern.
+ *
+ * Performs validation when a rule only contains
+ * a pattern property but is not declared as a string type.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function pattern$2(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value, 'string') && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (!isEmptyValue(value, 'string')) {
+ rules.pattern(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+function date(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+
+ if (!isEmptyValue(value)) {
+ var dateObject;
+
+ if (typeof value === 'number') {
+ dateObject = new Date(value);
+ } else {
+ dateObject = value;
+ }
+
+ rules.type(rule, dateObject, source, errors, options);
+
+ if (dateObject) {
+ rules.range(rule, dateObject.getTime(), source, errors, options);
+ }
+ }
+ }
+
+ callback(errors);
+}
+
+function required$1(rule, value, callback, source, options) {
+ var errors = [];
+ var type = Array.isArray(value) ? 'array' : typeof value;
+ rules.required(rule, value, source, errors, options, type);
+ callback(errors);
+}
+
+function type$1(rule, value, callback, source, options) {
+ var ruleType = rule.type;
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value, ruleType) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options, ruleType);
+
+ if (!isEmptyValue(value, ruleType)) {
+ rules.type(rule, value, source, errors, options);
+ }
+ }
+
+ callback(errors);
+}
+
+/**
+ * Performs validation for any type.
+ *
+ * @param rule The validation rule.
+ * @param value The value of the field on the source object.
+ * @param callback The callback function.
+ * @param source The source object being validated.
+ * @param options The validation options.
+ * @param options.messages The validation messages.
+ */
+
+function any(rule, value, callback, source, options) {
+ var errors = [];
+ var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
+
+ if (validate) {
+ if (isEmptyValue(value) && !rule.required) {
+ return callback();
+ }
+
+ rules.required(rule, value, source, errors, options);
+ }
+
+ callback(errors);
+}
+
+var validators = {
+ string: string,
+ method: method,
+ number: number,
+ "boolean": _boolean,
+ regexp: regexp,
+ integer: integer,
+ "float": floatFn,
+ array: array,
+ object: object,
+ "enum": enumerable$1,
+ pattern: pattern$2,
+ date: date,
+ url: type$1,
+ hex: type$1,
+ email: type$1,
+ required: required$1,
+ any: any
+};
+
+function newMessages() {
+ return {
+ "default": 'Validation error on field %s',
+ required: '%s is required',
+ "enum": '%s must be one of %s',
+ whitespace: '%s cannot be empty',
+ date: {
+ format: '%s date %s is invalid for format %s',
+ parse: '%s date could not be parsed, %s is invalid ',
+ invalid: '%s date %s is invalid'
+ },
+ types: {
+ string: '%s is not a %s',
+ method: '%s is not a %s (function)',
+ array: '%s is not an %s',
+ object: '%s is not an %s',
+ number: '%s is not a %s',
+ date: '%s is not a %s',
+ "boolean": '%s is not a %s',
+ integer: '%s is not an %s',
+ "float": '%s is not a %s',
+ regexp: '%s is not a valid %s',
+ email: '%s is not a valid %s',
+ url: '%s is not a valid %s',
+ hex: '%s is not a valid %s'
+ },
+ string: {
+ len: '%s must be exactly %s characters',
+ min: '%s must be at least %s characters',
+ max: '%s cannot be longer than %s characters',
+ range: '%s must be between %s and %s characters'
+ },
+ number: {
+ len: '%s must equal %s',
+ min: '%s cannot be less than %s',
+ max: '%s cannot be greater than %s',
+ range: '%s must be between %s and %s'
+ },
+ array: {
+ len: '%s must be exactly %s in length',
+ min: '%s cannot be less than %s in length',
+ max: '%s cannot be greater than %s in length',
+ range: '%s must be between %s and %s in length'
+ },
+ pattern: {
+ mismatch: '%s value %s does not match pattern %s'
+ },
+ clone: function clone() {
+ var cloned = JSON.parse(JSON.stringify(this));
+ cloned.clone = this.clone;
+ return cloned;
+ }
+ };
+}
+var messages = newMessages();
+
+/**
+ * Encapsulates a validation schema.
+ *
+ * @param descriptor An object declaring validation rules
+ * for this schema.
+ */
+
+function Schema(descriptor) {
+ this.rules = null;
+ this._messages = messages;
+ this.define(descriptor);
+}
+
+Schema.prototype = {
+ messages: function messages(_messages) {
+ if (_messages) {
+ this._messages = deepMerge(newMessages(), _messages);
+ }
+
+ return this._messages;
+ },
+ define: function define(rules) {
+ if (!rules) {
+ throw new Error('Cannot configure a schema with no rules');
+ }
+
+ if (typeof rules !== 'object' || Array.isArray(rules)) {
+ throw new Error('Rules must be an object');
+ }
+
+ this.rules = {};
+ var z;
+ var item;
+
+ for (z in rules) {
+ if (rules.hasOwnProperty(z)) {
+ item = rules[z];
+ this.rules[z] = Array.isArray(item) ? item : [item];
+ }
+ }
+ },
+ validate: function validate(source_, o, oc) {
+ var _this = this;
+
+ if (o === void 0) {
+ o = {};
+ }
+
+ if (oc === void 0) {
+ oc = function oc() {};
+ }
+
+ var source = source_;
+ var options = o;
+ var callback = oc;
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ if (!this.rules || Object.keys(this.rules).length === 0) {
+ if (callback) {
+ callback();
+ }
+
+ return Promise.resolve();
+ }
+
+ function complete(results) {
+ var i;
+ var errors = [];
+ var fields = {};
+
+ function add(e) {
+ if (Array.isArray(e)) {
+ var _errors;
+
+ errors = (_errors = errors).concat.apply(_errors, e);
+ } else {
+ errors.push(e);
+ }
+ }
+
+ for (i = 0; i < results.length; i++) {
+ add(results[i]);
+ }
+
+ if (!errors.length) {
+ errors = null;
+ fields = null;
+ } else {
+ fields = convertFieldsError(errors);
+ }
+
+ callback(errors, fields);
+ }
+
+ if (options.messages) {
+ var messages$1 = this.messages();
+
+ if (messages$1 === messages) {
+ messages$1 = newMessages();
+ }
+
+ deepMerge(messages$1, options.messages);
+ options.messages = messages$1;
+ } else {
+ options.messages = this.messages();
+ }
+
+ var arr;
+ var value;
+ var series = {};
+ var keys = options.keys || Object.keys(this.rules);
+ keys.forEach(function(z) {
+ arr = _this.rules[z];
+ value = source[z];
+ arr.forEach(function(r) {
+ var rule = r;
+
+ if (typeof rule.transform === 'function') {
+ if (source === source_) {
+ source = _extends({}, source);
+ }
+
+ value = source[z] = rule.transform(value);
+ }
+
+ if (typeof rule === 'function') {
+ rule = {
+ validator: rule
+ };
+ } else {
+ rule = _extends({}, rule);
+ }
+
+ rule.validator = _this.getValidationMethod(rule);
+ rule.field = z;
+ rule.fullField = rule.fullField || z;
+ rule.type = _this.getType(rule);
+
+ if (!rule.validator) {
+ return;
+ }
+
+ series[z] = series[z] || [];
+ series[z].push({
+ rule: rule,
+ value: value,
+ source: source,
+ field: z
+ });
+ });
+ });
+ var errorFields = {};
+ return asyncMap(series, options, function(data, doIt) {
+ var rule = data.rule;
+ var deep = (rule.type === 'object' || rule.type === 'array') && (typeof rule.fields === 'object' || typeof rule.defaultField ===
+ 'object');
+ deep = deep && (rule.required || !rule.required && data.value);
+ rule.field = data.field;
+
+ function addFullfield(key, schema) {
+ return _extends({}, schema, {
+ fullField: rule.fullField + "." + key
+ });
+ }
+
+ function cb(e) {
+ if (e === void 0) {
+ e = [];
+ }
+
+ var errors = e;
+
+ if (!Array.isArray(errors)) {
+ errors = [errors];
+ }
+
+ if (!options.suppressWarning && errors.length) {
+ Schema.warning('async-validator:', errors);
+ }
+
+ if (errors.length && rule.message) {
+ errors = [].concat(rule.message);
+ }
+
+ errors = errors.map(complementError(rule));
+
+ if (options.first && errors.length) {
+ errorFields[rule.field] = 1;
+ return doIt(errors);
+ }
+
+ if (!deep) {
+ doIt(errors);
+ } else {
+ // if rule is required but the target object
+ // does not exist fail at the rule level and don't
+ // go deeper
+ if (rule.required && !data.value) {
+ if (rule.message) {
+ errors = [].concat(rule.message).map(complementError(rule));
+ } else if (options.error) {
+ errors = [options.error(rule, format(options.messages.required, rule.field))];
+ } else {
+ errors = [];
+ }
+
+ return doIt(errors);
+ }
+
+ var fieldsSchema = {};
+
+ if (rule.defaultField) {
+ for (var k in data.value) {
+ if (data.value.hasOwnProperty(k)) {
+ fieldsSchema[k] = rule.defaultField;
+ }
+ }
+ }
+
+ fieldsSchema = _extends({}, fieldsSchema, {}, data.rule.fields);
+
+ for (var f in fieldsSchema) {
+ if (fieldsSchema.hasOwnProperty(f)) {
+ var fieldSchema = Array.isArray(fieldsSchema[f]) ? fieldsSchema[f] : [fieldsSchema[f]];
+ fieldsSchema[f] = fieldSchema.map(addFullfield.bind(null, f));
+ }
+ }
+
+ var schema = new Schema(fieldsSchema);
+ schema.messages(options.messages);
+
+ if (data.rule.options) {
+ data.rule.options.messages = options.messages;
+ data.rule.options.error = options.error;
+ }
+
+ schema.validate(data.value, data.rule.options || options, function(errs) {
+ var finalErrors = [];
+
+ if (errors && errors.length) {
+ finalErrors.push.apply(finalErrors, errors);
+ }
+
+ if (errs && errs.length) {
+ finalErrors.push.apply(finalErrors, errs);
+ }
+
+ doIt(finalErrors.length ? finalErrors : null);
+ });
+ }
+ }
+
+ var res;
+
+ if (rule.asyncValidator) {
+ res = rule.asyncValidator(rule, data.value, cb, data.source, options);
+ } else if (rule.validator) {
+ res = rule.validator(rule, data.value, cb, data.source, options);
+
+ if (res === true) {
+ cb();
+ } else if (res === false) {
+ cb(rule.message || rule.field + " fails");
+ } else if (res instanceof Array) {
+ cb(res);
+ } else if (res instanceof Error) {
+ cb(res.message);
+ }
+ }
+
+ if (res && res.then) {
+ res.then(function() {
+ return cb();
+ }, function(e) {
+ return cb(e);
+ });
+ }
+ }, function(results) {
+ complete(results);
+ });
+ },
+ getType: function getType(rule) {
+ if (rule.type === undefined && rule.pattern instanceof RegExp) {
+ rule.type = 'pattern';
+ }
+
+ if (typeof rule.validator !== 'function' && rule.type && !validators.hasOwnProperty(rule.type)) {
+ throw new Error(format('Unknown rule type %s', rule.type));
+ }
+
+ return rule.type || 'string';
+ },
+ getValidationMethod: function getValidationMethod(rule) {
+ if (typeof rule.validator === 'function') {
+ return rule.validator;
+ }
+
+ var keys = Object.keys(rule);
+ var messageIndex = keys.indexOf('message');
+
+ if (messageIndex !== -1) {
+ keys.splice(messageIndex, 1);
+ }
+
+ if (keys.length === 1 && keys[0] === 'required') {
+ return validators.required;
+ }
+
+ return validators[this.getType(rule)] || false;
+ }
+};
+
+Schema.register = function register(type, validator) {
+ if (typeof validator !== 'function') {
+ throw new Error('Cannot register a validator by type, validator is not a function');
+ }
+
+ validators[type] = validator;
+};
+
+Schema.warning = warning;
+Schema.messages = messages;
+
+export default Schema;
+//# sourceMappingURL=index.js.map
diff --git a/uview-ui/libs/util/city.js b/uview-ui/libs/util/city.js
new file mode 100644
index 0000000..94169eb
--- /dev/null
+++ b/uview-ui/libs/util/city.js
@@ -0,0 +1 @@
+var cityData=[[{"label":"市辖区","value":"1101"}],[{"label":"市辖区","value":"1201"}],[{"label":"石家庄市","value":"1301"},{"label":"唐山市","value":"1302"},{"label":"秦皇岛市","value":"1303"},{"label":"邯郸市","value":"1304"},{"label":"邢台市","value":"1305"},{"label":"保定市","value":"1306"},{"label":"张家口市","value":"1307"},{"label":"承德市","value":"1308"},{"label":"沧州市","value":"1309"},{"label":"廊坊市","value":"1310"},{"label":"衡水市","value":"1311"}],[{"label":"太原市","value":"1401"},{"label":"大同市","value":"1402"},{"label":"阳泉市","value":"1403"},{"label":"长治市","value":"1404"},{"label":"晋城市","value":"1405"},{"label":"朔州市","value":"1406"},{"label":"晋中市","value":"1407"},{"label":"运城市","value":"1408"},{"label":"忻州市","value":"1409"},{"label":"临汾市","value":"1410"},{"label":"吕梁市","value":"1411"}],[{"label":"呼和浩特市","value":"1501"},{"label":"包头市","value":"1502"},{"label":"乌海市","value":"1503"},{"label":"赤峰市","value":"1504"},{"label":"通辽市","value":"1505"},{"label":"鄂尔多斯市","value":"1506"},{"label":"呼伦贝尔市","value":"1507"},{"label":"巴彦淖尔市","value":"1508"},{"label":"乌兰察布市","value":"1509"},{"label":"兴安盟","value":"1522"},{"label":"锡林郭勒盟","value":"1525"},{"label":"阿拉善盟","value":"1529"}],[{"label":"沈阳市","value":"2101"},{"label":"大连市","value":"2102"},{"label":"鞍山市","value":"2103"},{"label":"抚顺市","value":"2104"},{"label":"本溪市","value":"2105"},{"label":"丹东市","value":"2106"},{"label":"锦州市","value":"2107"},{"label":"营口市","value":"2108"},{"label":"阜新市","value":"2109"},{"label":"辽阳市","value":"2110"},{"label":"盘锦市","value":"2111"},{"label":"铁岭市","value":"2112"},{"label":"朝阳市","value":"2113"},{"label":"葫芦岛市","value":"2114"}],[{"label":"长春市","value":"2201"},{"label":"吉林市","value":"2202"},{"label":"四平市","value":"2203"},{"label":"辽源市","value":"2204"},{"label":"通化市","value":"2205"},{"label":"白山市","value":"2206"},{"label":"松原市","value":"2207"},{"label":"白城市","value":"2208"},{"label":"延边朝鲜族自治州","value":"2224"}],[{"label":"哈尔滨市","value":"2301"},{"label":"齐齐哈尔市","value":"2302"},{"label":"鸡西市","value":"2303"},{"label":"鹤岗市","value":"2304"},{"label":"双鸭山市","value":"2305"},{"label":"大庆市","value":"2306"},{"label":"伊春市","value":"2307"},{"label":"佳木斯市","value":"2308"},{"label":"七台河市","value":"2309"},{"label":"牡丹江市","value":"2310"},{"label":"黑河市","value":"2311"},{"label":"绥化市","value":"2312"},{"label":"大兴安岭地区","value":"2327"}],[{"label":"市辖区","value":"3101"}],[{"label":"南京市","value":"3201"},{"label":"无锡市","value":"3202"},{"label":"徐州市","value":"3203"},{"label":"常州市","value":"3204"},{"label":"苏州市","value":"3205"},{"label":"南通市","value":"3206"},{"label":"连云港市","value":"3207"},{"label":"淮安市","value":"3208"},{"label":"盐城市","value":"3209"},{"label":"扬州市","value":"3210"},{"label":"镇江市","value":"3211"},{"label":"泰州市","value":"3212"},{"label":"宿迁市","value":"3213"}],[{"label":"杭州市","value":"3301"},{"label":"宁波市","value":"3302"},{"label":"温州市","value":"3303"},{"label":"嘉兴市","value":"3304"},{"label":"湖州市","value":"3305"},{"label":"绍兴市","value":"3306"},{"label":"金华市","value":"3307"},{"label":"衢州市","value":"3308"},{"label":"舟山市","value":"3309"},{"label":"台州市","value":"3310"},{"label":"丽水市","value":"3311"}],[{"label":"合肥市","value":"3401"},{"label":"芜湖市","value":"3402"},{"label":"蚌埠市","value":"3403"},{"label":"淮南市","value":"3404"},{"label":"马鞍山市","value":"3405"},{"label":"淮北市","value":"3406"},{"label":"铜陵市","value":"3407"},{"label":"安庆市","value":"3408"},{"label":"黄山市","value":"3410"},{"label":"滁州市","value":"3411"},{"label":"阜阳市","value":"3412"},{"label":"宿州市","value":"3413"},{"label":"六安市","value":"3415"},{"label":"亳州市","value":"3416"},{"label":"池州市","value":"3417"},{"label":"宣城市","value":"3418"}],[{"label":"福州市","value":"3501"},{"label":"厦门市","value":"3502"},{"label":"莆田市","value":"3503"},{"label":"三明市","value":"3504"},{"label":"泉州市","value":"3505"},{"label":"漳州市","value":"3506"},{"label":"南平市","value":"3507"},{"label":"龙岩市","value":"3508"},{"label":"宁德市","value":"3509"}],[{"label":"南昌市","value":"3601"},{"label":"景德镇市","value":"3602"},{"label":"萍乡市","value":"3603"},{"label":"九江市","value":"3604"},{"label":"新余市","value":"3605"},{"label":"鹰潭市","value":"3606"},{"label":"赣州市","value":"3607"},{"label":"吉安市","value":"3608"},{"label":"宜春市","value":"3609"},{"label":"抚州市","value":"3610"},{"label":"上饶市","value":"3611"}],[{"label":"济南市","value":"3701"},{"label":"青岛市","value":"3702"},{"label":"淄博市","value":"3703"},{"label":"枣庄市","value":"3704"},{"label":"东营市","value":"3705"},{"label":"烟台市","value":"3706"},{"label":"潍坊市","value":"3707"},{"label":"济宁市","value":"3708"},{"label":"泰安市","value":"3709"},{"label":"威海市","value":"3710"},{"label":"日照市","value":"3711"},{"label":"莱芜市","value":"3712"},{"label":"临沂市","value":"3713"},{"label":"德州市","value":"3714"},{"label":"聊城市","value":"3715"},{"label":"滨州市","value":"3716"},{"label":"菏泽市","value":"3717"}],[{"label":"郑州市","value":"4101"},{"label":"开封市","value":"4102"},{"label":"洛阳市","value":"4103"},{"label":"平顶山市","value":"4104"},{"label":"安阳市","value":"4105"},{"label":"鹤壁市","value":"4106"},{"label":"新乡市","value":"4107"},{"label":"焦作市","value":"4108"},{"label":"濮阳市","value":"4109"},{"label":"许昌市","value":"4110"},{"label":"漯河市","value":"4111"},{"label":"三门峡市","value":"4112"},{"label":"南阳市","value":"4113"},{"label":"商丘市","value":"4114"},{"label":"信阳市","value":"4115"},{"label":"周口市","value":"4116"},{"label":"驻马店市","value":"4117"},{"label":"省直辖县级行政区划","value":"4190"}],[{"label":"武汉市","value":"4201"},{"label":"黄石市","value":"4202"},{"label":"十堰市","value":"4203"},{"label":"宜昌市","value":"4205"},{"label":"襄阳市","value":"4206"},{"label":"鄂州市","value":"4207"},{"label":"荆门市","value":"4208"},{"label":"孝感市","value":"4209"},{"label":"荆州市","value":"4210"},{"label":"黄冈市","value":"4211"},{"label":"咸宁市","value":"4212"},{"label":"随州市","value":"4213"},{"label":"恩施土家族苗族自治州","value":"4228"},{"label":"省直辖县级行政区划","value":"4290"}],[{"label":"长沙市","value":"4301"},{"label":"株洲市","value":"4302"},{"label":"湘潭市","value":"4303"},{"label":"衡阳市","value":"4304"},{"label":"邵阳市","value":"4305"},{"label":"岳阳市","value":"4306"},{"label":"常德市","value":"4307"},{"label":"张家界市","value":"4308"},{"label":"益阳市","value":"4309"},{"label":"郴州市","value":"4310"},{"label":"永州市","value":"4311"},{"label":"怀化市","value":"4312"},{"label":"娄底市","value":"4313"},{"label":"湘西土家族苗族自治州","value":"4331"}],[{"label":"广州市","value":"4401"},{"label":"韶关市","value":"4402"},{"label":"深圳市","value":"4403"},{"label":"珠海市","value":"4404"},{"label":"汕头市","value":"4405"},{"label":"佛山市","value":"4406"},{"label":"江门市","value":"4407"},{"label":"湛江市","value":"4408"},{"label":"茂名市","value":"4409"},{"label":"肇庆市","value":"4412"},{"label":"惠州市","value":"4413"},{"label":"梅州市","value":"4414"},{"label":"汕尾市","value":"4415"},{"label":"河源市","value":"4416"},{"label":"阳江市","value":"4417"},{"label":"清远市","value":"4418"},{"label":"东莞市","value":"4419"},{"label":"中山市","value":"4420"},{"label":"潮州市","value":"4451"},{"label":"揭阳市","value":"4452"},{"label":"云浮市","value":"4453"}],[{"label":"南宁市","value":"4501"},{"label":"柳州市","value":"4502"},{"label":"桂林市","value":"4503"},{"label":"梧州市","value":"4504"},{"label":"北海市","value":"4505"},{"label":"防城港市","value":"4506"},{"label":"钦州市","value":"4507"},{"label":"贵港市","value":"4508"},{"label":"玉林市","value":"4509"},{"label":"百色市","value":"4510"},{"label":"贺州市","value":"4511"},{"label":"河池市","value":"4512"},{"label":"来宾市","value":"4513"},{"label":"崇左市","value":"4514"}],[{"label":"海口市","value":"4601"},{"label":"三亚市","value":"4602"},{"label":"三沙市","value":"4603"},{"label":"儋州市","value":"4604"},{"label":"省直辖县级行政区划","value":"4690"}],[{"label":"市辖区","value":"5001"},{"label":"县","value":"5002"}],[{"label":"成都市","value":"5101"},{"label":"自贡市","value":"5103"},{"label":"攀枝花市","value":"5104"},{"label":"泸州市","value":"5105"},{"label":"德阳市","value":"5106"},{"label":"绵阳市","value":"5107"},{"label":"广元市","value":"5108"},{"label":"遂宁市","value":"5109"},{"label":"内江市","value":"5110"},{"label":"乐山市","value":"5111"},{"label":"南充市","value":"5113"},{"label":"眉山市","value":"5114"},{"label":"宜宾市","value":"5115"},{"label":"广安市","value":"5116"},{"label":"达州市","value":"5117"},{"label":"雅安市","value":"5118"},{"label":"巴中市","value":"5119"},{"label":"资阳市","value":"5120"},{"label":"阿坝藏族羌族自治州","value":"5132"},{"label":"甘孜藏族自治州","value":"5133"},{"label":"凉山彝族自治州","value":"5134"}],[{"label":"贵阳市","value":"5201"},{"label":"六盘水市","value":"5202"},{"label":"遵义市","value":"5203"},{"label":"安顺市","value":"5204"},{"label":"毕节市","value":"5205"},{"label":"铜仁市","value":"5206"},{"label":"黔西南布依族苗族自治州","value":"5223"},{"label":"黔东南苗族侗族自治州","value":"5226"},{"label":"黔南布依族苗族自治州","value":"5227"}],[{"label":"昆明市","value":"5301"},{"label":"曲靖市","value":"5303"},{"label":"玉溪市","value":"5304"},{"label":"保山市","value":"5305"},{"label":"昭通市","value":"5306"},{"label":"丽江市","value":"5307"},{"label":"普洱市","value":"5308"},{"label":"临沧市","value":"5309"},{"label":"楚雄彝族自治州","value":"5323"},{"label":"红河哈尼族彝族自治州","value":"5325"},{"label":"文山壮族苗族自治州","value":"5326"},{"label":"西双版纳傣族自治州","value":"5328"},{"label":"大理白族自治州","value":"5329"},{"label":"德宏傣族景颇族自治州","value":"5331"},{"label":"怒江傈僳族自治州","value":"5333"},{"label":"迪庆藏族自治州","value":"5334"}],[{"label":"拉萨市","value":"5401"},{"label":"日喀则市","value":"5402"},{"label":"昌都市","value":"5403"},{"label":"林芝市","value":"5404"},{"label":"山南市","value":"5405"},{"label":"那曲地区","value":"5424"},{"label":"阿里地区","value":"5425"}],[{"label":"西安市","value":"6101"},{"label":"铜川市","value":"6102"},{"label":"宝鸡市","value":"6103"},{"label":"咸阳市","value":"6104"},{"label":"渭南市","value":"6105"},{"label":"延安市","value":"6106"},{"label":"汉中市","value":"6107"},{"label":"榆林市","value":"6108"},{"label":"安康市","value":"6109"},{"label":"商洛市","value":"6110"}],[{"label":"兰州市","value":"6201"},{"label":"嘉峪关市","value":"6202"},{"label":"金昌市","value":"6203"},{"label":"白银市","value":"6204"},{"label":"天水市","value":"6205"},{"label":"武威市","value":"6206"},{"label":"张掖市","value":"6207"},{"label":"平凉市","value":"6208"},{"label":"酒泉市","value":"6209"},{"label":"庆阳市","value":"6210"},{"label":"定西市","value":"6211"},{"label":"陇南市","value":"6212"},{"label":"临夏回族自治州","value":"6229"},{"label":"甘南藏族自治州","value":"6230"}],[{"label":"西宁市","value":"6301"},{"label":"海东市","value":"6302"},{"label":"海北藏族自治州","value":"6322"},{"label":"黄南藏族自治州","value":"6323"},{"label":"海南藏族自治州","value":"6325"},{"label":"果洛藏族自治州","value":"6326"},{"label":"玉树藏族自治州","value":"6327"},{"label":"海西蒙古族藏族自治州","value":"6328"}],[{"label":"银川市","value":"6401"},{"label":"石嘴山市","value":"6402"},{"label":"吴忠市","value":"6403"},{"label":"固原市","value":"6404"},{"label":"中卫市","value":"6405"}],[{"label":"乌鲁木齐市","value":"6501"},{"label":"克拉玛依市","value":"6502"},{"label":"吐鲁番市","value":"6504"},{"label":"哈密市","value":"6505"},{"label":"昌吉回族自治州","value":"6523"},{"label":"博尔塔拉蒙古自治州","value":"6527"},{"label":"巴音郭楞蒙古自治州","value":"6528"},{"label":"阿克苏地区","value":"6529"},{"label":"克孜勒苏柯尔克孜自治州","value":"6530"},{"label":"喀什地区","value":"6531"},{"label":"和田地区","value":"6532"},{"label":"伊犁哈萨克自治州","value":"6540"},{"label":"塔城地区","value":"6542"},{"label":"阿勒泰地区","value":"6543"},{"label":"自治区直辖县级行政区划","value":"6590"}],[{"label":"台北","value":"6601"},{"label":"高雄","value":"6602"},{"label":"基隆","value":"6603"},{"label":"台中","value":"6604"},{"label":"台南","value":"6605"},{"label":"新竹","value":"6606"},{"label":"嘉义","value":"6607"},{"label":"宜兰","value":"6608"},{"label":"桃园","value":"6609"},{"label":"苗栗","value":"6610"},{"label":"彰化","value":"6611"},{"label":"南投","value":"6612"},{"label":"云林","value":"6613"},{"label":"屏东","value":"6614"},{"label":"台东","value":"6615"},{"label":"花莲","value":"6616"},{"label":"澎湖","value":"6617"}],[{"label":"香港岛","value":"6701"},{"label":"九龙","value":"6702"},{"label":"新界","value":"6703"}],[{"label":"澳门半岛","value":"6801"},{"label":"氹仔岛","value":"6802"},{"label":"路环岛","value":"6803"},{"label":"路氹城","value":"6804"}]];export default cityData;
\ No newline at end of file
diff --git a/uview-ui/libs/util/emitter.js b/uview-ui/libs/util/emitter.js
new file mode 100644
index 0000000..228016e
--- /dev/null
+++ b/uview-ui/libs/util/emitter.js
@@ -0,0 +1,51 @@
+/**
+ * 递归使用 call 方式this指向
+ * @param componentName // 需要找的组件的名称
+ * @param eventName // 事件名称
+ * @param params // 需要传递的参数
+ */
+function broadcast(componentName, eventName, params) {
+ // 循环子节点找到名称一样的子节点 否则 递归 当前子节点
+ this.$children.map(child=>{
+ if (componentName===child.$options.name) {
+ child.$emit.apply(child,[eventName].concat(params))
+ }else {
+ broadcast.apply(child,[componentName,eventName].concat(params))
+ }
+ })
+}
+export default {
+ methods: {
+ /**
+ * 派发 (向上查找) (一个)
+ * @param componentName // 需要找的组件的名称
+ * @param eventName // 事件名称
+ * @param params // 需要传递的参数
+ */
+ dispatch(componentName, eventName, params) {
+ let parent = this.$parent || this.$root;//$parent 找到最近的父节点 $root 根节点
+ let name = parent.$options.name; // 获取当前组件实例的name
+ // 如果当前有节点 && 当前没名称 且 当前名称等于需要传进来的名称的时候就去查找当前的节点
+ // 循环出当前名称的一样的组件实例
+ while (parent && (!name||name!==componentName)) {
+ parent = parent.$parent;
+ if (parent) {
+ name = parent.$options.name;
+ }
+ }
+ // 有节点表示当前找到了name一样的实例
+ if (parent) {
+ parent.$emit.apply(parent,[eventName].concat(params))
+ }
+ },
+ /**
+ * 广播 (向下查找) (广播多个)
+ * @param componentName // 需要找的组件的名称
+ * @param eventName // 事件名称
+ * @param params // 需要传递的参数
+ */
+ broadcast(componentName, eventName, params) {
+ broadcast.call(this,componentName, eventName, params)
+ }
+ }
+}
diff --git a/uview-ui/libs/util/province.js b/uview-ui/libs/util/province.js
new file mode 100644
index 0000000..436b190
--- /dev/null
+++ b/uview-ui/libs/util/province.js
@@ -0,0 +1 @@
+var provinceData=[{"label":"北京市","value":"11"},{"label":"天津市","value":"12"},{"label":"河北省","value":"13"},{"label":"山西省","value":"14"},{"label":"内蒙古自治区","value":"15"},{"label":"辽宁省","value":"21"},{"label":"吉林省","value":"22"},{"label":"黑龙江省","value":"23"},{"label":"上海市","value":"31"},{"label":"江苏省","value":"32"},{"label":"浙江省","value":"33"},{"label":"安徽省","value":"34"},{"label":"福建省","value":"35"},{"label":"江西省","value":"36"},{"label":"山东省","value":"37"},{"label":"河南省","value":"41"},{"label":"湖北省","value":"42"},{"label":"湖南省","value":"43"},{"label":"广东省","value":"44"},{"label":"广西壮族自治区","value":"45"},{"label":"海南省","value":"46"},{"label":"重庆市","value":"50"},{"label":"四川省","value":"51"},{"label":"贵州省","value":"52"},{"label":"云南省","value":"53"},{"label":"西藏自治区","value":"54"},{"label":"陕西省","value":"61"},{"label":"甘肃省","value":"62"},{"label":"青海省","value":"63"},{"label":"宁夏回族自治区","value":"64"},{"label":"新疆维吾尔自治区","value":"65"},{"label":"台湾","value":"66"},{"label":"香港","value":"67"},{"label":"澳门","value":"68"}];export default provinceData;
\ No newline at end of file
diff --git a/uview-ui/package.json b/uview-ui/package.json
new file mode 100644
index 0000000..b0e2476
--- /dev/null
+++ b/uview-ui/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "uview-ui",
+ "version": "1.7.8",
+ "description": "uView UI,是uni-app生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水",
+ "main": "index.js",
+ "keywords": [
+ "uview",
+ "uView",
+ "uni-app",
+ "uni-app ui",
+ "uniapp",
+ "uviewui",
+ "uview ui",
+ "uviewUI",
+ "uViewui",
+ "uViewUI",
+ "uView UI",
+ "uni ui",
+ "uni UI",
+ "uniapp ui",
+ "ui",
+ "UI框架",
+ "uniapp ui框架",
+ "uniapp UI"
+ ],
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": ""
+ },
+ "devDependencies": {
+ "node-sass": "^4.14.0",
+ "sass-loader": "^8.0.2"
+ },
+ "author": "uView",
+ "license": "MIT"
+}
diff --git a/uview-ui/theme.scss b/uview-ui/theme.scss
new file mode 100644
index 0000000..f3bb36d
--- /dev/null
+++ b/uview-ui/theme.scss
@@ -0,0 +1,38 @@
+// 此文件为uView的主题变量,这些变量目前只能通过uni.scss引入才有效,另外由于
+// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
+// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
+
+$u-main-color: #303133;
+$u-content-color: #606266;
+$u-tips-color: #909399;
+$u-light-color: #c0c4cc;
+$u-border-color: #e4e7ed;
+$u-bg-color: #f3f4f6;
+
+$u-type-primary: #2979ff;
+$u-type-primary-light: #ecf5ff;
+$u-type-primary-disabled: #a0cfff;
+$u-type-primary-dark: #2b85e4;
+
+$u-type-warning: #ff9900;
+$u-type-warning-disabled: #fcbd71;
+$u-type-warning-dark: #f29100;
+$u-type-warning-light: #fdf6ec;
+
+$u-type-success: #19be6b;
+$u-type-success-disabled: #71d5a1;
+$u-type-success-dark: #18b566;
+$u-type-success-light: #dbf1e1;
+
+$u-type-error: #fa3534;
+$u-type-error-disabled: #fab6b6;
+$u-type-error-dark: #dd6161;
+$u-type-error-light: #fef0f0;
+
+$u-type-info: #909399;
+$u-type-info-disabled: #c8c9cc;
+$u-type-info-dark: #82848a;
+$u-type-info-light: #f4f4f5;
+
+$u-form-item-height: 70rpx;
+$u-form-item-border-color: #dcdfe6;