This commit is contained in:
2025-10-27 15:55:29 +08:00
commit 6632080b83
513 changed files with 117442 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
## 为减小组件包的大小默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
## 功能介绍
- 全端支持(含 `v3、NVUE`
- 支持丰富的标签(包括 `table``video``svg` 等)
- 支持丰富的事件效果(自动预览图片、链接处理等)
- 支持设置占位图(加载中、出错时、预览时)
- 支持锚点跳转、长按复制等丰富功能
- 支持大部分 *html* 实体
- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
- 效率高、容错性强且轻量化
查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多
## 使用方法
- `uni_modules` 方式
1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<!-- 不需要引入,可直接使用 -->
<mp-html :content="html" />
```
```javascript
export default {
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可
- 源码方式
1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码
插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from '@/components/mp-html/mp-html'
export default {
// HBuilderX 2.5.5+ 可以通过 easycom 自动引入
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
- npm 方式
1. 在项目根目录下执行
```bash
npm install mp-html
```
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
export default {
// 不可省略
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时执行以下命令即可
```bash
npm update mp-html
```
使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)
如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行
查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多
## 组件属性
| 属性 | 类型 | 默认值 | 说明 |
|:---:|:---:|:---:|---|
| container-style | String | | 容器的样式([2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210) |
| content | String | | 用于渲染的 html 字符串 |
| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
| domain | String | | 主域名(用于链接拼接) |
| error-img | String | | 图片出错时的占位图链接 |
| lazy-load | Boolean | false | 是否开启图片懒加载 |
| loading-img | String | | 图片加载过程中的占位图链接 |
| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
| selectable | Boolean | false | 是否开启文本长按复制 |
| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
| tag-style | Object | | 设置标签的默认样式 |
| use-anchor | Boolean | false | 是否使用锚点链接 |
查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多
## 组件事件
| 名称 | 触发时机 |
|:---:|---|
| load | dom 树加载完毕时 |
| ready | 图片加载完毕时 |
| error | 发生渲染错误时 |
| imgtap | 图片被点击时 |
| linktap | 链接被点击时 |
| play | 音视频播放时 |
查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多
## api
组件实例上提供了一些 `api` 方法可供调用
| 名称 | 作用 |
|:---:|---|
| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
| navigateTo | 锚点跳转 |
| getText | 获取文本内容 |
| getRect | 获取富文本内容的位置和大小 |
| setContent | 设置富文本内容 |
| imgList | 获取所有图片的数组 |
| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222) |
| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240) |
查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多
## 插件扩展
除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
| 名称 | 作用 |
|:---:|---|
| audio | 音乐播放器 |
| editable | 富文本 **编辑**[示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip) |
| emoji | 解析 emoji |
| highlight | 代码块高亮显示 |
| markdown | 渲染 markdown |
| search | 关键词搜索 |
| style | 匹配 style 标签中的样式 |
| txv-video | 使用腾讯视频 |
| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:
1. 获取完整组件包
```bash
npm install mp-html
```
2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件
3. 生成新的组件包
在 `node_modules/mp-html` 目录下执行
```bash
npm install
npm run build:uni-app
```
4. 拷贝 `dist/uni-app` 中的内容到项目根目录
查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多
## 关于 nvue
`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面
由于渲染方式与其他端不同,有以下限制:
1. 不支持 `lazy-load` 属性
2. 视频不支持全屏播放
3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下)
## 立即体验
![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg)
## 问题反馈
遇到问题时,请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题
可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)
提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复
欢迎加入 `QQ` 交流群:
群1已满`699734691`
群2已满`778239129`
群3`960265313`
查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多

View File

@@ -0,0 +1,150 @@
## v2.5.02024-04-22
1. `U` `play` 事件增加返回 `src` 等信息 [详细](https://github.com/jin-yufeng/mp-html/issues/526)
2. `U` `preview-img` 属性支持设置为 `all` 开启 `base64` 图片预览 [详细](https://github.com/jin-yufeng/mp-html/issues/536)
3. `U` `editable` 插件增加简易模式(点击文字直接编辑)
4. `U` `latex` 插件支持块级公式 [详细](https://github.com/jin-yufeng/mp-html/issues/582)
5. `F` 修复了表格部分情况下背景丢失的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/587)
6. `F` 修复了部分 `svg` 无法显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/591)
7. `F` 修复了 `h5``app` 端部分情况下样式无法识别的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/518)
8. `F` 修复了 `latex` 插件部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/580)
9. `F` 修复了 `editable` 插件表格无法删除的问题
10. `F` 修复了 `editable` 插件 `vue3` `h5` 端点击图片报错的问题
11. `F` 修复了 `editable` 插件点击表格没有菜单栏的问题
## v2.4.32024-01-21
1. `A` 增加 [card](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#card) 插件 [详细](https://github.com/jin-yufeng/mp-html/pull/533) by [@whoooami](https://github.com/whoooami)
2. `F` 修复了 `svg` 中包含 `foreignobject` 可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/523)
3. `F` 修复了合并单元格的表格部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/561)
4. `F` 修复了 `img` 标签设置 `object-fit` 无效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/567)
5. `F` 修复了 `latex` 插件公式会换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/540)
6. `F` 修复了 `editable``audio` 插件共用时点击 `audio` 无法编辑的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/529) by [@whoooami](https://github.com/whoooami)
7. `F` 修复了微信小程序部分情况下图片会报错 `replace of undefined` 的问题
8. `F` 修复了快手小程序图片不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/571)
## v2.4.22023-05-14
1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254)
2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505)
3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472)
4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513)
5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945)
6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515)
7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题
## v2.4.12022-12-25
1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题
2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470)
3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457)
4. `F` 修复了 `vue3` 运行到 `app``HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480)
5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题
6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题
## v2.4.02022-08-27
1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate) 的 `api`,可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452)
2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo)
3. `U` 优化 `ready` 事件触发时机,未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195)
4. `U` `highlight` 插件在编辑状态下不进行高亮处理,便于编辑
5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题
6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题
7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448)
8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题
9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题
10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449)
## v2.3.22022-08-13
1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件,可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J)
2. `U` 优化根节点下有很多标签的长内容渲染速度
3. `U` `highlight` 插件适配 `lang-xxx` 格式
4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie)
5. `F` 修复了 `editable` 插件输入连续空格无效的问题
6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438)
7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题
## v2.3.12022-05-20
1. `U` `app` 端支持使用本地图片
2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable)
3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430)
4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414)
5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题
6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题
## v2.3.02022-04-01
1. `A` 增加了 `play` 事件,音视频播放时触发,可用于与页面其他音视频进行互斥播放 [详细](basic/event#play)
2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单
3. `U` 优化 `wxs` 处理,提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413)
4. `U` `video` 标签支持 `object-fit` 属性
5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418)
6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410)
7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411)
8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413)
9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417)
10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题
11. `F` 修复了 `uni-app``vue3` 使用 `audio` 插件报错的问题
12. `F` 修复了 `uni-app``highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416)
## v2.2.22022-02-26
1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia) 的 `api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)
2. `U` 优化了长内容的加载速度
3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)、[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)、[#400](https://github.com/jin-yufeng/mp-html/issues/400)
4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题
5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)
## v2.2.12021-12-24
1. `A` `editable` 插件增加上下移动标签功能
2. `U` `editable` 插件支持在文本中间光标处插入内容
3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题
4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)
5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)
6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题
## v2.2.02021-10-12
1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)
2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)
3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件
4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)
5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名
6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)
7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)
8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题
## v2.1.52021-08-13
1. `A` 增加支持标签的 `dir` 属性
2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)
3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题
4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
## v2.1.42021-07-14
1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)
2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题
3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)
4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)
## v2.1.32021-06-12
1. `A` `editable` 插件增加 `insertTable` 方法
2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)
3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)
4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)
5. `F` 修复了包含 `linearGradient``svg` 可能无法显示的问题
6. `F` 修复了编译到头条小程序时可能报错的问题
7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题
8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题
9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题
10. `F` 修复了 `editable` 插件插入音频不显示的问题
## v2.1.22021-04-24
1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)
2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)
3. `U` 代码风格符合 [standard](https://standardjs.com) 标准
4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)
5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题
6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)
## v2.1.12021-04-09
1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题
2. 修复了 `svg` 标签中的文本无法显示的问题
3. 修复了使用 `editable` 插件编辑表格时可能报错的问题
4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)
5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)
6. 修复了 `style` 插件连续子选择器失效的问题
7. 修复了 `editable` 插件无法修改图片和字体大小的问题
## v2.1.0.22021-03-21
修复了 `nvue` 端使用可能报错的问题
## v2.1.02021-03-20
1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)
2. `A` 增加支持 `strike` 标签
3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)
6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题
## v2.0.52021-03-12
1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)
2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block` [详细](https://github.com/jin-yufeng/mp-html/issues/267)
3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)
4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题
5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)
6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)

View File

@@ -0,0 +1,498 @@
<template>
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
<!-- #endif -->
<!-- #ifdef APP-PLUS-NVUE -->
<web-view ref="web" src="/uni_modules/mp-html/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
<!-- #endif -->
</view>
</template>
<script>
/**
* mp-html v2.5.0
* @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式
* @property {String} content 用于渲染的 html 字符串
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
* @property {String} domain 主域名,用于拼接链接
* @property {String} error-img 图片出错时的占位图链接
* @property {Boolean} lazy-load 是否开启图片懒加载
* @property {string} loading-img 图片加载过程中的占位图链接
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
* @property {Boolean | String} selectable 是否开启长按复制
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
* @property {Object} tag-style 标签的默认样式
* @property {Boolean | Number} use-anchor 是否使用锚点链接
* @event {Function} load dom 结构加载完毕时触发
* @event {Function} ready 所有图片加载完毕时触发
* @event {Function} imgtap 图片被点击时触发
* @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发
*/
// #ifndef APP-PLUS-NVUE
import node from './node/node'
// #endif
import Parser from './parser'
const plugins=[]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data () {
return {
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
containerStyle: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
copyLink: {
type: [Boolean, String],
default: true
},
domain: String,
errorImg: {
type: String,
default: ''
},
lazyLoad: {
type: [Boolean, String],
default: false
},
loadingImg: {
type: String,
default: ''
},
pauseVideo: {
type: [Boolean, String],
default: true
},
previewImg: {
type: [Boolean, String],
default: true
},
scrollTable: [Boolean, String],
selectable: [Boolean, String],
setTitle: {
type: [Boolean, String],
default: true
},
showImgMenu: {
type: [Boolean, String],
default: true
},
tagStyle: Object,
useAnchor: [Boolean, Number]
},
// #ifdef VUE3
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
// #endif
// #ifndef APP-PLUS-NVUE
components: {
node
},
// #endif
watch: {
content (content) {
this.setContent(content)
}
},
created () {
this.plugins = []
for (let i = plugins.length; i--;) {
this.plugins.push(new plugins[i](this))
}
},
mounted () {
if (this.content && !this.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy () {
this._hook('onDetached')
},
methods: {
/**
* @description 将锚点跳转的范围限定在一个 scroll-view 内
* @param {Object} page scroll-view 所在页面的示例
* @param {String} selector scroll-view 的选择器
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
*/
in (page, selector, scrollTop) {
// #ifndef APP-PLUS-NVUE
if (page && selector && scrollTop) {
this._in = {
page,
selector,
scrollTop
}
}
// #endif
},
/**
* @description 锚点跳转
* @param {String} id 要跳转的锚点 id
* @param {Number} offset 跳转位置的偏移量
* @returns {Promise}
*/
navigateTo (id, offset) {
return new Promise((resolve, reject) => {
if (!this.useAnchor) {
reject(Error('Anchor is disabled'))
return
}
offset = offset || parseInt(this.useAnchor) || 0
// #ifdef APP-PLUS-NVUE
if (!id) {
dom.scrollToElement(this.$refs.web, {
offset
})
resolve()
} else {
this._navigateTo = {
resolve,
reject,
offset
}
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
}
// #endif
// #ifndef APP-PLUS-NVUE
let deep = ' '
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
deep = '>>>'
// #endif
const selector = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this._in ? this._in.page : this)
// #endif
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
if (this._in) {
selector.select(this._in.selector).scrollOffset()
.select(this._in.selector).boundingClientRect()
} else {
// 获取 scroll-view 的位置和滚动距离
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
}
selector.exec(res => {
if (!res[0]) {
reject(Error('Label not found'))
return
}
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
if (this._in) {
// scroll-view 跳转
this._in.page[this._in.scrollTop] = scrollTop
} else {
// 页面跳转
uni.pageScrollTo({
scrollTop,
duration: 300
})
}
resolve()
})
// #endif
})
},
/**
* @description 获取文本内容
* @return {String}
*/
getText (nodes) {
let text = '';
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text') {
text += node.text.replace(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
// 块级标签前后加换行
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
// 递归获取子节点的文本
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect () {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia () {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate (rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent (content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
let height = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms 总高度无变化就触发 ready 事件
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
// 未设置懒加载,等待所有图片加载完毕
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook (name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set (nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage (e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view 初始化完毕
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// 内容 dom 加载完毕
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
// 所有图片加载完毕
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => {
this.$emit('ready', {})
})
break
// 总高度发生变化
case 'onHeightChange':
this.height = message.height
break
// 图片点击
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
// 链接点击
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
// 锚点跳转
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
// 打开外链
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
// 获取到锚点的偏移量
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
// 点击
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
// 出错
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
</style>

View File

@@ -0,0 +1,587 @@
<template>
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" v-bind:key="i">
<!-- 图片 -->
<!-- 占位图 -->
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
<!-- 显示图片 -->
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
<img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
<!-- 表格中的图片使用 rich-text 防止大小不正确 -->
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifdef MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
<!-- #endif -->
<!-- #ifdef APP-PLUS && VUE3 -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- 文本 -->
<!-- #ifdef MP-WEIXIN -->
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{n.text}}</text>
<!-- #endif -->
<text v-else-if="n.name==='br'">\n</text>
<!-- 链接 -->
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view>
<!-- 视频 -->
<!-- #ifdef APP-PLUS -->
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" @vplay.stop="play" />
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
<!-- 音频 -->
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<node :childs="tr.children" :opts="opts" />
</view>
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<!-- 富文本 -->
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
<!-- #endif -->
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
<!-- #endif -->
<!-- 继续递归 -->
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
</view>
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
</block>
</view>
</template>
<script module="handler" lang="wxs">
// 行内标签列表
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true
}
/**
* @description 判断是否为行内标签
*/
module.exports = {
isInline: function (tagName, style) {
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
}
}
</script>
<script>
import node from './node'
export default {
name: 'node',
options: {
// #ifdef MP-WEIXIN
virtualHost: true,
// #endif
// #ifdef MP-TOUTIAO
addGlobalClass: false
// #endif
},
data () {
return {
ctrl: {},
// #ifdef MP-WEIXIN
isiOS: uni.getSystemInfoSync().system.includes('iOS')
// #endif
}
},
props: {
name: String,
attrs: {
type: Object,
default () {
return {}
}
},
childs: Array,
opts: Array
},
components: {
// #ifndef (H5 || APP-PLUS) && VUE3
node
// #endif
},
mounted () {
this.$nextTick(() => {
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
})
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
let i
for (i = this.childs.length; i--;) {
if (this.childs[i].name === 'img') break
}
if (i !== -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy () {
// #ifdef H5 || APP-PLUS
if (this.observer) {
this.observer.disconnect()
}
// #endif
},
methods:{
// #ifdef MP-WEIXIN
toJSON () { return this },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
this.root.$emit('play', {
source: node.name,
attrs: {
...node.attrs,
src: node.src[this.ctrl[i] || 0]
}
})
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
let flag = false
const id = e.target.id
for (let i = this.root._videos.length; i--;) {
if (this.root._videos[i].id === id) {
flag = true
} else {
this.root._videos[i].pause() // 自动暂停其他视频
}
}
// 将自己加入列表
if (!flag) {
const ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
if (this.root.playbackRate) {
ctx.playbackRate(this.root.playbackRate)
}
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap (e) {
const node = this.childs[e.currentTarget.dataset.i]
if (node.a) {
this.linkTap(node.a)
return
}
if (node.attrs.ignore) return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
this.root.$emit('imgtap', node.attrs)
// 自动预览图片
if (this.root.previewImg) {
uni.previewImage({
// #ifdef MP-WEIXIN
showmenu: this.root.showImgMenu,
// #endif
// #ifdef MP-ALIPAY
enablesavephoto: this.root.showImgMenu,
enableShowPhotoDownload: this.root.showImgMenu,
// #endif
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
}
},
/**
* @description 图片长按
*/
imgLongTap (e) {
// #ifdef APP-PLUS
const attrs = this.childs[e.currentTarget.dataset.i].attrs
if (this.opts[3] && !attrs.ignore) {
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
const save = path => {
uni.saveImageToPhotosAlbum({
filePath: path,
success () {
uni.showToast({
title: '保存成功'
})
}
})
}
if (this.root.imgList[attrs.i].startsWith('http')) {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => save(res.tempFilePath)
})
} else {
save(this.root.imgList[attrs.i])
}
}
})
}
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad (e) {
const i = e.currentTarget.dataset.i
/* #ifndef H5 || (APP-PLUS && VUE2) */
if (!this.childs[i].w) {
// 设置原宽度
this.$set(this.ctrl, i, e.detail.width)
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
// 加载完毕,取消加载中占位图
this.$set(this.ctrl, i, 1)
}
this.checkReady()
},
/**
* @description 检查是否所有图片加载完毕
*/
checkReady () {
if (this.root && !this.root.lazyLoad) {
this.root._unloadimgs -= 1
if (!this.root._unloadimgs) {
setTimeout(() => {
this.root.getRect().then(rect => {
this.root.$emit('ready', rect)
}).catch(() => {
this.root.$emit('ready', {})
})
}, 350)
}
}
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap (e) {
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
const attrs = node.attrs || e
const href = attrs.href
this.root.$emit('linktap', Object.assign({
innerText: this.root.getText(node.children || []) // 链接内的文本内容
}, attrs))
if (href) {
if (href[0] === '#') {
// 跳转锚点
this.root.navigateTo(href.substring(1)).catch(() => { })
} else if (href.split('?')[0].includes('://')) {
// 复制外部链接
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
} else {
// 跳转页面
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href,
fail () { }
})
}
})
}
}
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
// 加载其他源
if (node.name === 'video' || node.name === 'audio') {
let index = (this.ctrl[i] || 0) + 1
if (index > node.src.length) {
index = 0
}
if (index < node.src.length) {
this.$set(this.ctrl, i, index)
return
}
} else if (node.name === 'img') {
// #ifdef H5 && VUE3
if (this.opts[0] && !this.ctrl.load) return
// #endif
// 显示错误占位图
if (this.opts[2]) {
this.$set(this.ctrl, i, -1)
}
this.checkReady()
}
if (this.root) {
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
// #ifndef H5 && VUE3
errMsg: e.detail.errMsg
// #endif
})
}
}
}
}
</script>
<style>
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._block {
display: block;
}
._b,
._strong {
font-weight: bold;
}
._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 {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
{
"id": "mp-html",
"displayName": "mp-html 富文本组件【全端支持支持编辑、latex等扩展】",
"version": "v2.5.0",
"description": "一个强大的富文本组件,高效轻量,功能丰富",
"keywords": [
"富文本",
"编辑器",
"html",
"rich-text",
"editor"
],
"repository": "https://github.com/jin-yufeng/mp-html",
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/mp-html",
"type": "component-vue"
},
"uni_modules": {
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1 @@
"use strict";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:"onError",source:"img",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:"onReady"}})}function o(r,s,c){for(var d=0;d<r.length;d++)!function(d){var u=r[d],l=void 0;if(u.type&&"node"!==u.type)l=document.createTextNode(u.text.replace(/&amp;/g,"&"));else{var g=u.name;"svg"===g&&(c="http://www.w3.org/2000/svg"),"html"!==g&&"body"!==g||(g="div"),l=c?document.createElementNS(c,g):document.createElement(g);for(var p in u.attrs)l.setAttribute(p,u.attrs[p]);if(u.children&&o(u.children,l,c),"img"===g){if(window.unloadimgs+=1,l.onload=n,l.onerror=n,!l.src&&l.getAttribute("data-src")&&(l.src=l.getAttribute("data-src")),u.attrs.ignore||(l.onclick=function(e){e.stopPropagation(),uni.postMessage({data:{action:"onImgTap",attrs:t(this)}})}),a[2]){var h=new Image;h.src=l.src,l.src=a[2],h.onload=function(){l.src=this.src},h.onerror=function(){l.onerror()}}l.onerror=e}else if("a"===g)l.addEventListener("click",function(e){e.stopPropagation(),e.preventDefault();var n,o=this.getAttribute("href");o&&"#"===o[0]&&(n=(document.getElementById(o.substr(1))||{}).offsetTop),uni.postMessage({data:{action:"onLinkTap",attrs:t(this),offset:n}})},!0);else if("video"===g||"audio"===g)i.push(l),u.attrs.autoplay||u.attrs.controls||l.setAttribute("controls","true"),l.onplay=function(){if(uni.postMessage({data:{action:"onPlay"}}),a[3])for(var t=0;t<i.length;t++)i[t]!==this&&i[t].pause()},l.onerror=function(){uni.postMessage({data:{action:"onError",source:g,attrs:t(this)}})};else if("table"===g&&a[4]&&!l.style.cssText.includes("inline")){var f=document.createElement("div");f.style.overflow="auto",f.appendChild(l),l=f}else"svg"===g&&(c=void 0)}s.appendChild(l)}(d)}document.addEventListener("UniAppJSBridgeReady",function(){document.body.onclick=function(){return uni.postMessage({data:{action:"onClick"}})},uni.postMessage({data:{action:"onJSBridgeReady"}})});var a,i=[];window.setContent=function(t,e,n){var r=document.getElementById("content");e[0]&&(document.body.style.cssText=e[0]),e[5]||(r.style.userSelect="none"),n||(r.innerHTML="",i=[]),a=e,window.unloadimgs=0;var s=document.createDocumentFragment();o(t,s),r.appendChild(s);var c=r.scrollHeight;uni.postMessage({data:{action:"onLoad",height:c}}),window.unloadimgs||uni.postMessage({data:{action:"onReady",height:c}}),clearInterval(window.timer),window.timer=setInterval(function(){r.scrollHeight!==c&&(c=r.scrollHeight,uni.postMessage({data:{action:"onHeightChange",height:c}}))},350)},window.onunload=function(){clearInterval(window.timer)};

View File

@@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function t(e,t){return n.call(e,t)}var i=[],a=function(e,n){var t={options:{timestamp:+new Date},name:e,arg:n};if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){if("postMessage"===e){var a={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(a):window.__dcloud_weex_.postMessage(JSON.stringify(a))}var o={type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(o):window.__dcloud_weex_.postMessageToService(JSON.stringify(o))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:t,pageId:""},"*");if(0===i.length){var r=plus.webview.currentWebview();if(!r)throw new Error("plus.webview.currentWebview() is undefined");var d=r.parent(),s="";s=d?d.id:r.id,i.push(s)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}},"__uniapp__service");else{var w=JSON.stringify(t);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(w,",").concat(JSON.stringify(i),");"))}},o={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;a("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("redirectTo",{url:encodeURI(n)})},getEnv:function(e){window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a("postMessage",e.data||{})}},r=/uni-app/i.test(navigator.userAgent),d=/Html5Plus/i.test(navigator.userAgent),s=/complete|loaded|interactive/;var w=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var u=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var g=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var v=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);for(var l,_=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},f=[function(e){if(r||d)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&s.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),o},function(e){if(v)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(w){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(u)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){return document.addEventListener("DOMContentLoaded",e),o}],m=0;m<f.length&&!(l=f[m](_));m++);l||(l={});var E="undefined"!=typeof uni?uni:{};if(!E.navigateTo)for(var b in l)t(l,b)&&(E[b]=l[b]);return E.webView=l,E}));

View File

@@ -0,0 +1 @@
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>

View File

@@ -0,0 +1,85 @@
## 2.2.22021-12-10
- 修复 clear-icon 属性在小程序平台不生效的 bug
## 2.2.12021-12-10
- 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的 bug
## 2.2.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker)
## 2.1.52021-11-09
- 新增 提供组件设计资源,组件样式调整
## 2.1.42021-09-10
- 修复 hide-second 在移动端的 bug
- 修复 单选赋默认值时,赋值日期未高亮的 bug
- 修复 赋默认值时,移动端未正确显示时间的 bug
## 2.1.32021-09-09
- 新增 hide-second 属性,支持只使用时分,隐藏秒
## 2.1.22021-09-03
- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次
- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法
- 优化 调整字号大小,美化日历界面
- 修复 因国际化导致的 placeholder 失效的 bug
## 2.1.12021-08-24
- 新增 支持国际化
- 优化 范围选择器在 pc 端过宽的问题
## 2.1.02021-08-09
- 新增 适配 vue3
## 2.0.192021-08-09
- 新增 支持作为 uni-forms 子组件相关功能
- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug
## 2.0.182021-08-05
- 修复 type 属性动态赋值无效的 bug
- 修复 ‘确认’按钮被 tabbar 遮盖 bug
- 修复 组件未赋值时范围选左、右日历相同的 bug
## 2.0.172021-08-04
- 修复 范围选未正确显示当前值的 bug
- 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug
## 2.0.162021-07-21
- 新增 return-type 属性支持返回 date 日期对象
## 2.0.152021-07-14
- 修复 单选日期类型,初始赋值后不在当前日历的 bug
- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效)
- 优化 移动端移除显示框的清空按钮,无实际用途
## 2.0.142021-07-14
- 修复 组件赋值为空,界面未更新的 bug
- 修复 start 和 end 不能动态赋值的 bug
- 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的 bug
## 2.0.132021-07-08
- 修复 范围选择不能动态赋值的 bug
## 2.0.122021-07-08
- 修复 范围选择的初始时间在一个月内时造成无法选择的bug
## 2.0.112021-07-08
- 优化 弹出层在超出视窗边缘定位不准确的问题
## 2.0.102021-07-08
- 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的 bug
- 优化 弹出层在超出视窗边缘被遮盖的问题
## 2.0.92021-07-07
- 新增 maskClick 事件
- 修复 特殊情况日历 rpx 布局错误的 bugrpx -> px
- 修复 范围选择时清空返回值不合理的bug['', ''] -> []
## 2.0.82021-07-07
- 新增 日期时间显示框支持插槽
## 2.0.72021-07-01
- 优化 添加 uni-icons 依赖
## 2.0.62021-05-22
- 修复 图标在小程序上不显示的 bug
- 优化 重命名引用组件,避免潜在组件命名冲突
## 2.0.52021-05-20
- 优化 代码目录扁平化
## 2.0.42021-05-12
- 新增 组件示例地址
## 2.0.32021-05-10
- 修复 ios 下不识别 '-' 日期格式的 bug
- 优化 pc 下弹出层添加边框和阴影
## 2.0.22021-05-08
- 修复 在 admin 中获取弹出层定位错误的bug
## 2.0.12021-05-08
- 修复 type 属性向下兼容,默认值从 date 变更为 datetime
## 2.0.02021-04-30
- 支持日历形式的日期+时间的范围选择
> 注意此版本不向后兼容不再支持单独时间选择type=time及相关的 hide-second 属性(时间选可使用内置组件 picker
## 1.0.62021-03-18
- 新增 hide-second 属性,时间支持仅选择时、分
- 修复 选择跟显示的日期不一样的 bug
- 修复 chang事件触发2次的 bug
- 修复 分、秒 end 范围错误的 bug
- 优化 更好的 nvue 适配

View File

@@ -0,0 +1,185 @@
<template>
<view class="uni-calendar-item__weeks-box" :class="{
'uni-calendar-item--disable':weeks.disable,
'uni-calendar-item--before-checked-x':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked-x':weeks.afterMultiple,
}" @click="choiceDate(weeks)" @mouseenter="handleMousemove(weeks)">
<view class="uni-calendar-item__weeks-box-item" :class="{
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && (calendar.userChecked || !checkHover),
'uni-calendar-item--checked-range-text': checkHover,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">
<text v-if="selected&&weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
<text class="uni-calendar-item__weeks-box-text uni-calendar-item__weeks-box-text-disable uni-calendar-item--checked-text">{{weeks.date}}</text>
</view>
<view :class="{'uni-calendar-item--isDay': weeks.isDay}"></view>
</view>
</template>
<script>
export default {
props: {
weeks: {
type: Object,
default () {
return {}
}
},
calendar: {
type: Object,
default: () => {
return {}
}
},
selected: {
type: Array,
default: () => {
return []
}
},
lunar: {
type: Boolean,
default: false
},
checkHover: {
type: Boolean,
default: false
}
},
methods: {
choiceDate(weeks) {
this.$emit('change', weeks)
},
handleMousemove(weeks) {
this.$emit('handleMouse', weeks)
}
}
}
</script>
<style lang="scss" scoped>
.uni-calendar-item__weeks-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
margin: 1px 0;
position: relative;
}
.uni-calendar-item__weeks-box-text {
font-size: 14px;
// font-family: Lato-Bold, Lato;
font-weight: bold;
color: #455997;
}
.uni-calendar-item__weeks-lunar-text {
font-size: 12px;
color: #333;
}
.uni-calendar-item__weeks-box-item {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-calendar-item__weeks-box-circle {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 8px;
background-color: #dd524d;
}
.uni-calendar-item__weeks-box .uni-calendar-item--disable {
// background-color: rgba(249, 249, 249, $uni-opacity-disabled);
cursor: default;
}
.uni-calendar-item--disable .uni-calendar-item__weeks-box-text-disable {
color: #D1D1D1;
}
.uni-calendar-item--isDay {
position: absolute;
top: 10px;
right: 17%;
background-color: #dd524d;
width:6px;
height: 6px;
border-radius: 50%;
}
.uni-calendar-item--extra {
color: #dd524d;
opacity: 0.8;
}
.uni-calendar-item__weeks-box .uni-calendar-item--checked {
background-color: #007aff;
border-radius: 50%;
box-sizing: border-box;
border: 3px solid #fff;
}
.uni-calendar-item--checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--checked-range-text {
color: #333;
}
.uni-calendar-item--multiple {
background-color: #F6F7FC;
// color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--before-checked,
.uni-calendar-item--multiple .uni-calendar-item--after-checked {
background-color: #409eff;
border-radius: 50%;
box-sizing: border-box;
border: 3px solid #F6F7FC;
}
.uni-calendar-item--before-checked .uni-calendar-item--checked-text,
.uni-calendar-item--after-checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--before-checked-x {
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
box-sizing: border-box;
background-color: #F6F7FC;
}
.uni-calendar-item--after-checked-x {
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
background-color: #F6F7FC;
}
</style>

View File

@@ -0,0 +1,898 @@
<template>
<view class="uni-calendar" @mouseleave="leaveCale">
<view v-if="!insert&&show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}"
@click="clean"></view>
<view v-if="insert || show" class="uni-calendar__content"
:class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow, 'uni-calendar__content-mobile': aniMaskShow}">
<view class="uni-calendar__header" :class="{'uni-calendar__header-mobile' :!insert}">
<view v-if="left" class="uni-calendar__header-btn-box" @click.stop="pre">
<view class="uni-calendar__header-btn uni-calendar--left"></view>
</view>
<picker mode="date" :value="date" fields="month" @change="bindDateChange">
<text
class="uni-calendar__header-text">{{ (nowDate.year||'') + ' 年 ' + ( nowDate.month||'') +' 月'}}</text>
</picker>
<view v-if="right" class="uni-calendar__header-btn-box" @click.stop="next">
<view class="uni-calendar__header-btn uni-calendar--right"></view>
</view>
<view v-if="!insert" class="dialog-close" @click="clean">
<view class="dialog-close-plus" data-id="close"></view>
<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
</view>
<!-- <text class="uni-calendar__backtoday" @click="backtoday">回到今天</text> -->
</view>
<view class="uni-calendar__box">
<view v-if="showMonth" class="uni-calendar__box-bg">
<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
</view>
<view class="uni-calendar__weeks" style="padding-bottom: 7px;">
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{monText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
</view>
</view>
<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar"
:selected="selected" :lunar="lunar" :checkHover="range" @change="choiceDate"
@handleMouse="handleMouse">
</calendar-item>
</view>
</view>
</view>
<view v-if="!insert && !range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top"
style="padding: 0 80px;">
<view class="uni-date-changed--time-date">{{tempSingleDate ? tempSingleDate : selectDateText}}</view>
<time-picker type="time" :start="reactStartTime" :end="reactEndTime" v-model="time"
:disabled="!tempSingleDate" :border="false" :hide-second="hideSecond" class="time-picker-style">
</time-picker>
</view>
<view v-if="!insert && range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top">
<view class="uni-date-changed--time-start">
<view class="uni-date-changed--time-date">{{tempRange.before ? tempRange.before : startDateText}}
</view>
<time-picker type="time" :start="reactStartTime" v-model="timeRange.startTime" :border="false"
:hide-second="hideSecond" :disabled="!tempRange.before" class="time-picker-style">
</time-picker>
</view>
<uni-icons type="arrowthinright" color="#999" style="line-height: 50px;"></uni-icons>
<view class="uni-date-changed--time-end">
<view class="uni-date-changed--time-date">{{tempRange.after ? tempRange.after : endDateText}}</view>
<time-picker type="time" :end="reactEndTime" v-model="timeRange.endTime" :border="false"
:hide-second="hideSecond" :disabled="!tempRange.after" class="time-picker-style">
</time-picker>
</view>
</view>
<view v-if="!insert" class="uni-date-changed uni-date-btn--ok">
<!-- <view class="uni-calendar__header-btn-box">
<text class="uni-calendar__button-text uni-calendar--fixed-width">{{okText}}</text>
</view> -->
<view class="uni-datetime-picker--btn" @click="confirm">确认</view>
</view>
</view>
</view>
</template>
<script>
import Calendar from './util.js';
import calendarItem from './calendar-item.vue'
import timePicker from './time-picker.vue'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
/**
* Calendar 日历
* @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
* @tutorial https://ext.dcloud.net.cn/plugin?id=56
* @property {String} date 自定义当前时间,默认为今天
* @property {Boolean} lunar 显示农历
* @property {String} startDate 日期选择范围-开始日期
* @property {String} endDate 日期选择范围-结束日期
* @property {Boolean} range 范围选择
* @property {Boolean} insert = [true|false] 插入模式,默认为false
* @value true 弹窗模式
* @value false 插入模式
* @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
* @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
* @property {Boolean} showMonth 是否选择月份为背景
* @event {Function} change 日期改变,`insert :ture` 时生效
* @event {Function} confirm 确认选择`insert :false` 时生效
* @event {Function} monthSwitch 切换月份时触发
* @example <uni-calendar :insert="true":lunar="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
*/
export default {
components: {
calendarItem,
timePicker
},
props: {
date: {
type: String,
default: ''
},
defTime: {
type: [String, Object],
default: ''
},
selectableTimes: {
type: [Object],
default () {
return {}
}
},
selected: {
type: Array,
default () {
return []
}
},
lunar: {
type: Boolean,
default: false
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
range: {
type: Boolean,
default: false
},
typeHasTime: {
type: Boolean,
default: false
},
insert: {
type: Boolean,
default: true
},
showMonth: {
type: Boolean,
default: true
},
clearDate: {
type: Boolean,
default: true
},
left: {
type: Boolean,
default: true
},
right: {
type: Boolean,
default: true
},
checkHover: {
type: Boolean,
default: true
},
hideSecond: {
type: [Boolean],
default: false
},
pleStatus: {
type: Object,
default () {
return {
before: '',
after: '',
data: [],
fulldate: ''
}
}
}
},
data() {
return {
show: false,
weeks: [],
calendar: {},
nowDate: '',
aniMaskShow: false,
firstEnter: true,
time: '',
timeRange: {
startTime: '',
endTime: ''
},
tempSingleDate: '',
tempRange: {
before: '',
after: ''
}
}
},
watch: {
date: {
immediate: true,
handler(newVal, oldVal) {
if (!this.range) {
this.tempSingleDate = newVal
setTimeout(() => {
this.init(newVal)
}, 100)
}
}
},
defTime: {
immediate: true,
handler(newVal, oldVal) {
if (!this.range) {
this.time = newVal
} else {
// console.log('-----', newVal);
this.timeRange.startTime = newVal.start
this.timeRange.endTime = newVal.end
}
}
},
startDate(val) {
this.cale.resetSatrtDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
endDate(val) {
this.cale.resetEndDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
selected(newVal) {
this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
this.weeks = this.cale.weeks
},
pleStatus: {
immediate: true,
handler(newVal, oldVal) {
const {
before,
after,
fulldate,
which
} = newVal
this.tempRange.before = before
this.tempRange.after = after
setTimeout(() => {
if (fulldate) {
this.cale.setHoverMultiple(fulldate)
if (before && after) {
this.cale.lastHover = true
if (this.rangeWithinMonth(after, before)) return
this.setDate(before)
} else {
this.cale.setMultiple(fulldate)
this.setDate(this.nowDate.fullDate)
this.calendar.fullDate = ''
this.cale.lastHover = false
}
} else {
this.cale.setDefaultMultiple(before, after)
if (which === 'left') {
this.setDate(before)
this.weeks = this.cale.weeks
} else {
this.setDate(after)
this.weeks = this.cale.weeks
}
this.cale.lastHover = true
}
}, 16)
}
}
},
computed: {
reactStartTime() {
const activeDate = this.range ? this.tempRange.before : this.calendar.fullDate
const res = activeDate === this.startDate ? this.selectableTimes.start : ''
return res
},
reactEndTime() {
const activeDate = this.range ? this.tempRange.after : this.calendar.fullDate
const res = activeDate === this.endDate ? this.selectableTimes.end : ''
return res
},
/**
* for i18n
*/
selectDateText() {
return t("uni-datetime-picker.selectDate")
},
startDateText() {
return this.startPlaceholder || t("uni-datetime-picker.startDate")
},
endDateText() {
return this.endPlaceholder || t("uni-datetime-picker.endDate")
},
okText() {
return t("uni-datetime-picker.ok")
},
monText() {
return t("uni-calender.MON")
},
TUEText() {
return t("uni-calender.TUE")
},
WEDText() {
return t("uni-calender.WED")
},
THUText() {
return t("uni-calender.THU")
},
FRIText() {
return t("uni-calender.FRI")
},
SATText() {
return t("uni-calender.SAT")
},
SUNText() {
return t("uni-calender.SUN")
},
},
created() {
// 获取日历方法实例
this.cale = new Calendar({
// date: new Date(),
selected: this.selected,
startDate: this.startDate,
endDate: this.endDate,
range: this.range,
// multipleStatus: this.pleStatus
})
// 选中某一天
// this.cale.setDate(this.date)
this.init(this.date)
// this.setDay
},
methods: {
leaveCale() {
this.firstEnter = true
},
handleMouse(weeks) {
if (weeks.disable) return
if (this.cale.lastHover) return
let {
before,
after
} = this.cale.multipleStatus
if (!before) return
this.calendar = weeks
// 设置范围选
this.cale.setHoverMultiple(this.calendar.fullDate)
this.weeks = this.cale.weeks
// hover时进入一个日历更新另一个
if (this.firstEnter) {
this.$emit('firstEnterCale', this.cale.multipleStatus)
this.firstEnter = false
}
},
rangeWithinMonth(A, B) {
const [yearA, monthA] = A.split('-')
const [yearB, monthB] = B.split('-')
return yearA === yearB && monthA === monthB
},
// 取消穿透
clean() {
this.close()
},
clearCalender() {
if (this.range) {
this.timeRange.startTime = ''
this.timeRange.endTime = ''
this.tempRange.before = ''
this.tempRange.after = ''
this.cale.multipleStatus.before = ''
this.cale.multipleStatus.after = ''
this.cale.multipleStatus.data = []
this.cale.lastHover = false
} else {
this.time = ''
this.tempSingleDate = ''
}
this.calendar.fullDate = ''
this.setDate()
},
bindDateChange(e) {
const value = e.detail.value + '-1'
this.init(value)
},
/**
* 初始化日期显示
* @param {Object} date
*/
init(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.calendar = this.cale.getInfo(date)
},
// choiceDate(weeks) {
// if (weeks.disable) return
// this.calendar = weeks
// // 设置多选
// this.cale.setMultiple(this.calendar.fullDate, true)
// this.weeks = this.cale.weeks
// this.tempSingleDate = this.calendar.fullDate
// this.tempRange.before = this.cale.multipleStatus.before
// this.tempRange.after = this.cale.multipleStatus.after
// this.change()
// },
/**
* 打开日历弹窗
*/
open() {
// 弹窗模式并且清理数据
if (this.clearDate && !this.insert) {
this.cale.cleanMultipleStatus()
// this.cale.setDate(this.date)
this.init(this.date)
}
this.show = true
this.$nextTick(() => {
setTimeout(() => {
this.aniMaskShow = true
}, 50)
})
},
/**
* 关闭日历弹窗
*/
close() {
this.aniMaskShow = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
this.$emit('close')
}, 300)
})
},
/**
* 确认按钮
*/
confirm() {
this.setEmit('confirm')
this.close()
},
/**
* 变化触发
*/
change() {
if (!this.insert) return
this.setEmit('change')
},
/**
* 选择月份触发
*/
monthSwitch() {
let {
year,
month
} = this.nowDate
this.$emit('monthSwitch', {
year,
month: Number(month)
})
},
/**
* 派发事件
* @param {Object} name
*/
setEmit(name) {
let {
year,
month,
date,
fullDate,
lunar,
extraInfo
} = this.calendar
this.$emit(name, {
range: this.cale.multipleStatus,
year,
month,
date,
time: this.time,
timeRange: this.timeRange,
fulldate: fullDate,
lunar,
extraInfo: extraInfo || {}
})
},
/**
* 选择天触发
* @param {Object} weeks
*/
choiceDate(weeks) {
if (weeks.disable) return
this.calendar = weeks
this.calendar.userChecked = true
// 设置多选
this.cale.setMultiple(this.calendar.fullDate, true)
this.weeks = this.cale.weeks
this.tempSingleDate = this.calendar.fullDate
this.tempRange.before = this.cale.multipleStatus.before
this.tempRange.after = this.cale.multipleStatus.after
this.change()
},
/**
* 回到今天
*/
backtoday() {
let date = this.cale.getDate(new Date()).fullDate
// this.cale.setDate(date)
this.init(date)
this.change()
},
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
},
/**
* 上个月
*/
pre() {
const preDate = this.cale.getDate(this.nowDate.fullDate, -1, 'month').fullDate
this.setDate(preDate)
this.monthSwitch()
},
/**
* 下个月
*/
next() {
const nextDate = this.cale.getDate(this.nowDate.fullDate, +1, 'month').fullDate
this.setDate(nextDate)
this.monthSwitch()
},
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
}
}
}
</script>
<style lang="scss" scoped>
.uni-calendar {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.uni-calendar__mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--mask-show {
opacity: 1
}
.uni-calendar--fixed {
position: fixed;
bottom: calc(var(--window-bottom));
left: 0;
right: 0;
transition-property: transform;
transition-duration: 0.3s;
transform: translateY(460px);
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--ani-show {
transform: translateY(0);
}
.uni-calendar__content {
background-color: #fff;
}
.uni-calendar__content-mobile {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.1);
}
.uni-calendar__header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
height: 50px;
}
.uni-calendar__header-mobile {
padding: 10px;
padding-bottom: 0;
}
.uni-calendar--fixed-top {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
border-top-color: rgba(0, 0, 0, 0.4);
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--fixed-width {
width: 50px;
}
.uni-calendar__backtoday {
position: absolute;
right: 0;
top: 25rpx;
padding: 0 5px;
padding-left: 10px;
height: 25px;
line-height: 25px;
font-size: 12px;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
color: #fff;
background-color: #f1f1f1;
}
.uni-calendar__header-text {
text-align: center;
width: 100px;
font-size: 15px;
color: #666;
}
.uni-calendar__button-text {
text-align: center;
width: 100px;
font-size: 14px;
color: #007aff;
/* #ifndef APP-NVUE */
letter-spacing: 3px;
/* #endif */
}
.uni-calendar__header-btn-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}
.uni-calendar__header-btn {
width: 9px;
height: 9px;
border-left-color: #808080;
border-left-style: solid;
border-left-width: 1px;
border-top-color: #555555;
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--left {
transform: rotate(-45deg);
}
.uni-calendar--right {
transform: rotate(135deg);
}
.uni-calendar__weeks {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-calendar__weeks-item {
flex: 1;
}
.uni-calendar__weeks-day {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 40px;
border-bottom-color: #F5F5F5;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar__weeks-day-text {
font-size: 12px;
color: #B2B2B2;
}
.uni-calendar__box {
position: relative;
// padding: 0 10px;
padding-bottom: 7px;
}
.uni-calendar__box-bg {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.uni-calendar__box-bg-text {
font-size: 200px;
font-weight: bold;
color: #999;
opacity: 0.1;
text-align: center;
/* #ifndef APP-NVUE */
line-height: 1;
/* #endif */
}
.uni-date-changed {
padding: 0 10px;
// line-height: 50px;
text-align: center;
color: #333;
border-top-color: #DCDCDC;
;
border-top-style: solid;
border-top-width: 1px;
flex: 1;
}
.uni-date-btn--ok {
padding: 20px 15px;
}
.uni-date-changed--time-start {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-end {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-date {
color: #999;
line-height: 50px;
margin-right: 5px;
// opacity: 0.6;
}
.time-picker-style {
// width: 62px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center
}
.mr-10 {
margin-right: 10px;
}
.dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 0 25px;
margin-top: 10px;
}
.dialog-close-plus {
width: 16px;
height: 2px;
background-color: #737987;
border-radius: 2px;
transform: rotate(45deg);
}
.dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.uni-datetime-picker--btn {
border-radius: 100px;
height: 40px;
line-height: 40px;
background-color: #007aff;
color: #fff;
font-size: 16px;
letter-spacing: 5px;
}
/* #ifndef APP-NVUE */
.uni-datetime-picker--btn:active {
opacity: 0.7;
}
/* #endif */
</style>

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "select date",
"uni-datetime-picker.selectTime": "select time",
"uni-datetime-picker.selectDateTime": "select datetime",
"uni-datetime-picker.startDate": "start date",
"uni-datetime-picker.endDate": "end date",
"uni-datetime-picker.startTime": "start time",
"uni-datetime-picker.endTime": "end time",
"uni-datetime-picker.ok": "ok",
"uni-datetime-picker.clear": "clear",
"uni-datetime-picker.cancel": "cancel",
"uni-calender.MON": "MON",
"uni-calender.TUE": "TUE",
"uni-calender.WED": "WED",
"uni-calender.THU": "THU",
"uni-calender.FRI": "FRI",
"uni-calender.SAT": "SAT",
"uni-calender.SUN": "SUN"
}

View File

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

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "选择日期",
"uni-datetime-picker.selectTime": "选择时间",
"uni-datetime-picker.selectDateTime": "选择日期时间",
"uni-datetime-picker.startDate": "开始日期",
"uni-datetime-picker.endDate": "结束日期",
"uni-datetime-picker.startTime": "开始时间",
"uni-datetime-picker.endTime": "结束时间",
"uni-datetime-picker.ok": "确定",
"uni-datetime-picker.clear": "清除",
"uni-datetime-picker.cancel": "取消",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

@@ -0,0 +1,19 @@
{
"uni-datetime-picker.selectDate": "選擇日期",
"uni-datetime-picker.selectTime": "選擇時間",
"uni-datetime-picker.selectDateTime": "選擇日期時間",
"uni-datetime-picker.startDate": "開始日期",
"uni-datetime-picker.endDate": "結束日期",
"uni-datetime-picker.startTime": "開始时间",
"uni-datetime-picker.endTime": "結束时间",
"uni-datetime-picker.ok": "確定",
"uni-datetime-picker.clear": "清除",
"uni-datetime-picker.cancel": "取消",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}

View File

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

View File

@@ -0,0 +1,927 @@
<template>
<view class="uni-datetime-picker">
<view @click="initTimePicker">
<slot>
<view class="uni-datetime-picker-timebox-pointer"
:class="{'uni-datetime-picker-disabled': disabled, 'uni-datetime-picker-timebox': border}">
<text class="uni-datetime-picker-text">{{time}}</text>
<view v-if="!time" class="uni-datetime-picker-time">
<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
</view>
</view>
</slot>
</view>
<view v-if="visible" id="mask" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view>
<view v-if="visible" class="uni-datetime-picker-popup" :class="[dateShow && timeShow ? '' : 'fix-nvue-height']"
:style="fixNvueBug">
<view class="uni-title">
<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
</view>
<view v-if="dateShow" class="uni-datetime-picker__container-box">
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd"
@change="bindDateChange">
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
</picker-view>
<!-- 兼容 nvue 不支持伪类 -->
<text class="uni-datetime-picker-sign sign-left">-</text>
<text class="uni-datetime-picker-sign sign-right">-</text>
</view>
<view v-if="timeShow" class="uni-datetime-picker__container-box">
<picker-view class="uni-datetime-picker-view" :class="[hideSecond ? 'time-hide-second' : '']"
:indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange">
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
<picker-view-column v-if="!hideSecond">
<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">
<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
</view>
</picker-view-column>
</picker-view>
<!-- 兼容 nvue 不支持伪类 -->
<text class="uni-datetime-picker-sign" :class="[hideSecond ? 'sign-center' : 'sign-left']">:</text>
<text v-if="!hideSecond" class="uni-datetime-picker-sign sign-right">:</text>
</view>
<view class="uni-datetime-picker-btn">
<view @click="clearTime">
<text class="uni-datetime-picker-btn-text">{{clearText}}</text>
</view>
<view class="uni-datetime-picker-btn-group">
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">
<text class="uni-datetime-picker-btn-text">{{cancelText}}</text>
</view>
<view @click="setTime">
<text class="uni-datetime-picker-btn-text">{{okText}}</text>
</view>
</view>
</view>
</view>
<!-- #ifdef H5 -->
<!-- <keypress v-if="visible" @esc="tiggerTimePicker" @enter="setTime" /> -->
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress'
// #endif
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const { t } = initVueI18n(messages)
/**
* DatetimePicker 时间选择器
* @description 可以同时选择日期和时间的选择器
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} type = [datetime | date | time] 显示模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {String|Number} value 默认值
* @property {String|Number} start 起始日期或时间
* @property {String|Number} end 起始日期或时间
* @property {String} return-type = [timestamp | string]
* @event {Function} change 选中发生变化触发
*/
export default {
name: 'UniDatetimePicker',
components: {
// #ifdef H5
keypress
// #endif
},
data() {
return {
indicatorStyle: `height: 50px;`,
visible: false,
fixNvueBug: {},
dateShow: true,
timeShow: true,
title: '日期和时间',
// 输入框当前时间
time: '',
// 当前的年月日时分秒
year: 1920,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
// 起始时间
startYear: 1920,
startMonth: 1,
startDay: 1,
startHour: 0,
startMinute: 0,
startSecond: 0,
// 结束时间
endYear: 2120,
endMonth: 12,
endDay: 31,
endHour: 23,
endMinute: 59,
endSecond: 59,
}
},
props: {
type: {
type: String,
default: 'datetime'
},
value: {
type: [String, Number],
default: ''
},
modelValue: {
type: [String, Number],
default: ''
},
start: {
type: [Number, String],
default: ''
},
end: {
type: [Number, String],
default: ''
},
returnType: {
type: String,
default: 'string'
},
disabled: {
type: [Boolean, String],
default: false
},
border: {
type: [Boolean, String],
default: true
},
hideSecond: {
type: [Boolean, String],
default: false
}
},
watch: {
value: {
handler(newVal, oldVal) {
if (newVal) {
this.parseValue(this.fixIosDateFormat(newVal)) //兼容 iOS、safari 日期格式
this.initTime(false)
} else {
this.time = ''
this.parseValue(Date.now())
}
},
immediate: true
},
type: {
handler(newValue) {
if (newValue === 'date') {
this.dateShow = true
this.timeShow = false
this.title = '日期'
} else if (newValue === 'time') {
this.dateShow = false
this.timeShow = true
this.title = '时间'
} else {
this.dateShow = true
this.timeShow = true
this.title = '日期和时间'
}
},
immediate: true
},
start: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'start') //兼容 iOS、safari 日期格式
},
immediate: true
},
end: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'end') //兼容 iOS、safari 日期格式
},
immediate: true
},
// 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项
months(newVal) {
this.checkValue('month', this.month, newVal)
},
days(newVal) {
this.checkValue('day', this.day, newVal)
},
hours(newVal) {
this.checkValue('hour', this.hour, newVal)
},
minutes(newVal) {
this.checkValue('minute', this.minute, newVal)
},
seconds(newVal) {
this.checkValue('second', this.second, newVal)
}
},
computed: {
// 当前年、月、日、时、分、秒选择范围
years() {
return this.getCurrentRange('year')
},
months() {
return this.getCurrentRange('month')
},
days() {
return this.getCurrentRange('day')
},
hours() {
return this.getCurrentRange('hour')
},
minutes() {
return this.getCurrentRange('minute')
},
seconds() {
return this.getCurrentRange('second')
},
// picker 当前值数组
ymd() {
return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay]
},
hms() {
return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond]
},
// 当前 date 是 start
currentDateIsStart() {
return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay
},
// 当前 date 是 end
currentDateIsEnd() {
return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay
},
// 当前年、月、日、时、分、秒的最小值和最大值
minYear() {
return this.startYear
},
maxYear() {
return this.endYear
},
minMonth() {
if (this.year === this.startYear) {
return this.startMonth
} else {
return 1
}
},
maxMonth() {
if (this.year === this.endYear) {
return this.endMonth
} else {
return 12
}
},
minDay() {
if (this.year === this.startYear && this.month === this.startMonth) {
return this.startDay
} else {
return 1
}
},
maxDay() {
if (this.year === this.endYear && this.month === this.endMonth) {
return this.endDay
} else {
return this.daysInMonth(this.year, this.month)
}
},
minHour() {
if (this.type === 'datetime') {
if (this.currentDateIsStart) {
return this.startHour
} else {
return 0
}
}
if (this.type === 'time') {
return this.startHour
}
},
maxHour() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd) {
return this.endHour
} else {
return 23
}
}
if (this.type === 'time') {
return this.endHour
}
},
minMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
},
maxMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.endHour) {
return this.endMinute
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour) {
return this.endMinute
} else {
return 59
}
}
},
minSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
},
maxSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.endHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
},
/**
* for i18n
*/
selectTimeText() {
return t("uni-datetime-picker.selectTime")
},
okText() {
return t("uni-datetime-picker.ok")
},
clearText() {
return t("uni-datetime-picker.clear")
},
cancelText() {
return t("uni-datetime-picker.cancel")
}
},
mounted() {
// #ifdef APP-NVUE
const res = uni.getSystemInfoSync();
this.fixNvueBug = {
top: res.windowHeight / 2,
left: res.windowWidth / 2
}
// #endif
},
methods: {
/**
* @param {Object} item
* 小于 10 在前面加个 0
*/
lessThanTen(item) {
return item < 10 ? '0' + item : item
},
/**
* 解析时分秒字符串例如00:00:00
* @param {String} timeString
*/
parseTimeType(timeString) {
if (timeString) {
let timeArr = timeString.split(':')
this.hour = Number(timeArr[0])
this.minute = Number(timeArr[1])
this.second = Number(timeArr[2])
}
},
/**
* 解析选择器初始值类型可以是字符串、时间戳例如2000-10-02、'08:30:00'、 1610695109000
* @param {String | Number} datetime
*/
initPickerValue(datetime) {
let defaultValue = null
if (datetime) {
defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end)
} else {
defaultValue = Date.now()
defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end)
}
this.parseValue(defaultValue)
},
/**
* 初始值规则:
* - 用户设置初始值 value
* - 设置了起始时间 start、终止时间 end并 start < value < end初始值为 value 否则初始值为 start
* - 只设置了起始时间 start并 start < value初始值为 value否则初始值为 start
* - 只设置了终止时间 end并 value < end初始值为 value否则初始值为 end
* - 无起始终止时间,则初始值为 value
* - 无初始值 value则初始值为当前本地时间 Date.now()
* @param {Object} value
* @param {Object} dateBase
*/
compareValueWithStartAndEnd(value, start, end) {
let winner = null
value = this.superTimeStamp(value)
start = this.superTimeStamp(start)
end = this.superTimeStamp(end)
if (start && end) {
if (value < start) {
winner = new Date(start)
} else if (value > end) {
winner = new Date(end)
} else {
winner = new Date(value)
}
} else if (start && !end) {
winner = start <= value ? new Date(value) : new Date(start)
} else if (!start && end) {
winner = value <= end ? new Date(value) : new Date(end)
} else {
winner = new Date(value)
}
return winner
},
/**
* 转换为可比较的时间戳,接受日期、时分秒、时间戳
* @param {Object} value
*/
superTimeStamp(value) {
let dateBase = ''
if (this.type === 'time' && value && typeof value === 'string') {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
dateBase = year + '/' + month + '/' + day + ' '
}
if (Number(value) && typeof value !== NaN) {
value = parseInt(value)
dateBase = 0
}
return this.createTimeStamp(dateBase + value)
},
/**
* 解析默认值 value字符串、时间戳
* @param {Object} defaultTime
*/
parseValue(value) {
if (!value) {
return
}
if (this.type === 'time' && typeof value === "string") {
this.parseTimeType(value)
} else {
let defaultDate = null
defaultDate = new Date(value)
if (this.type !== 'time') {
this.year = defaultDate.getFullYear()
this.month = defaultDate.getMonth() + 1
this.day = defaultDate.getDate()
}
if (this.type !== 'date') {
this.hour = defaultDate.getHours()
this.minute = defaultDate.getMinutes()
this.second = defaultDate.getSeconds()
}
}
if (this.hideSecond) {
this.second = 0
}
},
/**
* 解析可选择时间范围 start、end年月日字符串、时间戳
* @param {Object} defaultTime
*/
parseDatetimeRange(point, pointType) {
// 时间为空,则重置为初始值
if (!point) {
if (pointType === 'start') {
this.startYear = 1920
this.startMonth = 1
this.startDay = 1
this.startHour = 0
this.startMinute = 0
this.startSecond = 0
}
if (pointType === 'end') {
this.endYear = 2120
this.endMonth = 12
this.endDay = 31
this.endHour = 23
this.endMinute = 59
this.endSecond = 59
}
return
}
if (this.type === 'time') {
const pointArr = point.split(':')
this[pointType + 'Hour'] = Number(pointArr[0])
this[pointType + 'Minute'] = Number(pointArr[1])
this[pointType + 'Second'] = Number(pointArr[2])
} else {
if (!point) {
pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60
return
}
if (Number(point) && Number(point) !== NaN) {
point = parseInt(point)
}
// datetime 的 end 没有时分秒, 则不限制
const hasTime = /[0-9]:[0-9]/
if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test(
point)) {
point = point + ' 23:59:59'
}
const pointDate = new Date(point)
this[pointType + 'Year'] = pointDate.getFullYear()
this[pointType + 'Month'] = pointDate.getMonth() + 1
this[pointType + 'Day'] = pointDate.getDate()
if (this.type === 'datetime') {
this[pointType + 'Hour'] = pointDate.getHours()
this[pointType + 'Minute'] = pointDate.getMinutes()
this[pointType + 'Second'] = pointDate.getSeconds()
}
}
},
// 获取 年、月、日、时、分、秒 当前可选范围
getCurrentRange(value) {
const range = []
for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) {
range.push(i)
}
return range
},
// 字符串首字母大写
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
// 检查当前值是否在范围内,不在则当前值重置为可选范围第一项
checkValue(name, value, values) {
if (values.indexOf(value) === -1) {
this[name] = values[0]
}
},
// 每个月的实际天数
daysInMonth(year, month) { // Use 1 for January, 2 for February, etc.
return new Date(year, month, 0).getDate();
},
//兼容 iOS、safari 日期格式
fixIosDateFormat(value) {
if (typeof value === 'string') {
value = value.replace(/-/g, '/')
}
return value
},
/**
* 生成时间戳
* @param {Object} time
*/
createTimeStamp(time) {
if (!time) return
if (typeof time === "number") {
return time
} else {
time = time.replace(/-/g, '/')
if (this.type === 'date') {
time = time + ' ' + '00:00:00'
}
return Date.parse(time)
}
},
/**
* 生成日期或时间的字符串
*/
createDomSting() {
const yymmdd = this.year +
'-' +
this.lessThanTen(this.month) +
'-' +
this.lessThanTen(this.day)
let hhmmss = this.lessThanTen(this.hour) +
':' +
this.lessThanTen(this.minute)
if (!this.hideSecond) {
hhmmss = hhmmss + ':' + this.lessThanTen(this.second)
}
if (this.type === 'date') {
return yymmdd
} else if (this.type === 'time') {
return hhmmss
} else {
return yymmdd + ' ' + hhmmss
}
},
/**
* 初始化返回值,并抛出 change 事件
*/
initTime(emit = true) {
this.time = this.createDomSting()
if (!emit) return
if (this.returnType === 'timestamp' && this.type !== 'time') {
this.$emit('change', this.createTimeStamp(this.time))
this.$emit('input', this.createTimeStamp(this.time))
this.$emit('update:modelValue', this.createTimeStamp(this.time))
} else {
this.$emit('change', this.time)
this.$emit('input', this.time)
this.$emit('update:modelValue', this.time)
}
},
/**
* 用户选择日期或时间更新 data
* @param {Object} e
*/
bindDateChange(e) {
const val = e.detail.value
this.year = this.years[val[0]]
this.month = this.months[val[1]]
this.day = this.days[val[2]]
},
bindTimeChange(e) {
const val = e.detail.value
this.hour = this.hours[val[0]]
this.minute = this.minutes[val[1]]
this.second = this.seconds[val[2]]
},
/**
* 初始化弹出层
*/
initTimePicker() {
if (this.disabled) return
const value = this.fixIosDateFormat(this.value)
this.initPickerValue(value)
this.visible = !this.visible
},
/**
* 触发或关闭弹框
*/
tiggerTimePicker(e) {
this.visible = !this.visible
},
/**
* 用户点击“清空”按钮,清空当前值
*/
clearTime() {
this.time = ''
this.$emit('change', this.time)
this.$emit('input', this.time)
this.$emit('update:modelValue', this.time)
this.tiggerTimePicker()
},
/**
* 用户点击“确定”按钮
*/
setTime() {
this.initTime()
this.tiggerTimePicker()
}
}
}
</script>
<style>
.uni-datetime-picker {
/* #ifndef APP-NVUE */
/* width: 100%; */
/* #endif */
}
.uni-datetime-picker-view {
height: 130px;
width: 270px;
/* #ifndef APP-NVUE */
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-item {
height: 50px;
line-height: 50px;
text-align: center;
font-size: 14px;
}
.uni-datetime-picker-btn {
margin-top: 60px;
/* #ifndef APP-NVUE */
display: flex;
cursor: pointer;
/* #endif */
flex-direction: row;
justify-content: space-between;
}
.uni-datetime-picker-btn-text {
font-size: 14px;
color: #007AFF;
}
.uni-datetime-picker-btn-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-datetime-picker-cancel {
margin-right: 30px;
}
.uni-datetime-picker-mask {
position: fixed;
bottom: 0px;
top: 0px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.4);
transition-duration: 0.3s;
z-index: 998;
}
.uni-datetime-picker-popup {
border-radius: 8px;
padding: 30px;
width: 270px;
/* #ifdef APP-NVUE */
height: 500px;
/* #endif */
/* #ifdef APP-NVUE */
width: 330px;
/* #endif */
background-color: #fff;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition-duration: 0.3s;
z-index: 999;
}
.fix-nvue-height {
/* #ifdef APP-NVUE */
height: 330px;
/* #endif */
}
.uni-datetime-picker-time {
color: grey;
}
.uni-datetime-picker-column {
height: 50px;
}
.uni-datetime-picker-timebox {
border: 1px solid #E5E5E5;
border-radius: 5px;
padding: 7px 10px;
/* #ifndef APP-NVUE */
box-sizing: border-box;
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-timebox-pointer {
/* #ifndef APP-NVUE */
cursor: pointer;
/* #endif */
}
.uni-datetime-picker-disabled {
opacity: 0.4;
/* #ifdef H5 */
cursor: not-allowed !important;
/* #endif */
}
.uni-datetime-picker-text {
font-size: 14px;
}
.uni-datetime-picker-sign {
position: absolute;
top: 53px;
/* 减掉 10px 的元素高度兼容nvue */
color: #999;
/* #ifdef APP-NVUE */
font-size: 16px;
/* #endif */
}
.sign-left {
left: 86px;
}
.sign-right {
right: 86px;
}
.sign-center {
left: 135px;
}
.uni-datetime-picker__container-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-top: 40px;
}
.time-hide-second {
width: 180px;
}
</style>

View File

@@ -0,0 +1,981 @@
<template>
<view class="uni-date">
<view class="uni-date-editor" @click="show">
<slot>
<view class="uni-date-editor--x" :class="{'uni-date-editor--x__disabled': disabled,
'uni-date-x--border': border}">
<view v-if="!isRange" class="uni-date-x uni-date-single">
<uni-icons type="calendar" color="#e1e1e1" size="22"></uni-icons>
<input class="uni-date__x-input" type="text" v-model="singleVal"
:placeholder="singlePlaceholderText" :disabled="true" />
</view>
<view v-else class="uni-date-x uni-date-range">
<uni-icons type="calendar" color="#e1e1e1" size="22"></uni-icons>
<input class="uni-date__x-input t-c" type="text" v-model="range.startDate"
:placeholder="startPlaceholderText" :disabled="true" />
<slot>
<view class="">{{rangeSeparator}}</view>
</slot>
<input class="uni-date__x-input t-c" type="text" v-model="range.endDate"
:placeholder="endPlaceholderText" :disabled="true" />
</view>
<view v-if="showClearIcon" class="uni-date__icon-clear" @click.stop="clear">
<uni-icons type="clear" color="#e1e1e1" size="18"></uni-icons>
</view>
</view>
</slot>
</view>
<view v-show="popup" class="uni-date-mask" @click="close"></view>
<view v-if="!isPhone" ref="datePicker" v-show="popup" class="uni-date-picker__container">
<view v-if="!isRange" class="uni-date-single--x" :style="popover">
<view class="uni-popper__arrow"></view>
<view v-if="hasTime" class="uni-date-changed popup-x-header">
<input class="uni-date__input t-c" type="text" v-model="tempSingleDate"
:placeholder="selectDateText" />
<time-picker type="time" v-model="time" :border="false" :disabled="!tempSingleDate"
:start="reactStartTime" :end="reactEndTime" :hideSecond="hideSecond" style="width: 100%;">
<input class="uni-date__input t-c" type="text" v-model="time" :placeholder="selectTimeText"
:disabled="!tempSingleDate" />
</time-picker>
</view>
<calendar ref="pcSingle" :showMonth="false"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :date="defSingleDate"
@change="singleChange" style="padding: 0 8px;" />
<view v-if="hasTime" class="popup-x-footer">
<!-- <text class="">此刻</text> -->
<text class="confirm" @click="confirmSingleChange">{{okText}}</text>
</view>
<view class="uni-date-popper__arrow"></view>
</view>
<view v-else class="uni-date-range--x" :style="popover">
<view class="uni-popper__arrow"></view>
<view v-if="hasTime" class="popup-x-header uni-date-changed">
<view class="popup-x-header--datetime">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.startDate"
:placeholder="startDateText" />
<time-picker type="time" v-model="tempRange.startTime" :start="reactStartTime" :border="false"
:disabled="!tempRange.startDate" :hideSecond="hideSecond">
<input class="uni-date__input uni-date-range__input" type="text"
v-model="tempRange.startTime" :placeholder="startTimeText"
:disabled="!tempRange.startDate" />
</time-picker>
</view>
<uni-icons type="arrowthinright" color="#999" style="line-height: 40px;"></uni-icons>
<view class="popup-x-header--datetime">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endDate"
:placeholder="endDateText" />
<time-picker type="time" v-model="tempRange.endTime" :end="reactEndTime" :border="false"
:disabled="!tempRange.endDate" :hideSecond="hideSecond">
<input class="uni-date__input uni-date-range__input" type="text" v-model="tempRange.endTime"
:placeholder="endTimeText" :disabled="!tempRange.endDate" />
</time-picker>
</view>
</view>
<view class="popup-x-body">
<calendar ref="left" :showMonth="false"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :range="true"
@change="leftChange" :pleStatus="endMultipleStatus" @firstEnterCale="updateRightCale"
@monthSwitch="leftMonthSwitch" style="padding: 0 8px;" />
<calendar ref="right" :showMonth="false"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :range="true"
@change="rightChange" :pleStatus="startMultipleStatus" @firstEnterCale="updateLeftCale"
@monthSwitch="rightMonthSwitch" style="padding: 0 8px;border-left: 1px solid #F1F1F1;" />
</view>
<view v-if="hasTime" class="popup-x-footer">
<text class="" @click="clear">{{clearText}}</text>
<text class="confirm" @click="confirmRangeChange">{{okText}}</text>
</view>
</view>
</view>
<calendar v-show="isPhone" ref="mobile" :clearDate="false" :date="defSingleDate" :defTime="reactMobDefTime"
:start-date="caleRange.startDate" :end-date="caleRange.endDate" :selectableTimes="mobSelectableTime"
:pleStatus="endMultipleStatus" :showMonth="false" :range="isRange" :typeHasTime="hasTime" :insert="false"
:hideSecond="hideSecond" @confirm="mobileChange" />
</view>
</template>
<script>
/**
* DatetimePicker 时间选择器
* @description 同时支持 PC 和移动端使用日历选择日期和日期范围
* @tutorial https://ext.dcloud.net.cn/plugin?id=3962
* @property {String} type 选择器类型
* @property {String|Number|Array|Date} value 绑定值
* @property {String} placeholder 单选择时的占位内容
* @property {String} start 起始时间
* @property {String} end 终止时间
* @property {String} start-placeholder 范围选择时开始日期的占位内容
* @property {String} end-placeholder 范围选择时结束日期的占位内容
* @property {String} range-separator 选择范围时的分隔符
* @property {Boolean} border = [true|false] 是否有边框
* @property {Boolean} disabled = [true|false] 是否禁用
* @property {Boolean} clearIcon = [true|false] 是否显示清除按钮仅PC端适用
* @event {Function} change 确定日期时触发的事件
* @event {Function} show 打开弹出层
* @event {Function} close 关闭弹出层
* @event {Function} clear 清除上次选中的状态和值
**/
import calendar from './calendar.vue'
import timePicker from './time-picker.vue'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
export default {
name: 'UniDatetimePicker',
components: {
calendar,
timePicker
},
data() {
return {
isRange: false,
hasTime: false,
mobileRange: false,
// 单选
singleVal: '',
tempSingleDate: '',
defSingleDate: '',
time: '',
// 范围选
caleRange: {
startDate: '',
startTime: '',
endDate: '',
endTime: ''
},
range: {
startDate: '',
// startTime: '',
endDate: '',
// endTime: ''
},
tempRange: {
startDate: '',
startTime: '',
endDate: '',
endTime: ''
},
// 左右日历同步数据
startMultipleStatus: {
before: '',
after: '',
data: [],
fulldate: ''
},
endMultipleStatus: {
before: '',
after: '',
data: [],
fulldate: ''
},
visible: false,
popup: false,
popover: null,
isEmitValue: false,
isPhone: false,
isFirstShow: true,
}
},
props: {
type: {
type: String,
default: 'datetime'
},
value: {
type: [String, Number, Array, Date],
default: ''
},
modelValue: {
type: [String, Number, Array, Date],
default: ''
},
start: {
type: [Number, String],
default: ''
},
end: {
type: [Number, String],
default: ''
},
returnType: {
type: String,
default: 'string'
},
placeholder: {
type: String,
default: ''
},
startPlaceholder: {
type: String,
default: ''
},
endPlaceholder: {
type: String,
default: ''
},
rangeSeparator: {
type: String,
default: '-'
},
border: {
type: [Boolean],
default: true
},
disabled: {
type: [Boolean],
default: false
},
clearIcon: {
type: [Boolean],
default: true
},
hideSecond: {
type: [Boolean],
default: false
}
},
watch: {
type: {
immediate: true,
handler(newVal, oldVal) {
if (newVal.indexOf('time') !== -1) {
this.hasTime = true
} else {
this.hasTime = false
}
if (newVal.indexOf('range') !== -1) {
this.isRange = true
} else {
this.isRange = false
}
}
},
value: {
immediate: true,
handler(newVal, oldVal) {
if (this.isEmitValue) {
this.isEmitValue = false
return
}
this.initPicker(newVal)
}
},
start: {
immediate: true,
handler(newVal, oldVal) {
if (!newVal) return
const {
defDate,
defTime
} = this.parseDate(newVal)
this.caleRange.startDate = defDate
if (this.hasTime) {
this.caleRange.startTime = defTime
}
}
},
end: {
immediate: true,
handler(newVal, oldVal) {
if (!newVal) return
const {
defDate,
defTime
} = this.parseDate(newVal)
this.caleRange.endDate = defDate
if (this.hasTime) {
this.caleRange.endTime = defTime
}
}
},
},
computed: {
reactStartTime() {
const activeDate = this.isRange ? this.tempRange.startDate : this.tempSingleDate
const res = activeDate === this.caleRange.startDate ? this.caleRange.startTime : ''
return res
},
reactEndTime() {
const activeDate = this.isRange ? this.tempRange.endDate : this.tempSingleDate
const res = activeDate === this.caleRange.endDate ? this.caleRange.endTime : ''
return res
},
reactMobDefTime() {
const times = {
start: this.tempRange.startTime,
end: this.tempRange.endTime
}
return this.isRange ? times : this.time
},
mobSelectableTime() {
return {
start: this.caleRange.startTime,
end: this.caleRange.endTime
}
},
datePopupWidth() {
// todo
return this.isRange ? 653 : 301
},
/**
* for i18n
*/
singlePlaceholderText() {
return this.placeholder || (this.type === 'date' ? this.selectDateText : t(
"uni-datetime-picker.selectDateTime"))
},
startPlaceholderText() {
return this.startPlaceholder || this.startDateText
},
endPlaceholderText() {
return this.endPlaceholder || this.endDateText
},
selectDateText() {
return t("uni-datetime-picker.selectDate")
},
selectTimeText() {
return t("uni-datetime-picker.selectTime")
},
startDateText() {
return this.startPlaceholder || t("uni-datetime-picker.startDate")
},
startTimeText() {
return t("uni-datetime-picker.startTime")
},
endDateText() {
return this.endPlaceholder || t("uni-datetime-picker.endDate")
},
endTimeText() {
return t("uni-datetime-picker.endTime")
},
okText() {
return t("uni-datetime-picker.ok")
},
clearText() {
return t("uni-datetime-picker.clear")
},
showClearIcon() {
const { clearIcon, disabled, singleVal, range } = this
const bool = clearIcon && !disabled && (singleVal || (range.startDate && range.endDate))
return bool
}
},
created() {
this.form = this.getForm('uniForms')
this.formItem = this.getForm('uniFormsItem')
// if (this.formItem) {
// if (this.formItem.name) {
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
},
mounted() {
this.platform()
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
initPicker(newVal) {
if (!newVal || Array.isArray(newVal) && !newVal.length) {
this.$nextTick(() => {
this.clear(false)
})
return
}
if (!Array.isArray(newVal) && !this.isRange) {
const {
defDate,
defTime
} = this.parseDate(newVal)
this.singleVal = defDate
this.tempSingleDate = defDate
this.defSingleDate = defDate
if (this.hasTime) {
this.singleVal = defDate + ' ' + defTime
this.time = defTime
}
} else {
const [before, after] = newVal
if (!before && !after) return
const defBefore = this.parseDate(before)
const defAfter = this.parseDate(after)
const startDate = defBefore.defDate
const endDate = defAfter.defDate
this.range.startDate = this.tempRange.startDate = startDate
this.range.endDate = this.tempRange.endDate = endDate
if (this.hasTime) {
this.range.startDate = defBefore.defDate + ' ' + defBefore.defTime
this.range.endDate = defAfter.defDate + ' ' + defAfter.defTime
this.tempRange.startTime = defBefore.defTime
this.tempRange.endTime = defAfter.defTime
}
const defaultRange = {
before: defBefore.defDate,
after: defAfter.defDate
}
this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, defaultRange, {
which: 'right'
})
this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, defaultRange, {
which: 'left'
})
}
},
updateLeftCale(e) {
const left = this.$refs.left
// 设置范围选
left.cale.setHoverMultiple(e.after)
left.setDate(this.$refs.left.nowDate.fullDate)
},
updateRightCale(e) {
const right = this.$refs.right
// 设置范围选
right.cale.setHoverMultiple(e.after)
right.setDate(this.$refs.right.nowDate.fullDate)
},
platform() {
const systemInfo = uni.getSystemInfoSync()
this.isPhone = systemInfo.windowWidth <= 500
this.windowWidth = systemInfo.windowWidth
},
show(event) {
if (this.disabled) {
return
}
this.platform()
if (this.isPhone) {
this.$refs.mobile.open()
return
}
this.popover = {
top: '10px'
}
const dateEditor = uni.createSelectorQuery().in(this).select(".uni-date-editor")
dateEditor.boundingClientRect(rect => {
if (this.windowWidth - rect.left < this.datePopupWidth) {
this.popover.right = 0
}
}).exec()
setTimeout(() => {
this.popup = !this.popup
if (!this.isPhone && this.isRange && this.isFirstShow) {
this.isFirstShow = false
const {
startDate,
endDate
} = this.range
if (startDate && endDate) {
if (this.diffDate(startDate, endDate) < 30) {
this.$refs.right.next()
}
} else {
this.$refs.right.next()
this.$refs.right.cale.lastHover = false
}
}
}, 50)
},
close() {
setTimeout(() => {
this.popup = false
this.$emit('maskClick', this.value)
}, 20)
},
setEmit(value) {
if (this.returnType === "timestamp" || this.returnType === "date") {
if (!Array.isArray(value)) {
if (!this.hasTime) {
value = value + ' ' + '00:00:00'
}
value = this.createTimestamp(value)
if (this.returnType === "date") {
value = new Date(value)
}
} else {
if (!this.hasTime) {
value[0] = value[0] + ' ' + '00:00:00'
value[1] = value[1] + ' ' + '00:00:00'
}
value[0] = this.createTimestamp(value[0])
value[1] = this.createTimestamp(value[1])
if (this.returnType === "date") {
value[0] = new Date(value[0])
value[1] = new Date(value[1])
}
}
}
this.formItem && this.formItem.setValue(value)
this.$emit('change', value)
this.$emit('input', value)
this.$emit('update:modelValue', value)
this.isEmitValue = true
},
createTimestamp(date) {
date = this.fixIosDateFormat(date)
return Date.parse(new Date(date))
},
singleChange(e) {
this.tempSingleDate = e.fulldate
if (this.hasTime) return
this.confirmSingleChange()
},
confirmSingleChange() {
if (!this.tempSingleDate) {
this.popup = false
return
}
if (this.hasTime) {
this.singleVal = this.tempSingleDate + ' ' + (this.time ? this.time : '00:00:00')
} else {
this.singleVal = this.tempSingleDate
}
this.setEmit(this.singleVal)
this.popup = false
},
leftChange(e) {
const {
before,
after
} = e.range
this.rangeChange(before, after)
const obj = {
before: e.range.before,
after: e.range.after,
data: e.range.data,
fulldate: e.fulldate
}
this.startMultipleStatus = Object.assign({}, this.startMultipleStatus, obj)
},
rightChange(e) {
const {
before,
after
} = e.range
this.rangeChange(before, after)
const obj = {
before: e.range.before,
after: e.range.after,
data: e.range.data,
fulldate: e.fulldate
}
this.endMultipleStatus = Object.assign({}, this.endMultipleStatus, obj)
},
mobileChange(e) {
if (this.isRange) {
const {
before,
after
} = e.range
this.handleStartAndEnd(before, after, true)
if (this.hasTime) {
const {
startTime,
endTime
} = e.timeRange
this.tempRange.startTime = startTime
this.tempRange.endTime = endTime
}
this.confirmRangeChange()
} else {
if (this.hasTime) {
this.singleVal = e.fulldate + ' ' + e.time
} else {
this.singleVal = e.fulldate
}
this.setEmit(this.singleVal)
}
this.$refs.mobile.close()
},
rangeChange(before, after) {
if (!(before && after)) return
this.handleStartAndEnd(before, after, true)
if (this.hasTime) return
this.confirmRangeChange()
},
confirmRangeChange() {
if (!this.tempRange.startDate && !this.tempRange.endDate) {
this.popup = false
return
}
let start, end
if (!this.hasTime) {
start = this.range.startDate = this.tempRange.startDate
end = this.range.endDate = this.tempRange.endDate
} else {
start = this.range.startDate = this.tempRange.startDate + ' ' +
(this.tempRange.startTime ? this.tempRange.startTime : '00:00:00')
end = this.range.endDate = this.tempRange.endDate + ' ' +
(this.tempRange.endTime ? this.tempRange.endTime : '00:00:00')
}
const displayRange = [start, end]
this.setEmit(displayRange)
this.popup = false
},
handleStartAndEnd(before, after, temp = false) {
if (!(before && after)) return
const type = temp ? 'tempRange' : 'range'
if (this.dateCompare(before, after)) {
this[type].startDate = before
this[type].endDate = after
} else {
this[type].startDate = after
this[type].endDate = before
}
},
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
},
/**
* 比较时间差
*/
diffDate(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
const diff = (endDate - startDate) / (24 * 60 * 60 * 1000)
return Math.abs(diff)
},
clear(needEmit = true) {
if (!this.isRange) {
this.singleVal = ''
this.tempSingleDate = ''
this.time = ''
if (this.isPhone) {
this.$refs.mobile && this.$refs.mobile.clearCalender()
} else {
this.$refs.pcSingle && this.$refs.pcSingle.clearCalender()
}
if (needEmit) {
this.formItem && this.formItem.setValue('')
this.$emit('change', '')
this.$emit('input', '')
this.$emit('update:modelValue', '')
}
} else {
this.range.startDate = ''
this.range.endDate = ''
this.tempRange.startDate = ''
this.tempRange.startTime = ''
this.tempRange.endDate = ''
this.tempRange.endTime = ''
if (this.isPhone) {
this.$refs.mobile && this.$refs.mobile.clearCalender()
} else {
this.$refs.left && this.$refs.left.clearCalender()
this.$refs.right && this.$refs.right.clearCalender()
this.$refs.right && this.$refs.right.next()
}
if (needEmit) {
this.formItem && this.formItem.setValue([])
this.$emit('change', [])
this.$emit('input', [])
this.$emit('update:modelValue', [])
}
}
},
parseDate(date) {
date = this.fixIosDateFormat(date)
const defVal = new Date(date)
const year = defVal.getFullYear()
const month = defVal.getMonth() + 1
const day = defVal.getDate()
const hour = defVal.getHours()
const minute = defVal.getMinutes()
const second = defVal.getSeconds()
const defDate = year + '-' + this.lessTen(month) + '-' + this.lessTen(day)
const defTime = this.lessTen(hour) + ':' + this.lessTen(minute) + (this.hideSecond ? '' : (':' + this
.lessTen(second)))
return {
defDate,
defTime
}
},
lessTen(item) {
return item < 10 ? '0' + item : item
},
//兼容 iOS、safari 日期格式
fixIosDateFormat(value) {
if (typeof value === 'string') {
value = value.replace(/-/g, '/')
}
return value
},
leftMonthSwitch(e) {
// console.log('leftMonthSwitch 返回:', e)
},
rightMonthSwitch(e) {
// console.log('rightMonthSwitch 返回:', e)
}
}
}
</script>
<style>
.uni-date-x {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 10px;
border-radius: 4px;
background-color: #fff;
color: #666;
font-size: 14px;
}
.uni-date-x--border {
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.uni-date-editor--x {
position: relative;
}
.uni-date-editor--x .uni-date__icon-clear {
position: absolute;
top: 0;
right: 0;
display: inline-block;
box-sizing: border-box;
border: 9px solid transparent;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-date__x-input {
padding: 0 8px;
height: 40px;
width: 100%;
line-height: 40px;
font-size: 14px;
}
.t-c {
text-align: center;
}
.uni-date__input {
height: 40px;
width: 100%;
line-height: 40px;
font-size: 14px;
}
.uni-date-range__input {
text-align: center;
max-width: 142px;
}
.uni-date-picker__container {
position: relative;
/* position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
box-sizing: border-box;
z-index: 996;
font-size: 14px; */
}
.uni-date-mask {
position: fixed;
bottom: 0px;
top: 0px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0);
transition-duration: 0.3s;
z-index: 996;
}
.uni-date-single--x {
/* padding: 0 8px; */
background-color: #fff;
position: absolute;
top: 0;
z-index: 999;
border: 1px solid #EBEEF5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.uni-date-range--x {
/* padding: 0 8px; */
background-color: #fff;
position: absolute;
top: 0;
z-index: 999;
border: 1px solid #EBEEF5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.uni-date-editor--x__disabled {
opacity: 0.4;
cursor: default;
}
.uni-date-editor--logo {
width: 16px;
height: 16px;
vertical-align: middle;
}
/* 添加时间 */
.popup-x-header {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
/* justify-content: space-between; */
}
.popup-x-header--datetime {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex: 1;
}
.popup-x-body {
display: flex;
}
.popup-x-footer {
padding: 0 15px;
border-top-color: #F1F1F1;
border-top-style: solid;
border-top-width: 1px;
/* background-color: #fff; */
line-height: 40px;
text-align: right;
color: #666;
}
.popup-x-footer text:hover {
color: #007aff;
cursor: pointer;
opacity: 0.8;
}
.popup-x-footer .confirm {
margin-left: 20px;
color: #007aff;
}
.uni-date-changed {
/* background-color: #fff; */
text-align: center;
color: #333;
border-bottom-color: #F1F1F1;
border-bottom-style: solid;
border-bottom-width: 1px;
/* padding: 0 50px; */
}
.uni-date-changed--time text {
/* padding: 0 20px; */
height: 50px;
line-height: 50px;
}
.uni-date-changed .uni-date-changed--time {
/* display: flex; */
flex: 1;
}
.uni-date-changed--time-date {
color: #333;
opacity: 0.6;
}
.mr-50 {
margin-right: 50px;
}
/* picker 弹出层通用的指示小三角, todo扩展至上下左右方向定位 */
.uni-popper__arrow,
.uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.uni-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #EBEEF5;
}
.uni-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
</style>

View File

@@ -0,0 +1,410 @@
class Calendar {
constructor({
date,
selected,
startDate,
endDate,
range,
// multipleStatus
} = {}) {
// 当前日期
this.date = this.getDate(new Date()) // 当前初入日期
// 打点信息
this.selected = selected || [];
// 范围开始
this.startDate = startDate
// 范围结束
this.endDate = endDate
this.range = range
// 多选状态
this.cleanMultipleStatus()
// 每周日期
this.weeks = {}
// this._getWeek(this.date.fullDate)
// this.multipleStatus = multipleStatus
this.lastHover = false
}
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.selectDate = this.getDate(date)
this._getWeek(this.selectDate.fullDate)
}
/**
* 清理多选状态
*/
cleanMultipleStatus() {
this.multipleStatus = {
before: '',
after: '',
data: []
}
}
/**
* 重置开始日期
*/
resetSatrtDate(startDate) {
// 范围开始
this.startDate = startDate
}
/**
* 重置结束日期
*/
resetEndDate(endDate) {
// 范围结束
this.endDate = endDate
}
/**
* 获取任意时间
*/
getDate(date, AddDayCount = 0, str = 'day') {
if (!date) {
date = new Date()
}
if (typeof date !== 'object') {
date = date.replace(/-/g, '/')
}
const dd = new Date(date)
switch (str) {
case 'day':
dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期
break
case 'month':
if (dd.getDate() === 31) {
dd.setDate(dd.getDate() + AddDayCount)
} else {
dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期
}
break
case 'year':
dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期
break
}
const y = dd.getFullYear()
const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期不足10补0
const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号不足10补0
return {
fullDate: y + '-' + m + '-' + d,
year: y,
month: m,
date: d,
day: dd.getDay()
}
}
/**
* 获取上月剩余天数
*/
_getLastMonthDays(firstDay, full) {
let dateArr = []
for (let i = firstDay; i > 0; i--) {
const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate()
dateArr.push({
date: beforeDate,
month: full.month - 1,
disable: true
})
}
return dateArr
}
/**
* 获取本月天数
*/
_currentMonthDys(dateData, full) {
let dateArr = []
let fullDate = this.date.fullDate
for (let i = 1; i <= dateData; i++) {
let isinfo = false
let nowDate = full.year + '-' + (full.month < 10 ?
full.month : full.month) + '-' + (i < 10 ?
'0' + i : i)
// 是否今天
let isDay = fullDate === nowDate
// 获取打点信息
let info = this.selected && this.selected.find((item) => {
if (this.dateEqual(nowDate, item.date)) {
return item
}
})
// 日期禁用
let disableBefore = true
let disableAfter = true
if (this.startDate) {
// let dateCompBefore = this.dateCompare(this.startDate, fullDate)
// disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate)
disableBefore = this.dateCompare(this.startDate, nowDate)
}
if (this.endDate) {
// let dateCompAfter = this.dateCompare(fullDate, this.endDate)
// disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate)
disableAfter = this.dateCompare(nowDate, this.endDate)
}
let multiples = this.multipleStatus.data
let checked = false
let multiplesStatus = -1
if (this.range) {
if (multiples) {
multiplesStatus = multiples.findIndex((item) => {
return this.dateEqual(item, nowDate)
})
}
if (multiplesStatus !== -1) {
checked = true
}
}
let data = {
fullDate: nowDate,
year: full.year,
date: i,
multiple: this.range ? checked : false,
beforeMultiple: this.isLogicBefore(nowDate, this.multipleStatus.before, this.multipleStatus.after),
afterMultiple: this.isLogicAfter(nowDate, this.multipleStatus.before, this.multipleStatus.after),
month: full.month,
disable: !(disableBefore && disableAfter),
isDay,
userChecked: false
}
if (info) {
data.extraInfo = info
}
dateArr.push(data)
}
return dateArr
}
/**
* 获取下月天数
*/
_getNextMonthDays(surplus, full) {
let dateArr = []
for (let i = 1; i < surplus + 1; i++) {
dateArr.push({
date: i,
month: Number(full.month) + 1,
disable: true
})
}
return dateArr
}
/**
* 获取当前日期详情
* @param {Object} date
*/
getInfo(date) {
if (!date) {
date = new Date()
}
const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate)
return dateInfo
}
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
}
/**
* 比较时间是否相等
*/
dateEqual(before, after) {
// 计算截止时间
before = new Date(before.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
after = new Date(after.replace('-', '/').replace('-', '/'))
if (before.getTime() - after.getTime() === 0) {
return true
} else {
return false
}
}
/**
* 比较真实起始日期
*/
isLogicBefore(currentDay, before, after) {
let logicBefore = before
if (before && after) {
logicBefore = this.dateCompare(before, after) ? before : after
}
return this.dateEqual(logicBefore, currentDay)
}
isLogicAfter(currentDay, before, after) {
let logicAfter = after
if (before && after) {
logicAfter = this.dateCompare(before, after) ? after : before
}
return this.dateEqual(logicAfter, currentDay)
}
/**
* 获取日期范围内所有日期
* @param {Object} begin
* @param {Object} end
*/
geDateAll(begin, end) {
var arr = []
var ab = begin.split('-')
var ae = end.split('-')
var db = new Date()
db.setFullYear(ab[0], ab[1] - 1, ab[2])
var de = new Date()
de.setFullYear(ae[0], ae[1] - 1, ae[2])
var unixDb = db.getTime() - 24 * 60 * 60 * 1000
var unixDe = de.getTime() - 24 * 60 * 60 * 1000
for (var k = unixDb; k <= unixDe;) {
k = k + 24 * 60 * 60 * 1000
arr.push(this.getDate(new Date(parseInt(k))).fullDate)
}
return arr
}
/**
* 获取多选状态
*/
setMultiple(fullDate) {
let {
before,
after
} = this.multipleStatus
if (!this.range) return
if (before && after) {
if (!this.lastHover) {
this.lastHover = true
return
}
this.multipleStatus.before = fullDate
this.multipleStatus.after = ''
this.multipleStatus.data = []
this.multipleStatus.fulldate = ''
this.lastHover = false
} else {
if (!before) {
this.multipleStatus.before = fullDate
this.lastHover = false
} else {
this.multipleStatus.after = fullDate
if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus
.after);
} else {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus
.before);
}
this.lastHover = true
}
}
this._getWeek(fullDate)
}
/**
* 鼠标 hover 更新多选状态
*/
setHoverMultiple(fullDate) {
let {
before,
after
} = this.multipleStatus
if (!this.range) return
if (this.lastHover) return
if (!before) {
this.multipleStatus.before = fullDate
} else {
this.multipleStatus.after = fullDate
if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after);
} else {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before);
}
}
this._getWeek(fullDate)
}
/**
* 更新默认值多选状态
*/
setDefaultMultiple(before, after) {
this.multipleStatus.before = before
this.multipleStatus.after = after
if (before && after) {
if (this.dateCompare(before, after)) {
this.multipleStatus.data = this.geDateAll(before, after);
this._getWeek(after)
} else {
this.multipleStatus.data = this.geDateAll(after, before);
this._getWeek(before)
}
}
}
/**
* 获取每周数据
* @param {Object} dateData
*/
_getWeek(dateData) {
const {
fullDate,
year,
month,
date,
day
} = this.getDate(dateData)
let firstDay = new Date(year, month - 1, 1).getDay()
let currentDay = new Date(year, month, 0).getDate()
let dates = {
lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天
currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数
nextMonthDays: [], // 下个月开始几天
weeks: []
}
let canlender = []
const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length)
dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData))
canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays)
let weeks = {}
// 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天
for (let i = 0; i < canlender.length; i++) {
if (i % 7 === 0) {
weeks[parseInt(i / 7)] = new Array(7)
}
weeks[parseInt(i / 7)][i % 7] = canlender[i]
}
this.canlender = canlender
this.weeks = weeks
}
//静态方法
// static init(date) {
// if (!this.instance) {
// this.instance = new Calendar(date);
// }
// return this.instance;
// }
}
export default Calendar

View File

@@ -0,0 +1,90 @@
{
"id": "uni-datetime-picker",
"displayName": "uni-datetime-picker 日期选择器",
"version": "2.2.2",
"description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择",
"keywords": [
"uni-datetime-picker",
"uni-ui",
"uniui",
"日期时间选择器",
"日期时间"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
> `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容不再支持单独的时间选择type=time及相关的 hide-second 属性(时间选可使用内置组件 picker。若仍需使用旧版本可在插件市场下载*非uni_modules版本*,旧版本将不再维护`
## DatetimePicker 时间选择器
> **组件名uni-datetime-picker**
> 代码块: `uDatetimePicker`
该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。
若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。
**_点击 picker 默认值规则_**
- 若设置初始值 value, 会显示在 picker 显示框中
- 若无初始值 value则初始值 value 为当前本地时间 Date.now() 但不会显示在 picker 显示框中
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,8 @@
## 1.0.32022-01-21
- 优化 组件示例
## 1.0.22021-11-22
- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题
## 1.0.12021-11-22
- 修复 vue3中scss语法兼容问题
## 1.0.02021-11-18
- init

View File

@@ -0,0 +1 @@
@import './styles/index.scss';

View File

@@ -0,0 +1,82 @@
{
"id": "uni-scss",
"displayName": "uni-scss 辅助样式",
"version": "1.0.3",
"description": "uni-sass是uni-ui提供的一套全局样式 通过一些简单的类名和sass变量实现简单的页面布局操作比如颜色、边距、圆角等。",
"keywords": [
"uni-scss",
"uni-ui",
"辅助样式"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"category": [
"JS SDK",
"通用 SDK"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "n",
"联盟": "n"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
`uni-sass``uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@@ -0,0 +1,7 @@
@import './setting/_variables.scss';
@import './setting/_border.scss';
@import './setting/_color.scss';
@import './setting/_space.scss';
@import './setting/_radius.scss';
@import './setting/_text.scss';
@import './setting/_styles.scss';

View File

@@ -0,0 +1,3 @@
.uni-border {
border: 1px $uni-border-1 solid;
}

View File

@@ -0,0 +1,66 @@
// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐
// @mixin get-styles($k,$c) {
// @if $k == size or $k == weight{
// font-#{$k}:#{$c}
// }@else{
// #{$k}:#{$c}
// }
// }
$uni-ui-color:(
// 主色
primary: $uni-primary,
primary-disable: $uni-primary-disable,
primary-light: $uni-primary-light,
// 辅助色
success: $uni-success,
success-disable: $uni-success-disable,
success-light: $uni-success-light,
warning: $uni-warning,
warning-disable: $uni-warning-disable,
warning-light: $uni-warning-light,
error: $uni-error,
error-disable: $uni-error-disable,
error-light: $uni-error-light,
info: $uni-info,
info-disable: $uni-info-disable,
info-light: $uni-info-light,
// 中性色
main-color: $uni-main-color,
base-color: $uni-base-color,
secondary-color: $uni-secondary-color,
extra-color: $uni-extra-color,
// 背景色
bg-color: $uni-bg-color,
// 边框颜色
border-1: $uni-border-1,
border-2: $uni-border-2,
border-3: $uni-border-3,
border-4: $uni-border-4,
// 黑色
black:$uni-black,
// 白色
white:$uni-white,
// 透明
transparent:$uni-transparent
) !default;
@each $key, $child in $uni-ui-color {
.uni-#{"" + $key} {
color: $child;
}
.uni-#{"" + $key}-bg {
background-color: $child;
}
}
.uni-shadow-sm {
box-shadow: $uni-shadow-sm;
}
.uni-shadow-base {
box-shadow: $uni-shadow-base;
}
.uni-shadow-lg {
box-shadow: $uni-shadow-lg;
}
.uni-mask {
background-color:$uni-mask;
}

View File

@@ -0,0 +1,55 @@
@mixin radius($r,$d:null ,$important: false){
$radius-value:map-get($uni-radius, $r) if($important, !important, null);
// Key exists within the $uni-radius variable
@if (map-has-key($uni-radius, $r) and $d){
@if $d == t {
border-top-left-radius:$radius-value;
border-top-right-radius:$radius-value;
}@else if $d == r {
border-top-right-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == b {
border-bottom-left-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == l {
border-top-left-radius:$radius-value;
border-bottom-left-radius:$radius-value;
}@else if $d == tl {
border-top-left-radius:$radius-value;
}@else if $d == tr {
border-top-right-radius:$radius-value;
}@else if $d == br {
border-bottom-right-radius:$radius-value;
}@else if $d == bl {
border-bottom-left-radius:$radius-value;
}
}@else{
border-radius:$radius-value;
}
}
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $key} {
@include radius($key)
}
}@else{
.uni-radius {
@include radius($key)
}
}
}
@each $direction in t, r, b, l,tl, tr, br, bl {
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $direction}-#{"" + $key} {
@include radius($key,$direction,false)
}
}@else{
.uni-radius-#{$direction} {
@include radius($key,$direction,false)
}
}
}
}

View File

@@ -0,0 +1,56 @@
@mixin fn($space,$direction,$size,$n) {
@if $n {
#{$space}-#{$direction}: #{$size*$uni-space-root}px
} @else {
#{$space}-#{$direction}: #{-$size*$uni-space-root}px
}
}
@mixin get-styles($direction,$i,$space,$n){
@if $direction == t {
@include fn($space, top,$i,$n);
}
@if $direction == r {
@include fn($space, right,$i,$n);
}
@if $direction == b {
@include fn($space, bottom,$i,$n);
}
@if $direction == l {
@include fn($space, left,$i,$n);
}
@if $direction == x {
@include fn($space, left,$i,$n);
@include fn($space, right,$i,$n);
}
@if $direction == y {
@include fn($space, top,$i,$n);
@include fn($space, bottom,$i,$n);
}
@if $direction == a {
@if $n {
#{$space}:#{$i*$uni-space-root}px;
} @else {
#{$space}:#{-$i*$uni-space-root}px;
}
}
}
@each $orientation in m,p {
$space: margin;
@if $orientation == m {
$space: margin;
} @else {
$space: padding;
}
@for $i from 0 through 16 {
@each $direction in t, r, b, l, x, y, a {
.uni-#{$orientation}#{$direction}-#{$i} {
@include get-styles($direction,$i,$space,true);
}
.uni-#{$orientation}#{$direction}-n#{$i} {
@include get-styles($direction,$i,$space,false);
}
}
}
}

View File

@@ -0,0 +1,167 @@
/* #ifndef APP-NVUE */
$-color-white:#fff;
$-color-black:#000;
@mixin base-style($color) {
color: #fff;
background-color: $color;
border-color: mix($-color-black, $color, 8%);
&:not([hover-class]):active {
background: mix($-color-black, $color, 10%);
border-color: mix($-color-black, $color, 20%);
color: $-color-white;
outline: none;
}
}
@mixin is-color($color) {
@include base-style($color);
&[loading] {
@include base-style($color);
&::before {
margin-right:5px;
}
}
&[disabled] {
&,
&[loading],
&:not([hover-class]):active {
color: $-color-white;
border-color: mix(darken($color,10%), $-color-white);
background-color: mix($color, $-color-white);
}
}
}
@mixin base-plain-style($color) {
color:$color;
background-color: mix($-color-white, $color, 90%);
border-color: mix($-color-white, $color, 70%);
&:not([hover-class]):active {
background: mix($-color-white, $color, 80%);
color: $color;
outline: none;
border-color: mix($-color-white, $color, 50%);
}
}
@mixin is-plain($color){
&[plain] {
@include base-plain-style($color);
&[loading] {
@include base-plain-style($color);
&::before {
margin-right:5px;
}
}
&[disabled] {
&,
&:active {
color: mix($-color-white, $color, 40%);
background-color: mix($-color-white, $color, 90%);
border-color: mix($-color-white, $color, 80%);
}
}
}
}
.uni-btn {
margin: 5px;
color: #393939;
border:1px solid #ccc;
font-size: 16px;
font-weight: 200;
background-color: #F9F9F9;
// TODO 暂时处理边框隐藏一边的问题
overflow: visible;
&::after{
border: none;
}
&:not([type]),&[type=default] {
color: #999;
&[loading] {
background: none;
&::before {
margin-right:5px;
}
}
&[disabled]{
color: mix($-color-white, #999, 60%);
&,
&[loading],
&:active {
color: mix($-color-white, #999, 60%);
background-color: mix($-color-white,$-color-black , 98%);
border-color: mix($-color-white, #999, 85%);
}
}
&[plain] {
color: #999;
background: none;
border-color: $uni-border-1;
&:not([hover-class]):active {
background: none;
color: mix($-color-white, $-color-black, 80%);
border-color: mix($-color-white, $-color-black, 90%);
outline: none;
}
&[disabled]{
&,
&[loading],
&:active {
background: none;
color: mix($-color-white, #999, 60%);
border-color: mix($-color-white, #999, 85%);
}
}
}
}
&:not([hover-class]):active {
color: mix($-color-white, $-color-black, 50%);
}
&[size=mini] {
font-size: 16px;
font-weight: 200;
border-radius: 8px;
}
&.uni-btn-small {
font-size: 14px;
}
&.uni-btn-mini {
font-size: 12px;
}
&.uni-btn-radius {
border-radius: 999px;
}
&[type=primary] {
@include is-color($uni-primary);
@include is-plain($uni-primary)
}
&[type=success] {
@include is-color($uni-success);
@include is-plain($uni-success)
}
&[type=error] {
@include is-color($uni-error);
@include is-plain($uni-error)
}
&[type=warning] {
@include is-color($uni-warning);
@include is-plain($uni-warning)
}
&[type=info] {
@include is-color($uni-info);
@include is-plain($uni-info)
}
}
/* #endif */

View File

@@ -0,0 +1,24 @@
@mixin get-styles($k,$c) {
@if $k == size or $k == weight{
font-#{$k}:#{$c}
}@else{
#{$k}:#{$c}
}
}
@each $key, $child in $uni-headings {
/* #ifndef APP-NVUE */
.uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
/* #ifdef APP-NVUE */
.container .uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
}

View File

@@ -0,0 +1,146 @@
// @use "sass:math";
@import '../tools/functions.scss';
// 间距基础倍数
$uni-space-root: 2 !default;
// 边框半径默认值
$uni-radius-root:5px !default;
$uni-radius: () !default;
// 边框半径断点
$uni-radius: map-deep-merge(
(
0: 0,
// TODO 当前版本暂时不支持 sm 属性
// 'sm': math.div($uni-radius-root, 2),
null: $uni-radius-root,
'lg': $uni-radius-root * 2,
'xl': $uni-radius-root * 6,
'pill': 9999px,
'circle': 50%
),
$uni-radius
);
// 字体家族
$body-font-family: 'Roboto', sans-serif !default;
// 文本
$heading-font-family: $body-font-family !default;
$uni-headings: () !default;
$letterSpacing: -0.01562em;
$uni-headings: map-deep-merge(
(
'h1': (
size: 32px,
weight: 300,
line-height: 50px,
// letter-spacing:-0.01562em
),
'h2': (
size: 28px,
weight: 300,
line-height: 40px,
// letter-spacing: -0.00833em
),
'h3': (
size: 24px,
weight: 400,
line-height: 32px,
// letter-spacing: normal
),
'h4': (
size: 20px,
weight: 400,
line-height: 30px,
// letter-spacing: 0.00735em
),
'h5': (
size: 16px,
weight: 400,
line-height: 24px,
// letter-spacing: normal
),
'h6': (
size: 14px,
weight: 500,
line-height: 18px,
// letter-spacing: 0.0125em
),
'subtitle': (
size: 12px,
weight: 400,
line-height: 20px,
// letter-spacing: 0.00937em
),
'body': (
font-size: 14px,
font-weight: 400,
line-height: 22px,
// letter-spacing: 0.03125em
),
'caption': (
'size': 12px,
'weight': 400,
'line-height': 20px,
// 'letter-spacing': 0.03333em,
// 'text-transform': false
)
),
$uni-headings
);
// 主色
$uni-primary: #2979ff !default;
$uni-primary-disable:lighten($uni-primary,20%) !default;
$uni-primary-light: lighten($uni-primary,25%) !default;
// 辅助色
// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。
$uni-success: #18bc37 !default;
$uni-success-disable:lighten($uni-success,20%) !default;
$uni-success-light: lighten($uni-success,25%) !default;
$uni-warning: #f3a73f !default;
$uni-warning-disable:lighten($uni-warning,20%) !default;
$uni-warning-light: lighten($uni-warning,25%) !default;
$uni-error: #e43d33 !default;
$uni-error-disable:lighten($uni-error,20%) !default;
$uni-error-light: lighten($uni-error,25%) !default;
$uni-info: #8f939c !default;
$uni-info-disable:lighten($uni-info,20%) !default;
$uni-info-light: lighten($uni-info,25%) !default;
// 中性色
// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。
$uni-main-color: #3a3a3a !default; // 主要文字
$uni-base-color: #6a6a6a !default; // 常规文字
$uni-secondary-color: #909399 !default; // 次要文字
$uni-extra-color: #c7c7c7 !default; // 辅助说明
// 边框颜色
$uni-border-1: #F0F0F0 !default;
$uni-border-2: #EDEDED !default;
$uni-border-3: #DCDCDC !default;
$uni-border-4: #B9B9B9 !default;
// 常规色
$uni-black: #000000 !default;
$uni-white: #ffffff !default;
$uni-transparent: rgba($color: #000000, $alpha: 0) !default;
// 背景色
$uni-bg-color: #f7f7f7 !default;
/* 水平间距 */
$uni-spacing-sm: 8px !default;
$uni-spacing-base: 15px !default;
$uni-spacing-lg: 30px !default;
// 阴影
$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default;
$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default;
$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default;
// 蒙版
$uni-mask: rgba($color: #000000, $alpha: 0.4) !default;

View File

@@ -0,0 +1,19 @@
// 合并 map
@function map-deep-merge($parent-map, $child-map){
$result: $parent-map;
@each $key, $child in $child-map {
$parent-has-key: map-has-key($result, $key);
$parent-value: map-get($result, $key);
$parent-type: type-of($parent-value);
$child-type: type-of($child);
$parent-is-map: $parent-type == map;
$child-is-map: $child-type == map;
@if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){
$result: map-merge($result, ( $key: $child ));
}@else {
$result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) ));
}
}
@return $result;
};

View File

@@ -0,0 +1,31 @@
// 间距基础倍数
$uni-space-root: 2;
// 边框半径默认值
$uni-radius-root:5px;
// 主色
$uni-primary: #2979ff;
// 辅助色
$uni-success: #4cd964;
// 警告色
$uni-warning: #f0ad4e;
// 错误色
$uni-error: #dd524d;
// 描述色
$uni-info: #909399;
// 中性色
$uni-main-color: #303133;
$uni-base-color: #606266;
$uni-secondary-color: #909399;
$uni-extra-color: #C0C4CC;
// 背景色
$uni-bg-color: #f5f5f5;
// 边框颜色
$uni-border-1: #DCDFE6;
$uni-border-2: #E4E7ED;
$uni-border-3: #EBEEF5;
$uni-border-4: #F2F6FC;
// 常规色
$uni-black: #000000;
$uni-white: #ffffff;
$uni-transparent: rgba($color: #000000, $alpha: 0);

View File

@@ -0,0 +1,62 @@
@import './styles/setting/_variables.scss';
// 间距基础倍数
$uni-space-root: 2;
// 边框半径默认值
$uni-radius-root:5px;
// 主色
$uni-primary: #2979ff;
$uni-primary-disable:mix(#fff,$uni-primary,50%);
$uni-primary-light: mix(#fff,$uni-primary,80%);
// 辅助色
// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。
$uni-success: #18bc37;
$uni-success-disable:mix(#fff,$uni-success,50%);
$uni-success-light: mix(#fff,$uni-success,80%);
$uni-warning: #f3a73f;
$uni-warning-disable:mix(#fff,$uni-warning,50%);
$uni-warning-light: mix(#fff,$uni-warning,80%);
$uni-error: #e43d33;
$uni-error-disable:mix(#fff,$uni-error,50%);
$uni-error-light: mix(#fff,$uni-error,80%);
$uni-info: #8f939c;
$uni-info-disable:mix(#fff,$uni-info,50%);
$uni-info-light: mix(#fff,$uni-info,80%);
// 中性色
// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。
$uni-main-color: #3a3a3a; // 主要文字
$uni-base-color: #6a6a6a; // 常规文字
$uni-secondary-color: #909399; // 次要文字
$uni-extra-color: #c7c7c7; // 辅助说明
// 边框颜色
$uni-border-1: #F0F0F0;
$uni-border-2: #EDEDED;
$uni-border-3: #DCDCDC;
$uni-border-4: #B9B9B9;
// 常规色
$uni-black: #000000;
$uni-white: #ffffff;
$uni-transparent: rgba($color: #000000, $alpha: 0);
// 背景色
$uni-bg-color: #f7f7f7;
/* 水平间距 */
$uni-spacing-sm: 8px;
$uni-spacing-base: 15px;
$uni-spacing-lg: 30px;
// 阴影
$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5);
$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2);
$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
// 蒙版
$uni-mask: rgba($color: #000000, $alpha: 0.4);

View File

@@ -0,0 +1,11 @@
## 1.0.42023-09-02
1、优化一些显示的细节问题
## 1.0.32023-09-01
1、新增动画、时间、颜色等相关属性
## 1.0.22023-08-31
1、回滚到v1.0.0版本,暂时取消动画时间的修改属性
## 1.0.12023-08-31
1、新增动画持续时间属性
2、新增淡出持续时间属性
## 1.0.02023-08-31
第一个版本

View File

@@ -0,0 +1,147 @@
export default {
methods: {
bannerConfigs() {
return {
padding: '20rpx',
gridRows: 1,
gridColumns: 1,
gridRowsGap: '40rpx',
gridColumnsGap: '24rpx',
itemDirection: 'row',
itemGap: '30rpx',
itemAlign: 'center',
headShow: true,
headWidth: '100%',
headHeight: '300rpx',
headBorderRadius: '20rpx',
textShow: false,
textRows: 3,
textRowsGap: '20rpx',
textWidth: '100%',
textHeight: '30rpx',
textBorderRadius: '6rpx',
...this.configs
}
},
infoConfigs() {
return {
padding: '20rpx',
gridRows: 1,
gridColumns: 1,
gridRowsGap: '50rpx',
gridColumnsGap: '24rpx',
itemDirection: 'row',
itemGap: '30rpx',
itemAlign: 'flex-start',
headShow: true,
headWidth: '100rpx',
headHeight: '100rpx',
headBorderRadius: '50%',
textShow: true,
textRows: 4,
textRowsGap: '30rpx',
textWidth: ['50%', '100%', '100%', '80%'],
textHeight: ['40rpx', '24rpx', '24rpx', '24rpx'],
textBorderRadius: '6rpx',
...this.configs
}
},
textConfigs() {
return {
padding: '20rpx',
gridRows: 1,
gridColumns: 1,
gridRowsGap: '50rpx',
gridColumnsGap: '24rpx',
itemDirection: 'row',
itemGap: '30rpx',
itemAlign: 'flex-start',
headShow: false,
headWidth: '100rpx',
headHeight: '100rpx',
headBorderRadius: '50%',
textShow: true,
textRows: 4,
textRowsGap: '30rpx',
textWidth: ['50%', '100%', '100%', '80%'],
textHeight: '30rpx',
textBorderRadius: '6rpx',
...this.configs
}
},
menuConfigs() {
return {
padding: '20rpx',
gridRows: 2,
gridColumns: 5,
gridRowsGap: '40rpx',
gridColumnsGap: '40rpx',
itemDirection: 'column',
itemGap: '16rpx',
itemAlign: 'center',
headShow: true,
headWidth: '100rpx',
headHeight: '100rpx',
headBorderRadius: '50%',
textShow: true,
textRows: 1,
textRowsGap: '0rpx',
textWidth: '100%',
textHeight: '24rpx',
textBorderRadius: '6rpx',
...this.configs
}
},
listConfigs() {
return {
padding: '20rpx',
gridRows: 2,
gridColumns: 1,
gridRowsGap: '50rpx',
gridColumnsGap: '24rpx',
itemDirection: 'row',
itemGap: '30rpx',
itemAlign: 'flex-start',
headShow: true,
headWidth: '200rpx',
headHeight: '200rpx',
headBorderRadius: '16rpx',
textShow: true,
textRows: 4,
textRowsGap: '30rpx',
textWidth: ['50%', '100%', '100%', '80%'],
textHeight: ['38rpx', '24rpx', '24rpx', '24rpx'],
textBorderRadius: '6rpx',
...this.configs
}
},
waterfallConfigs() {
return {
padding: '20rpx',
gridRows: 2,
gridColumns: 2,
gridRowsGap: '40rpx',
gridColumnsGap: '24rpx',
itemDirection: 'column',
itemGap: '16rpx',
itemAlign: 'center',
headShow: true,
headWidth: '100%',
headHeight: '400rpx',
headBorderRadius: '12rpx',
textShow: true,
textRows: 3,
textRowsGap: '12rpx',
textWidth: ['40%', '85%', '60%'],
textHeight: ['30rpx', '20rpx', '20rpx'],
textBorderRadius: '6rpx',
...this.configs
}
},
}
}

View File

@@ -0,0 +1,324 @@
<template>
<view class="x-skeleton" :style="variableStr">
<!-- 骨架屏 -->
<view
v-if="skeletonLoading"
class="x-skeleton__wrapper"
:class="[ startFadeOut && 'fade-out' ]"
:style="{ padding: skeletonConfigs.padding }"
>
<view
v-for="(row, rowIndex) in gridRowsArr" :key="rowIndex"
class="x-skeleton__wrapper__rows"
:style="{ marginBottom: rowIndex < gridRowsArr.length - 1 ? skeletonConfigs.gridRowsGap : 0 }"
>
<view
v-for="(column, columnIndex) in gridColumnsArr" :key="columnIndex"
class="x-skeleton__wrapper__columns"
:style="{
flexDirection: skeletonConfigs.itemDirection,
alignItems: skeletonConfigs.itemAlign,
marginRight: columnIndex < gridColumnsArr.length - 1 ? skeletonConfigs.gridColumnsGap : 0,
}"
>
<view
v-if="skeletonConfigs.headShow"
class="x-skeleton__wrapper__head"
:class="[ animate && 'animate' ]"
:style="{
width: skeletonConfigs.headWidth,
height: skeletonConfigs.headHeight,
borderRadius: skeletonConfigs.headBorderRadius,
marginRight: (skeletonConfigs.itemDirection == 'row' && skeletonConfigs.textShow) ? skeletonConfigs.itemGap : 0,
marginBottom: (skeletonConfigs.itemDirection == 'column' && skeletonConfigs.textShow) ? skeletonConfigs.itemGap : 0
}"
></view>
<view v-if="skeletonConfigs.textShow" class="x-skeleton__wrapper__text">
<view
v-for="(text, textIndex) in textRowsArr" :key="textIndex"
class="x-skeleton__wrapper__text__row"
:class="[animate && 'animate']"
:style="{
width: text.width,
height: text.height,
borderRadius: skeletonConfigs.textBorderRadius,
marginBottom: textIndex < textRowsArr.length - 1 ? skeletonConfigs.textRowsGap : 0
}"
></view>
</view>
</view>
</view>
</view>
<!-- 插槽 -->
<view v-else>
<slot></slot>
</view>
</view>
</template>
<script>
import XSkeletonConfigs from './x-skeleton-configs.js'
export default {
name: "x-skeleton",
mixins: [ XSkeletonConfigs ],
props: {
// 骨架屏类型
type: {
type: String,
default: '' //banner轮播图、info个人信息、text段落、menu菜单、list列表、waterfall瀑布流
},
// 是否展示骨架组件
loading: {
type: Boolean,
default: true
},
// 是否开启动画效果
animate: {
type: Boolean,
default: true
},
// 动画效果持续时间,单位秒
animateTime: {
type: Number | String,
default: 1.8
},
// 是否开启淡出动画
fadeOut: {
type: Boolean,
default: true
},
// 淡出效果持续时间,单位秒
fadeOutTime: {
type: Number | String,
default: 0.5
},
// 骨架的背景色
bgColor: {
type: String,
default: '#EAEDF5'
},
// 骨架的动画高亮背景色
highlightBgColor: {
type: String,
default: '#F9FAFF'
},
// 自定义配置
configs: {
type: Object,
default: () => {
return {
// padding: '30rpx', //内边距
// gridRows: 3, //行数
// gridColumns: 2, //列数
// gridRowsGap: '40rpx', //行间隔
// gridColumnsGap: '24rpx', //竖间距
// itemDirection: 'column', //head与text之间的排列方向row、column
// itemGap: '16rpx', //head与text之间的间隔
// itemAlign: 'center', //head与text之间的纵轴对齐方式center、flex-start、flex-end、baseline
// headShow: true, //head是否展示
// headWidth: '100%', //head宽度支持百分比
// headHeight: '400rpx', //head高度
// headBorderRadius: '12rpx', //head圆角支持百分比
// textShow: true, //文本是否展示
// textRows: 3, //文本的行数
// textRowsGap: '12rpx', //文本间距
// textWidth: ['40%', '85%', '60%'], //文本的宽度,可以为百分比,数值,带单位字符串等,可通过数组传入指定每个段落行的宽度
// textHeight: ['30rpx', '20rpx', '20rpx'], //文本的高度,可以为数值,带单位字符串等,可通过数组传入指定每个段落行的高度
// textBorderRadius: '6rpx', //文本的圆角,支持百分比
}
}
}
},
computed: {
gridRowsArr() {
return new Array(Number(this.skeletonConfigs?.gridRows || []));
},
gridColumnsArr() {
return new Array(Number(this.skeletonConfigs?.gridColumns || []));
},
textRowsArr() {
if (!this.skeletonConfigs?.textShow) return [];
if (/%$/.test(this.skeletonConfigs.textHeight)) {
console.error('x-skeleton: textHeight参数不支持百分比单位');
}
const rows = []
for (let i = 0; i < this.skeletonConfigs.textRows; i++) {
const { gridRows, textWidth, textHeight } = this.skeletonConfigs;
let item = {},
// 需要预防超出数组边界的情况
rowWidth = this.isArray(textWidth) ? (textWidth[i] || (i === gridRows - 1 ? '70%' : '100%')) : i === gridRows - 1 ? '70%' : textWidth,
rowHeight = this.isArray(textHeight) ? (textHeight[i] || '30rpx') : textHeight
// 非百分比的宽度时,调整像素单位
if (/%$/.test(rowWidth)) {
item.width = rowWidth;
} else {
item.width = this.addUnit(rowWidth)
}
item.height = this.addUnit(rowHeight)
rows.push(item)
}
return rows
},
variableStr() {
let keys = ['animateTime', 'fadeOutTime', 'bgColor', 'highlightBgColor'];
let str = keys.map(item => {
if (item.indexOf('Time') > -1) {
return `--${item}:${this[item]}s`
} else {
return `--${item}:${this[item]}`
}
}).join(";");
return str;
}
},
watch: {
loading: {
immediate: true,
handler(value) {
if (value) {
this.skeletonLoading = true;
} else {
if (this.fadeOut) {
this.startFadeOut = true;
setTimeout(() => {
this.skeletonLoading = false;
this.startFadeOut = false;
}, this.fadeOutTime * 1000);
} else {
this.skeletonLoading = false;
}
}
}
},
type: {
immediate: true,
handler(value) {
if (value === 'banner') {
this.skeletonConfigs = this.bannerConfigs();
} else if (value === 'info') {
this.skeletonConfigs = this.infoConfigs();
} else if (value === 'text') {
this.skeletonConfigs = this.textConfigs();
} else if (value === 'menu') {
this.skeletonConfigs = this.menuConfigs();
} else if (value === 'list') {
this.skeletonConfigs = this.listConfigs();
} else if (value === 'waterfall') {
this.skeletonConfigs = this.waterfallConfigs();
} else {
this.skeletonConfigs = this.configs || {};
}
}
}
},
data() {
return {
skeletonConfigs: this.configs || {},
skeletonLoading: this.loading,
startFadeOut: false,
width: 0
};
},
mounted() {
},
methods: {
/**
* @description 是否为数组
* @param {object} value 需要判断的对象
*/
isArray(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
},
/**
* @description 添加单位如果有rpxupx%px等单位结尾或者值为auto直接返回否则加上px单位结尾
* @param {string|number} value 需要添加单位的值
* @param {string} unit 添加的单位名 比如px
*/
addUnit(value = 'auto', unit = 'px') {
value = String(value);
// 用uView内置验证规则中的number判断是否为数值
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value) ? `${value}${unit}` : value;
}
}
}
</script>
<style lang="scss" scoped>
@mixin background {
background: linear-gradient(90deg, var(--bgColor) 25%, var(--highlightBgColor) 37%, var(--bgColor) 50%);
background-size: 400% 100%;
}
.x-skeleton {
width: 100%;
box-sizing: border-box;
.x-skeleton__wrapper {
display: flex;
flex-direction: column;
&__rows {
display: flex;
align-items: center;
justify-content: space-between;
}
&__columns {
display: flex;
align-items: center;
flex: 1;
}
&__head {
width: 100%;
@include background;
}
&__text {
flex: 1;
width: 100%;
&__row {
@include background;
}
}
}
.fade-out {
opacity: 0;
animation: fadeOutAnim var(--fadeOutTime);
}
@keyframes fadeOutAnim {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.animate {
animation: skeletonAnim var(--animateTime) ease infinite;
}
@keyframes skeletonAnim {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
}
</style>

View File

@@ -0,0 +1,84 @@
{
"id": "x-skeleton",
"displayName": "skeleton骨架屏可任意配置-易用-灵活-动画)",
"version": "1.0.4",
"description": "x-skeleton骨架屏可随意配置内容扩展性强简单易用内含常用的骨架类型",
"keywords": [
"skeleton",
"骨架屏",
"加载效果",
"vue",
"微信小程序"
],
"repository": "",
"engines": {
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
# x-skeleton
# 功能介绍
- 支持 H5、微信小程序其他端未测试过
- 使用简单、灵活,高度自定义
- 加载时支持动画
- 消失时加了动画,不再生硬切换页面
- 支持绝大部分常用场景:
1、轮播图
2、个人信息
3、段落
4、菜单
5、列表
6、瀑布流
7、自定义...
# 属性说明
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
| ------- | --------------------------------------------------- | ------- | ------ | --- |
| type | 骨架类型,为空时是完全自定义 | String | - |banner轮播图、info个人信息、text段落、menu菜单、list列表、waterfall瀑布流|
| loading | 是否显示骨架占位图设置为false将会展示子组件内容 | Boolean | true |true、false|
| animate | 是否开启动画效果 | Boolean | true |true、false|
| animateTime | 动画效果持续时间,单位秒 | Number \| String | 1.8 |-|
| fadeOut | 是否开启淡出动画 | Boolean | true |true、false|
| fadeOutTime | 淡出效果持续时间,单位秒 | Number \| String | 0.5 |-|
| bgColor | 骨架的背景色 | String | #EAEDF5 |-|
| highlightBgColor | 骨架的动画高亮背景色 | String | #F9FAFF |-|
| configs | 自定义配置,具体看下方 | Object | {} |-|
## configs参数说明
| 参数 | 说明 | 类型 |
| ------- | --------------------------------------------------- | ------- |
| padding | 骨架内边距,同 css 的 padding | String |
| gridRows | 行数 | Number |
| gridColumns | 列数 | Number |
| gridRowsGap | 行间隔 | String |
| gridColumnsGap | 竖间距 | String |
| itemDirection | head与text之间的排列方向row、column | String |
| itemGap | head与text之间的间隔 | String |
| itemAlign | head与text之间的纵轴对齐方式同 flex 的align-itemscenter、flex-start、flex-end等 | String |
| headShow | head是否展示 | Boolean |
| headWidth | head宽度支持百分比 | String |
| headHeight | head高度 | String |
| headBorderRadius | head圆角支持百分比 | String |
| textShow | text是否展示 | Boolean |
| textRows | text的行数 | Number |
| textRowsGap | text间距 | String |
| textWidth | text的宽度可以为百分比数值带单位字符串等可通过数组传入指定每个段落行的宽度 | String \| Array \| Number |
| textHeight | text的高度可以为数值带单位字符串等可通过数组传入指定每个段落行的高度 | String \| Array \| Number |
| textBorderRadius | text的圆角支持百分比 | String |
大部分情况下,直接指定相应的 type 已经够用了,如果大家想进行样式的微调、完全自定义可通过设置 configs 来实现。
简单解释一下这些参数(右边有结构布局图示):
布局总共分成 4 块,分别是 grid、item、head、text。
1、grid包含 item指定每一行有多少个 item每一列有多少个 item
2、item包含 head、text可设置他们之间的排列方式、间距
3、head一个 item 只有一个 head可设置宽高、圆角
4、text一个 item 可以有多行 text可分别设置宽高、圆角、间距
# 使用示例
```html
<x-skeleton type="banner" :loading="loading">
<view>我是轮播图</view>
</x-skeleton>
```
```js
export default {
data() {
return {
loading: true,
}
},
onLoad() {
setTimeout(() => {
this.loading = false;
}, 2000);
},
}
```
更多用法请下载查看示例代码,有问题可以留言