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

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 yang1206
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,83 @@
<p align="center">
<img src="https://s2.loli.net/2023/08/30/1AxH9rbqi4kvCls.png"
width="100"
height="100" style="max-width: 100%;" alt="logo" />
</p>
<h1 align="center">nutui-uniapp</h1>
<p align="center">京东风格的轻量级 uni-app 组件库,支持移动端 H5 和 小程序开发</p>
<p align="center">
<a href="https://github.com/nutui-uniapp/nutui-uniapp">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/nutui-uniapp/nutui-uniapp?logo=github&color=%234d80f0&link=https%3A%2F%2Fgithub.com%2nutui-uniapp%2Fnutui-uniapp">
</a>
<a href="https://www.npmjs.com/package/nutui-uniapp">
<img alt="npm" src="https://img.shields.io/npm/v/nutui-uniapp?logo=npm&color=%234d80f0&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fnutui-uniapp">
</a>
<a href="https://www.npmjs.com/package/nutui-uniapp">
<img alt="npm" src="https://img.shields.io/npm/dw/nutui-uniapp?logo=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fnutui-uniapp">
</a>
<a href="https://www.npmjs.com/package/nutui-uniapp">
<img src="https://img.shields.io/npm/dt/nutui-uniapp?style=flat-square" alt="downloads">
</a>
<a href="https://app.netlify.com/sites/nutui-uniapp/deploys">
<img src="https://api.netlify.com/api/v1/badges/dbbf78de-0649-4b88-a06a-f3b18d053776/deploy-status" alt="netlify" />
</a>
</p>
<p align="center">
<img src="https://s2.loli.net/2023/07/05/eJwPvqCY8EcZ7Vi.png" width="164" alt="NutUI" />
&nbsp;
<img src="https://s2.loli.net/2023/07/05/QyW2RHcmnuvIFwp.jpg" width="166" title="请用微信扫码" alt="NutUI>
&nbsp;
</p>
## 介绍
nutui-uniapp 组件库基于Taro版[`NutUi`](https://nutui.jd.com/#/) 4.x版本修改而来适配了uni-app, 使用 Vue 技术栈开发小程序应用,开箱即用,拥有丰富的业务组件。
## 特性
- 🚀 80+ 高质量组件,覆盖移动端主流场景
- 💪 支持一套代码同时开发多端
- 📖 基于京东 APP 10.0 视觉规范
- 🍭 支持按需引用
- 💪 支持 TypeScript
- 💪 支持动态定制主题
- 🍭 支持暗黑模式
- 🌍 支持国际化
## 快速上手
请参考[快速上手](https://nutui-uniapp.pages.dev/guide/quick-start.html)。
## 链接
- [意见反馈](https://github.com/nutui-uniapp/nutui-uniapp/issues)
- [更新日志](https://github.com/nutui-uniapp/nutui-uniapp/releases)
- [常见问题](https://nutui-uniapp.pages.dev/guide/faq.html)
- [Discussions 讨论区](https://github.com/nutui-uniapp/nutui-uniapp/discussions)
## 贡献指南
修改代码请阅读我们的 [贡献指南](https://github.com/nutui-uniapp/nutui-uniapp/blob/main/CONTRIBUTING.md)。
使用过程中发现任何问题都可以提 [Issue](https://github.com/nutui-uniapp/nutui-uniapp/issues) 给我们,当然,我们也非常欢迎你给我们发 [PR](https://github.com/nutui-uniapp/nutui-uniapp/pulls)。
## 贡献者们
感谢以下所有给 nutui-uniapp 贡献过代码的 [开发者](https://github.com/nutui-uniapp/nutui-uniapp/graphs/contributors)。
<a href="https://github.com/nutui-uniapp/nutui-uniapp/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nutui-uniapp/nutui-uniapp" alt="contributors" />
</a>
## 感谢
- [ano-ui](https://github.com/ano-ui/ano-ui)
- [NutUi](https://github.com/jdf2e/nutui)
- [Uni-NutUi](https://github.com/jwaterwater/uni-nutui)
- [vin-ui](https://github.com/vingogo/vin-ui)
- [uni-helper](https://github.com/uni-helper)
## License
[MIT](https://github.com/nutui-uniapp/nutui-uniapp/blob/main/LICENSE) License &copy; 2023-PRESENT [Yang1206](https://github.com/yang1206) and all contributors.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,885 @@
## 1.8.02024-10-14
### Bug Fixes
* **dialog:** 修复小程序平台启用 `close-on-popstate``onMounted` 报错 ([#435](https://github.com/nutui-uniapp/nutui-uniapp/issues/435)) ([93127f5](https://github.com/nutui-uniapp/nutui-uniapp/commit/93127f557ad6cbbf04b88611868f9fb59a786875))
* **popup:** 修复 `visible` 初始值为 `true` 时丢失 `z-index` 问题 ([#437](https://github.com/nutui-uniapp/nutui-uniapp/issues/437)) ([47adc2a](https://github.com/nutui-uniapp/nutui-uniapp/commit/47adc2a17ac85413d310d7849186782b1414e809))
* **short-password:** 修复 `close-on-click-overlay` 属性无效 ([#436](https://github.com/nutui-uniapp/nutui-uniapp/issues/436)) ([e62dfb0](https://github.com/nutui-uniapp/nutui-uniapp/commit/e62dfb0ac69fa2b7c4834d640dee0426d43cb5b8))
# [1.8.0](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.17...v1.8.0) (2024-10-14)
### Bug Fixes
* **dialog:** 修复小程序平台启用 `close-on-popstate``onMounted` 报错 ([#435](https://github.com/nutui-uniapp/nutui-uniapp/issues/435)) ([93127f5](https://github.com/nutui-uniapp/nutui-uniapp/commit/93127f557ad6cbbf04b88611868f9fb59a786875))
* **popup:** 修复 `visible` 初始值为 `true` 时丢失 `z-index` 问题 ([#437](https://github.com/nutui-uniapp/nutui-uniapp/issues/437)) ([47adc2a](https://github.com/nutui-uniapp/nutui-uniapp/commit/47adc2a17ac85413d310d7849186782b1414e809))
* **short-password:** 修复 `close-on-click-overlay` 属性无效 ([#436](https://github.com/nutui-uniapp/nutui-uniapp/issues/436)) ([e62dfb0](https://github.com/nutui-uniapp/nutui-uniapp/commit/e62dfb0ac69fa2b7c4834d640dee0426d43cb5b8))
## [1.7.17](https://github.com/xiaohe0601/nutui-uniapp/compare/v1.7.16...v1.7.17) (2024-09-18)
### Bug Fixes
* **inputnumber:** 新增step-strictly属性 ([#421](https://github.com/xiaohe0601/nutui-uniapp/issues/421)) ([c0a3c47](https://github.com/xiaohe0601/nutui-uniapp/commit/c0a3c47099f19db14716704d5ba2b52dc31541ec))
* **inputnumber:** 修复失焦后未正确按照最大/最小值修改输入值 ([#420](https://github.com/xiaohe0601/nutui-uniapp/issues/420)) ([be3059f](https://github.com/xiaohe0601/nutui-uniapp/commit/be3059f5e82e713a7df5f33eb31cc8fc89b7766a))
### Features
* **cell:** 新增title-width属性 ([#418](https://github.com/xiaohe0601/nutui-uniapp/issues/418)) ([3079495](https://github.com/xiaohe0601/nutui-uniapp/commit/3079495a522af1ae75073efa0830dde43835bd00))
## [1.7.16](https://github.com/xiaohe0601/nutui-uniapp/compare/v1.7.15...v1.7.16) (2024-09-04)
### Bug Fixes
* **popover,shakedice,tour:** fix scss warning ([#413](https://github.com/xiaohe0601/nutui-uniapp/issues/413)) ([28df492](https://github.com/xiaohe0601/nutui-uniapp/commit/28df492f6daa00da26e72f619e8f2d5073489785))
### Features
* **notify:** 新增useNotify ([873b454](https://github.com/xiaohe0601/nutui-uniapp/commit/873b4545ec277b6227ea90d23d8880d0d531edb8))
## [1.7.15](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.14...v1.7.15) (2024-08-17)
### Bug Fixes
* 修复部分机型不支持css inset ([#400](https://github.com/nutui-uniapp/nutui-uniapp/issues/400)) ([f282aad](https://github.com/nutui-uniapp/nutui-uniapp/commit/f282aad819bad85060f2ec6b4302df9c7e1ad61c))
* **number-keyboard:** 修复部分机型不支持inset ([#396](https://github.com/nutui-uniapp/nutui-uniapp/issues/396)) ([d06336e](https://github.com/nutui-uniapp/nutui-uniapp/commit/d06336e2996fbe264b56de7d8e142c9a5368ddea))
* **range:** 修复可能存在flex-shrink为0时的样式异常 ([0f3ced7](https://github.com/nutui-uniapp/nutui-uniapp/commit/0f3ced77bdb2edc0c3bbd1691af8a6916cc39da3))
* **short-password:** 修复部分机型不支持inset ([#397](https://github.com/nutui-uniapp/nutui-uniapp/issues/397)) ([7d4eb7e](https://github.com/nutui-uniapp/nutui-uniapp/commit/7d4eb7ed4caf13a0ff9b76411602e0e359a8a359))
* **short-password:** 修复忘记密码文本对齐问题 ([#393](https://github.com/nutui-uniapp/nutui-uniapp/issues/393)) ([bb000ae](https://github.com/nutui-uniapp/nutui-uniapp/commit/bb000ae7dc1add2f58a5a1927c9a12339f8dcbe9))
* **sticky:** 修复降级为fixed实现时效果异常 ([#402](https://github.com/nutui-uniapp/nutui-uniapp/issues/402)) ([15f8338](https://github.com/nutui-uniapp/nutui-uniapp/commit/15f83380e467dfbd3fad6cfe77e157668b024260))
* update type declaration from '@vue/runtime-core' to 'vue' for Vu… ([#407](https://github.com/nutui-uniapp/nutui-uniapp/issues/407)) ([14d9d78](https://github.com/nutui-uniapp/nutui-uniapp/commit/14d9d785b71b0a8be3437b286f42b2fda5340d42))
### Features
* **form:** 为form组件的validate方法的返回添加类型声明 ([#389](https://github.com/nutui-uniapp/nutui-uniapp/issues/389)) ([b3e2f3b](https://github.com/nutui-uniapp/nutui-uniapp/commit/b3e2f3b6e93f96f9cbf0cdf3b186b6d4a6fcb4ec))
## [1.7.14](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.13...v1.7.14) (2024-08-05)
### Bug Fixes
* **range:** 修复点击控制导致NaN问题 ([415c87c](https://github.com/nutui-uniapp/nutui-uniapp/commit/415c87c7d7738d57f4406387208875409cebfa31))
### Features
* **cell:** 新增icon、clickable属性 ([#384](https://github.com/nutui-uniapp/nutui-uniapp/issues/384)) ([3e4ff54](https://github.com/nutui-uniapp/nutui-uniapp/commit/3e4ff54ff558fbe37ace5201aa2721fbd07e659f))
* **checkbox:** 新增checked-value、unchecked-value属性 ([#385](https://github.com/nutui-uniapp/nutui-uniapp/issues/385)) ([9ef1b34](https://github.com/nutui-uniapp/nutui-uniapp/commit/9ef1b34f9ac63eb12d54cf60ac5f6ad9c6945043))
## [1.7.13](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.12...v1.7.13) (2024-07-30)
### Bug Fixes
* **avatar:** 修复支付宝小程序不支持??语法 ([b7df6be](https://github.com/nutui-uniapp/nutui-uniapp/commit/b7df6beddf1044a078c67681f75d7f3eb90bf4e6))
* **navbar:** 修复 navbar safe-area-inset-top 不生效的问题 ([#379](https://github.com/nutui-uniapp/nutui-uniapp/issues/379)) ([74ff480](https://github.com/nutui-uniapp/nutui-uniapp/commit/74ff480985067e7f0ebeebb74d264fe5aa3aa1d4))
* **popup,safearea:** 修复部分低版本系统safearea不生效问题 ([271e24a](https://github.com/nutui-uniapp/nutui-uniapp/commit/271e24a9b206487627bf5b59a364b306e79f50fa))
### Features
* **menu:** add scroll-top prop ([#377](https://github.com/nutui-uniapp/nutui-uniapp/issues/377)) ([7b31fdf](https://github.com/nutui-uniapp/nutui-uniapp/commit/7b31fdf9db08a0696a6ce4d6c8c52d437666aa64))
## [1.7.12](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.11...v1.7.12) (2024-07-09)
### Bug Fixes
* **input:** 修复由于[#324](https://github.com/nutui-uniapp/nutui-uniapp/issues/324)、[#325](https://github.com/nutui-uniapp/nutui-uniapp/issues/325)引出的blur事件失效 ([afb5c76](https://github.com/nutui-uniapp/nutui-uniapp/commit/afb5c76bf10972321d79c51c7a240be5eb9559a6))
## [1.7.11](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.10...v1.7.11) (2024-07-08)
### Bug Fixes
* 修复comment生产环境web端卡死 ([#352](https://github.com/nutui-uniapp/nutui-uniapp/issues/352)) ([be73ccc](https://github.com/nutui-uniapp/nutui-uniapp/commit/be73cccab1afc1d4a93d8c653437bd17b7ce4a23))
* **actionsheet:** 修复custom-style表现异常 ([#360](https://github.com/nutui-uniapp/nutui-uniapp/issues/360)) ([25fc6a1](https://github.com/nutui-uniapp/nutui-uniapp/commit/25fc6a12fa943fd1b6c233dc15b529867fd03446))
* **input:** 修复小程序端clear清除困难问题 ([#324](https://github.com/nutui-uniapp/nutui-uniapp/issues/324)) ([155b00b](https://github.com/nutui-uniapp/nutui-uniapp/commit/155b00bcb3255763634a0227f197894dd2e1aae2))
* **input:** 修复由于capture引起的blur参数丢失 ([#325](https://github.com/nutui-uniapp/nutui-uniapp/issues/325)) ([a1351ad](https://github.com/nutui-uniapp/nutui-uniapp/commit/a1351addfe187914983fad1da4f9039348574eda))
* **popup:** 修复z-index表现异常 ([#364](https://github.com/nutui-uniapp/nutui-uniapp/issues/364)) ([cd50fc3](https://github.com/nutui-uniapp/nutui-uniapp/commit/cd50fc31d3b8e13771b30429de003cf6522e81ee))
* **toast:** 修复部分情况下toast的duration属性无效 ([1885ded](https://github.com/nutui-uniapp/nutui-uniapp/commit/1885ded405a0cadf00dd2adb9865c98a05f7e682))
### Features
* **menuitem:** 新增item-click事件 ([#323](https://github.com/nutui-uniapp/nutui-uniapp/issues/323)) ([df9eeed](https://github.com/nutui-uniapp/nutui-uniapp/commit/df9eeed564f89f3ed111f91f2837c0ec8b0088cc))
* **tab-pane:** 新增padding、background样式变量 ([#326](https://github.com/nutui-uniapp/nutui-uniapp/issues/326)) ([bf5ebae](https://github.com/nutui-uniapp/nutui-uniapp/commit/bf5ebae39aa9769c9c212fae0910c2e12621fdec))
## [1.7.10](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.9...v1.7.10) (2024-05-08)
### Bug Fixes
* **input-number:** 修复小程序不支持vBind ([60427bc](https://github.com/nutui-uniapp/nutui-uniapp/commit/60427bc61a23383a42985fa276c40ada4a5c6b6d))
## [1.7.9](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.8...v1.7.9) (2024-05-08)
### Features
* **input-number:** 支持透传属性至 input 元素 ([7547ed0](https://github.com/nutui-uniapp/nutui-uniapp/commit/7547ed0d7f458babb76c19ddd3c5bd093e021474))
* **number-keyboard:** 新增 confirm 事件 ([0a6b06c](https://github.com/nutui-uniapp/nutui-uniapp/commit/0a6b06cfd21fe3e7957e082115947e393a7072f7))
## [1.7.8](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.7...v1.7.8) (2024-04-23)
### Bug Fixes
* 修复useSelectorQuery在app-plus环境获取instance报错 ([#307](https://github.com/nutui-uniapp/nutui-uniapp/issues/307)) ([3499c41](https://github.com/nutui-uniapp/nutui-uniapp/commit/3499c41bd6a3d2420ba32d5272a41b12c8da86d7))
* 修改tabs选项卡微笑曲线属性在navbar组件content slot中样式出错bug ([0f04867](https://github.com/nutui-uniapp/nutui-uniapp/commit/0f048670229498aa5a3dc8ec63c0fe744d8cfad2))
* **address:** 修复在选择地址后,下一级地址无法滚动到顶部 ([#301](https://github.com/nutui-uniapp/nutui-uniapp/issues/301)) ([6971ece](https://github.com/nutui-uniapp/nutui-uniapp/commit/6971ece48b5e039f82d184e672556bad5aafb6af))
* **cell:** 修复to属性无效 ([#294](https://github.com/nutui-uniapp/nutui-uniapp/issues/294)) ([f691dff](https://github.com/nutui-uniapp/nutui-uniapp/commit/f691dffeed47aed81a83d42aad625526b4efe85a))
* **sku:** 移除defineExpose导入 ([e10294b](https://github.com/nutui-uniapp/nutui-uniapp/commit/e10294bfd4f6217df21a2bbc5c620c1587a178e9))
* **tabs:** 修复 vertical 方式 tab 数量过多时滚动定位不准确的问题 ([a1334df](https://github.com/nutui-uniapp/nutui-uniapp/commit/a1334df9eebec32ebe0638242922d70ad5a9fe23))
* **uploader:** 修复uid生成可能重复 ([1f49808](https://github.com/nutui-uniapp/nutui-uniapp/commit/1f49808b03fbaa0aab5865a2cbe65cbf933ce00c)), closes [#306](https://github.com/nutui-uniapp/nutui-uniapp/issues/306)
### Features
* **sku:** 新增重置商品数量方法 ([#305](https://github.com/nutui-uniapp/nutui-uniapp/issues/305)) ([8e8260b](https://github.com/nutui-uniapp/nutui-uniapp/commit/8e8260bfddf884169a8c4f3f271a7dd58bc0c66a))
* **switch:** add disabled prop & mark disable deprecated ([#299](https://github.com/nutui-uniapp/nutui-uniapp/issues/299)) ([9e8ac05](https://github.com/nutui-uniapp/nutui-uniapp/commit/9e8ac05e4edd41116fa5f7f80d2cf98f0d3a81cd))
## [1.7.7](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.6...v1.7.7) (2024-04-12)
### Bug Fixes
* **ecard:** 修复输入框表现异常 ([#291](https://github.com/nutui-uniapp/nutui-uniapp/issues/291)) ([5763f25](https://github.com/nutui-uniapp/nutui-uniapp/commit/5763f25855bdd33e0068a9a1c7b2556f1e9dfaaa))
* **swipe:** 修复组件宽度超出范围以及点击关闭问题 ([#284](https://github.com/nutui-uniapp/nutui-uniapp/issues/284)) ([48cc120](https://github.com/nutui-uniapp/nutui-uniapp/commit/48cc12026e0ec9befb7bc7215621649dee75032c))
### Features
* **datepicker:** 事件新增date参数 ([#289](https://github.com/nutui-uniapp/nutui-uniapp/issues/289)) ([29947b5](https://github.com/nutui-uniapp/nutui-uniapp/commit/29947b5660586650e6d8d281fa353ec47a6e6680))
* **tag:** 新增disbaled、close-icon-size属性 ([#293](https://github.com/nutui-uniapp/nutui-uniapp/issues/293)) ([85b329c](https://github.com/nutui-uniapp/nutui-uniapp/commit/85b329cdb897164be082f4abe35c56289db1710c))
## [1.7.6](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.5...v1.7.6) (2024-04-10)
### Bug Fixes
* **input:** 修复H5环境下type为number无效 ([#279](https://github.com/nutui-uniapp/nutui-uniapp/issues/279)) ([ec41815](https://github.com/nutui-uniapp/nutui-uniapp/commit/ec41815ae502f56341575706364e3a0eafeb9253))
* **picker:** 修复非H5平台mask缺失 ([96666cf](https://github.com/nutui-uniapp/nutui-uniapp/commit/96666cf7c93c8e38c5b22c3118e2c1a7fe7a2247))
* **searchbar:** 修复app端输入框对齐问题 ([#282](https://github.com/nutui-uniapp/nutui-uniapp/issues/282)) ([1f77dab](https://github.com/nutui-uniapp/nutui-uniapp/commit/1f77dab96d3fa0d589e383565c9a1254a309a31a))
### Features
* **addresslist:** 新增是否使用插槽相关属性 ([#274](https://github.com/nutui-uniapp/nutui-uniapp/issues/274)) ([1727859](https://github.com/nutui-uniapp/nutui-uniapp/commit/1727859c45d65322271e3f7121b825f10845ccac))
* **calendar:** 添加calendar组件点击遮罩关闭事件和点击关闭图标事件 ([#273](https://github.com/nutui-uniapp/nutui-uniapp/issues/273)) ([e70e654](https://github.com/nutui-uniapp/nutui-uniapp/commit/e70e654b57a8884cc4589877f56670652160c33d))
## [1.7.5](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.4...v1.7.5) (2024-03-29)
### Bug Fixes
* **popover:** 修复Popover 气泡弹出框功能问题 ([#269](https://github.com/nutui-uniapp/nutui-uniapp/issues/269)) ([bbf03e5](https://github.com/nutui-uniapp/nutui-uniapp/commit/bbf03e5a973b95d66f76edeb59114872afc606a6))
* **list:** List 虚拟列表 listData 类型错误 ([8cba423](https://github.com/nutui-uniapp/nutui-uniapp/commit/8cba4236b178c2def23ccfd07ee1528f7530c007))
* **checkbox:** 修复checkbox重复添加至checkboxgroup.children的问题 ([#266](https://github.com/nutui-uniapp/nutui-uniapp/issues/266)) ([3f2c2b3](https://github.com/nutui-uniapp/nutui-uniapp/commit/3f2c2b3afa0348032eadff9ef5aa8415843ecac8))
* **dialog:** 修复dialog title响应式失效的问题 ([#267](https://github.com/nutui-uniapp/nutui-uniapp/issues/267)) ([1a0c7cf](https://github.com/nutui-uniapp/nutui-uniapp/commit/1a0c7cf72c448e6e5ff6d7890dd7739e5bbd13b0))
### Features
* **toast:** 新增useToast控制方式 ([#260](https://github.com/nutui-uniapp/nutui-uniapp/issues/260)) ([c014e7a](https://github.com/nutui-uniapp/nutui-uniapp/commit/c014e7a8ed31b9ad4d6bdbf11942528abea1bb26))
### Refactors
* **drag**: 重构 drag组件修复drag组件多次拖动时位置瞬移到起始点的问题 ([5989869](<https://github.com/nutui-uniapp/nutui-uniapp/commit/5989869>))
## [1.7.4](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.3...v1.7.4) (2024-03-20)
### Bug Fixes
* **picker:** 修复model-value值污染问题 ([ce2e865](https://github.com/nutui-uniapp/nutui-uniapp/commit/ce2e86546b4c2a03f2f5235899071b00dff98d7f))
### Features
* 新增clone、equal工具方法 ([ef1611c](https://github.com/nutui-uniapp/nutui-uniapp/commit/ef1611c44e7382f5a58fbc154de513c15f1a1e59))
## [1.7.3](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.2...v1.7.3) (2024-03-09)
### Bug Fixes
* **elevator:** 修复app端点击索引无效并且滚动页面索引无法跟随切换问题 ([#237](https://github.com/nutui-uniapp/nutui-uniapp/issues/237)) ([e149974](https://github.com/nutui-uniapp/nutui-uniapp/commit/e1499747ae9997bda7a5679259c211935e4d1713))
* **picker:** 修复微信小程序暗黑模式picker背景颜色显示错误的问题 ([#236](https://github.com/nutui-uniapp/nutui-uniapp/issues/236)) ([fa425f7](https://github.com/nutui-uniapp/nutui-uniapp/commit/fa425f72584ff515b4b0a2f5810ca21f4f5760f6))
* **picker:** 修复ts类型错误 ([#241](https://github.com/nutui-uniapp/nutui-uniapp/issues/241)) ([1e83035](https://github.com/nutui-uniapp/nutui-uniapp/commit/1e83035dab748c311ae2d207950bf219da2bcb11))
* **sku:** 修复默认底部按钮不显示的问题 ([#238](https://github.com/nutui-uniapp/nutui-uniapp/issues/238)) ([6e2c296](https://github.com/nutui-uniapp/nutui-uniapp/commit/6e2c296ad8c2e77a84f504aa5ffe756002f98061))
* **sku:** 修复组件内容超出,出现横向滚动条的问题 ([#240](https://github.com/nutui-uniapp/nutui-uniapp/issues/240)) ([c0bf4ab](https://github.com/nutui-uniapp/nutui-uniapp/commit/c0bf4ab341a4adbb142da6d282146825af14601d))
* **uploader:** 修复禁用时能删除文件的问题 ([#234](https://github.com/nutui-uniapp/nutui-uniapp/issues/234)) ([834f9bc](https://github.com/nutui-uniapp/nutui-uniapp/commit/834f9bc7b00eaa0e9ebde33d9c56e1c9ed990d54))
### Features
* **input,textarea:** 新增输入框单独控制样式属性 ([#233](https://github.com/nutui-uniapp/nutui-uniapp/issues/233)) ([e9749b0](https://github.com/nutui-uniapp/nutui-uniapp/commit/e9749b0faa64d026c4c53e1faf349e1c42154a28))
## [1.7.2](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.1...v1.7.2) (2024-03-04)
### Bug Fixes
* **avatar-group:** 修复z-index无效 ([#215](https://github.com/nutui-uniapp/nutui-uniapp/issues/215)) ([1b64ffc](https://github.com/nutui-uniapp/nutui-uniapp/commit/1b64ffc8be7923ed742cf7e0a7225738657dd81c))
* **avatar:** 修复size、shape无效以及无法正确从group继承 ([13ce31a](https://github.com/nutui-uniapp/nutui-uniapp/commit/13ce31a8a80bd6895919d2313869c2dcf2491be7))
* **cascader:** 修复leaf等于false并且children为空时可以选择成功的问题 ([#212](https://github.com/nutui-uniapp/nutui-uniapp/issues/212)) ([a3f27f6](https://github.com/nutui-uniapp/nutui-uniapp/commit/a3f27f6cebf36cd549f4ddab4c155efba2a8533a))
* **input,textarea:** 修复input事件返回值错误 ([745a19c](https://github.com/nutui-uniapp/nutui-uniapp/commit/745a19c51359d827cc28307f942b6425e02e275f))
* **textarea:** 修复类型导出缺失 ([1fa9f29](https://github.com/nutui-uniapp/nutui-uniapp/commit/1fa9f2942256bbb975e2dbf276c7c17d2aa096cb))
* **textarea:** 修复autosize类型错误 ([7c2d56a](https://github.com/nutui-uniapp/nutui-uniapp/commit/7c2d56a3a4c962e5b2b6024e1ab68a75032af240))
* **uploader:** 修复beforeDelete默认值无效以及不支持promise ([54a9b0a](https://github.com/nutui-uniapp/nutui-uniapp/commit/54a9b0acde8590996729fbf2d750ec6271eb10a6))
### Features
* **textarea:** 新增input事件 ([2a8a898](https://github.com/nutui-uniapp/nutui-uniapp/commit/2a8a8984318c71bcd24ab1a06ca5dd134a8afe2c))
* **uploader:** 新增accept配置 ([#217](https://github.com/nutui-uniapp/nutui-uniapp/issues/217)) ([a6ba625](https://github.com/nutui-uniapp/nutui-uniapp/commit/a6ba625f0638fdcba1cdd9a34ac0c61dbdb5e095))
### Reverts
* **textarea:** 修复autosize类型错误 ([bc02ceb](https://github.com/nutui-uniapp/nutui-uniapp/commit/bc02ceb8a7882ea363b655cec06aba3268e01b45))
## [1.7.1](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.7.0...v1.7.1) (2024-02-29)
### Bug Fixes
* **form:** 修复prop类型为数组时required校验无效的问题 ([00453d6](https://github.com/nutui-uniapp/nutui-uniapp/commit/00453d68c21973215b57e5248050da4dc19647a2))
* **menu:** 偏移位置错误, app 端v-show 无效 ([#197](https://github.com/nutui-uniapp/nutui-uniapp/issues/197)) ([2fba3fe](https://github.com/nutui-uniapp/nutui-uniapp/commit/2fba3fe91b751d63b0aa007442d1ffb94516fd93))
* **popup,menu:** 修复 popup组件update:visible事件重复调用与menu 组件close 事件在小程序中无法触发 ([#205](https://github.com/nutui-uniapp/nutui-uniapp/issues/205)) ([0ce7d69](https://github.com/nutui-uniapp/nutui-uniapp/commit/0ce7d690b6a401428ab083eba81887e7e4767178)), closes [#196](https://github.com/nutui-uniapp/nutui-uniapp/issues/196)
* **textarea:** 修复ConfirmType导出类型名称重复以及默认值错误 ([748a849](https://github.com/nutui-uniapp/nutui-uniapp/commit/748a8498df99a543d28b37307f877ececa0fe1b4))
* **transition:** app端destroyOnClose属性无效 ([1238ef6](https://github.com/nutui-uniapp/nutui-uniapp/commit/1238ef6cd4dd7fe59250d47752e00349f56292a0))
### Features
* 新增辅助样式 ([ae3775b](https://github.com/nutui-uniapp/nutui-uniapp/commit/ae3775b501b78ed8af955aa6a3b57c444d49356c))
# [1.7.0](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.9...v1.7.0) (2024-02-22)
### Bug Fixes
* **input:** 修复readonly无效 ([f1547bd](https://github.com/nutui-uniapp/nutui-uniapp/commit/f1547bdb455771926c5384979ab5132d4d6df5ab))
* **tabs:** align属性类型错误 ([f9b0eb4](https://github.com/nutui-uniapp/nutui-uniapp/commit/f9b0eb453dc7ce7114c784bb2cc69b5a29eb5d12))
## [1.6.9](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.8...v1.6.9) (2024-02-22)
### Bug Fixes
* **navbar:** 修复在APP上顶部安全距离无效的问题 ([#186](https://github.com/nutui-uniapp/nutui-uniapp/issues/186)) ([e475e7e](https://github.com/nutui-uniapp/nutui-uniapp/commit/e475e7e449e2bddb50484efe0ee8ee03f1febc72))
* **navbar:** 修复占位元素重复计算状态栏高度的问题 ([#187](https://github.com/nutui-uniapp/nutui-uniapp/issues/187)) ([e35083c](https://github.com/nutui-uniapp/nutui-uniapp/commit/e35083cda311201492719b0deb64e3097dc2dec0))
### Features
* **textarea:** 新增placeholder-style、placeholder-class属性 ([ada249f](https://github.com/nutui-uniapp/nutui-uniapp/commit/ada249ff100f60c2217adb31ab59692a6299f924))
## [1.6.8](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.7...v1.6.8) (2024-02-20)
### Bug Fixes
* **ellipsis:** symbol属性默认值丢失 ([b685f40](https://github.com/nutui-uniapp/nutui-uniapp/commit/b685f40a7192d80e279f91c2784dc76cd0c5327f))
* **picker:** className option ([60caafb](https://github.com/nutui-uniapp/nutui-uniapp/commit/60caafb83ee9194cdae85333ae80d4b8864c1c87))
* randomid生成错误 ([2556b99](https://github.com/nutui-uniapp/nutui-uniapp/commit/2556b998452bc75597e5fe5d7b1d3ed870ad1cf3))
### Features
* **form:** 新增 disabled 属性,支持禁用 form 下全部数据录入组件 ([#184](https://github.com/nutui-uniapp/nutui-uniapp/issues/184)) ([ecd03a3](https://github.com/nutui-uniapp/nutui-uniapp/commit/ecd03a3f3d75aa8281d926d270fa5612d02e360e))
* **marquee:** 新增disabled属性 ([#183](https://github.com/nutui-uniapp/nutui-uniapp/issues/183)) ([d64eca2](https://github.com/nutui-uniapp/nutui-uniapp/commit/d64eca23c4124138ccc8a7dc36fe5439a5e8458c))
## [1.6.7](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.6...v1.6.7) (2024-02-06)
### Bug Fixes
* **input:** 移除InputType重复类型 ([15ac2cd](https://github.com/nutui-uniapp/nutui-uniapp/commit/15ac2cd1dd9955feec57df21e8c3b33126e1ab94))
* **inputnumber:** 组件输入内容不会触发change事件 ([9d97d4e](https://github.com/nutui-uniapp/nutui-uniapp/commit/9d97d4e6b90d1cd35815328f2b6b66be94b6a806))
* **navbar:** 修复zIndex无效问题 ([ecfbd16](https://github.com/nutui-uniapp/nutui-uniapp/commit/ecfbd168a7d392604739e6db6bf811e96c7b3dff))
* **noticebar:** remove blank element of left-icon ([160bc37](https://github.com/nutui-uniapp/nutui-uniapp/commit/160bc3763eb0a1b193c914b967814a0b2a728bed))
* **table:** data属性ts类型问题 ([91c434c](https://github.com/nutui-uniapp/nutui-uniapp/commit/91c434c669a9a2fed5762150c3aa641c4a61db0a))
### Features
* **button:** 新增hover-class相关属性 ([987185c](https://github.com/nutui-uniapp/nutui-uniapp/commit/987185cf8d93f474de5a0df4a34b908575ef2dc7))
* **input:** 新增input事件 ([494f10a](https://github.com/nutui-uniapp/nutui-uniapp/commit/494f10ada6143930e421f1a773599cb0fe1f8761))
## [1.6.6](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.5...v1.6.6) (2024-01-26)
### Bug Fixes
* **button:** disabled下 open-type 仍有效 ([af5f5ac](https://github.com/nutui-uniapp/nutui-uniapp/commit/af5f5ac34d9187ac2f0b955935e97d3f815df546)), closes [#173](https://github.com/nutui-uniapp/nutui-uniapp/issues/173)
## [1.6.5](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.4...v1.6.5) (2024-01-23)
### Bug Fixes
* **form:** 修复搭配 input 使用时的样式问题 ([2ce7c20](https://github.com/nutui-uniapp/nutui-uniapp/commit/2ce7c20523694cb8a04dd22bd7a4c533e9ebd297)), closes [#165](https://github.com/nutui-uniapp/nutui-uniapp/issues/165) [#167](https://github.com/nutui-uniapp/nutui-uniapp/issues/167)
### Features
* 添加web-types以提升WebStorm开发体验 ([18795e1](https://github.com/nutui-uniapp/nutui-uniapp/commit/18795e1ad86267cca09ff4abc28f0f9b61aaea7e))
## [1.6.4](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.3...v1.6.4) (2024-01-15)
### Bug Fixes
* **calendar:** 优化初次打开默认值定位问题 ([35931ae](https://github.com/nutui-uniapp/nutui-uniapp/commit/35931ae28c60cd9c0502e3a2e65d154b5fa6c793))
* **categorypane:** 商品分类面板组件默认插槽不生效 ([1d263e1](https://github.com/nutui-uniapp/nutui-uniapp/commit/1d263e1492e5f831a218d9f9cf57f90466b372a4))
* **date-picker:** 修复 hour-minute 类型下选项刷新问题 ([291e721](https://github.com/nutui-uniapp/nutui-uniapp/commit/291e72151a66fb68b0e3968252620ce11d8f0209))
* **date-picker:** 修复数据联动更新问题 ([e4d2d03](https://github.com/nutui-uniapp/nutui-uniapp/commit/e4d2d03ffb56da500d0dd2e1d573775a829512d8))
* **dialog:** 修复 cancel 按钮的样式问题 ([bb62248](https://github.com/nutui-uniapp/nutui-uniapp/commit/bb62248bedaacc3cbcb3c92f414bcb6dacfaf59e))
* **menuitem:** close open 事件无效 ([15b73c6](https://github.com/nutui-uniapp/nutui-uniapp/commit/15b73c6a1a2c28a3f688ea9ada38687ad905be8e)), closes [#163](https://github.com/nutui-uniapp/nutui-uniapp/issues/163)
* **picker:** 修复 Safari 下遮罩层级样式问题 ([1d0149e](https://github.com/nutui-uniapp/nutui-uniapp/commit/1d0149e7a5b56aac62ed12a00f91c3f11e57085c))
* **picker:** 优化 select、cursor 样式 ([a1ad4b0](https://github.com/nutui-uniapp/nutui-uniapp/commit/a1ad4b0ffd01ac92d99fa397b7ac271099af15b9))
### Features
* **card:** 商品卡片组件增加shopName插槽用于店铺名称自定义 ([996726d](https://github.com/nutui-uniapp/nutui-uniapp/commit/996726ddc73d97994a394999a7484b858e9e6f00))
## [1.6.3](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.2...v1.6.3) (2024-01-08)
### Performance Improvements
* **input:** 使用computed代替函数调用 ([374e24c](https://github.com/nutui-uniapp/nutui-uniapp/commit/374e24c34c667a72ca754b84a9c73b7850a57771))
## [1.6.2](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.1...v1.6.2) (2024-01-08)
### Bug Fixes
* **calendar:** 日历组件在微信小程序弹窗展示时,无法滚动到默认日期 ([8f3b3aa](https://github.com/nutui-uniapp/nutui-uniapp/commit/8f3b3aa4d157db1fc842825598441ec78d2723ae))
* **calendar:** 日历组件在微信小程序中无法显示今天标记 ([15aadca](https://github.com/nutui-uniapp/nutui-uniapp/commit/15aadcae48b112eee323b2593e616ccb935e8c77))
* **calendar:** 优化初始滚动判断条件 ([5961f8e](https://github.com/nutui-uniapp/nutui-uniapp/commit/5961f8edee6ade9835329306a7543f3fcd29cece))
* **input:** modelValue为空时错误调用方法 ([782676b](https://github.com/nutui-uniapp/nutui-uniapp/commit/782676bfa69c9c82b1a3483f48a5264ee5e09d8a)), closes [#155](https://github.com/nutui-uniapp/nutui-uniapp/issues/155)
## [1.6.1](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.6.0...v1.6.1) (2024-01-04)
### Features
* **ecard:** 支持配置是否显示自定与步进 ([584f091](https://github.com/nutui-uniapp/nutui-uniapp/commit/584f091d35e78a2ec02e72bd8578ca091628d666))
# [1.6.0](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.5.10...v1.6.0) (2024-01-04)
### Bug Fixes
* **input:** form下 input 样式错误 ([4c0f900](https://github.com/nutui-uniapp/nutui-uniapp/commit/4c0f9009a7d2543933644b209d223fab4a10217f)), closes [#148](https://github.com/nutui-uniapp/nutui-uniapp/issues/148)
* **tabs:** 修复 placeholder 展示错误 ([754633e](https://github.com/nutui-uniapp/nutui-uniapp/commit/754633e18bef14c37c13068cda9bcb205f1164e7))
* **tabs:** 夜间模式样式错误 ([32c679a](https://github.com/nutui-uniapp/nutui-uniapp/commit/32c679a2e716a0f82ddc09d34a4b785ae21192ee))
### Features
* **swiper:** 支持修改未选中时的分页指示器的颜色 ([e9c4058](https://github.com/nutui-uniapp/nutui-uniapp/commit/e9c4058baf0f5bccbfd74c3f6e905ba359109eaa))
* **tabs:** 开启 title-scroll 时不再需要设置 name ([4085996](https://github.com/nutui-uniapp/nutui-uniapp/commit/4085996b2daf0981e72c97dda5eab1678d1dbda6))
* **tabs:** 支持配置标题对齐方式 ([d46c8ff](https://github.com/nutui-uniapp/nutui-uniapp/commit/d46c8fffd9840405a8327abc85edfc3fe5f66b5c))
* **watermark:** 支持多行文字水印 ([8b42b97](https://github.com/nutui-uniapp/nutui-uniapp/commit/8b42b97237f9cbc92e99fb12c807c4cdfee1e687))
## [1.5.10](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.5.9...v1.5.10) (2023-12-31)
### Bug Fixes
* **popup:** app端show属性无效以及destroy-on-close逻辑错误 ([522f620](https://github.com/nutui-uniapp/nutui-uniapp/commit/522f6200e48cdf67ccd1767eb2aa89c697f03a49))
* **popup:** destroy-on-close逻辑错误 ([6d9d12c](https://github.com/nutui-uniapp/nutui-uniapp/commit/6d9d12cbf5dd082806b14bb783ce690c1cafc76c))
* **sku:** 修复SkuStepper依赖循环,样式错误 ([2575b24](https://github.com/nutui-uniapp/nutui-uniapp/commit/2575b24fb2f92d0d0ee41edbb0174f9528b2ce61))
* textarea启用autosize时readonly样式不统一 ([d6c161b](https://github.com/nutui-uniapp/nutui-uniapp/commit/d6c161bce9e8af50dfa162faa9d4b3be8d31a82f))
## [1.5.9](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.5.8...v1.5.9) (2023-12-18)
### Bug Fixes
* **navbar:** 修复标题居中等样式问题 ([b5ddff1](https://github.com/nutui-uniapp/nutui-uniapp/commit/b5ddff18ebfbc00137dc88d8993d8d6928c2d3c3))
* **sku:** 修复小程序下选项内容的滚动问题 ([715a4db](https://github.com/nutui-uniapp/nutui-uniapp/commit/715a4db500fe303ccd5eafc8140b0adb328a3ebb))
* **tabs:** 修复嵌套使用时的样式错乱问题 ([a8c9822](https://github.com/nutui-uniapp/nutui-uniapp/commit/a8c98229836318b50aa4c7cae83aedf31949eae3))
### Features
* **radio:** 组件设置 button 形状时支持改变大小 ([ff808c0](https://github.com/nutui-uniapp/nutui-uniapp/commit/ff808c06ac6713b69e0383fb4b5b9f6a0544fa3b))
## [1.5.8](https://github.com/nutui-uniapp/nutui-uniapp/compare/v1.5.7...v1.5.8) (2023-12-14)
### Bug Fixes
* **menu:** 去除无效输出 ([3f36ff8](https://github.com/nutui-uniapp/nutui-uniapp/commit/3f36ff8535f662bf90fa09e0d9c97c1aa1775a12))
* **menu:** 修复menu闪屏动画错误以及位置偏移 ([48c85f5](https://github.com/nutui-uniapp/nutui-uniapp/commit/48c85f5246f1de10bcf3535e31ef202ee8e28d9f)), closes [#138](https://github.com/nutui-uniapp/nutui-uniapp/issues/138)
* **popup:** 修复destroy-on-close无效 ([0e64ee2](https://github.com/nutui-uniapp/nutui-uniapp/commit/0e64ee2c1f89eba08d9438c5e1378d0ba49f495c)), closes [#136](https://github.com/nutui-uniapp/nutui-uniapp/issues/136)
### Features
* **safearea:** 新增SafeArea组件 ([e346920](https://github.com/nutui-uniapp/nutui-uniapp/commit/e346920108081d0857143d28dbfbd495351c996e))
### Reverts
* Revert "chore: release v1.5.8" ([8c0f8e0](https://github.com/nutui-uniapp/nutui-uniapp/commit/8c0f8e0dfd034a84996b10bafa8c5e1afbbb4535))
* Revert "docs: update changelog" ([108a458](https://github.com/nutui-uniapp/nutui-uniapp/commit/108a458dea38728bba55d9b36946cc3720260e14))
## [1.5.7](https://github.com/yang1206/uniapp-nutui/compare/v1.5.6...v1.5.7) (2023-12-09)
### Bug Fixes
* **range:** 优化滑动事件处理逻辑 ([be20418](https://github.com/yang1206/uniapp-nutui/commit/be2041839d2c449e9cf22b0029d437eebb4c68fa))
### Features
* **calendar:** 新增 btn-slot属性控制是否使用btn 插槽 ([3b66919](https://github.com/yang1206/uniapp-nutui/commit/3b66919bfd86c0857ce1a8ea265ae5fa9e3f0b9f)), closes [#131](https://github.com/yang1206/uniapp-nutui/issues/131)
* **form:** 支持配置表单布局 ([882dce4](https://github.com/yang1206/uniapp-nutui/commit/882dce417ad6799cf18f31e43a6734f5cc7cfea7))
## [1.5.6](https://github.com/yang1206/uniapp-nutui/compare/v1.5.5...v1.5.6) (2023-11-29)
### Bug Fixes
* **avatar:** 修复maxCount属性无效 ([341f488](https://github.com/yang1206/uniapp-nutui/commit/341f48806545f1f7f84cd208d6790f73858d749e))
* **numberkeyboard:** 类名错误 ([6a15803](https://github.com/yang1206/uniapp-nutui/commit/6a15803a4e8875fadf9172b2d388a732f2fc3e71))
* **utils:** isPromise 判断错误 ([42820e2](https://github.com/yang1206/uniapp-nutui/commit/42820e2ad20c571ea00afc588254ae4dbe5df1b7))
## [1.5.5](https://github.com/yang1206/uniapp-nutui/compare/v1.5.4...v1.5.5) (2023-11-25)
### Bug Fixes
* **formitem:** 提高传入 required 的优先级 ([300ef79](https://github.com/yang1206/uniapp-nutui/commit/300ef797e05c2cacbe2e144bafd1e7402116fc83))
* **inputnumber:** 修复步进按钮禁用功能无效 ([75fc3cc](https://github.com/yang1206/uniapp-nutui/commit/75fc3cc78a7dd6154b5418db0306f4ce1dcfc25b))
* **swiper:** 修复 swiper-item 宽高错误问题 ([0840982](https://github.com/yang1206/uniapp-nutui/commit/084098288d3511f43db2f4db0b4c723ddc53c3a5))
## [1.5.4](https://github.com/yang1206/uniapp-nutui/compare/v1.5.3...v1.5.4) (2023-11-23)
### Bug Fixes
* **address:** 选择最后一级时 onChange 缺少 value 字段 ([04d9262](https://github.com/yang1206/uniapp-nutui/commit/04d92627b6b08e9f439208a24c627c8975f91906))
* **badge:** 修复微信小程序 props编译丢失 ([e95ff0d](https://github.com/yang1206/uniapp-nutui/commit/e95ff0df44abe6cc1a99db1b21b6696135d0c9d6))
* **elevator:** 修复滚动后无法点击问题 ([6956662](https://github.com/yang1206/uniapp-nutui/commit/695666260596b9b5e5925451de74bd23556b0af0))
* **picker:** 调整 columns 类型定义 ([42f1b9c](https://github.com/yang1206/uniapp-nutui/commit/42f1b9c3b7fc4f6d434c63791b20b84b09d0bcb0))
### Features
* **cascader:** 新增title插槽 ([80b2f29](https://github.com/yang1206/uniapp-nutui/commit/80b2f291d66920d990dc15a21752400b35fdd4e4)), closes [#119](https://github.com/yang1206/uniapp-nutui/issues/119)
* **form:** formItem的required的星号支持从rules中自动判断是否显示 ([1ddda92](https://github.com/yang1206/uniapp-nutui/commit/1ddda92b4a6c4e312a502be9d10d6db64da80e31))
* **input:** modelValue的类型支持number ([2e308e7](https://github.com/yang1206/uniapp-nutui/commit/2e308e741f7c888b5bb96d19b8d2fd02f627db5a))
## [1.5.3](https://github.com/yang1206/uniapp-nutui/compare/v1.5.2...v1.5.3) (2023-11-22)
### Bug Fixes
* 修复支付宝小程序编译错误 ([203f3bd](https://github.com/yang1206/uniapp-nutui/commit/203f3bdd0c164faad471a74ce9eb4bffc7fb29ea)), closes [#117](https://github.com/yang1206/uniapp-nutui/issues/117)
## [1.5.2](https://github.com/yang1206/uniapp-nutui/compare/v1.5.1...v1.5.2) (2023-11-20)
### Bug Fixes
* **menu:** 修复自定义选中颜色失效 ([b441e89](https://github.com/yang1206/uniapp-nutui/commit/b441e89113c25f62eb3a42f75c822eaee6d0a149))
* **notify:** 修复重复点击偶尔无法触发 ([d044ac9](https://github.com/yang1206/uniapp-nutui/commit/d044ac9592e27dc25ca7453e9579593a188e8337))
* **rate:** 修复无法自定义图标大小 ([1d38d71](https://github.com/yang1206/uniapp-nutui/commit/1d38d712fa92bd9181f6d4302df547bd090af896)), closes [#111](https://github.com/yang1206/uniapp-nutui/issues/111)
* **tabs:** 修复h5端滑动切换卡顿 ([a17e35e](https://github.com/yang1206/uniapp-nutui/commit/a17e35edf132488b0b1c8098f87e23d89e30120a))
* **toast:** 修复重复点击造成卡死 ([4847ae9](https://github.com/yang1206/uniapp-nutui/commit/4847ae964ecd50fd466b336ec92a0121fd589bc9))
### Features
* 全部组件均已支持自定义类名与style ([5a29a83](https://github.com/yang1206/uniapp-nutui/commit/5a29a837a6ba3b7b1ca04edcbf76027877839a8c))
* **noticebar:** 新增`field-name`属性,支持传入数组对象 ([3f95345](https://github.com/yang1206/uniapp-nutui/commit/3f95345f2d97d9ce9dbb2e2004e269b1cf986d05)), closes [#109](https://github.com/yang1206/uniapp-nutui/issues/109)
## [1.5.1](https://github.com/yang1206/uniapp-nutui/compare/v1.5.0...v1.5.1) (2023-11-17)
### Bug Fixes
* **calendar:** 过期时间未被禁用 ([5b767b8](https://github.com/yang1206/uniapp-nutui/commit/5b767b88be6c32b8c8edf1212eedec2c60929a57))
* **radio:** 修复`text-position`属性失效 ([1eadf1a](https://github.com/yang1206/uniapp-nutui/commit/1eadf1ac25087fc259d40c30723918e61b818c86))
### Features
* **(signature):** signature组件小程序端支持导出图片 ([2992d39](https://github.com/yang1206/uniapp-nutui/commit/2992d39522e18e5f6dd51f82f237171fdb789d2b))
# [1.5.0](https://github.com/yang1206/uniapp-nutui/compare/v1.4.1...v1.5.0) (2023-11-15)
### Bug Fixes
* **cascader:** 级联二级切换无法被监听到 ([c78f382](https://github.com/yang1206/uniapp-nutui/commit/c78f382c02f76241919037b35c1834a73aeb6337)), closes [#105](https://github.com/yang1206/uniapp-nutui/issues/105)
* textarea的autosize表现异常 ([d14813d](https://github.com/yang1206/uniapp-nutui/commit/d14813d6b32e2131a48abc7a8d6ec668c89023ce))
* fix(overlay)!: 小程序与 APP平台不再支持`lock-scroll`属性 ([cd3a740](https://github.com/yang1206/uniapp-nutui/commit/cd3a74066829123299915787feaf2d675a8a487c)), closes [#103](https://github.com/yang1206/uniapp-nutui/issues/103)
### Features
* 组件props增加注释说明 ([a585716](https://github.com/yang1206/uniapp-nutui/commit/a585716ca5dc71b9577a491fc15d696a69aed0fb)), closes [#100](https://github.com/yang1206/uniapp-nutui/issues/100)
### BREAKING CHANGES
* 小程序移除`lock-scroll`属性支持
所有依赖overlay的组件的`lock-scroll`属性同理也将不再支持小程序与 APP 平台,禁用滚动穿透方法可参考文档
## [1.4.1](https://github.com/yang1206/uniapp-nutui/compare/v1.4.0...v1.4.1) (2023-11-08)
### Bug Fixes
* **badge:** badge的top、right不支持rpx单位 ([964e4ea](https://github.com/yang1206/uniapp-nutui/commit/964e4ea6e472889c7f43bc70491d3e3ac613cf31))
# [1.4.0](https://github.com/yang1206/uniapp-nutui/compare/v1.3.5...v1.4.0) (2023-11-08)
### Bug Fixes
* **address:** backIcon会一直存在 ([3819937](https://github.com/yang1206/uniapp-nutui/commit/3819937ec0b59d6d869a986a24e1449fd7dd93f1))
* **card:** price颜色未覆盖 ([2665bd1](https://github.com/yang1206/uniapp-nutui/commit/2665bd137c5d27f57b614ebe70e25299e89ff481))
* **checkbox:** 修复样式对齐问题 ([155e25b](https://github.com/yang1206/uniapp-nutui/commit/155e25ba84676d1c5b816541da622f4c893db857))
* **comment:** label 样式无法正确省略 ([5e56649](https://github.com/yang1206/uniapp-nutui/commit/5e56649f4f87d9e3f914c3923a9d68a4d724ae26))
* **progress:** icon 颜色错误 ([18662d5](https://github.com/yang1206/uniapp-nutui/commit/18662d57046acfb6e5c57e7bef214dde1ce52c88))
* **uploader:** h5进度遮罩样式错误 ([e4ccf4a](https://github.com/yang1206/uniapp-nutui/commit/e4ccf4abdfeb74141a703cda8685e1576590ae8d))
* fix!: 短横线命名slot无法被正确替换 ([52587ed](https://github.com/yang1206/uniapp-nutui/commit/52587ed78feae3e35cd5f38e92cd6878f4c6c715)), closes [#96](https://github.com/yang1206/uniapp-nutui/issues/96)
### Features
* **calendar:** 增加自定义禁用函数disabled-date ([2b0363f](https://github.com/yang1206/uniapp-nutui/commit/2b0363fc58ca0087768a141b3f9dcfd4e4f15d44))
* **calendar:** 增加footerSlot属性判断是否使用 footer 插槽 ([6e53d43](https://github.com/yang1206/uniapp-nutui/commit/6e53d435d179d7865087d03e1ecfd323d58f0162))
* **image-preview:** 新增 long-press 事件 ([42893a5](https://github.com/yang1206/uniapp-nutui/commit/42893a50e1c5d59ec8611d0092f97c92f3797a6e))
* popup 关联组件增加 z-index 属性 ([fe52d5f](https://github.com/yang1206/uniapp-nutui/commit/fe52d5fa7b05fdfec711b9eff7750cd51c291cf3))
### BREAKING CHANGES
* 组件的slot名称由短横线替换为驼峰格式
受到影响的组件有: address , addresslist , calendar , card , comment , inputnumber , navbar , noticebar ,
pagination , progress , sku , trendarrow , uploader
## [1.3.5](https://github.com/yang1206/uniapp-nutui/compare/v1.3.4...v1.3.5) (2023-11-07)
## [1.3.4](https://github.com/yang1206/uniapp-nutui/compare/v1.3.3...v1.3.4) (2023-11-06)
### Bug Fixes
* **watermark:** props丢失响应性 ([21984f5](https://github.com/yang1206/uniapp-nutui/commit/21984f551c77027fa4913200a6d72976cb5e9035)), closes [#95](https://github.com/yang1206/uniapp-nutui/issues/95)
## [1.3.3](https://github.com/yang1206/uniapp-nutui/compare/v1.3.2...v1.3.3) (2023-11-05)
## [1.3.2](https://github.com/yang1206/uniapp-nutui/compare/v1.3.1...v1.3.2) (2023-11-03)
### Features
* **price:** 支持customClass与 customStyle ([43516f4](https://github.com/yang1206/uniapp-nutui/commit/43516f43f33540684ed98a4878b4fbd4925116dd))
## [1.3.1](https://github.com/yang1206/uniapp-nutui/compare/v1.3.0...v1.3.1) (2023-11-01)
### Bug Fixes
* **sku:** 小程序真机调试错误 ([1bb8380](https://github.com/yang1206/uniapp-nutui/commit/1bb8380f4c1cabb443938f31dd6e0d1aa84ea104))
# [1.3.0](https://github.com/yang1206/uniapp-nutui/compare/v1.2.4...v1.3.0) (2023-10-31)
### Bug Fixes
* **circle-progress:** 修复数值变化在 iOS 下的 border 闪烁问题 ([f3f3934](https://github.com/yang1206/uniapp-nutui/commit/f3f3934292a11362c7c4e47cf450a7581e92efab))
* **input:** 优化input属性问题 ([3fe20c7](https://github.com/yang1206/uniapp-nutui/commit/3fe20c76404a1dafc9fe2e66ffb0ea3419c1f07d))
* **inputnumber:** 修复点击icon后超过min-max范围问题 ([17d8f7e](https://github.com/yang1206/uniapp-nutui/commit/17d8f7e6cb29500934c1d800e59b970390cb7b72))
* **picker:** 去除columns有值才触发watch监听 ([6e39bc3](https://github.com/yang1206/uniapp-nutui/commit/6e39bc319d6892460a982e01e020435649554e47))
* **swiper:** 修复动态修改 height 后组件视图未更新问题 ([b9bac98](https://github.com/yang1206/uniapp-nutui/commit/b9bac98a30215a847b536bfa252f5901d21b523d))
* **swiper:** 修复页面中未设置 z-index 的 fixed 元素无法覆盖 swiper 问题 ([08e559b](https://github.com/yang1206/uniapp-nutui/commit/08e559bf2bc4101732067f5bce8231017aa9f788))
### Features
* **collapse:** 增加自定义插槽icon ([539ba35](https://github.com/yang1206/uniapp-nutui/commit/539ba35fd48506a21faa84af9b30b115f2870485))
* **comment:** 追评中若是有图片的话,增加响应事件 ([767cea6](https://github.com/yang1206/uniapp-nutui/commit/767cea6f7208a314e6fad0e98be73e4441aa223e))
* **dialog:** add ok-auto-close ([2957f5a](https://github.com/yang1206/uniapp-nutui/commit/2957f5a98c9f11513c92a511b26bf7afa8807d43))
* **navbar:** 适配小程序状态栏高度,优化代码 ([4ee0118](https://github.com/yang1206/uniapp-nutui/commit/4ee01186d6ac60ce211464dcb2e833b9eff7933f))
## [1.2.4](https://github.com/yang1206/uniapp-nutui/compare/v1.2.3...v1.2.4) (2023-10-27)
### Bug Fixes
* type error ([0b8ad0f](https://github.com/yang1206/uniapp-nutui/commit/0b8ad0f96d2d7b403023622ea0794e4b141466a7))
* type error ([8498e02](https://github.com/yang1206/uniapp-nutui/commit/8498e02011786a9638ee2ebbb9a237de7eb5e449))
### Features
* **input:** 新增 placeholder-style 和 placeholder-class 属性 ([1409880](https://github.com/yang1206/uniapp-nutui/commit/14098807bdb90026d179eb24c739b4f854eeaf72))
* 补充input、textarea部分属性 &nbsp;-&nbsp; by @xiaohe0601 [<samp>(b5bc4cf)</samp>](https://github.com/nutui-uniapp/nutui-uniapp/commit/b5bc4cf)
## [1.2.3](https://github.com/yang1206/uniapp-nutui/compare/v1.2.2...v1.2.3) (2023-10-23)
### Bug Fixes
* (searchbar) autofocus无效 ([f467a11](https://github.com/yang1206/uniapp-nutui/commit/f467a111337bafcc4963d5fa7a4238de797f46e0))
## [1.2.2](https://github.com/yang1206/uniapp-nutui/compare/v1.2.1...v1.2.2) (2023-10-23)
### Bug Fixes
* (input) 修复 input 事件缺失 ([a34e543](https://github.com/yang1206/uniapp-nutui/commit/a34e543a7ae37ff1d0fad1cee06e79be8f868ecf)), closes [#79](https://github.com/yang1206/uniapp-nutui/issues/79)
* 修复lockScroll无效 ([520d693](https://github.com/yang1206/uniapp-nutui/commit/520d6939639d76871255b1953ecbbf8045d2318d))
### Features
* (imagepreview) 支持双指缩放图片 ([c7a9a90](https://github.com/yang1206/uniapp-nutui/commit/c7a9a9073fdc017ce93ddc47825c2290b53e34e7))
## [1.2.1](https://github.com/yang1206/uniapp-nutui/compare/v1.2.0...v1.2.1) (2023-10-18)
### Bug Fixes
* (number-keyboard) type="rightColumn"时,某些版本安卓机无法正常显示 ([a5307ba](https://github.com/yang1206/uniapp-nutui/commit/a5307bae5cc65cb0695823646a0b767edde4f399)), closes [#78](https://github.com/yang1206/uniapp-nutui/issues/78)
# [1.2.0](https://github.com/yang1206/uniapp-nutui/compare/v1.1.10...v1.2.0) (2023-10-18)
### Bug Fixes
* (input) clear 事件回调参数错误 ([2d506ba](https://github.com/yang1206/uniapp-nutui/commit/2d506ba86430ed0b46ed223251e959e37ef598e3))
* (radio) 修复按钮类型激活背景边框样式 ([8736e91](https://github.com/yang1206/uniapp-nutui/commit/8736e9191643c741b20a24ba2fa7ad21640ecc6e))
* (radio) 修复radio-group的textPosition属性没有响应式 ([85b2fa4](https://github.com/yang1206/uniapp-nutui/commit/85b2fa4ef24beccc3ecdfc55667214351ddb822a))
* (tabs) 修复item设置disabled时仍可以滑动过去 ([3b7c77a](https://github.com/yang1206/uniapp-nutui/commit/3b7c77ab19fc56231444f5ee87305eb55356ba46))
* (textarea) 修复readonly 属性失效与无法渲染换行 ([b8d37d4](https://github.com/yang1206/uniapp-nutui/commit/b8d37d40029fe484cd67e482efea00b704d9549f))
### Features
* (searchbar) 增加cursor-spacing属性 ([42393ac](https://github.com/yang1206/uniapp-nutui/commit/42393acf21303d4b8553df8adf5dd9dbd2d0ac3d))
* (sticky) 重构sticky组件 ([bb9457b](https://github.com/yang1206/uniapp-nutui/commit/bb9457b2ee7d2b90cf1f8fcdcf9b870c84b280a0))
* 基础组件最外层元素 flex -> inline-flex ([e065f9f](https://github.com/yang1206/uniapp-nutui/commit/e065f9f034d0da2deed659c1a55cef93efde17b0))
## [1.1.10](https://github.com/yang1206/uniapp-nutui/compare/v1.1.9...v1.1.10) (2023-10-12)
## [1.1.9](https://github.com/yang1206/uniapp-nutui/compare/v1.1.8...v1.1.9) (2023-10-10)
### Bug Fixes
* (address-list) 小程序点击事件无法阻止冒泡 ([38616a9](https://github.com/yang1206/uniapp-nutui/commit/38616a967ea792c99cd5ef12a866132467eff88b))
## [1.1.8](https://github.com/yang1206/uniapp-nutui/compare/v1.1.7...v1.1.8) (2023-09-22)
### Bug Fixes
* type error ([85f9daf](https://github.com/yang1206/uniapp-nutui/commit/85f9daf3526233eeba6e3170554e4116886a95d5)), closes [#62](https://github.com/yang1206/uniapp-nutui/issues/62) [#62](https://github.com/yang1206/uniapp-nutui/issues/62)
## [1.1.7](https://github.com/yang1206/uniapp-nutui/compare/v1.1.6...v1.1.7) (2023-09-20)
### Features
* (cascader) 增加标题配置项 ([d1fde7b](https://github.com/yang1206/uniapp-nutui/commit/d1fde7b09c209778f114bccc7a130c4762eb6c04))
## [1.1.6](https://github.com/yang1206/uniapp-nutui/compare/v1.1.5...v1.1.6) (2023-09-20)
### Bug Fixes
* (searchbar) 修复抖音小程序searchbar输入抖动的问题 ([d634587](https://github.com/yang1206/uniapp-nutui/commit/d6345871aeb1a19244b87ca278d9dec4bd936e37))
### Features
* (calendar) 日历底部增加插槽 ([ff956dc](https://github.com/yang1206/uniapp-nutui/commit/ff956dc4bfe67cb57b6bd556efe814dc5f8080f2))
* (cell) add desc slot ([eb0facf](https://github.com/yang1206/uniapp-nutui/commit/eb0facfe84e1ee7848053c198683d9f63ea73377))
* (searchbar) 增加safe-area-inset-bottom 属性 ([8fc2907](https://github.com/yang1206/uniapp-nutui/commit/8fc2907ec987bdc2952d7f94c72996a5ba3eb5f9))
## [1.1.5](https://github.com/yang1206/uniapp-nutui/compare/v1.1.4...v1.1.5) (2023-09-18)
### Features
* (button) 支持 open-type 等开放能力属性 ([eb72466](https://github.com/yang1206/uniapp-nutui/commit/eb724660ca3e163387863fc0260155004c832feb)), closes [#58](https://github.com/yang1206/uniapp-nutui/issues/58) [#58](https://github.com/yang1206/uniapp-nutui/issues/58)
## [1.1.4](https://github.com/yang1206/uniapp-nutui/compare/v1.1.3...v1.1.4) (2023-09-14)
### Bug Fixes
* (cascader) 动态加载标题无法正确显示 ([a3d7594](https://github.com/yang1206/uniapp-nutui/commit/a3d7594510bdf5088646b0dd3cfb8ed62eb70b2c))
## [1.1.3](https://github.com/yang1206/uniapp-nutui/compare/v1.1.2...v1.1.3) (2023-09-08)
## [1.1.2](https://github.com/yang1206/uniapp-nutui/compare/v1.1.1...v1.1.2) (2023-09-08)
### Bug Fixes
* 修复支付宝小程序编译失败 ([d814b4f](https://github.com/yang1206/uniapp-nutui/commit/d814b4ff073bc1f11bd971df8d035f0924ff16fe))
## [1.1.1](https://github.com/yang1206/uniapp-nutui/compare/v1.1.0...v1.1.1) (2023-09-07)
### Bug Fixes
* (input) 自动聚焦无效 ([8744788](https://github.com/yang1206/uniapp-nutui/commit/874478877e38272c51b437cf17d20c2f272d8876)), closes [#39](https://github.com/yang1206/uniapp-nutui/issues/39)
# [1.1.0](https://github.com/yang1206/uniapp-nutui/compare/v1.0.1...v1.1.0) (2023-09-07)
### Features
* :sparkles: 新增Input组件customStyle自定义属性 ([c42a5fe](https://github.com/yang1206/uniapp-nutui/commit/c42a5fe0fc6ecb4bf89ff9a7213529055c2c8670))
## [1.0.1](https://github.com/yang1206/uniapp-nutui/compare/v1.0.0...v1.0.1) (2023-09-05)
### Bug Fixes
* :bug: 修改inputnumber组件失去焦点以及change的时候展示错误 ([54d7e50](https://github.com/yang1206/uniapp-nutui/commit/54d7e5036af02f171c0e3dc2e12fb9db20f1bd80))
* (picker)修复在非h5环境下多余引入导致无法真机调试 ([c9c8236](https://github.com/yang1206/uniapp-nutui/commit/c9c8236376cf190d20708202deb2c4b81cb7b691))
* **input:** 修复type值number与digit两个校验逻辑反了 ([5c6c3a4](https://github.com/yang1206/uniapp-nutui/commit/5c6c3a4c1dcf24450f966988d93baf293fe4c720))
### Features
* :sparkles: 地址列表组件新增index索引回调 ([3f3bbc0](https://github.com/yang1206/uniapp-nutui/commit/3f3bbc00c74f6673322dc6cfa509f297dacc8299))
# [1.0.0](https://github.com/yang1206/uniapp-nutui/compare/v0.3.1...v1.0.0) (2023-08-30)
### Bug Fixes
* :bug: 解决因组件使用img标签导致微信小程序编译出错问题 ([856f317](https://github.com/yang1206/uniapp-nutui/commit/856f3177ea4a34a6473c51d864324b2b314d5927))
## [0.3.1](https://github.com/yang1206/uniapp-nutui/compare/v0.3.0...v0.3.1) (2023-08-29)
### Features
* (card) 不填写价格则不展示 ([5f3bf98](https://github.com/yang1206/uniapp-nutui/commit/5f3bf98257d58ec530a853c8217b0fa4c6117084))
# [0.3.0](https://github.com/yang1206/uniapp-nutui/compare/v0.2.5...v0.3.0) (2023-08-28)
### Bug Fixes
* (actionsheet) 修复 close-abled 失效问题 ([fabe18d](https://github.com/yang1206/uniapp-nutui/commit/fabe18dca78f30b4146d2fefebb053be1fa85882))
* (countdown) millisecond format ([086c2a9](https://github.com/yang1206/uniapp-nutui/commit/086c2a93736a5fb8b763447a41275522b3c15f7b))
* (picker) 小程序环境切换选项无响应 ([06ca0e8](https://github.com/yang1206/uniapp-nutui/commit/06ca0e86c990aa8caec56c1edbdc6217f576a664))
* (picker) 修复 field-names 在级联模式下的问题 ([a5fa8cf](https://github.com/yang1206/uniapp-nutui/commit/a5fa8cf18fabc526156ff7aec0207b79bc6fc811))
### Features
* (picker) support custom column field names ([808482d](https://github.com/yang1206/uniapp-nutui/commit/808482dbf9b50d7559bbef9c4977fbb6d7a1445e))
## [0.2.5](https://github.com/yang1206/uniapp-nutui/compare/v0.2.4...v0.2.5) (2023-08-24)
### Bug Fixes
* (form) 样式未正确覆盖 ([838db2a](https://github.com/yang1206/uniapp-nutui/commit/838db2ab3af66b79355392e3038cc4c9388168c5)), closes [#28](https://github.com/yang1206/uniapp-nutui/issues/28)
* (dialog) 修复通过ref调用对话框时noCancelBtn属性无效的bug ([6f7d516](https://github.com/yang1206/uniapp-nutui/commit/6f7d516320044ca8d96104531725eb8556732367))
* (input) 修复左右插件默认文字无法显示 ([8b59931](https://github.com/yang1206/uniapp-nutui/commit/8b59931b4c1f6080ee320af9fa0a4072ec03c235))
## [0.2.4](https://github.com/yang1206/uniapp-nutui/compare/v0.2.3...v0.2.4) (2023-08-18)
### Bug Fixes
* (radio) 自定义图标 slot丢失 ([7ac6772](https://github.com/yang1206/uniapp-nutui/commit/7ac6772e0ea1775ff739803386cff1d2a8b17617)), closes [#15](https://github.com/yang1206/uniapp-nutui/issues/15)
## [0.2.3](https://github.com/yang1206/uniapp-nutui/compare/v0.2.2...v0.2.3) (2023-08-18)
### Bug Fixes
* (configProvider) 修复取值错误问题 ([3a5d5fb](https://github.com/yang1206/uniapp-nutui/commit/3a5d5fb36c33ec35394b74aa443c62ad753ec992))
## [0.2.2](https://github.com/yang1206/uniapp-nutui/compare/v0.2.1...v0.2.2) (2023-08-09)
### Bug Fixes
* (form) 调整 label 中 min-width 样式权重 ([c368e2c](https://github.com/yang1206/uniapp-nutui/commit/c368e2c37dae7425b400b20399b0c2fe53403f75))
* (input) 小程序部分type 属性失效 ([a88fa69](https://github.com/yang1206/uniapp-nutui/commit/a88fa69346dd0ce9b68c6d33b94f59915ab09ae5))
* (tabbar) 切换事件回调参数丢失 ([9979494](https://github.com/yang1206/uniapp-nutui/commit/9979494fbb4c35b73ced81baee8ba94ea44761dc)), closes [#10](https://github.com/yang1206/uniapp-nutui/issues/10) [#10](https://github.com/yang1206/uniapp-nutui/issues/10)
## [0.2.1](https://github.com/yang1206/uniapp-nutui/compare/v0.2.0...v0.2.1) (2023-08-05)
### Bug Fixes
* (cascader) 修复在 Popup 中的滑动问题 ([302cd53](https://github.com/yang1206/uniapp-nutui/commit/302cd531426f892425810989b73a8376d0231175))
* (input) 修复空白节点导致的样式对齐问题 ([11b1d87](https://github.com/yang1206/uniapp-nutui/commit/11b1d870030a3a9b4ba9037c2d14735186b65ab0))
* (uploader) name参数无效 ([b21acd0](https://github.com/yang1206/uniapp-nutui/commit/b21acd0c1b61fae6a6cdd6ecb1d8db7084820bbc)), closes [#9](https://github.com/yang1206/uniapp-nutui/issues/9)
# [0.2.0](https://github.com/yang1206/uniapp-nutui/compare/v0.1.8...v0.2.0) (2023-07-30)
### Bug Fixes
* (animate) 小程序触发动画失效 ([a152121](https://github.com/yang1206/uniapp-nutui/commit/a15212198653e7b8315aa4562663f88fbb91e02b))
* (calendar) 修复自定义起始日高亮列错误问题 ([dfeaebc](https://github.com/yang1206/uniapp-nutui/commit/dfeaebc7a79102765f7d0cca01f098716137fe71))
* (countdown) 方法调用错误 ([6aac458](https://github.com/yang1206/uniapp-nutui/commit/6aac458bc5b3599b8f40a6da59539a208e14239d))
* (form) 小程序验证失效 ([9067f4d](https://github.com/yang1206/uniapp-nutui/commit/9067f4dc167ecc13587bc7714810ae4fea0eb0d4))
* (price) 修复小程序下符号转义丢失问题 ([9194c4c](https://github.com/yang1206/uniapp-nutui/commit/9194c4c59dab0a64383d5b86eb78473c43e4c019))
## [0.1.8](https://github.com/yang1206/uniapp-nutui/compare/v0.1.7...v0.1.8) (2023-07-24)
### Bug Fixes
* (codeInput) 双向绑定失效 ([b9ef603](https://github.com/yang1206/uniapp-nutui/commit/b9ef6034a79b043991b63e032f2a24212d8bb46a))
## [0.1.7](https://github.com/yang1206/uniapp-nutui/compare/v0.1.6...v0.1.7) (2023-07-22)
### Features
* 同步修复 ([dad75c8](https://github.com/yang1206/uniapp-nutui/commit/dad75c84907a6f9464db94581223d73108af6aab))
## [0.1.6](https://github.com/yang1206/uniapp-nutui/compare/v0.1.5...v0.1.6) (2023-07-15)
### Bug Fixes
* 同步nutui的修复 ([94b3b27](https://github.com/yang1206/uniapp-nutui/commit/94b3b27b7cf097be660e0d254bb6001cca577a07))
## [0.1.5](https://github.com/yang1206/uniapp-nutui/compare/v0.1.4...v0.1.5) (2023-07-13)
### Features
* 新增 codeInput组件 ([9b9516b](https://github.com/yang1206/uniapp-nutui/commit/9b9516ba7ce4a147c731a04f25532dfdaef730b6))
* 新增loadingpage组件 ([4ae2c12](https://github.com/yang1206/uniapp-nutui/commit/4ae2c125240e933e5b95209411904d4854a89413))
* 移植nutbingo的部分抽奖组件 ([6e19b1a](https://github.com/yang1206/uniapp-nutui/commit/6e19b1a9c62dd878bf77a32a90fce17b35d83afb))
## [0.1.4](https://github.com/yang1206/uniapp-nutui/compare/v0.1.3...v0.1.4) (2023-07-09)
### Bug Fixes
* 修复elevator在小程序环境文字不显示 ([87fc4e9](https://github.com/yang1206/uniapp-nutui/commit/87fc4e91222e3052d1c0ee849d8b7715d8768177))
### Features
* 增加组件全局类型定义文件 ([c84da47](https://github.com/yang1206/uniapp-nutui/commit/c84da47d8ad9a414965f8bc9f8d033f5bbe90435))
## [0.1.3](https://github.com/yang1206/uniapp-nutui/compare/v0.1.2...v0.1.3) (2023-07-09)
### Bug Fixes
* 修复h5部分组件样式错误 ([cfb1e4b](https://github.com/yang1206/uniapp-nutui/commit/cfb1e4b4a88a8a6d3f674fb0bacf9c5283caabe9))
### Features
* 新增uni_modules安装方式 ([9df523b](https://github.com/yang1206/uniapp-nutui/commit/9df523bd65fcab1b3c57a1686381a0df278855a9))
## [0.1.2](https://github.com/yang1206/uniapp-nutui/compare/v0.1.1...v0.1.2) (2023-07-08)
### Bug Fixes
* 修复运行时环境判断错误 ([ce3b0c7](https://github.com/yang1206/uniapp-nutui/commit/ce3b0c70e18826defb87074117294326240f42ec))
## [0.1.1](https://github.com/yang1206/uniapp-nutui/compare/v0.1.0...v0.1.1) (2023-07-07)
### Bug Fixes
* 修复picker 在h5中适配错误 ([cdd9aa4](https://github.com/yang1206/uniapp-nutui/commit/cdd9aa4da991145776b10c4f76ae02dbc156110b))
* 修复uploader组件自定义上传错误 ([af87d68](https://github.com/yang1206/uniapp-nutui/commit/af87d6852dca14f7f212141bfbb80f247bcb8cbf))
# [0.1.0](https://github.com/yang1206/uniapp-nutui/compare/v0.0.6...v0.1.0) (2023-07-07)
### Bug Fixes
* 修复小程序暗黑模式失效 ([a445f04](https://github.com/yang1206/uniapp-nutui/commit/a445f042bd8ab1d0ecdc9f738a9eff704a299fe7))
* 修复支付宝小程序不支持编译错误 ([2c8e0dd](https://github.com/yang1206/uniapp-nutui/commit/2c8e0dda43ad9687211d8f939a6ad20230c4d486))
* 修复支付宝小程序中部分兼容问题 ([6fa5132](https://github.com/yang1206/uniapp-nutui/commit/6fa513211ffdf60bf9592be198befb2e5c690122))
* 修复h5组件之间引用样式丢失 ([d884e28](https://github.com/yang1206/uniapp-nutui/commit/d884e28d8210308bed1aeb4a488bc3f9f0963aed))
## [0.0.9](https://github.com/yang1206/uniapp-nutui/compare/v0.0.6...v0.0.9) (2023-07-06)
### Bug Fixes
* 修复小程序暗黑模式失效 ([a445f04](https://github.com/yang1206/uniapp-nutui/commit/a445f042bd8ab1d0ecdc9f738a9eff704a299fe7))
* 修复支付宝小程序不支持编译错误 ([2c8e0dd](https://github.com/yang1206/uniapp-nutui/commit/2c8e0dda43ad9687211d8f939a6ad20230c4d486))
* 修复支付宝小程序中部分兼容问题 ([6fa5132](https://github.com/yang1206/uniapp-nutui/commit/6fa513211ffdf60bf9592be198befb2e5c690122))
* 修复h5组件之间引用样式丢失 ([d884e28](https://github.com/yang1206/uniapp-nutui/commit/d884e28d8210308bed1aeb4a488bc3f9f0963aed))
## [0.0.8](https://github.com/yang1206/uniapp-nutui/compare/v0.0.7...v0.0.8) (2023-07-05)
### Bug Fixes
* 修复支付宝小程序编译错误 ([bdd6540](https://github.com/yang1206/uniapp-nutui/commit/bdd65408d77aff73509da7d18a2d04d11b105904))
* 修复h5组件之间引用样式丢失 ([b784635](https://github.com/yang1206/uniapp-nutui/commit/b7846354ffb7355a1328f804aa689b1e85487807))
## [0.0.7](https://github.com/yang1206/uniapp-nutui/compare/v0.0.6...v0.0.7) (2023-07-04)
### Bug Fixes
* 修复小程序暗黑模式失效 ([753f87a](https://github.com/yang1206/uniapp-nutui/commit/753f87ae5c7a9f69e789e18b346ac1b4393f581d))
## [0.0.6](https://github.com/yang1206/uniapp-nutui/compare/v0.0.5...v0.0.6) (2023-07-03)
### Bug Fixes
* sku 样式错乱 ([ebdd0df](https://github.com/yang1206/uniapp-nutui/commit/ebdd0dfeba50ba53a6126e8e3dc12b0f69c39f6c))
## [0.0.5](https://github.com/yang1206/uniapp-nutui/compare/v0.0.4...v0.0.5) (2023-07-03)
### Bug Fixes
* 修复addresslist 样式丢失 ([a36e7e2](https://github.com/yang1206/uniapp-nutui/commit/a36e7e248bc5071978574a016b1e6a967053f690))
## [0.0.4](https://github.com/yang1206/uniapp-nutui/compare/v0.0.3...v0.0.4) (2023-07-03)
### Bug Fixes
* 微信小程序打包生成无意义的index.js文件 ([9c9804d](https://github.com/yang1206/uniapp-nutui/commit/9c9804dea5646a74da163feb176058129d0e2e34))
* 修复steps 在小程序样式混乱 ([72bbb19](https://github.com/yang1206/uniapp-nutui/commit/72bbb19f34d93598fb40f50e3d54c3b5257825ff))
## [0.0.1](https://github.com/yang1206/uniapp-nutui/compare/89499ddbcc62fc62228a0f18ce076511287b5dd5...v0.0.1) (2023-07-02)
### Bug Fixes
* 解决一些已知问题 ([b00a7c4](https://github.com/yang1206/uniapp-nutui/commit/b00a7c416b1f8077968b6e599637b92814481926))
### Features
* 国际化 ([17166d9](https://github.com/yang1206/uniapp-nutui/commit/17166d916e3aa473d463083ba535713b32212f82))
* addresslist, barrage, card, category, comment, ecard, invoice, timeselect ([a7d1b61](https://github.com/yang1206/uniapp-nutui/commit/a7d1b610c55070c5b8dac02a06fe1b00c128021b))
* backtop, drag , swipe , switch ([5f8270d](https://github.com/yang1206/uniapp-nutui/commit/5f8270d6e9930199f1a5ac7bc4fe6cd06727ce11))
* calendar,cascader ([a1f2d76](https://github.com/yang1206/uniapp-nutui/commit/a1f2d76ba520c1833cdab154f3012f319484fceb))
* cell, overlay , transition ([2fe21ca](https://github.com/yang1206/uniapp-nutui/commit/2fe21cab85d9eb0a25effe2e12befee4cda4c3f5))
* checkbox, picker , datepicker ([55c72cc](https://github.com/yang1206/uniapp-nutui/commit/55c72cc1165a0940d7e9827644ac7caafa337a41))
* circleprogress, collapse, animate, countup ([4720c1b](https://github.com/yang1206/uniapp-nutui/commit/4720c1b4af45da56589d1d8e56410014df938944))
* countdown ([89499dd](https://github.com/yang1206/uniapp-nutui/commit/89499ddbcc62fc62228a0f18ce076511287b5dd5))
* dark mode ([d275211](https://github.com/yang1206/uniapp-nutui/commit/d275211c6d6c8948423396c8d63d65acccac711d))
* dialog ([80494b3](https://github.com/yang1206/uniapp-nutui/commit/80494b3fd4bf6c4bbabe1a933de1f5f8b683aaf5))
* elevator, fixednav, indicator ([0ce90ef](https://github.com/yang1206/uniapp-nutui/commit/0ce90efe571c826c73168e57f374ccf1674ae219))
* ellipsis, empty, imagepreview, list, swiper ([d20c07f](https://github.com/yang1206/uniapp-nutui/commit/d20c07f4c045dcc9e301bc79e1e913a77315c19e))
* infiniteloading, divider, grid, layout, sticky ([889d930](https://github.com/yang1206/uniapp-nutui/commit/889d930c81b21c7e766b7f6352814386476b7bbc))
* input,textarea,inputnumber,numberkeyboard,radio,radiogroup ([97a1c4c](https://github.com/yang1206/uniapp-nutui/commit/97a1c4c1407b3a664245be5b32c9660368e37008))
* menu ([3ddf33e](https://github.com/yang1206/uniapp-nutui/commit/3ddf33eab33f90c69bbdf336dda7a1c1951e1ba2))
* noticebar, popover, price, skeleton, steps ([3211db9](https://github.com/yang1206/uniapp-nutui/commit/3211db9431d51c2dbf68e960de6b6e807ee458f0))
* range, rate , searchbar, shortpassword, uploader, form, progress ([c8595fe](https://github.com/yang1206/uniapp-nutui/commit/c8595fe070dc631d6514e898f9a0fa90d692d8f4))
* sidenavbar, pagination, tabbar , badge, avatar ([312acf8](https://github.com/yang1206/uniapp-nutui/commit/312acf85c46a770a163fdfadb9da00c0515e9f70))
* table, tag, tour, trendarrow, watermark, address ([2cb9e78](https://github.com/yang1206/uniapp-nutui/commit/2cb9e78d047e6fee3fcb1a3c2c727865b238229f))
* tabs ([e31e9f5](https://github.com/yang1206/uniapp-nutui/commit/e31e9f546c2054c2f196212ca2e03d4bba40168d))
* toast ([281f225](https://github.com/yang1206/uniapp-nutui/commit/281f225ba296b463e124dce51f69689a0bdb3c23))
* transition 支持自定义动画 ([4fd9314](https://github.com/yang1206/uniapp-nutui/commit/4fd931469a5aa867a040cc625a5b6226b5cb5c77))

View File

@@ -0,0 +1,18 @@
export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const UPDATE_VISIBLE_EVENT = 'update:visible'
export const CHANGE_EVENT = 'change'
export const INPUT_EVENT = 'input'
export const CLICK_EVENT = 'click'
export const OPEN_EVENT = 'open'
export const CLOSE_EVENT = 'close'
export const OPENED_EVENT = 'opened'
export const CLOSED_EVENT = 'closed'
export const FOCUS_EVENT = 'focus'
export const BLUR_EVENT = 'blur'
export const CONFIRM_EVENT = 'confirm'
export const SEARCH_EVENT = 'search'
export const CLEAR_EVENT = 'clear'
export const CANCEL_EVENT = 'cancel'
export const CHOOSE_EVENT = 'choose'
export const SELECT_EVENT = 'select'
export const SELECTED_EVENT = 'selected'

View File

@@ -0,0 +1,3 @@
export * from './event'
export * from './prefix'
export * from './types'

View File

@@ -0,0 +1 @@
export const PREFIX = 'nut'

View File

@@ -0,0 +1,11 @@
import type { NutAnimationName } from '../transition'
export type Position = 'center' | 'top' | 'bottom' | 'left' | 'right'
export const animationName: Record<Position, NutAnimationName> = {
center: 'fade',
top: 'slide-down',
bottom: 'slide-up',
left: 'slide-left',
right: 'slide-right',
}

View File

@@ -0,0 +1,11 @@
export * from './useExpose'
export * from './useGlobalZIndex'
export * from './useInject'
export * from './useLockScroll'
export * from './useProvide'
export * from './useRect'
export * from './useRelation'
export * from './useRouter'
export * from './useSelectorQuery'
export * from './useStyle'
export * from './useTouch'

View File

@@ -0,0 +1,8 @@
import type { ComponentPublicInstance } from 'vue'
import { getCurrentInstance } from 'vue'
export function useExpose(apis: Record<string, any>) {
const instance = getCurrentInstance()
if (instance)
Object.assign(instance.proxy as ComponentPublicInstance, apis)
}

View File

@@ -0,0 +1,9 @@
let globalZIndex = 2000
export function useGlobalZIndex() {
return ++globalZIndex
}
export function setGlobalZIndex(value: number) {
globalZIndex = value
}

View File

@@ -0,0 +1,32 @@
import { computed, getCurrentInstance, inject, onUnmounted, ref } from 'vue'
import type { ComponentInternalInstance, InjectionKey } from 'vue'
type ParentProvide<T> = T & {
add: (child: ComponentInternalInstance) => void
remove: (child: ComponentInternalInstance) => void
internalChildren: ComponentInternalInstance[]
}
export function useInject<T>(key: InjectionKey<ParentProvide<T>>) {
const parent = inject(key, null)
if (parent) {
const instance = getCurrentInstance()!
const { add, remove, internalChildren } = parent
add(instance)
onUnmounted(() => remove(instance))
const index = computed(() => internalChildren.indexOf(instance))
return {
parent,
index,
}
}
return {
parent: null,
index: ref(-1),
}
}

View File

@@ -0,0 +1,31 @@
let count = 0
const CLSNAME = 'nut-overflow-hidden'
export function useLockScroll(isLock: () => boolean) {
const lock = () => {
if (isLock()) {
try {
!count && document.body.classList.add(CLSNAME)
count++
}
catch (error) {
console.error(error)
}
}
}
const unlock = () => {
if (isLock() && count) {
try {
count--
!count && document.body.classList.remove(CLSNAME)
}
catch (error) {
console.error(error)
}
}
}
return [lock, unlock]
}

View File

@@ -0,0 +1,93 @@
import { getCurrentInstance, markRaw, provide, shallowReactive } from 'vue'
import type {
ComponentInternalInstance,
ConcreteComponent,
InjectionKey,
VNode,
VNodeNormalizedChildren,
} from 'vue'
// TODO: uniapp 不支持 vue 直接导出的 isVNode
export function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}
export function flattenVNodes(shouldTraverseChildren: VNodeNormalizedChildren, childName?: string) {
const result: VNode[] = []
const traverse = (children: VNodeNormalizedChildren) => {
if (!Array.isArray(children))
return
children.forEach((child) => {
if (!isVNode(child))
return
if (childName) {
if (child.type && (child.type as ConcreteComponent).name === childName) {
result.push(child)
return
}
}
else {
result.push(child)
}
if (child.component?.subTree)
traverse(child.component.subTree.children)
if (child.children)
traverse(child.children)
})
}
traverse(shouldTraverseChildren)
return result
}
export function sortChildren(
parent: ComponentInternalInstance,
internalChildren: ComponentInternalInstance[],
childName?: string,
) {
const vnodes = flattenVNodes(parent && parent.subTree && parent.subTree.children, childName)
internalChildren.sort((a, b) => {
return vnodes.indexOf(a.vnode) - vnodes.indexOf(b.vnode)
})
}
// 如果指定组件名称,则只查找此组件并且查到后结束。也就是不关心此组件下的内容,在大部分场景下节省查找消耗。
export function useProvide<ProvideValue>(key: InjectionKey<ProvideValue>, childName?: string) {
const internalChildren: ComponentInternalInstance[] = shallowReactive([])
const publicChildren = shallowReactive<any[]>([])
const parent = getCurrentInstance()!
const add = (child: ComponentInternalInstance) => {
if (!child.proxy)
return
internalChildren.push(markRaw(child))
publicChildren.push(markRaw(child.proxy))
sortChildren(parent, internalChildren, childName)
}
const remove = (child: ComponentInternalInstance) => {
if (child.proxy) {
internalChildren.splice(internalChildren.indexOf(markRaw(child)), 1)
publicChildren.splice(publicChildren.indexOf(markRaw(child.proxy)), 1)
}
}
return (value?: ProvideValue) => {
provide(key, {
add,
remove,
internalChildren,
...value,
} as any)
return {
internalChildren,
children: publicChildren,
}
}
}

View File

@@ -0,0 +1,7 @@
import type { ComponentInternalInstance } from 'vue'
import { useSelectorQuery } from './useSelectorQuery'
export function useRect(id: string, instance?: ComponentInternalInstance): Promise<UniApp.NodeInfo> {
const { getSelectorNodeInfo } = useSelectorQuery(instance)
return getSelectorNodeInfo(`#${id}`)
}

View File

@@ -0,0 +1,8 @@
import type { ComponentPublicInstance } from 'vue'
import { getCurrentInstance } from 'vue'
export function useExtend<T>(apis: T) {
const instance = getCurrentInstance()
if (instance)
Object.assign(instance.proxy as ComponentPublicInstance, apis)
}

View File

@@ -0,0 +1,28 @@
export type NavigateToOptions = string | UniApp.NavigateToOptions
export type RedirectToOptions = string | UniApp.RedirectToOptions
export type RouterOptions = UniApp.NavigateToOptions | UniApp.RedirectToOptions
export function useRouter() {
const push = (options: NavigateToOptions) => {
if (typeof options === 'string') {
uni.navigateTo({ url: options })
return
}
uni.navigateTo(options)
}
const replace = (options: RedirectToOptions) => {
if (typeof options === 'string') {
uni.redirectTo({ url: options })
return
}
uni.redirectTo(options)
}
return {
push,
replace,
}
}

View File

@@ -0,0 +1,67 @@
import { getCurrentInstance } from 'vue'
import type { ComponentInternalInstance } from 'vue'
export function useSelectorQuery(instance?: ComponentInternalInstance | null) {
let query: UniApp.SelectorQuery | null = null
if (!instance)
instance = getCurrentInstance()
if (!instance)
console.warn('useSelectorQuery', 'useSelectorQuery必须在setup函数中使用')
// #ifndef MP-ALIPAY
query = uni.createSelectorQuery().in(instance)
// #endif
// #ifdef MP-ALIPAY
query = uni.createSelectorQuery().in(null)
// #endif
const getSelectorNodeInfo = (selector: string): Promise<UniApp.NodeInfo> => {
return new Promise((resolve, reject) => {
if (query) {
query
.select(selector)
.boundingClientRect((res) => {
const selectRes: UniApp.NodeInfo = res as UniApp.NodeInfo
if (selectRes)
resolve(selectRes)
else
reject(new Error(`未找到对应节点: ${selector}`))
})
.exec()
}
else {
reject(new Error('未找到对应的SelectorQuery实例'))
}
})
}
const getSelectorNodeInfos = (
selector: string,
): Promise<UniApp.NodeInfo[]> => {
return new Promise((resolve, reject) => {
if (query) {
query
.selectAll(selector)
.boundingClientRect((res) => {
const selectRes: UniApp.NodeInfo[] = res as UniApp.NodeInfo[]
if (selectRes && selectRes.length > 0)
resolve(selectRes)
else
reject(new Error(`未找到对应节点: ${selector}`))
})
.exec()
}
else {
reject(new Error('未找到对应的SelectorQuery实例'))
}
})
}
return {
query,
getSelectorNodeInfo,
getSelectorNodeInfos,
}
}

View File

@@ -0,0 +1,29 @@
import { computed, normalizeClass, normalizeStyle } from 'vue'
import { stringifyStyle } from '../_utils'
export function useStyleContext(props: any, componentName: string) {
const mainClass = computed(() => {
const cls = normalizeClass([props.customClass, { [componentName]: true }])
return cls
})
const mainStyle = computed(() => {
return stringifyStyle(normalizeStyle(props.customStyle))
})
const getMainClass = (cls: unknown) => {
return normalizeClass([props.customClass, { [componentName]: true }, cls])
}
const getMainStyle = (style: unknown) => {
return stringifyStyle(normalizeStyle([props.customStyle, style]))
}
return {
mainClass,
mainStyle,
getMainClass,
getMainStyle,
}
}

View File

@@ -0,0 +1,74 @@
import { ref } from 'vue'
const MIN_DISTANCE = 10
type Direction = '' | 'vertical' | 'horizontal'
function getDirection(x: number, y: number) {
if (x > y && x > MIN_DISTANCE)
return 'horizontal'
if (y > x && y > MIN_DISTANCE)
return 'vertical'
return ''
}
export function useTouch() {
const startX = ref(0)
const startY = ref(0)
const moveX = ref(0)
const moveY = ref(0)
const deltaX = ref(0)
const deltaY = ref(0)
const offsetX = ref(0)
const offsetY = ref(0)
const direction = ref<Direction>('')
const isVertical = () => direction.value === 'vertical'
const isHorizontal = () => direction.value === 'horizontal'
const reset = () => {
deltaX.value = 0
deltaY.value = 0
offsetX.value = 0
offsetY.value = 0
direction.value = ''
}
const start = ((event: TouchEvent) => {
reset()
startX.value = event.touches[0].clientX
startY.value = event.touches[0].clientY
}) as EventListener
const move = ((event: TouchEvent) => {
const touch = event.touches[0]
deltaX.value = touch.clientX - startX.value
deltaY.value = touch.clientY - startY.value
moveX.value = touch.clientX
moveY.value = touch.clientY
offsetX.value = Math.abs(deltaX.value)
offsetY.value = Math.abs(deltaY.value)
if (!direction.value)
direction.value = getDirection(offsetX.value, offsetY.value)
}) as EventListener
return {
move,
start,
reset,
startX,
startY,
moveX,
moveY,
deltaX,
deltaY,
offsetX,
offsetY,
direction,
isVertical,
isHorizontal,
}
}

View File

@@ -0,0 +1,277 @@
import { isArray, isDef, isObject } from './is'
// 变量类型判断
export function TypeOfFun(value: any) {
if (value === null)
return 'null'
const type = typeof value
if (type === 'undefined' || type === 'string')
return type
const typeString = toString.call(value)
switch (typeString) {
case '[object Array]':
return 'array'
case '[object Date]':
return 'date'
case '[object Boolean]':
return 'boolean'
case '[object Number]':
return 'number'
case '[object Function]':
return 'function'
case '[object RegExp]':
return 'regexp'
case '[object Object]':
if (undefined !== value.nodeType) {
if (value.nodeType === 3)
return /\S/.test(value.nodeValue) ? 'textnode' : 'whitespace'
else
return 'element'
}
else {
return 'object'
}
default:
return 'unknow'
}
}
//
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string => objectToString.call(value)
export function toRawType(value: unknown): string {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
export const win = window
export const docu = document
export const body = docu.body
export function getPropByPath(obj: any, keyPath: string) {
try {
return keyPath.split('.').reduce((prev, curr) => prev[curr], obj)
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (error) {
return ''
}
}
export function floatData(format: any, dataOp: any, mapOps: any) {
const mergeFormat = Object.assign({}, format)
const mergeMapOps = Object.assign({}, mapOps)
if (Object.keys(dataOp).length > 0) {
Object.keys(mergeFormat).forEach((keys) => {
if (Object.prototype.hasOwnProperty.call(mergeMapOps, keys)) {
const tof = TypeOfFun(mergeMapOps[keys])
if (tof === 'function')
mergeFormat[keys] = mergeMapOps[keys](dataOp)
if (tof === 'string')
mergeFormat[keys] = dataOp[mergeMapOps[keys]]
}
else {
if (dataOp[keys])
mergeFormat[keys] = dataOp[keys]
}
})
return mergeFormat
}
return format
}
export function myFixed(num: any, digit = 2) {
if (Object.is(Number.parseFloat(num), Number.NaN))
return console.warn(`传入的值:${num}不是一个数字`)
num = Number.parseFloat(num)
return (Math.round((num + Number.EPSILON) * 10 ** digit) / 10 ** digit).toFixed(digit)
}
export function preventDefault(event: Event, isStopPropagation?: boolean) {
if (typeof event.cancelable !== 'boolean' || event.cancelable)
event.preventDefault()
if (isStopPropagation)
event.stopPropagation()
}
function cacheStringFunction<T extends (str: string) => string>(fn: T): T {
const cache: Record<string, string> = Object.create(null)
return ((str: string) => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}) as T
}
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cacheStringFunction((str: string) =>
str.replace(hyphenateRE, '-$1').toLowerCase(),
)
export function padZero(num: number | string, length = 2): string {
num += ''
while ((num as string).length < length)
num = `0${num}`
return num.toString()
}
export const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max)
export function getScrollTopRoot(): number {
return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
}
type ObjectIndex = Record<string, unknown>
const { hasOwnProperty } = Object.prototype
function assignKey(to: ObjectIndex, from: ObjectIndex, key: string) {
const val = from[key]
if (!isDef(val))
return
if (!hasOwnProperty.call(to, key) || !isObject(val))
to[key] = val
else
// eslint-disable-next-line unicorn/new-for-builtins
to[key] = deepAssign(Object(to[key]), val)
}
export function deepAssign(to: ObjectIndex, from: ObjectIndex): ObjectIndex {
Object.keys(from).forEach((key) => {
assignKey(to, from, key)
})
return to
}
export function omit(obj: Record<string, unknown>, keys: string[]) {
if (Object.prototype.toString.call(obj) === '[object Object]')
return obj
return Object.keys(obj).reduce((prev, key) => {
if (!keys.includes(key))
prev[key] = obj[key]
return prev
}, {} as Record<string, unknown>)
}
export interface Deferred<T> extends Promise<T> {
resolve: (value?: T) => void
reject: (value?: any) => void
}
export function createDeferred<T>(): Deferred<T> {
let resolve: Deferred<T>['resolve'] = noop
let reject: Deferred<T>['reject'] = noop
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
}) as unknown as Deferred<T>
promise.resolve = resolve
promise.reject = reject
return promise
}
export function toArray<T>(value?: T | T[]): T[] {
if (!value)
return []
return Array.isArray(value) ? value : [value]
}
export function noop() { }
export function getRandomId() {
return Math.random().toString(36).slice(-8)
}
export function isLooseEqual(a: any, b: any): boolean {
if (a === b)
return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB)
return JSON.stringify(a) === JSON.stringify(b)
else if (!isObjectA && !isObjectB)
return String(a) === String(b)
else
return false
}
export function isEqualArray(a: any, b: any): boolean {
if (a === b)
return true
if (!isArray(a) || !isArray(b))
return false
if (a.length !== b.length)
return false
for (let i = 0; i < a.length; i++) {
if (!isLooseEqual(a[i], b[i]))
return false
}
return true
}
export function isEqualValue(a: any, b: any): boolean {
if (a === b)
return true
if (isArray(a) && isArray(b))
return isEqualArray(a, b)
return isLooseEqual(a, b)
}
export function cloneDeep<T = any>(obj: T, cache = new WeakMap()): T {
if (obj === null || typeof obj !== 'object')
return obj
if (cache.has(obj))
return cache.get(obj)
let clone
if (obj instanceof Date) {
clone = new Date(obj.getTime())
}
else if (obj instanceof RegExp) {
clone = new RegExp(obj)
}
else if (obj instanceof Map) {
clone = new Map(Array.from(obj, ([key, value]) => [key, cloneDeep(value, cache)]))
}
else if (obj instanceof Set) {
clone = new Set(Array.from(obj, value => cloneDeep(value, cache)))
}
else if (Array.isArray(obj)) {
clone = obj.map(value => cloneDeep(value, cache))
}
else if (Object.prototype.toString.call(obj) === '[object Object]') {
clone = Object.create(Object.getPrototypeOf(obj))
cache.set(obj, clone)
for (const [key, value] of Object.entries(obj))
clone[key] = cloneDeep(value, cache)
}
else {
clone = Object.assign({}, obj)
}
cache.set(obj, clone)
return clone
}

View File

@@ -0,0 +1,167 @@
/**
* 时间戳转换 或 获取当前时间的时间戳
*/
export function getTimeStamp(timeStr?: string | number) {
if (!timeStr)
return Date.now()
let t = timeStr
t = (t as number > 0) ? +t : t.toString().replace(/-/g, '/')
return new Date(t).getTime()
}
/**
* 是否为闫年
* @return {Boolse} true|false
*/
export function isLeapYear(y: number): boolean {
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0
}
/**
* 返回星期数
* @return {string}
*/
export function getWhatDay(year: number, month: number, day: number): string {
const date = new Date(`${year}/${month}/${day}`)
const index = date.getDay()
const dayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return dayNames[index]
}
/**
* 返回星期数
* @return {number}
*/
export function getMonthPreDay(year: number, month: number): number {
const date = new Date(`${year}/${month}/01`)
let day = date.getDay()
if (day === 0)
day = 7
return day
}
/**
* 返回月份天数
* @return {number}
*/
export function getMonthDays(year: string, month: string): number {
if (month.startsWith('0'))
month = month.split('')[1]
return ([0, 31, isLeapYear(Number(year)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as number[])[
month as any
]
}
/**
* 补齐数字位数
* @return {string}
*/
export function getNumTwoBit(n: number): string {
n = Number(n)
return (n > 9 ? '' : '0') + n
}
/**
* 日期对象转成字符串
* @return {string}
*/
export function date2Str(date: Date, split?: string): string {
split = split || '-'
const y = date.getFullYear()
const m = getNumTwoBit(date.getMonth() + 1)
const d = getNumTwoBit(date.getDate())
return [y, m, d].join(split)
}
/**
* 返回日期格式字符串
* @param i 0返回今天的日期、1返回明天的日期2返回后天得日期依次类推
* @return {string} '2014-12-31'
*/
export function getDay(i: number): string {
i = i || 0
let date = new Date()
const diff = i * (1000 * 60 * 60 * 24)
date = new Date(date.getTime() + diff)
return date2Str(date)
}
/**
* 时间比较
* @return {boolean}
*/
export function compareDate(date1: string, date2: string): boolean {
const startTime = new Date(date1.replace('-', '/').replace('-', '/'))
const endTime = new Date(date2.replace('-', '/').replace('-', '/'))
if (startTime >= endTime)
return false
return true
}
/**
* 时间是否相等
* @return {boolean}
*/
export function isEqual(date1: string, date2: string): boolean {
const startTime = new Date(date1).getTime()
const endTime = new Date(date2).getTime()
if (startTime === endTime)
return true
return false
}
export function getMonthWeek(year: string, month: string, date: string, firstDayOfWeek = 0): number {
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
let w = dateNow.getDay() // 星期数
const d = dateNow.getDate()
let remainder = 6 - w
if (firstDayOfWeek !== 0) {
w = w === 0 ? 7 : w
remainder = 7 - w
}
return Math.ceil((d + remainder) / 7)
}
export function getYearWeek(year: string, month: string, date: string): number {
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
const dateFirst = new Date(Number(year), 0, 1)
const dataNumber = Math.round((dateNow.valueOf() - dateFirst.valueOf()) / 86400000)
return Math.ceil((dataNumber + (dateFirst.getDay() + 1 - 1)) / 7)
}
export function getWeekDate(year: string, month: string, date: string, firstDayOfWeek = 0): string[] {
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
const nowTime = dateNow.getTime()
let day = dateNow.getDay()
if (firstDayOfWeek === 0) {
const oneDayTime = 24 * 60 * 60 * 1000
// 显示周日
const SundayTime = nowTime - day * oneDayTime // 本周的周日
// 显示周六
const SaturdayTime = nowTime + (6 - day) * oneDayTime // 本周的周六
const sunday = date2Str(new Date(SundayTime))
const saturday = date2Str(new Date(SaturdayTime))
return [sunday, saturday]
}
else {
day = day === 0 ? 7 : day
const oneDayTime = 24 * 60 * 60 * 1000
// 显示周一
const MondayTime = nowTime - (day - 1) * oneDayTime // 本周的周一
// 显示周日
const SundayTime = nowTime + (7 - day) * oneDayTime // 本周的周日
const monday = date2Str(new Date(MondayTime))
const sunday = date2Str(new Date(SundayTime))
return [monday, sunday]
}
}
export function formatResultDate(date: string) {
const days = [...date.split('-')]
days[2] = getNumTwoBit(Number(days[2]))
days[3] = `${days[0]}-${days[1]}-${days[2]}`
days[4] = getWhatDay(+days[0], +days[1], +days[2])
return days
}

View File

@@ -0,0 +1,100 @@
/** 枚举EPlatform */
enum EPlatform {
/** App */
AppPlus = 'APP-PLUS',
/** App nvue */
AppPlusNvue = 'APP-PLUS-NVUE',
/** H5 */
H5 = 'H5',
/** 微信小程序 */
MpWeixin = 'MP-WEIXIN',
/** 支付宝小程序 */
MpAlipay = 'MP-ALIPAY',
/** 百度小程序 */
MpBaidu = 'MP-BAIDU',
/** 字节跳动小程序 */
MpToutiao = 'MP-TOUTIAO',
/** QQ小程序 */
MpQq = 'MP-QQ',
/** 360小程序 */
Mp360 = 'MP-360',
/** 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/QQ小程序/360小程序 */
Mp = 'MP',
/** 快应用通用(包含联盟、华为) */
QuickappWebview = 'quickapp-webview',
/** 快应用联盟 */
QuickappWebviewUnion = 'quickapp-webview-union',
/** 快应用华为 */
QuickappWebviewHuawei = 'quickapp-webview-huawei',
}
/** 使用条件编译获取平台信息 */
function ifDefPlatform(): EPlatform {
let platform: EPlatform
// #ifdef APP-PLUS
platform = EPlatform.AppPlus
// #endif
// #ifdef APP-PLUS-NVUE
platform = EPlatform.AppPlusNvue
// #endif
// #ifdef H5
platform = EPlatform.H5
// #endif
// #ifdef MP-WEIXIN
platform = EPlatform.MpWeixin
// #endif
// #ifdef MP-ALIPAY
platform = EPlatform.MpAlipay
// #endif
// #ifdef MP-BAIDU
platform = EPlatform.MpBaidu
// #endif
// #ifdef MP-TOUTIAO
platform = EPlatform.MpToutiao
// #endif
// #ifdef MP-QQ
platform = EPlatform.MpQq
// #endif
// #ifdef MP-360
platform = EPlatform.Mp360
// #endif
// #ifdef MP
platform = EPlatform.Mp
// #endif
// #ifdef quickapp-webview
platform = EPlatform.QuickappWebview
// #endif
// #ifdef quickapp-webview-union
platform = EPlatform.QuickappWebviewUnion
// #endif
// #ifdef quickapp-webview-huawei
platform = EPlatform.QuickappWebviewHuawei
// #endif
return platform
}
/** 平台类型 */
export const platform: EPlatform = ifDefPlatform()
/** H5 */
export const isH5 = platform === EPlatform.H5
/** 微信小程序 */
export const isMpWeixin = platform === EPlatform.MpWeixin
/** 支付宝小程序 */
export const isMpAlipay = platform === EPlatform.MpAlipay
/** 百度小程序 */
export const isMpBaidu = platform === EPlatform.MpBaidu
/** 字节跳动小程序 */
export const isMpToutiao = platform === EPlatform.MpToutiao
/** QQ小程序 */
export const isMpQq = platform === EPlatform.MpQq
/** 360小程序 */
export const isMp360 = platform === EPlatform.Mp360
/** 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/QQ小程序/360小程序 */
export const isMp = platform === EPlatform.Mp
/** 快应用通用(包含联盟、华为) */
export const isQuickappWebview = platform === EPlatform.QuickappWebview
/** 快应用联盟 */
export const isQuickappWebviewUnion = platform === EPlatform.QuickappWebviewUnion
/** 快应用华为 */
export const isQuickappWebviewHuawei = platform === EPlatform.QuickappWebviewHuawei

View File

@@ -0,0 +1,9 @@
export * from './common'
export * from './date'
export * from './env'
export * from './interceptor'
export * from './is'
export * from './props'
export * from './pxCheck'
export * from './raf'
export * from './style'

View File

@@ -0,0 +1,37 @@
import { isPromise } from './is'
export type Interceptor = (...args: any[]) => Promise<boolean> | boolean | undefined | void
export function funInterceptor(interceptor: Interceptor | undefined, {
args = [],
done,
canceled,
}: {
args?: unknown[]
done: (val?: any) => void
canceled?: () => void
}) {
if (interceptor) {
const returnVal = interceptor(null, ...args)
if (isPromise(returnVal)) {
returnVal
.then((value) => {
if (value)
done(value)
else if (canceled)
canceled()
})
.catch(() => {})
}
else if (returnVal) {
done()
}
else if (canceled) {
canceled()
}
}
else {
done()
}
}

View File

@@ -0,0 +1,96 @@
const toString = Object.prototype.toString
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== 'undefined'
}
export function isUnDef<T = unknown>(val?: T): val is T {
return !isDef(val)
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, 'Object')
}
export function isEmpty<T = unknown>(val: T): val is T {
if (isArray(val) || isString(val))
return val.length === 0
if (val instanceof Map || val instanceof Set)
return val.size === 0
if (isObject(val))
return Object.keys(val).length === 0
return false
}
export function isDate(val: unknown): val is Date {
return is(val, 'Date')
}
export function isNull(val: unknown): val is null {
return val === null
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val)
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val)
}
export function isNumber(val: unknown): val is number {
return is(val, 'Number')
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return (
is(val, 'Promise')
|| ((isObject(val) || isFunction(val))
&& isFunction((val as any).then)
&& isFunction((val as any).catch))
)
}
export function isString(val: unknown): val is string {
return is(val, 'String')
}
export function isFunction(val: unknown): val is () => void {
return typeof val === 'function'
}
export function isBoolean(val: unknown): val is boolean {
return is(val, 'Boolean')
}
export function isRegExp(val: unknown): val is RegExp {
return is(val, 'RegExp')
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val)
}
export function isWindow(val: any): val is Window {
return typeof window !== 'undefined' && is(val, 'Window')
}
export function isElement(val: unknown): val is Element {
return isObject(val) && !!val.tagName
}
export function isMap(val: unknown): val is Map<any, any> {
return is(val, 'Map')
}
export function isUrl(path: string): boolean {
const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/
return reg.test(path)
}

View File

@@ -0,0 +1,83 @@
/**
* prop type helpers
* help us to write less code and reduce bundle size
* copy from https://github.com/youzan/vant/blob/main/packages/vant/src/utils/props.ts
*/
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
export const unknownProp = null as unknown as PropType<unknown>
export const numericProp = [Number, String]
export const truthProp = {
type: Boolean,
default: true as const,
}
export const nullableBooleanProp = {
type: Boolean as PropType<boolean | undefined>,
default: undefined,
}
export function makeRequiredProp<T>(type: T) {
return {
type,
required: true as const,
}
}
export function makeArrayProp<T>(defaultVal: T[] = []) {
return {
type: Array as PropType<T[]>,
default: () => defaultVal,
}
}
export function makeObjectProp<T>(defaultVal: T) {
return {
type: Object as PropType<T>,
default: () => defaultVal,
}
}
export function makeNumberProp<T>(defaultVal: T) {
return {
type: Number,
default: defaultVal,
}
}
export function makeNumericProp<T>(defaultVal: T) {
return {
type: numericProp,
default: defaultVal,
}
}
export function makeStringProp<T>(defaultVal: T) {
return {
type: String as unknown as PropType<T>,
default: defaultVal,
}
}
export type ClassType = string | object | Array<ClassType>
export const commonProps = {
/**
* @description 自定义类名
*/
customClass: {
type: [String, Object, Array] as PropType<ClassType>,
default: '',
},
/**
* @description 自定义样式
*/
customStyle: {
type: [String, Object, Array] as PropType<StyleValue>,
default: '',
},
}
export type CommonProps = ExtractPropTypes<typeof commonProps>

View File

@@ -0,0 +1,3 @@
export function pxCheck(value: string | number): string {
return Number.isNaN(Number(value)) ? String(value) : `${value}px`
}

View File

@@ -0,0 +1,30 @@
const _window = window as any
export const inBrowser = typeof window !== 'undefined'
function requestAniFrame() {
if (typeof _window !== 'undefined') {
return (
_window.requestAnimationFrame
|| _window.webkitRequestAnimationFrame
|| function (callback: () => void) {
_window.setTimeout(callback, 1000 / 60)
}
)
}
else {
return function (callback: () => void) {
setTimeout(callback, 1000 / 60)
}
}
}
export function cancelRaf(id: number) {
if (inBrowser)
cancelAnimationFrame(id)
else
clearTimeout(id)
}
export default requestAniFrame()

View File

@@ -0,0 +1,167 @@
import type { CSSProperties } from 'vue'
import { hyphenate } from './common'
import { isArray, isEmpty, isNumber, isObject, isString } from './is'
import type { CommonProps } from './props'
export type NormalizedStyle = Record<string, string | number>
const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:([\s\S]+)/
const styleCommentRE = /\/\*.*?\*\//g
export function parseStringStyle(cssText: string): NormalizedStyle {
const ret: NormalizedStyle = {}
cssText
.replace(styleCommentRE, '')
.split(listDelimiterRE)
.forEach((item) => {
if (item) {
const tmp = item.split(propertyDelimiterRE)
tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())
}
})
return ret
}
export function stringifyStyle(styles: NormalizedStyle | string | undefined): string {
let ret = ''
if (!styles || isString(styles))
return ret
for (const key in styles) {
const value = styles[key]
const normalizedKey = key.startsWith('--') ? key : hyphenate(key)
if (isString(value) || typeof value === 'number') {
// only render valid values
ret += `${normalizedKey}:${value};`
}
}
return ret
}
export function getPx(value: string | number, unit = false) {
if (isNumber(value))
return unit ? `${value}px` : Number(value)
return unit ? `${Number.parseInt(value)}px` : Number.parseInt(value)
}
/**
* @description 样式转换
* 对象转字符串,或者字符串转对象
* @param {object | string} customStyle 需要转换的目标
* @param {string} target 转换的目的object-转为对象string-转为字符串
*/
export function addStyle(customStyle: string | object, target = 'object') {
// 字符串转字符串,对象转对象情形,直接返回
if (
isEmpty(customStyle)
|| (typeof customStyle === 'object' && target === 'object')
|| (target === 'string' && typeof customStyle === 'string')
) {
return customStyle
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格(中间的空格不能去掉比如padding: 20px 0如果去掉了就错了),空格是无用的
customStyle = trim(customStyle)
// 根据";"将字符串转为数组形式
const styleArray = customStyle.split(';')
const style: any = {}
// 历遍数组,拼接成对象
for (let i = 0; i < styleArray.length; i++) {
// 'font-size:20px;color:red;',如此最后字符串有";"的话会导致styleArray最后一个元素为空字符串这里需要过滤
if (styleArray[i]) {
const item = styleArray[i].split(':')
style[trim(item[0])] = trim(item[1])
}
}
return style
}
// 这里为对象转字符串形式
let string = ''
for (const i in customStyle as any) {
// 驼峰转为中划线的形式否则css内联样式无法识别驼峰样式属性名
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
string += `${key}:${customStyle[i]};`
}
// 去除两端空格
return trim(string)
}
/**
* @description 去除空格
* @param str 需要去除空格的字符串
* @param pos both(左右)|left|right|all 默认both
*/
export function trim(str: string, pos = 'both') {
str = String(str)
if (pos === 'both')
return str.replace(/^\s+|\s+$/g, '')
if (pos === 'left')
return str.replace(/^\s*/, '')
if (pos === 'right')
return str.replace(/(\s*$)/g, '')
if (pos === 'all')
return str.replace(/\s+/g, '')
return str
}
export function normalizeStyle(value: unknown): NormalizedStyle | string | undefined {
if (isArray(value)) {
const res: NormalizedStyle = {}
for (let i = 0; i < value.length; i++) {
const item = value[i]
const normalized = isString(item)
? parseStringStyle(item)
: (normalizeStyle(item) as NormalizedStyle)
if (normalized) {
for (const key in normalized) {
if (!isEmpty(normalized[key]))
res[key] = normalized[key]
}
}
}
return res
}
if (isString(value))
return value
if (isObject(value))
return value
}
export function normalizeClass(value: unknown): string {
let res = ''
if (isString(value)) {
res = value
}
else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized)
res += `${normalized} `
}
}
else if (isObject(value)) {
for (const name in value) {
if (value[name])
res += `${name} `
}
}
return res.trim()
}
export function getMainClass(props: CommonProps, componentName: string, cls?: object) {
return normalizeClass([props.customClass, { [componentName]: true }, cls])
}
export function getMainStyle(props: CommonProps, style?: CSSProperties) {
return stringifyStyle(normalizeStyle([props.customStyle, style]))
}

View File

@@ -0,0 +1,82 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
import { CANCEL_EVENT, CHOOSE_EVENT, CLOSE_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
import { commonProps, isBoolean, isNumber, makeArrayProp, makeStringProp, truthProp } from '../_utils'
import { popupProps } from '../popup/popup'
export interface ActionSheetOption {
disable?: boolean
loading?: boolean
color?: string
name: string
subname?: string
}
export const actionsheetProps = {
...popupProps,
...commonProps,
/**
* @description 是否显示圆角
*/
round: truthProp,
/**
* @description 是否开启 iPhone 系列全面屏底部安全区适配,仅当 `position` 为 `bottom` 时有效
*/
safeAreaInsetBottom: truthProp,
/**
* @description 遮罩显示时的背景是否锁定
*/
lockScroll: truthProp,
/**
* @description 自定义 popup 弹框样式
*/
popStyle: {
type: Object as PropType<CSSProperties>,
},
/**
* @description 取消文案
*/
cancelTxt: makeStringProp(''),
/**
* @description 设置列表项标题展示使用参数
*/
optionTag: makeStringProp<keyof ActionSheetOption>('name'),
/**
* @description 设置列表项二级标题展示使用参数
*/
optionSubTag: makeStringProp<keyof ActionSheetOption>('subname'),
/**
* @description 设置选中项的值,和 'option-tag' 的值对应
*/
chooseTagValue: makeStringProp(''),
/**
* @description 设置列表项标题
*/
title: makeStringProp(''),
/**
* @description 选中项颜色,当 choose-tag-value == option-tag 的值 生效
*/
customColor: makeStringProp('#ee0a24'),
/**
* @description 设置列表项副标题/描述
*/
description: makeStringProp(''),
/**
* @description 列表项
*/
menuItems: makeArrayProp<ActionSheetOption>([]),
/**
* @description 遮罩层是否可关闭
*/
closeAbled: truthProp,
}
export type ActionsheetProps = ExtractPropTypes<typeof actionsheetProps>
export const actionsheetEmits = {
[CLOSE_EVENT]: () => true,
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
[CANCEL_EVENT]: () => true,
[CHOOSE_EVENT]: (item: ActionSheetOption, index: number) => item instanceof Object && isNumber(index),
}
export type ActionsheetEmits = typeof actionsheetEmits

View File

@@ -0,0 +1,120 @@
<script setup lang="ts">
import { computed, defineComponent, useSlots } from 'vue'
import { CANCEL_EVENT, CHOOSE_EVENT, CLOSE_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
import { getMainClass } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutPopup from '../popup/popup.vue'
import type { ActionSheetOption } from './actionsheet'
import { actionsheetEmits, actionsheetProps } from './actionsheet'
const props = defineProps(actionsheetProps)
const emit = defineEmits(actionsheetEmits)
const slotDefault = !!useSlots().default
const classes = computed(() => {
return getMainClass(props, componentName)
})
function isHighlight(item: ActionSheetOption) {
return props.chooseTagValue && props.chooseTagValue === item[props.optionTag] ? props.customColor : ''
}
function cancelActionSheet() {
emit(CANCEL_EVENT)
emit(UPDATE_VISIBLE_EVENT, false)
}
function chooseItem(item: ActionSheetOption, index: number) {
if (!item.disable && !item.loading) {
emit(CHOOSE_EVENT, item, index)
emit(UPDATE_VISIBLE_EVENT, false)
}
}
function close() {
if (props.closeAbled) {
emit(CLOSE_EVENT)
emit(UPDATE_VISIBLE_EVENT, false)
}
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-action-sheet`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutPopup
:pop-class="props.popClass"
:custom-style="props.popStyle"
:visible="props.visible"
position="bottom"
:overlay="props.overlay"
:round="props.round"
:safe-area-inset-bottom="props.safeAreaInsetBottom"
:z-index="props.zIndex"
:duration="props.duration"
:overlay-class="props.overlayClass"
:overlay-style="props.overlayStyle"
:lock-scroll="props.lockScroll"
:close-on-click-overlay="props.closeAbled"
@click-overlay="close"
>
<view :class="classes" :style="props.customStyle">
<view v-if="props.title" class="nut-action-sheet__title">
{{ props.title }}
</view>
<slot />
<view v-if="!slotDefault">
<view v-if="props.description" class="nut-action-sheet__item nut-action-sheet__desc">
{{ props.description }}
</view>
<view v-if="props.menuItems.length" class="nut-action-sheet__menu">
<view
v-for="(item, index) of props.menuItems"
:key="index"
class="nut-action-sheet__item"
:class="{
'nut-action-sheet__item--disabled': item.disable,
'nut-action-sheet__item--loading': item.loading,
}"
:style="{ color: isHighlight(item) || item.color }"
@click="chooseItem(item, index)"
>
<NutIcon v-if="item.loading" name="loading" />
<view v-else>
{{ item[props.optionTag] }}
</view>
<view class="nut-action-sheet__subdesc">
{{ item[props.optionSubTag] }}
</view>
</view>
</view>
<view v-if="props.cancelTxt" class="nut-action-sheet__cancel" @click="cancelActionSheet">
{{ props.cancelTxt }}
</view>
</view>
</view>
</NutPopup>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,82 @@
@import "../popup/index";
.nut-theme-dark {
.nut-action-sheet {
.nut-action-sheet__cancel {
border-top: 1px solid $dark-background2;
}
.nut-action-sheet__title {
border-bottom: 1px solid $dark-background2;
}
.nut-action-sheet__cancel,
.nut-action-sheet__item,
.nut-action-sheet__title {
color: $dark-color;
background: $dark-background;
}
}
}
.nut-action-sheet {
display: block;
.nut-action-sheet__title {
display: block;
padding: 10px;
margin: 0;
font-size: $font-size-base;
color: $title-color;
text-align: center;
background-color: $white;
border-bottom: 1px solid $actionsheet-light-color;
}
.nut-action-sheet__menu {
display: block;
padding: 0;
margin: 0;
list-style: none;
}
.nut-action-sheet__cancel,
.nut-action-sheet__item {
display: block;
padding: 10px;
font-size: $actionsheet-item-font-size;
line-height: $actionsheet-item-line-height;
color: $actionsheet-item-font-color;
text-align: center;
cursor: pointer;
background-color: #fff;
border-bottom: $actionsheet-item-border-bottom;
}
.nut-action-sheet__desc {
font-size: $actionsheet-item-font-size;
color: #999;
cursor: default;
}
.nut-action-sheet__subdesc {
display: block;
font-size: $actionsheet-item-subdesc-font-size;
color: #999;
}
.nut-action-sheet__item--disabled {
color: #e1e1e1 !important;
cursor: not-allowed;
}
.nut-action-sheet__item--loading {
cursor: default;
}
.nut-action-sheet__cancel {
margin-top: 5px;
border-top: $actionsheet-item-cancel-border-top;
}
}

View File

@@ -0,0 +1 @@
export * from './actionsheet'

View File

@@ -0,0 +1,104 @@
import type { ExtractPropTypes } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, SELECTED_EVENT, UPDATE_MODEL_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
import { commonProps, isBoolean, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
import { popupProps } from '../popup'
import type { AddressExistRegionData, AddressRegionData, AddressType } from './type'
export const addressProps = {
...popupProps,
...commonProps,
/**
* @description 设置默认选中值
*/
modelValue: makeArrayProp<any>([]),
/**
* @description 是否打开地址选择
*/
visible: Boolean,
/**
* @description 地址选择类型:'exist' | 'custom' | 'custom2'
*/
type: makeStringProp<AddressType>('custom'),
/**
* @description 自定义地址选择标题
*/
customAddressTitle: makeStringProp(''),
/**
* @description 省份数据
*/
province: makeArrayProp<AddressRegionData>([]),
/**
* @description 城市数据
*/
city: makeArrayProp<AddressRegionData>([]),
/**
* @description 县区数据
*/
country: makeArrayProp<AddressRegionData>([]),
/**
* @description 乡镇数据
*/
town: makeArrayProp<AddressRegionData>([]),
/**
* @description 是否显示 '选择其他地区' 按钮。仅在类型为 'exist' 时生效
*/
isShowCustomAddress: truthProp,
/**
* @description 现存地址列表
*/
existAddress: makeArrayProp<AddressExistRegionData>([]),
/**
* @description 已有地址标题
*/
existAddressTitle: makeStringProp(''),
/**
* @description 切换自定义地址和已有地址的按钮标题
*/
customAndExistTitle: makeStringProp(''),
/**
* @description 弹层中内容容器的高度
*/
height: makeNumericProp('200'),
/**
* @description 列提示文字
*/
columnsPlaceholder: {
type: [String, Array],
default: '',
},
}
export type AddressProps = ExtractPropTypes<typeof addressProps>
export const addressEmits = {
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
[UPDATE_MODEL_EVENT]: () => true,
[CLOSE_EVENT]: (val: {
data: any
type: string
}) => val instanceof Object,
[CHANGE_EVENT]: (val: {
next?: string
value?: AddressRegionData
custom: string
}) => val instanceof Object,
switchModule: (val: { type: AddressType }) => val instanceof Object,
closeMask: (val: { closeWay: 'self' | 'mask' | 'cross' }) => val instanceof Object,
[SELECTED_EVENT]: (prevExistAdd: AddressExistRegionData, item: AddressExistRegionData, copyExistAdd: AddressExistRegionData[]) => prevExistAdd instanceof Object && item instanceof Object && copyExistAdd instanceof Object,
}
export type AddressEmits = typeof addressEmits

View File

@@ -0,0 +1,443 @@
<script setup lang="ts">
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
import { computed, defineComponent, reactive, ref, watch } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, PREFIX, SELECTED_EVENT, UPDATE_MODEL_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
import { getMainClass } from '../_utils'
import requestAniFrame from '../_utils/raf'
import { useTranslate } from '../../locale'
import NutElevator from '../elevator/elevator.vue'
import NutIcon from '../icon/icon.vue'
import NutPopup from '../popup/popup.vue'
import { addressEmits, addressProps } from './address'
import type { AddressExistRegionData, AddressRegionData, CustomRegionData } from './type'
const props = defineProps(addressProps)
const emit = defineEmits(addressEmits)
const classes = computed(() => {
return getMainClass(props, componentName)
})
const showPopup = ref(props.visible)
const privateType = ref(props.type)
const tabIndex = ref(0)
const prevTabIndex = ref(0)
const tabName = ref(['province', 'city', 'country', 'town'])
const scrollDis = ref([0, 0, 0, 0])
const scrollTop = ref(0)
const regionData = reactive<Array<AddressRegionData[]>>([])
const regionList = computed(() => {
switch (tabIndex.value) {
case 0:
return props.province
case 1:
return props.city
case 2:
return props.country
default:
return props.town
}
})
function transformData(data: AddressRegionData[]) {
if (!Array.isArray(data))
throw new TypeError('params muse be array.')
if (!data.length)
return []
data.forEach((item: AddressRegionData) => {
if (!item.title)
console.warn('[NutUI] <Address> 请检查数组选项的 title 值是否有设置 ,title 为必填项 .')
})
const newData: CustomRegionData[] = []
data = data.sort((a: AddressRegionData, b: AddressRegionData) => {
return a.title.localeCompare(b.title)
})
data.forEach((item: AddressRegionData) => {
const index = newData.findIndex((value: CustomRegionData) => value.title === item.title)
if (index <= -1) {
newData.push({
title: item.title,
list: ([] as any).concat(item),
})
}
else {
newData[index].list.push(item)
}
})
return newData
}
const selectedRegion = ref<AddressRegionData[]>([])
let selectedExistAddress = reactive({}) // 当前选择的地址
const closeWay = ref<'self' | 'mask' | 'cross'>('self')
// 设置选中省市县
function initCustomSelected() {
regionData[0] = props.province || []
regionData[1] = props.city || []
regionData[2] = props.country || []
regionData[3] = props.town || []
const defaultValue = props.modelValue
const num = defaultValue.length
if (num > 0) {
tabIndex.value = num - 1
if (regionList.value.length === 0) {
tabIndex.value = 0
return
}
for (let index = 0; index < num; index++) {
const arr: AddressRegionData[] = regionData[index]
selectedRegion.value[index] = arr.filter((item: AddressRegionData) => item.id === defaultValue[index])[0]
}
scrollTo()
}
}
function getTabName(item: AddressRegionData | null, index: number) {
if (item && item.name)
return item.name
if (tabIndex.value < index && item)
return item.name
else
return props.columnsPlaceholder[index] || translate('select')
}
// 手动关闭 点击叉号(cross),或者蒙层(mask)
function handClose(type = 'self') {
closeWay.value = type === 'cross' ? 'cross' : 'self'
showPopup.value = false
}
// 点击遮罩层关闭
function clickOverlay() {
closeWay.value = 'mask'
}
// 切换下一级列表
function nextAreaList(item: AddressRegionData) {
const tab = tabIndex.value
prevTabIndex.value = tabIndex.value
const callBackParams: {
next?: string
value?: AddressRegionData
custom: string
} = {
custom: tabName.value[tab],
}
selectedRegion.value[tab] = item
// 删除右边已选择数据
selectedRegion.value.splice(tab + 1, selectedRegion.value.length - (tab + 1))
callBackParams.value = item
if (regionData[tab + 1]?.length > 0) {
tabIndex.value = tab + 1
callBackParams.next = tabName.value[tabIndex.value]
scrollToTop()
}
else {
handClose()
emit(UPDATE_MODEL_EVENT)
}
emit(CHANGE_EVENT, callBackParams)
}
// 切换地区Tab
function changeRegionTab(item: AddressRegionData, index: number) {
prevTabIndex.value = tabIndex.value
if (getTabName(item, index)) {
tabIndex.value = index
scrollTo()
}
}
function scrollChange(e: ScrollViewOnScrollEvent) {
scrollDis.value[tabIndex.value] = e.detail.scrollTop
}
function scrollToTop() {
// scrollTop 不会实时变更。当再次赋值时scrollTop无变化时不会触发滚动
scrollTop.value += 1
requestAniFrame(() => {
setTimeout(() => {
// 直接设置为0无效
scrollTop.value = 0.01
}, 100)
})
}
function scrollTo() {
// scrollTop 不会实时变更。当再次赋值时scrollTop无变化时不会触发滚动
scrollTop.value += 1
requestAniFrame(() => {
setTimeout(() => {
scrollTop.value = scrollDis.value[tabIndex.value]
}, 10)
})
}
// 选择现有地址
function selectedExist(item: AddressExistRegionData) {
const copyExistAdd = props.existAddress
let prevExistAdd: AddressExistRegionData = {} as AddressExistRegionData
copyExistAdd.forEach((list: AddressExistRegionData) => {
if (list && list.selectedAddress)
prevExistAdd = list
list.selectedAddress = false
})
item.selectedAddress = true
selectedExistAddress = item
emit(SELECTED_EVENT, prevExistAdd, item, copyExistAdd)
handClose()
}
// 初始化
function initAddress() {
selectedRegion.value = []
tabIndex.value = 0
scrollTo()
}
// 关闭
function close() {
const data = {
addressIdStr: '',
addressStr: '',
province: selectedRegion.value[0],
city: selectedRegion.value[1],
country: selectedRegion.value[2],
town: selectedRegion.value[3],
}
const callBackParams = {
data: {},
type: privateType.value,
}
if (['custom', 'custom2'].includes(privateType.value)) {
[0, 1, 2, 3].forEach((i) => {
const item = selectedRegion.value[i]
data.addressIdStr += `${i ? '_' : ''}${(item && item.id) || 0}`
data.addressStr += (item && item.name) || ''
})
callBackParams.data = data
}
else {
callBackParams.data = selectedExistAddress
}
initAddress()
if (closeWay.value === 'self')
emit(CLOSE_EVENT, callBackParams)
else
emit('closeMask', { closeWay: closeWay.value })
emit(UPDATE_VISIBLE_EVENT, false)
}
// 选择其他地址
function switchModule() {
const type = privateType.value
privateType.value = type === 'exist' ? 'custom' : 'exist'
initAddress()
emit('switchModule', { type: privateType.value })
}
function handleElevatorItem(key: string, item: AddressRegionData) {
nextAreaList(item)
}
watch(
() => props.visible,
(value) => {
showPopup.value = value
},
)
watch(
() => showPopup.value,
(value) => {
if (value)
initCustomSelected()
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-address`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutPopup
v-model:visible="showPopup"
:z-index="zIndex"
position="bottom"
:lock-scroll="lockScroll"
:round="round"
@close="close"
@click-overlay="clickOverlay"
@open="closeWay = 'self'"
>
<view :class="classes" :style="customStyle">
<view class="nut-address__header">
<view class="nut-address__header-back" @click="switchModule">
<slot v-if="type === 'exist' && privateType === 'custom'" name="backIcon">
<NutIcon name="left" size="14px" />
</slot>
</view>
<view class="nut-address__header__title">
{{
privateType === 'custom'
? customAddressTitle || translate('selectRegion')
: existAddressTitle || translate('deliveryTo')
}}
</view>
<view class="nut-address__header-close" @click="handClose('cross')">
<slot name="closeIcon">
<NutIcon name="close" custom-color="#cccccc" size="14px" />
</slot>
</view>
</view>
<!-- 请选择 -->
<view v-if="['custom', 'custom2'].includes(privateType)" class="nut-address__custom">
<view class="nut-address__region">
<view
v-for="(item, index) in selectedRegion"
:key="index"
class="nut-address__region-item "
:class="[index === tabIndex ? 'active' : '']"
@click="changeRegionTab(item, index)"
>
<view>{{ getTabName(item, index) }} </view>
<view class="nut-address__region-line--mini" :class="{ active: index === tabIndex }" />
</view>
<view v-if="tabIndex === selectedRegion.length" class="active nut-address__region-item">
<view>{{ getTabName(null, selectedRegion.length) }} </view>
<view class="nut-address__region-line--mini active" />
</view>
</view>
<view v-if="privateType === 'custom'" class="nut-address__detail">
<div class="nut-address__detail-list">
<scroll-view
:scroll-y="true"
:style="{ height: '100%' }"
:scroll-top="scrollTop"
@scroll="scrollChange"
>
<div
v-for="(item, index) in regionList"
:key="index"
class="nut-address__detail-item"
:class="[selectedRegion[tabIndex]?.id === item.id ? 'active' : '']"
@click="nextAreaList(item)"
>
<view>
<slot v-if="selectedRegion[tabIndex]?.id === item.id" name="icon">
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
</slot>{{ item.name }}
</view>
</div>
</scroll-view>
</div>
</view>
<view v-else class="nut-address__elevator-group">
<NutElevator
:height="height"
:index-list="transformData(regionList)"
@click-item="handleElevatorItem"
/>
</view>
</view>
<!-- 配送至 -->
<view v-else-if="privateType === 'exist'" class="nut-address__exist">
<div class="nut-address__exist-group">
<ul class="nut-address__exist-group-list">
<li
v-for="(item, index) in existAddress"
:key="index"
class="nut-address__exist-group-item"
:class="[item.selectedAddress ? 'active' : '']"
@click="selectedExist(item)"
>
<slot v-if="!item.selectedAddress" name="unselectedIcon">
<NutIcon name="location2" custom-class="nut-address-select-icon" width="13px" />
</slot>
<slot v-if="item.selectedAddress" name="icon">
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
</slot>
<div class="nut-address__exist-item-info">
<div v-if="item.name && item.phone" class="nut-address__exist-item-info-top">
<div class="nut-address__exist-item-info-name">
{{ item.name }}
</div>
<div class="nut-address__exist-item-info-phone">
{{ item.phone }}
</div>
</div>
<div class="nut-address__exist-item-info-bottom">
<view>
{{ item.provinceName + item.cityName + item.countyName + item.townName + item.addressDetail }}
</view>
</div>
</div>
</li>
</ul>
</div>
<div v-if="isShowCustomAddress" class="nut-address__exist-choose" @click="switchModule">
<div class="nut-address__exist-choose-btn">
{{
customAndExistTitle || translate('chooseAnotherAddress')
}}
</div>
</div>
<template v-if="!isShowCustomAddress">
<slot name="bottom" />
</template>
</view>
</view>
</NutPopup>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,232 @@
@import '../popup/index';
@import '../elevator/index';
.nut-theme-dark {
.nut-address {
&__header {
color: $dark-color;
&__title {
color: $dark-color;
}
}
.nut-address__custom {
.nut-address__region {
color: $dark-color;
}
.nut-address__detail {
.nut-address__detail-list {
.nut-address__detail-item {
color: $dark-color;
}
}
}
}
.nut-address__exist {
.nut-address__exist-group {
.nut-address__exist-group-list {
.nut-address__exist-group-item {
color: $dark-color;
}
}
}
.nut-address__exist-choose {
border-top: 1px solid $dark-background;
}
}
&-custom-buttom {
border-top: 1px solid $dark-background;
}
}
}
.nut-address {
display: block;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
height: 68px;
padding: 0 20px;
font-weight: bold;
color: #333;
text-align: center;
&__title {
display: block;
font-size: $address-header-title-font-size;
color: $address-header-title-color;
}
}
// 请选择
.nut-address__custom {
display: block;
.nut-address__region {
position: relative;
display: flex;
// margin-top: 32px;
padding: 0 20px;
font-size: $address-region-tab-font-size;
color: $address-region-tab-color;
.nut-address__region-item {
position: relative;
display: block;
min-width: 2px;
margin-right: 30px;
&.active {
font-weight: $address-region-tab-active-item-font-weight;
}
view {
display: block;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nut-address__region-line--mini {
position: absolute;
bottom: -10px;
left: 0;
display: inline-block;
width: 0;
height: 3px;
margin-top: 5px;
background: $address-region-tab-line;
transition: 0.2s all linear;
&.active {
width: 26px;
}
}
}
.nut-address__region-line {
position: absolute;
bottom: -10px;
left: 20px;
display: inline-block;
width: 26px;
height: 3px;
margin-top: 5px;
background: $address-region-tab-line;
border-radius: $address-region-tab-line-border-radius;
opacity: $address-region-tab-line-opacity;
transition: 0.2s all linear;
}
}
.nut-address__detail {
display: block;
margin: 20px 20px 0;
.nut-address__detail-list {
// overflow-y: auto;
box-sizing: border-box;
height: 270px;
padding: 0;
padding-top: 15px;
.nut-address__detail-item {
display: flex;
align-items: center;
font-size: $address-region-item-font-size;
color: $address-region-item-color;
&.active {
font-weight: bold;
}
> view {
display: flex;
align-items: center;
margin: 10px 0;
}
}
}
}
.nut-address__elevator-group {
display: flex;
margin-top: 20px;
}
}
// 配送至
.nut-address__exist {
display: block;
margin-top: 15px;
.nut-address__exist-group {
height: 279px;
padding: 15px 20px 0;
overflow-y: scroll;
.nut-address__exist-group-list {
box-sizing: border-box;
padding: 0;
.nut-address__exist-group-item {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $font-size-1;
line-height: 14px;
color: #333;
&.active {
font-weight: bold;
}
.exist-item-icon {
margin-right: $address-item-margin-right;
color: $address-icon-color !important;
}
// span {
// display: inline-block;
// flex: 1;
// }
}
}
}
.nut-address__exist-choose {
width: 100%;
height: 54px;
padding: 6px 0 0;
border-top: 1px solid #f2f2f2;
.nut-address__exist-choose-btn {
width: 90%;
height: 42px;
margin: auto;
font-size: 15px;
line-height: 42px;
color: $white;
text-align: center;
background: $button-primary-background-color;
border-radius: 21px;
}
}
}
&-select-icon {
margin-right: $address-item-margin-right;
color: $address-icon-color !important;
}
}

View File

@@ -0,0 +1,2 @@
export * from './address'
export * from './type'

View File

@@ -0,0 +1,23 @@
export interface AddressRegionData {
name: string
[key: string]: any
}
export interface CustomRegionData {
title: string
list: any[]
}
export interface AddressExistRegionData {
id?: string | number
provinceName: string
cityName: string
countyName: string
townName: string
addressDetail: string
selectedAddress: boolean
[key: string]: any
}
export const addressType = ['exist', 'custom', 'custom2'] as const
export type AddressType = (typeof addressType)[number]

View File

@@ -0,0 +1,42 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, isNumber, isString, makeArrayProp, makeObjectProp, truthProp } from '../_utils'
import type { AddressListOptions } from './type'
export const addresslistProps = {
...commonProps,
/**
* @description 地址数组
*/
data: makeArrayProp<any>([]),
/**
* @description 长按功能
*/
longPress: Boolean,
/**
* @description 右滑功能
*/
swipeEdition: Boolean,
/**
* @description 是否展示底部按钮
*/
showBottomButton: truthProp,
/**
* @description 自定义 `key` 值时,设置映射关系
*/
options: makeObjectProp<AddressListOptions>({}),
}
export type AddressListProps = ExtractPropTypes<typeof addresslistProps>
export const addresslistEmits = {
delIcon: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
editIcon: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
clickItem: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
longCopy: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
longSet: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
longDel: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
swipeDel: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
add: (val: Event) => val instanceof Object,
}
export type AddressListEmits = typeof addresslistEmits

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import { computed, defineComponent, onMounted, reactive, ref, useSlots, watch } from 'vue'
import { PREFIX } from '../_constants'
import { floatData, getMainClass } from '../_utils'
import { useTranslate } from '../../locale'
import NutButton from '../button/button.vue'
import { addresslistEmits, addresslistProps } from './addresslist'
import GeneralShell from './compoents/generalshell.vue'
const props = defineProps(addresslistProps)
const emit = defineEmits(addresslistEmits)
const slots = useSlots()
function hasSlot(name: string) {
return Boolean(slots[name])
}
const dataArray = ref<any[]>([])
const dataInfo = reactive({
id: 2,
addressName: '姓名',
phone: '123****4567',
defaultAddress: false,
fullAddress: '北京市通州区测试测试测试测试测试测试测试测试测试',
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
// 磨平参数差异
function trowelData() {
if (Object.keys(props.options).length > 0) {
dataArray.value = props.data.map((item) => {
return floatData(dataInfo, item, props.options)
})
}
}
watch(
() => props.data,
() => trowelData(),
{ deep: true },
)
function handleDelIconClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('delIcon', event, item, index)
}
function handleEditIconClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('editIcon', event, item, index)
}
function handleContentItemClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('clickItem', event, item, index)
}
function handleLongCopyClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('longCopy', event, item, index)
}
function handleLongSetClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('longSet', event, item, index)
}
function handleLongDelClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('longDel', event, item, index)
}
function handleSwipeDelClick(event: any, item: any, index: number | string) {
event.stopPropagation()
emit('swipeDel', event, item, index)
}
function handleAddressAdd(event: any) {
event.stopPropagation()
emit('add', event)
}
onMounted(() => {
trowelData()
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-address-list`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
// #ifndef H5
styleIsolation: 'shared',
// #endif
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<GeneralShell
v-for="(item, index) in dataArray"
:key="index"
:address="item"
:long-press="props.longPress"
:swipe-edition="props.swipeEdition"
:use-content-info-slot="hasSlot('itemInfos')"
:use-content-icons-slot="hasSlot('itemIcon')"
:use-content-addrs-slot="hasSlot('itemAddr')"
:use-longpress-all-slot="hasSlot('longpressBtns')"
:use-swipe-right-btn-slot="hasSlot('swipeRight')"
@del-icon="handleDelIconClick($event, item, index)"
@edit-icon="handleEditIconClick($event, item, index)"
@click-item="handleContentItemClick($event, item, index)"
@swipe-del="handleSwipeDelClick($event, item, index)"
@long-copy="handleLongCopyClick($event, item, index)"
@long-set="handleLongSetClick($event, item, index)"
@long-del="handleLongDelClick($event, item, index)"
>
<template #content-info>
<slot name="itemInfos" :item="item" />
</template>
<template #content-icons>
<slot name="itemIcon" :item="item" />
</template>
<template #content-addrs>
<slot name="itemAddr" :item="item" />
</template>
<template v-if="props.longPress" #longpress-all>
<slot name="longpressBtns" :item="item" />
</template>
<template v-if="props.swipeEdition" #swipe-right-btn>
<slot name="swipeRight" :item="item" />
</template>
</GeneralShell>
<view v-if="props.showBottomButton" class="nut-address-list__bottom" @click="handleAddressAdd">
<NutButton type="danger" block>
{{ translate('addAddress') }}
</NutButton>
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,154 @@
<script lang="ts" setup>
import { defineComponent } from 'vue'
import { PREFIX } from '../../_constants'
import { useTranslate } from '../../../locale'
import NutIcon from '../../icon/icon.vue'
const props = defineProps({
item: {
type: Object,
default: () => ({}),
},
useContentTopSlot: Boolean,
useContentIconSlot: Boolean,
useContentAddrSlot: Boolean,
})
const emit = defineEmits(['delIcon', 'editIcon', 'clickItem'])
function handleDelIconClick(event: any) {
event.stopPropagation()
emit('delIcon', event, props.item)
}
function handleEditIconClick(event: any) {
event.stopPropagation()
emit('editIcon', event, props.item)
}
function handleContentsClick(event: any) {
event.stopPropagation()
emit('clickItem', event, props.item)
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-address-list-item`
const { translate } = useTranslate(`${PREFIX}-address-list`)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
// #ifndef H5
styleIsolation: 'shared',
// #endif
},
})
</script>
<template>
<view class="nut-address-list-item" @click="handleContentsClick">
<view class="nut-address-list-item__info">
<view class="nut-address-list-item__info-contact">
<slot v-if="props.useContentTopSlot" name="content-top" />
<template v-else>
<view class="nut-address-list-item__info-contact-name">
{{ props.item.addressName }}
</view>
<view class="nut-address-list-item__info-contact-tel">
{{ props.item.phone }}
</view>
<view v-if="props.item.defaultAddress" class="nut-address-list-item__info-contact-default">
{{ translate('default') }}
</view>
</template>
</view>
<view class="nut-address-list-item__info-handle">
<slot v-if="props.useContentIconSlot" name="content-icon" />
<template v-else>
<NutIcon name="del" custom-class="nut-address-list-item__info-handle-del" @tap.stop="handleDelIconClick" />
<NutIcon name="edit" custom-class="nut-address-list-item__info-handle-edit" @tap.stop="handleEditIconClick" />
</template>
</view>
</view>
<view class="nut-address-list-item__addr">
<slot v-if="props.useContentAddrSlot" name="content-addr" />
<template v-else>
{{ props.item.fullAddress }}
</template>
</view>
</view>
</template>
<style lang="scss">
.nut-theme-dark {
.nut-address-list {
&-item {
&__addr {
color: $dark-color-gray;
}
}
}
}
.nut-address-list {
&-item {
width: 100%;
&__info {
display: flex;
justify-content: space-between;
&-contact {
display: flex;
align-items: center;
justify-content: flex-start;
font-weight: bold;
&-name {
max-width: 145px;
word-wrap: break-word;
}
&-tel {
max-width: 110px;
margin-left: 8px;
word-wrap: break-word;
}
&-default {
height: 16px;
padding: 0 6px;
margin-left: 5px;
font-size: 12px;
line-height: 16px;
color: $addresslist-contnts-contact-color;
background: $addresslist-contnts-contact-default;
border-radius: 2px;
}
}
&-handle {
&-edit {
margin-left: 15px;
}
}
}
&__addr {
margin-top: 5px;
font-size: $addresslist-addr-font-size;
color: $addresslist-addr-font-color;
}
}
}
</style>

View File

@@ -0,0 +1,335 @@
<script lang="ts" setup>
import { defineComponent, ref } from 'vue'
import { PREFIX } from '../../_constants'
import NutButton from '../../button/button.vue'
import NutSwipe from '../../swipe/swipe.vue'
import ItemContents from './Itemcontents.vue'
const props = defineProps({
address: {
type: Object,
},
longPress: {
type: Boolean,
default: false,
},
swipeEdition: {
type: Boolean,
default: false,
},
useContentInfoSlot: Boolean,
useContentIconsSlot: Boolean,
useContentAddrsSlot: Boolean,
useLongpressAllSlot: Boolean,
useSwipeRightBtnSlot: Boolean,
})
const emit = defineEmits(['delIcon', 'editIcon', 'clickItem', 'longDown', 'longCopy', 'longSet', 'longDel', 'swipeDel'])
const moveRef = ref<boolean>(false)
const showMaskRef = ref<boolean>(false)
function handleDelIconClick(event: any) {
event.stopPropagation()
emit('delIcon', event, props.address)
}
function handleEditIconClick(event: any) {
event.stopPropagation()
emit('editIcon', event, props.address)
}
function handleItemClick(event: any) {
event.stopPropagation()
if (moveRef.value)
return
emit('clickItem', event, props.address)
}
function handleLongDelClick(event: any) {
event.stopPropagation()
emit('longDel', event, props.address)
}
let timer: NodeJS.Timeout | null = null
function destroyTimer() {
if (timer == null)
return
clearTimeout(timer)
timer = null
}
function startTimer(event: any) {
timer = setTimeout(() => {
showMaskRef.value = true
emit('longDown', event, props.address)
}, 300)
}
// 长按功能实现
function handleTouchStart(event: any) {
startTimer(event)
}
function handleTouchMove() {
// 滑动不触发长按
destroyTimer()
}
function handleTouchEnd() {
// 删除定时器,防止重复注册
destroyTimer()
}
function handleHideMaskClick() {
showMaskRef.value = false
}
function handleLongCopyClick(event: any) {
event.stopPropagation()
emit('longCopy', event, props.address)
}
function handleLongSetClick(event: any) {
event.stopPropagation()
emit('longSet', event, props.address)
}
function handleMaskClick(event: any) {
event.stopPropagation()
event.preventDefault()
if (timer != null) {
// 排除长按时触发点击的情况
showMaskRef.value = false
}
}
function handleSwipeDelClick(event: any) {
event.stopPropagation()
emit('swipeDel', event, props.address)
}
function handleSwipeStart() {
moveRef.value = false
}
function handleSwipeMove() {
moveRef.value = true
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-address-list-general`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
// #ifndef H5
styleIsolation: 'shared',
// #endif
},
})
</script>
<template>
<view v-if="!props.swipeEdition" class="nut-address-list-general">
<ItemContents
:item="props.address"
:use-content-top-slot="props.useContentInfoSlot"
:use-content-icon-slot="props.useContentIconsSlot"
:use-content-addr-slot="props.useContentAddrsSlot"
@del-icon="handleDelIconClick"
@edit-icon="handleEditIconClick"
@click-item="handleItemClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<template #content-top>
<slot name="content-info" />
</template>
<template #content-icon>
<slot name="content-icons" />
</template>
<template #content-addr>
<slot name="content-addrs" />
</template>
</ItemContents>
<view v-if="props.longPress && showMaskRef" class="nut-address-list-general__mask" @click="handleMaskClick">
<slot v-if="props.useLongpressAllSlot" name="longpress-all" />
<template v-else>
<view class="nut-address-list-general__mask-copy" @click="handleLongCopyClick">
复制地址
</view>
<view class="nut-address-list-general__mask-set" @click="handleLongSetClick">
设置默认
</view>
<view class="nut-address-list-general__mask-del" @click="handleLongDelClick">
删除地址
</view>
</template>
</view>
<view v-if="showMaskRef" class="nut-address-list__mask-bottom" @click="handleHideMaskClick" />
</view>
<NutSwipe v-else>
<view class="nut-address-list-swipe">
<ItemContents
:item="props.address"
:use-content-top-slot="props.useContentInfoSlot"
:use-content-icon-slot="props.useContentIconsSlot"
:use-content-addr-slot="props.useContentAddrsSlot"
@del-icon="handleDelIconClick"
@edit-icon="handleEditIconClick"
@click-item="handleItemClick"
@touchstart="handleSwipeStart"
@touchmove="handleSwipeMove"
>
<template #content-top>
<slot name="content-info" />
</template>
<template #content-icon>
<slot name="content-icons" />
</template>
<template #content-addr>
<slot name="content-addrs" />
</template>
</ItemContents>
</view>
<template #right>
<view style="height: 100%;">
<slot v-if="props.useSwipeRightBtnSlot" name="swipe-right-btn" />
<template v-else>
<NutButton
shape="square"
custom-style="height: 100%;"
type="danger"
@tap.stop="handleSwipeDelClick"
>
删除
</NutButton>
</template>
</view>
</template>
</NutSwipe>
</template>
<style lang="scss">
.nut-theme-dark {
.nut-address-list {
&-swipe,
&-general {
color: $dark-color;
background-color: $dark-background2;
border-bottom: 1px solid $dark-color-gray;
&__mask {
background-color: $dark-color3;
&-copy {
color: $dark-color-gray;
background-color: $dark-color;
}
}
}
}
}
.nut-address-list {
&-swipe,
&-general {
position: relative;
display: flex;
align-items: center;
min-height: 76px;
padding: 5px 10px;
font-size: $addresslist-font-size;
color: $addresslist-font-color;
background-color: $addresslist-bg;
border-bottom: 1px solid $addresslist-border;
&__mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2001;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 40px;
background-color: $addresslist-mask-bg;
&-copy,
&-set,
&-del {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 55px;
padding: 0 10px;
font-size: 14px;
text-align: center;
background-color: $white;
border-radius: 50%;
}
&-set {
color: $white;
background-color: $addresslist-set-bg;
}
&-del {
color: $white;
background-color: $addresslist-del-bg;
}
}
}
&-general {
&:last-child {
border-bottom: none;
}
}
.nut-swipe {
&:last-of-type {
.nut-address-list-swipe {
border-bottom: none;
}
}
}
.nut-address-list__mask-bottom {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2000;
background-color: transparent;
}
}
</style>

View File

@@ -0,0 +1,42 @@
@import "../button/index";
@import "../swipe/index";
.nut-theme-dark {
.nut-address-list {
&__bottom {
background-color: $dark-background2;
}
}
}
.nut-address-list {
overflow: hidden;
&:last-child {
padding-bottom: 84px;
}
&__bottom {
position: fixed;
right: 0;
bottom: 0;
bottom: constant(safe-area-inset-bottom);
bottom: env(safe-area-inset-bottom);
left: 0;
z-index: 100000;
box-sizing: border-box;
width: 100%;
padding: 12px 18px 24px;
background-color: $addresslist-bg;
}
.nut-address-list__mask-bottom {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2000;
background-color: transparent;
}
}

View File

@@ -0,0 +1,2 @@
export * from './addresslist'
export * from './type'

View File

@@ -0,0 +1,3 @@
export interface AddressListOptions {
[key: string]: string
}

View File

@@ -0,0 +1,44 @@
import type { ExtractPropTypes } from 'vue'
import { CLICK_EVENT } from '../_constants'
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
import type { AnimateAction, AnimateType } from './type'
export const animateProps = {
...commonProps,
/**
* @description 控制动画,当值从 false 变为 true 时会触发一次动画
*/
show: Boolean,
/**
* @description 动画类型
* @values 'fade', 'slide', 'zoom', ...
*/
type: makeStringProp<AnimateType | ''>(''),
/**
* @description 是否循环执行。`true`-循环执行; `false`-执行一次
*/
loop: Boolean,
/**
* @description 动画时长,单位 ms
*/
duration: makeNumericProp(500),
/**
* @description (不能与 show 同时使用)触发方式,`initial`-初始化执行; `click`-点击执行
* @values 'initial', 'click'
* @default initial
*/
action: makeStringProp<AnimateAction>('initial'),
}
export type AnimateProps = ExtractPropTypes<typeof animateProps>
export const animateEmits = {
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
animate: () => true,
}
export type AnimateEmits = typeof animateEmits

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
import { CLICK_EVENT, PREFIX } from '../_constants'
import { getMainClass, getMainStyle } from '../_utils'
import requestAniFrame from '../_utils/raf'
import { animateEmits, animateProps } from './animate'
const props = defineProps(animateProps)
const emit = defineEmits(animateEmits)
const animated = ref(props.action === 'initial' || props.show === true || props.loop)
const classes = computed(() => {
const obj = {
[`${componentName}__container`]: true,
[`${componentName}-${props.type}`]: animated.value,
loop: props.loop,
}
return getMainClass(props, componentName, obj)
})
const getStyle = computed(() => {
return getMainStyle(props, {
animationDuration: props.duration ? `${props.duration}ms` : undefined,
})
})
function animate() {
animated.value = false
// #ifdef H5
requestAniFrame(() => {
requestAniFrame(() => {
animated.value = true
})
})
}
function handleClick(event: unknown) {
if (props.action === 'click') {
animate()
emit(CLICK_EVENT, event as MouseEvent)
emit('animate')
}
}
watch(
() => props.show,
(val) => {
if (val) {
animate()
emit('animate')
}
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-animate`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view class="nut-animate">
<view
:class="classes"
:style="getStyle"
@click="handleClick"
>
<slot />
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,316 @@
.nut-animate {
.nut-animate__container {
display: inline-block;
}
/* Animation css */
[class*="nut-animate-"] {
animation-duration: 0.5s;
animation-timing-function: ease-out;
animation-fill-mode: both;
}
// 抖动
.nut-animate-shake {
animation-name: shake;
}
// 心跳
.nut-animate-ripple {
animation-name: ripple;
}
// 漂浮
.nut-animate-float {
position: relative;
animation-name: float-pop;
}
// 呼吸灯
.nut-animate-breath {
animation-name: breath;
animation-duration: 2700ms;
animation-timing-function: ease-in-out;
animation-direction: alternate;
}
// 右侧向左侧划入
.nut-animate-slide-right {
animation-name: slide-right;
}
// 右侧向左侧划入
.nut-animate-slide-left {
animation-name: slide-left;
}
// 上面向下面划入
.nut-animate-slide-top {
animation-name: slide-top;
}
// 下面向上面划入
.nut-animate-slide-bottom {
animation-name: slide-bottom;
}
.nut-animate-jump {
transform-origin: center center;
animation: jump 0.7s linear;
}
// 循环
.loop {
animation-iteration-count: infinite;
}
// 抖动动画
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10% {
transform: translateX(-9px);
}
20% {
transform: translateX(8px);
}
30% {
transform: translateX(-7px);
}
40% {
transform: translateX(6px);
}
50% {
transform: translateX(-5px);
}
60% {
transform: translateX(4px);
}
70% {
transform: translateX(-3px);
}
80% {
transform: translateX(2px);
}
90% {
transform: translateX(-1px);
}
}
// 心跳
@keyframes ripple {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
// 呼吸
@keyframes breath {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
// 右侧向左侧划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-right {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
// 左侧向右侧划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-left {
0% {
opacity: 0;
transform: translateX(-100%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
// 上面向下面划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-top {
0% {
opacity: 0;
transform: translateY(-100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
// 下面向上面划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-bottom {
0% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
// 漂浮 float
// stylelint-disable-next-line keyframes-name-pattern
@keyframes float-pop {
0% {
top: 0;
}
25% {
top: 1px;
}
50% {
top: 4px;
}
75% {
top: 1px;
}
100% {
top: 0;
}
}
// 跳跃
@keyframes jump {
0% {
transform: rotate(0deg) translateY(0);
animation-timing-function: ease-in;
}
25% {
transform: rotate(10deg) translateY(20 * 1px);
animation-timing-function: ease-out;
}
50% {
transform: rotate(0deg) translateY(-10 * 1px);
animation-timing-function: ease-in;
}
75% {
transform: rotate(-10deg) translateY(20 * 1px);
animation-timing-function: ease-out;
}
100% {
transform: rotate(0deg) translateY(0);
animation-timing-function: ease-in;
}
}
.nut-animate-twinkle {
position: relative;
&::after,
&::before {
position: absolute;
right: 50%;
z-index: 1;
box-sizing: border-box;
width: 60 * 1px;
height: 60 * 1px;
margin-top: calc(-30 / 2) * 1px;
margin-right: calc(-60 / 2) * 1px;
content: "";
border: 4 * 1px solid rgb(255 255 255 / 60%);
border-radius: calc(60 / 2) * 1px;
transform: scale(0);
animation: twinkle 2s ease-out infinite;
}
&::after {
animation-delay: 0.4s;
}
}
// 水波
@keyframes twinkle {
0% {
transform: scale(0);
}
20% {
opacity: 1;
}
50%,
100% {
opacity: 0;
transform: scale(1.4);
}
}
.nut-animate-flicker {
position: relative;
overflow: hidden;
&::after {
position: absolute;
top: 0;
left: 0;
width: 100 * 1px;
height: 60 * 1px;
content: "";
background-image: linear-gradient(106deg, rgb(232 224 255 / 0%) 24%, #e8e0ff 91%);
filter: blur(3 * 1px);
opacity: 0.73;
transform: skewX(-20deg);
animation: flicker 1.5s linear infinite;
}
}
@keyframes flicker {
0% {
transform: translateX(-100 * 1px) skewX(-20deg);
}
40%,
100% {
transform: translateX(150 * 1px) skewX(-20deg);
}
}
}

View File

@@ -0,0 +1 @@
export * from './animate'

View File

@@ -0,0 +1,4 @@
export const animateType = ['shake', 'ripple', 'breath', 'float', 'slide-right', 'slide-left', 'slide-top', 'slide-bottom', 'jump', 'twinkle', 'flicker'] as const
export type AnimateType = (typeof animateType)[number]
export const animateAction = ['initial', 'click', ''] as const
export type AnimateAction = (typeof animateAction)[number]

View File

@@ -0,0 +1,25 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
import type { AvatarShape, AvatarSize } from './type'
export const avatarProps = {
...commonProps,
/**
* @description 头像的大小,可选值为:`large`、`normal`、`small`,支持直接输入数字
*/
size: makeNumericProp<AvatarSize | string | number | undefined>(undefined),
/**
* @description 头像的形状,可选值为:`square`、`round`
*/
shape: makeStringProp<AvatarShape | undefined>(undefined),
/**
* @description 背景色
*/
bgColor: makeStringProp('#eee'),
/**
* @description 字体颜色
*/
customColor: makeStringProp('#666'),
}
export type AvatarProps = ExtractPropTypes<typeof avatarProps>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue'
import { PREFIX } from '../_constants'
import { useInject } from '../_hooks'
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
import type { AvatarGroupProps } from '../avatargroup'
import { AVATAR_GROUP_KEY } from '../avatargroup'
import { avatarProps } from './avatar'
import type { AvatarFinalSize, AvatarShape, AvatarSize } from './type'
import { avatarSize } from './type'
const props = defineProps(avatarProps)
const instance = getCurrentInstance()
const { parent } = useInject<{ props: Required<AvatarGroupProps> }>(AVATAR_GROUP_KEY)
const show = ref(true)
const innerZIndex = ref<number | undefined>(undefined)
watch(() => ({
maxCount: parent?.props.maxCount,
children: parent?.internalChildren,
}), ({ maxCount, children }) => {
if (maxCount == null || Number(maxCount) <= 0 || children == null || instance == null) {
show.value = true
innerZIndex.value = undefined
return
}
const index = children.findIndex((item) => {
return item.uid === instance.uid && !(item.props.customClass as string)?.includes('avatar-fold')
})
if (index < 0) {
show.value = true
innerZIndex.value = undefined
return
}
show.value = index < Number(maxCount)
if (parent?.props.zIndex === 'right')
innerZIndex.value = children.length - index
else
innerZIndex.value = undefined
}, {
immediate: true,
deep: true,
})
function getTrulySize() {
if (props.size != null)
return props.size
if (parent != null && parent.props.size != null)
return parent.props.size
return 'normal'
}
const finalSize = computed<AvatarFinalSize>(() => {
const size: string | number = getTrulySize()
const preset: boolean = avatarSize.includes(size as AvatarSize)
return {
preset,
value: preset ? (size as AvatarSize) : pxCheck(size),
}
})
const finalShape = computed<AvatarShape>(() => {
if (props.shape != null)
return props.shape
if (parent != null && parent.props.shape != null)
return parent.props.shape
return 'round'
})
const classes = computed(() => {
const value: Record<string, boolean> = {
[`nut-avatar-${finalShape.value}`]: true,
'nut-hidden': !show.value,
}
if (finalSize.value.preset)
value[`nut-avatar-${finalSize.value.value}`] = true
return getMainClass(props, componentName, value)
})
const styles = computed(() => {
const value: CSSProperties = {
backgroundColor: props.bgColor,
color: props.customColor,
}
if (!finalSize.value.preset) {
value.width = finalSize.value.value
value.height = finalSize.value.value
}
if (parent?.props.span)
value.marginLeft = pxCheck(parent?.props.span)
if (innerZIndex.value !== undefined)
value.zIndex = innerZIndex.value
return getMainStyle(props, value)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-avatar`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :style="styles" :class="classes">
<slot />
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,55 @@
.nut-avatar {
position: relative;
display: inline-block;
flex: 0 0 auto; // 防止被压缩
text-align: center;
vertical-align: top;
background-repeat: no-repeat;
background-position: center center;
background-size: 100% 100%;
image {
display: block;
width: 100%;
height: 100%;
}
.nut-icon {
position: absolute;
top: 50%;
left: 50%;
background-size: 100% 100%;
transform: translate(-50%, -50%);
}
}
.nut-avatar-large {
width: $avatar-large-width;
height: $avatar-large-height;
line-height: $avatar-large-height;
}
.nut-avatar-normal {
width: $avatar-normal-width;
height: $avatar-normal-height;
line-height: $avatar-normal-height;
}
.nut-avatar-small {
width: $avatar-small-width;
height: $avatar-small-height;
line-height: $avatar-small-height;
}
.nut-avatar-square {
border-radius: $avatar-square;
}
.nut-avatar-round {
border-radius: 50%;
}
.nut-avatar-square,
.nut-avatar-round {
overflow: hidden;
}

View File

@@ -0,0 +1,2 @@
export * from './avatar'
export * from './type'

View File

@@ -0,0 +1,16 @@
export const avatarSize = ['large', 'normal', 'small'] as const
export type AvatarSize = (typeof avatarSize)[number]
export const avatarShape = ['round', 'square'] as const
export type AvatarShape = (typeof avatarShape)[number]
export interface AvatarFinalSize {
/**
* 是否为预设尺寸
*/
preset: boolean
/**
* 尺寸值
*/
value: string
}

View File

@@ -0,0 +1,50 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
import type { AvatarShape, AvatarSize } from '../avatar'
export const AVATAR_GROUP_KEY = Symbol('avatarGroup')
export const avatargroupProps = {
...commonProps,
/**
* @description 显示的最大头像个数
*/
maxCount: makeNumericProp(-1),
/**
* @description 头像数量超出时,会出现一个头像折叠元素,该元素内容可为`...`、`more`、`+N`
*/
maxContent: makeStringProp(''),
/**
* @description 头像的大小,可选值为:`large`、`normal`、`small`,支持直接输入数字
*/
size: makeNumericProp<AvatarSize | string | number>('normal'),
/**
* @description 头像的形状,可选值为:`square`、`round`
*/
shape: makeStringProp<AvatarShape>('round'),
/**
* @description 头像折叠元素的字体颜色
*/
maxColor: makeStringProp('#666'),
/**
* @description 头像折叠元素的背景色
*/
maxBgColor: makeStringProp('#eee'),
/**
* @description 头像之间的间距
*/
span: makeNumericProp('-8'),
/**
* @description 组合头像之间的层级方向,可选值为:`left`、`right`
*/
zIndex: makeStringProp<'left' | 'right'>('left'),
}
export type AvatarGroupProps = ExtractPropTypes<typeof avatargroupProps>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, ref, watch } from 'vue'
import { PREFIX } from '../_constants'
import { useProvide } from '../_hooks'
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
import NutAvatar from '../avatar/avatar.vue'
import { AVATAR_GROUP_KEY, avatargroupProps } from './avatargroup'
const props = defineProps(avatargroupProps)
const { internalChildren } = useProvide(AVATAR_GROUP_KEY, `${PREFIX}-avatar`)({ props })
const innerMaxCount = computed<number>(() => {
return Number(props.maxCount)
})
const foldCount = ref(0)
watch(() => ({
maxCount: props.maxCount,
children: internalChildren,
}), ({ children }) => {
if (innerMaxCount.value > 0)
foldCount.value = Math.min(99, children.length - innerMaxCount.value)
else
foldCount.value = 0
}, {
immediate: true,
deep: true,
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
const styles = computed(() => {
return getMainStyle(props, {
marginLeft: `calc(0px - ${pxCheck(props.span)})`,
})
})
const foldStyles = computed<CSSProperties>(() => {
return {
marginLeft: pxCheck(props.span),
}
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-avatar-group`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="styles">
<slot />
<NutAvatar
v-if="foldCount > 0"
custom-class="avatar-fold"
:custom-style="foldStyles"
:size="props.size"
:shape="props.shape"
:bg-color="props.maxBgColor"
:custom-color="props.maxColor"
>
{{ props.maxContent || foldCount }}
</NutAvatar>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,12 @@
.nut-avatar-group {
position: relative;
display: flex;
flex: 0 0 auto; // 防止被压缩
background-repeat: no-repeat;
background-position: center center;
background-size: 100% 100%;
.nut-avatar {
border: 1px solid #fff;
}
}

View File

@@ -0,0 +1 @@
export * from './avatargroup'

View File

@@ -0,0 +1,44 @@
import type { ExtractPropTypes } from 'vue'
import { CLICK_EVENT } from '../_constants'
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
export const backtopProps = {
...commonProps,
/**
* @description 滚动区域的高度
*/
height: makeStringProp('100vh'),
/**
* @description 距离页面底部距离
*/
bottom: makeNumberProp(20),
/**
* @description 距离页面右侧距离
*/
right: makeNumberProp(10),
/**
* @description 页面垂直滚动多高后出现
*/
distance: makeNumberProp(200),
/**
* @description 设置组件页面层级
*/
zIndex: makeNumberProp(10),
/**
* @description 自定义图标颜色
*/
customColor: String,
}
export type BacktopProps = ExtractPropTypes<typeof backtopProps>
export const backtopEmits = {
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
}
export type BacktopEmits = typeof backtopEmits

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
import { computed, defineComponent, ref } from 'vue'
import { CLICK_EVENT, PREFIX } from '../_constants'
import { getMainClass, getMainStyle } from '../_utils'
import NutIcon from '../icon/icon.vue'
import { backtopEmits, backtopProps } from './backtop'
const props = defineProps(backtopProps)
const emit = defineEmits(backtopEmits)
const backTop = ref(false)
const scrollTop = ref(1)
const classes = computed(() => {
return getMainClass(props, componentName, {
show: backTop.value,
})
})
const style = computed(() => {
return getMainStyle(props, {
right: `${props.right}px`,
bottom: `${props.bottom}px`,
zIndex: props.zIndex,
})
})
function scroll(e: ScrollViewOnScrollEvent) {
scrollTop.value = 2
backTop.value = e.detail.scrollTop >= props.distance
}
function click(e: unknown) {
scrollTop.value = 1
emit(CLICK_EVENT, e as MouseEvent)
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-backtop`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view>
<scroll-view
:scroll-y="true"
:style="{ height }"
:scroll-top="scrollTop"
:scroll-with-animation="true"
@scroll="scroll"
>
<slot name="content" />
</scroll-view>
<view :class="classes" :style="style" @click.stop="click">
<slot name="icon">
<NutIcon
:custom-color="customColor"
name="top"
:size="19"
custom-class="nut-backtop-main"
/>
</slot>
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,40 @@
.nut-theme-dark {
.nut-backtop {
&.show {
color: $dark-color;
background: $dark-background;
border: 1px solid $dark-background;
}
&-main {
color:'#ffffff';
}
}
}
.nut-backtop {
position: fixed;
display: none;
&.show {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: $white;
border: 1px solid $backtop-border-color;
border-radius: 50%;
}
&.show :active {
background: rgb(0 0 0 / 10%);
}
&-main {
color:'#000000';
transition: all 0.2s ease-in-out
}
}

View File

@@ -0,0 +1 @@
export type * from './backtop'

View File

@@ -0,0 +1,52 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
export const badgeProps = {
...commonProps,
/**
* @description 显示的内容
*/
value: [String, Number],
/**
* @description `value` 为数值时,最大值
*/
max: makeNumberProp(10000),
/**
* @description 是否为小点
*/
dot: Boolean,
/**
* @description 是否为气泡形状
* @since >v4.0.0
*/
bubble: Boolean,
/**
* @description 是否隐藏
*/
hidden: Boolean,
/**
* @description 上下偏移量,支持单位设置,可设置为:`5px` 等
*/
top: makeStringProp('0'),
/**
* @description 左右偏移量,支持单位设置,可设置为:`5px` 等
*/
right: makeStringProp('0'),
/**
* @description 徽标的 `z-index` 值
*/
zIndex: makeNumberProp(10),
/**
* @description 徽标背景颜色
*/
customColor: makeStringProp(''),
}
export type BadgeProps = ExtractPropTypes<typeof badgeProps>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed, defineComponent } from 'vue'
import { PREFIX } from '../_constants'
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
import { badgeProps } from './badge'
const props = defineProps(badgeProps)
const getStyle = computed(() => {
return getMainStyle(props, {
top: pxCheck(props.top),
right: pxCheck(props.right),
zIndex: props.zIndex,
background: props.customColor,
})
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
const content = computed(() => {
if (props.dot)
return
const value = props.value
const max = props.max
if (typeof value === 'number' && typeof max === 'number')
return max < value ? `${max}+` : value
return value
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-badge`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes">
<view v-if="!props.hidden && !props.dot && $slots.icon" class="nut-badge__icon" :style="getStyle">
<slot name="icon" />
</view>
<slot />
<view
v-if="!props.hidden && (content || props.dot)"
class="nut-badge__content nut-badge__content--sup"
:class="{ 'nut-badge__content--dot': props.dot, 'nut-badge__content--bubble': !props.dot && props.bubble }"
:style="getStyle"
>
{{ content }}
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,54 @@
.nut-theme-dark {
.nut-badge {
&.show {
color: $dark-color;
background: $dark-background;
}
}
}
.nut-badge {
position: relative;
display: inline-block;
.nut-badge__icon {
position: absolute;
z-index: $badge-z-index;
display: flex;
align-items: center;
padding: $badge-icon-padding;
line-height: normal;
text-align: center;
background: $badge-background-color;
border-radius: $badge-border-radius;
transform: $badge-content-transform;
}
.nut-badge__content {
display: flex;
align-items: center;
transform: $badge-content-transform;
&--sup {
position: absolute;
padding: $badge-padding;
font-size: $badge-font-size;
font-weight: normal;
color: $badge-color;
text-align: center;
background: $badge-background-color;
border-radius: $badge-border-radius;
}
&--dot {
width: $badge-dot-width;
height: $badge-dot-height;
padding: $badge-dot-padding;
border-radius: $badge-dot-border-radius;
}
&--bubble {
border-bottom-left-radius: 0;
}
}
}

View File

@@ -0,0 +1 @@
export * from './badge'

View File

@@ -0,0 +1,41 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeArrayProp, makeNumberProp, truthProp } from '../_utils'
export const barrageProps = {
...commonProps,
/**
* @description 弹幕列表数据
*/
danmu: makeArrayProp<string>([]),
/**
* @description 可视区域内每个弹幕出现的时间间隔
*/
frequency: makeNumberProp<number>(500),
/**
* @description 每个弹幕的滚动时间
*/
speeds: makeNumberProp<number>(5000),
/**
* @description 弹幕行数,分几行展示
*/
rows: makeNumberProp<number>(3),
/**
* @description 弹幕垂直距离
*/
top: makeNumberProp<number>(10),
/**
* @description 是否循环播放
*/
loop: truthProp,
}
export type BarrageProps = ExtractPropTypes<typeof barrageProps>
export interface BarrageInst {
add: (word: string) => void
}

View File

@@ -0,0 +1,132 @@
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref, useSlots, watch } from 'vue'
import { PREFIX } from '../_constants'
import { useSelectorQuery } from '../_hooks'
import { getMainClass } from '../_utils'
import { barrageProps } from './barrage'
const props = defineProps(barrageProps)
defineExpose({ add })
const instance = getCurrentInstance() as ComponentInternalInstance
const { getSelectorNodeInfo } = useSelectorQuery(instance)
const classTime = new Date().getTime()
const slotDefault = !!useSlots().default
const timeId = ref(new Date().getTime())
const danmuList = ref<any>(props.danmu)
const rows = ref<number>(props.rows)
const top = ref<number>(props.top)
const speeds = props.speeds
const classes = computed(() => {
return getMainClass(props, componentName, {
[`nut-barrage--dmBody${timeId.value}`]: true,
})
})
onMounted(() => {
// #ifdef H5
if (slotDefault) {
const list = document
.getElementsByClassName(`nut-barrage__slotBody${classTime}`)[0]
.getElementsByClassName('nut-barrage__item')
const childrens = list?.[0]?.children || []
danmuList.value = childrens
}
// #endif
runStep()
})
watch(
() => props.danmu,
(newValue) => {
danmuList.value = [...newValue]
},
)
function add(word: string) {
danmuList.value = [...danmuList.value, word]
runStep()
}
function getNode(index: number) {
setTimeout(async () => {
let width = 100
const dmBodyNodeInfo = await getSelectorNodeInfo(`.nut-barrage--dmBody${timeId.value}`)
width = dmBodyNodeInfo?.width || 300
const itemNodeInfo = await getSelectorNodeInfo(`.nut-barrage__item${index}`)
const height = itemNodeInfo?.height
const nodeTop = `${(index % rows.value) * (height! + top.value) + 20}px`
styleInfo(index, nodeTop, width)
}, 500)
}
function runStep() {
danmuList.value.forEach((item: any, index: number) => {
getNode(index)
})
}
const styleList: any[] = reactive([])
function styleInfo(index: number, nodeTop: string, width: number) {
const timeIndex = index - rows.value > 0 ? index - rows.value : 0
const list = styleList
const time = list[timeIndex] ? Number(list[timeIndex]['--time']) : 0
// distance.value = '-' + (speeds / 1000) * 200 + '%';
const obj = {
'top': nodeTop,
'--time': `${props.frequency * index + time}`,
'animationDuration': `${speeds}ms`,
'animationIterationCount': `${props.loop ? 'infinite' : 1}`,
'animationDelay': `${props.frequency * index + time}ms`,
'--move-distance': `-${width}px`,
}
if (slotDefault && danmuList.value[index]?.el) {
const orginalSty = danmuList.value[index].el.style
danmuList.value[index].el.style = Object.assign(orginalSty, obj)
}
else {
styleList.push(obj)
}
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-barrage`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<div>
<div :class="[`nut-barrage__slotBody${classTime}`]">
<view
v-for="(item, index) of danmuList"
:key="`danmu${index}`"
class="nut-barrage__item move"
:class="[`nut-barrage__item${index}`]"
:style="styleList[index]"
>
{{ item.length > 8 ? `${item.substr(0, 8)}...` : item }}
</view>
</div>
</div>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,60 @@
.nut-barrage {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
--move-distance: '300%';
&__item {
position: absolute;
right: 0;
display: block;
width: 100px;
padding: 3px 25px;
font-size: 12px;
text-align: center;
white-space: pre;
background: linear-gradient(to right, rgb(0 0 0 / 15%), rgb(0 0 0 / 0%));
border-radius: 50px;
transform: translateX(100%);
&.move {
animation-name: moving;
animation-play-state: running;
animation-timing-function: linear;
will-change: transform;
}
@keyframes moving {
from {
transform: translateX(100%);
}
to {
transform: translateX(var(--move-distance));
}
}
@keyframes moving {
from {
transform: translateX(100%);
}
to {
transform: translateX(var(--move-distance));
}
}
}
}
.nut-theme-dark {
.nut-barrage {
.nut-barrage__item {
color: $dark-color-gray;
}
}
}

View File

@@ -0,0 +1 @@
export type * from './barrage'

View File

@@ -0,0 +1,139 @@
import type { ButtonLang, ButtonOnAddgroupappEvent, ButtonOnAgreeprivacyauthorizationEvent, ButtonOnChooseaddressEvent, ButtonOnChooseavatarEvent, ButtonOnChooseinvoicetitleEvent, ButtonOnErrorEvent, ButtonOnGetphonenumberEvent, ButtonOnLaunchappEvent, ButtonOnLoginEvent, ButtonOnOpensettingEvent, ButtonOnSubscribeEvent, ButtonOpenType } from '@uni-helper/uni-app-types'
import type { ExtractPropTypes, PropType } from 'vue'
import { CLICK_EVENT } from '../_constants'
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
import type { ButtonFormType, ButtonShape, ButtonSize, ButtonType } from './type'
export const buttonProps = {
...commonProps,
/**
* @description 指定按钮按下去的样式类
*/
hoverClass: makeStringProp('button-hover'),
/**
* @description 按住后多久出现点击态,单位毫秒
*/
hoverStartTime: makeNumberProp(20),
/**
* @description 手指松开后点击态保留时间,单位毫秒
*/
hoverStayTime: makeNumberProp(70),
/**
* @description 按钮颜色,支持传入 `linear-gradient` 渐变色
*/
customColor: String,
/**
* @description 形状,可选值为 `square` `round`
*/
shape: makeStringProp<ButtonShape>('round'),
/**
* @description 是否为朴素按钮
*/
plain: Boolean,
/**
* @description 按钮 `loading` 状态
*/
loading: Boolean,
/**
* @description 是否禁用按钮
*/
disabled: Boolean,
/**
* @description 按钮类型,可选值为 `primary` `info` `warning` `danger` `success` `default`
*/
type: makeStringProp<ButtonType>('default'),
/**
* @description 表单类型,可选值 `button` `submit` `reset`
*/
formType: makeStringProp<ButtonFormType>('button'),
/**
* @description 尺寸,可选值为 `large` `small` `mini` `normal`
*/
size: makeStringProp<ButtonSize>('normal'),
/**
* @description 是否为块级元素
*/
block: Boolean,
/**
* @description 小程序开放能力
*/
openType: String as PropType<ButtonOpenType>,
/**
* @description 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文
*/
lang: makeStringProp<ButtonLang>('en'),
/**
* @description 会话来源openType="contact"时有效
*/
sessionFrom: String,
/**
* @description 会话内消息卡片标题openType="contact"时有效
*/
sendMessageTitle: String,
/**
* @description 会话内消息卡片点击跳转小程序路径openType="contact"时有效
*/
sendMessagePath: String,
/**
* @description 会话内消息卡片图片openType="contact"时有效
*/
sendMessageImg: String,
/**
* @description 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息openType="contact"时有效
*/
showMessageCard: Boolean,
/**
* @description 打开群资料卡时传递的群号openType="openGroupProfile"时有效
*/
groupId: String,
/**
* @description 打开频道页面时,传递的频道号 openType="openGuildProfile"时有效
*/
guildId: makeStringProp(''),
/**
* @description 打开公众号资料卡时,传递的号码 openType="openPublicProfile"时有效
*/
publicId: String,
/**
* @description 客服的抖音号,openType="im"时有效
*/
dataImId: String,
/**
* @description IM卡片类型,openType="im"时有效
*/
dataImType: String,
/**
* @description 商品的id仅支持泛知识课程库和生活服务商品库中的商品,openType="im"时有效
*/
dataGoodsId: String,
/**
* @description 订单的id仅支持交易2.0订单, openType="im"时有效
*/
dataOrderId: String,
/**
* @description 商品类型“1”代表生活服务“2”代表泛知识。openType="im"时有效
*/
dataBizLine: String,
} as const
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
export const buttonEmits = {
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
getphonenumber: (evt: ButtonOnGetphonenumberEvent) => evt instanceof Object,
getuserinfo: (evt: any) => evt instanceof Object,
error: (evt: ButtonOnErrorEvent) => evt instanceof Object,
opensetting: (evt: ButtonOnOpensettingEvent) => evt instanceof Object,
launchapp: (evt: ButtonOnLaunchappEvent) => evt instanceof Object,
contact: (evt: any) => evt instanceof Object,
chooseavatar: (evt: ButtonOnChooseavatarEvent) => evt instanceof Object,
agreeprivacyauthorization: (evt: ButtonOnAgreeprivacyauthorizationEvent) => evt instanceof Object,
addgroupapp: (evt: ButtonOnAddgroupappEvent) => evt instanceof Object,
chooseaddress: (evt: ButtonOnChooseaddressEvent) => evt instanceof Object,
chooseinvoicetitle: (evt: ButtonOnChooseinvoicetitleEvent) => evt instanceof Object,
subscribe: (evt: ButtonOnSubscribeEvent) => evt instanceof Object,
login: (evt: ButtonOnLoginEvent) => evt instanceof Object,
im: (evt: any) => evt instanceof Object,
}
export type ButtonEmits = typeof buttonEmits

View File

@@ -0,0 +1,121 @@
<script lang="ts" setup>
import type { CSSProperties } from 'vue'
import { computed, defineComponent } from 'vue'
import { CLICK_EVENT, PREFIX } from '../_constants'
import { getMainClass, getMainStyle } from '../_utils'
import Icon from '../icon/icon.vue'
import { buttonEmits, buttonProps } from './button'
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
const classes = computed(() => {
return getMainClass(props, componentName, {
[`${componentName}--${props.type}`]: !!props.type,
[`${componentName}--${props.size}`]: !!props.size,
[`${componentName}--${props.shape}`]: !!props.shape,
[`${componentName}--plain`]: props.plain,
[`${componentName}--block`]: props.block,
[`${componentName}--disabled`]: props.disabled,
[`${componentName}--loading`]: props.loading,
[`${componentName}--hovercls`]: props.hoverClass !== 'button-hover',
})
})
const styles = computed(() => {
const value: CSSProperties = {}
if (props.customColor) {
if (props.plain) {
value.color = props.customColor
value.background = '#fff'
if (!props.customColor.includes('gradient'))
value.borderColor = props.customColor
}
else {
value.color = '#fff'
value.background = props.customColor
}
}
return getMainStyle(props, value)
})
function handleClick(event: any) {
if (props.disabled || props.loading)
return
emit(CLICK_EVENT, event)
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-button`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
// #ifndef H5
styleIsolation: 'shared',
// #endif
},
})
</script>
<template>
<button
:class="classes"
:style="styles"
:form-type="props.formType === 'button' ? undefined : props.formType"
:open-type="props.disabled || props.loading ? undefined : props.openType"
:hover-class="props.hoverClass"
:hover-start-time="props.hoverStartTime"
:hover-stay-time="props.hoverStayTime"
hover-stop-propagation
:lang="props.lang"
:session-from="props.sessionFrom"
:send-message-title="props.sendMessageTitle"
:send-message-path="props.sendMessagePath"
:send-message-img="props.sendMessageImg"
:show-message-card="props.showMessageCard"
:group-id="props.groupId"
:guild-id="props.guildId"
:public-id="props.publicId"
:data-im-id="props.dataImId"
:data-im-type="props.dataImType"
:data-goods-id="props.dataGoodsId"
:data-order-id="props.dataOrderId"
:data-biz-line="props.dataBizLine"
@click="handleClick"
@getphonenumber="emit('getphonenumber', $event)"
@getuserinfo="emit('getuserinfo', $event)"
@error="emit('error', $event)"
@opensetting="emit('opensetting', $event)"
@addgroupapp="emit('addgroupapp', $event)"
@chooseaddress="emit('chooseaddress', $event)"
@chooseavatar="emit('chooseavatar', $event)"
@chooseinvoicetitle="emit('chooseinvoicetitle', $event)"
@launchapp="emit('launchapp', $event)"
@login="emit('login', $event)"
@subscribe="emit('subscribe', $event)"
@contact="emit('contact', $event)"
@agreeprivacyauthorization="emit('agreeprivacyauthorization', $event)"
@im="emit('im', $event)"
>
<view class="nut-button__wrap">
<Icon v-if="loading" name="loading" class="nut-icon-loading" />
<slot v-if="$slots.icon && !loading" name="icon" />
<view v-if="$slots.default" :class="{ 'nut-button__text': $slots.icon || loading }">
<slot />
</view>
</view>
</button>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,292 @@
.nut-theme-dark {
.nut-button {
&--default {
color: $dark-color3;
background: $dark-background2;
border: $button-border-width solid $dark-background2;
}
&--plain {
background: $dark-background2;
}
&:not(.nut-button--hovercls) {
.nut-button--plain:not([disabled]):active {
background: $dark-background2;
}
.nut-button--default:not([disabled]):active {
color: $dark-color3;
background: $dark-background2;
border: $button-border-width solid $dark-background2;
}
}
}
}
.nut-button {
position: relative;
box-sizing: border-box;
display: inline-block;
flex-shrink: 0;
width: auto;
height: $button-default-height;
padding: 0;
margin: 0;
font-size: $button-default-font-size;
line-height: $button-default-line-height;
text-align: center;
vertical-align: bottom;
appearance: none;
touch-action: manipulation;
cursor: pointer;
user-select: none;
transition: opacity 0.2s;
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
-webkit-tap-highlight-color: transparent;
.nut-button__text {
margin-left: 5px;
}
&::before {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: "";
background-color: $black;
border: inherit;
border-color: $black;
border-radius: inherit;
opacity: 0;
transform: translate(-50%, -50%);
}
&::after {
display: none;
}
&:not(.nut-button--hovercls) {
&:active::before {
opacity: 0.1;
}
}
&__wrap {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
&--loading,
&--disabled {
&::before {
display: none;
}
}
&--default {
color: $button-default-color;
background: $button-default-bg-color;
background-origin: border-box;
border: $button-border-width solid $button-default-border-color;
}
&--primary {
color: $button-primary-color;
background: $button-primary-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
&--info {
color: $button-info-color;
background: $button-info-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
&--success {
color: $button-success-color;
background: $button-success-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
&--danger {
color: $button-danger-color;
background: $button-danger-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
&--warning {
color: $button-warning-color;
background: $button-warning-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
&--plain {
background: $button-plain-background-color;
background-origin: border-box;
&.nut-button--primary {
color: $button-primary-border-color;
border-color: $button-primary-border-color;
}
&.nut-button--info {
color: $button-info-border-color;
border-color: $button-info-border-color;
}
&.nut-button--success {
color: $button-success-border-color;
border-color: $button-success-border-color;
}
&.nut-button--danger {
color: $button-danger-border-color;
border-color: $button-danger-border-color;
}
&.nut-button--warning {
color: $button-warning-border-color;
border-color: $button-warning-border-color;
}
&:not(.nut-button--hovercls) {
&.nut-button--primary:not([disabled]):active {
color: $button-primary-border-color;
border-color: $button-primary-border-color;
}
&.nut-button--info:not([disabled]):active {
color: $button-info-border-color;
border-color: $button-info-border-color;
}
&.nut-button--success:not([disabled]):active {
color: $button-success-border-color;
border-color: $button-success-border-color;
}
&.nut-button--danger:not([disabled]):active {
color: $button-danger-border-color;
border-color: $button-danger-border-color;
}
&.nut-button--warning:not([disabled]):active {
color: $button-warning-border-color;
border-color: $button-warning-border-color;
}
}
}
&--large {
width: 100%;
height: $button-large-height;
font-size: $button-large-font-size;
line-height: $button-large-line-height;
}
&--normal {
padding: $button-default-padding;
font-size: $button-default-font-size;
}
&--small {
height: $button-small-height;
padding: $button-small-padding;
font-size: $button-small-font-size;
line-height: $button-small-line-height;
&.nut-button--round {
border-radius: $button-small-round-border-radius;
}
}
&--mini {
height: $button-mini-height;
padding: $button-mini-padding;
font-size: $button-mini-font-size;
line-height: $button-mini-line-height;
}
&--block {
display: block;
width: 100%;
}
&--disabled {
cursor: not-allowed;
opacity: $button-disabled-opacity;
}
&--loading {
cursor: default;
opacity: 0.9;
}
&--round {
border-radius: $button-border-radius;
}
&--square {
border-radius: 0;
}
&:not(.nut-button--hovercls) {
.nut-button--default:not([disabled]):active {
color: $button-default-color;
background: $button-default-bg-color;
background-origin: border-box;
border: $button-border-width solid $button-default-border-color;
}
.nut-button--primary:not([disabled]):active {
color: $button-primary-color;
background: $button-primary-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
.nut-button--info:not([disabled]):active {
color: $button-info-color;
background: $button-info-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
.nut-button--success:not([disabled]):active {
color: $button-success-color;
background: $button-success-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
.nut-button--danger:not([disabled]):active {
color: $button-danger-color;
background: $button-danger-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
.nut-button--warning:not([disabled]):active {
color: $button-warning-color;
background: $button-warning-background-color;
background-origin: border-box;
border: $button-border-width solid transparent;
}
.nut-button--plain:not([disabled]):active {
background: $button-plain-background-color;
background-origin: border-box;
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './button'
export * from './type'

View File

@@ -0,0 +1,8 @@
export const buttonType = ['default', 'primary', 'info', 'success', 'warning', 'danger'] as const
export type ButtonType = (typeof buttonType)[number]
export const buttonSize = ['large', 'normal', 'small', 'mini'] as const
export type ButtonSize = (typeof buttonSize)[number]
export const buttonShape = ['square', 'round'] as const
export type ButtonShape = (typeof buttonShape)[number]
export const buttonFormType = ['button', 'submit', 'reset'] as const
export type ButtonFormType = (typeof buttonFormType)[number]

View File

@@ -0,0 +1,125 @@
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
import {
CHOOSE_EVENT,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
SELECT_EVENT,
UPDATE_VISIBLE_EVENT,
} from '../_constants'
import { commonProps, getDay, makeNumberProp, makeStringProp, truthProp } from '../_utils'
import { popupProps } from '../popup/popup'
export const calendarProps = {
...popupProps,
...commonProps,
/**
* @description 是否可见
*/
visible: Boolean,
/**
* @description 类型,日期单选 `one`,区间选择 `range`,日期多选 `multiple`,周选择 `week`
*/
type: makeStringProp<'one' | 'range' | 'multiple' | 'week'>('one'),
/**
* @description 是否弹窗状态展示
*/
poppable: truthProp,
/**
* @description 自动回填
*/
isAutoBackFill: Boolean,
/**
* @description 显示标题
*/
title: makeStringProp('日期选择'),
/**
* @description 默认值,单个日期选择为 `string`,其他为 `string[]`
*/
defaultValue: {
type: [String, Array] as PropType<string | string[]>,
},
/**
* @description 开始日期
*/
startDate: makeStringProp(getDay(0)),
/**
* @description 结束日期
*/
endDate: makeStringProp(getDay(365)),
/**
* @description 范围选择,开始信息文案
*/
startText: makeStringProp('开始'),
/**
* @description 范围选择,结束信息文案
*/
endText: makeStringProp('结束'),
/**
* @description 底部确认按钮文案
*/
confirmText: makeStringProp('确认'),
/**
* @description 是否展示今天标记
*/
showToday: truthProp,
/**
* @description 是否在展示日历标题
*/
showTitle: truthProp,
/**
* @description 是否展示日期标题
*/
showSubTitle: truthProp,
/**
* @description 是否启动滚动动画
*/
toDateAnimation: truthProp,
/**
* @description 设置周起始日
*/
firstDayOfWeek: makeNumberProp(0),
/**
* @description 一个用来判断该日期是否被禁用的函数,接受一个 `年 - 月 - 日` 作为参数。 应该返回一个 Boolean 值。
* @default undefined
*/
disabledDate: Function,
/**
* @description 是否使用 footer 插槽,如果使用,此值必须为 true
*/
footerSlot: Boolean,
/**
* @description 是否使用 btn 插槽,如果使用,此值必须为 true
*/
btnSlot: Boolean,
/**
* @description 自定义弹窗样式
*/
popStyle: {
type: [String, Object, Array] as PropType<StyleValue>,
default: '',
},
/**
* @description 遮罩显示时的背景是否锁定
*/
lockScroll: truthProp,
}
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
/* eslint-disable unused-imports/no-unused-vars */
export const calendarEmits = {
[UPDATE_VISIBLE_EVENT]: (value: boolean) => true,
[CHOOSE_EVENT]: (value: string | object) => true,
[SELECT_EVENT]: (value: any) => true,
clickCloseIcon: () => true,
clickOverlay: () => true,
[OPEN_EVENT]: () => true,
[OPENED_EVENT]: () => true,
[CLOSE_EVENT]: () => true,
[CLOSED_EVENT]: () => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type CalendarEmits = typeof calendarEmits

View File

@@ -0,0 +1,262 @@
<script lang="ts" setup>
import { computed, defineComponent, ref, useSlots } from 'vue'
import {
CHOOSE_EVENT,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
PREFIX,
SELECT_EVENT,
UPDATE_VISIBLE_EVENT,
} from '../_constants'
import { getMainClass } from '../_utils'
import NutCalendarItem from '../calendaritem/calendaritem.vue'
import type { CalendarInst } from '../calendaritem/types'
import NutPopup from '../popup/popup.vue'
import { calendarEmits, calendarProps } from './calendar'
const props = defineProps(calendarProps)
const emit = defineEmits(calendarEmits)
const slots = useSlots()
const innerVisible = computed({
get() {
return props.visible
},
set(value) {
emit('update:visible', value)
},
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
const popClasses = computed(() => {
return `${componentName}__popup ${props.popClass}`
})
const popStyles = computed(() => {
return [{
height: '85vh',
}, props.popStyle]
})
const overlayClasses = computed(() => {
return `${componentName}__overlay ${props.overlayClass}`
})
const calendarRef = ref<CalendarInst | null>(null)
function scrollToDate(date: string) {
calendarRef.value?.scrollToDate(date)
}
function initPosition() {
calendarRef.value?.initPosition()
}
function close() {
emit(UPDATE_VISIBLE_EVENT, false)
emit(CLOSE_EVENT)
}
function choose(param: string | object) {
close()
emit(CHOOSE_EVENT, param)
}
function select(param: string) {
emit(SELECT_EVENT, param)
}
function update() {
emit(UPDATE_VISIBLE_EVENT, false)
}
function handleCloseIconClick() {
emit('clickCloseIcon')
}
function handleOverlayClick() {
emit('clickOverlay')
}
function handleOpen() {
emit(OPEN_EVENT)
}
function handleOpened() {
emit(OPENED_EVENT)
if (props.defaultValue) {
if (Array.isArray(props.defaultValue)) {
if (props.defaultValue.length > 0) {
scrollToDate(props.defaultValue[0])
}
}
else {
scrollToDate(props.defaultValue)
}
}
}
function handleClose() {
emit(CLOSE_EVENT)
}
function handleClosed() {
emit(CLOSED_EVENT)
}
defineExpose({
scrollToDate,
initPosition,
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-calendar`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle">
<template v-if="props.poppable">
<NutPopup
v-model:visible="innerVisible"
:custom-class="popClasses"
:custom-style="popStyles"
:overlay-class="overlayClasses"
:overlay-style="props.overlayStyle"
position="bottom"
round
:closeable="props.closeable"
:close-icon="props.closeIcon"
:close-icon-position="props.closeIconPosition"
:z-index="props.zIndex"
:lock-scroll="props.lockScroll"
:overlay="props.overlay"
:close-on-click-overlay="props.closeOnClickOverlay"
:destroy-on-close="false"
@click-close-icon="handleCloseIconClick"
@click-overlay="handleOverlayClick"
@open="handleOpen"
@opened="handleOpened"
@close="handleClose"
@closed="handleClosed"
>
<NutCalendarItem
ref="calendarRef"
:visible="innerVisible"
:type="props.type"
:poppable="props.poppable"
:is-auto-back-fill="props.isAutoBackFill"
:title="props.title"
:default-value="props.defaultValue"
:start-date="props.startDate"
:end-date="props.endDate"
:start-text="props.startText"
:end-text="props.endText"
:confirm-text="props.confirmText"
:show-today="props.showToday"
:show-title="props.showTitle"
:show-sub-title="props.showSubTitle"
:to-date-animation="props.toDateAnimation"
:first-day-of-week="props.firstDayOfWeek"
:disabled-date="props.disabledDate"
:footer-slot="props.footerSlot"
:btn-slot="props.btnSlot"
@choose="choose"
@select="select"
@update="update"
@close="close"
>
<template v-if="slots.btn" #btn>
<slot name="btn" />
</template>
<template v-if="slots.day" #day="{ date }">
<slot name="day" :date="date" />
</template>
<template v-if="slots.topInfo" #topInfo="{ date }">
<slot name="topInfo" :date="date" />
</template>
<template v-if="slots.bottomInfo" #bottomInfo="{ date }">
<slot name="bottomInfo" :date="date" />
</template>
<template v-if="slots.footer" #footer="{ date }">
<slot name="footer" :date="date" />
</template>
</NutCalendarItem>
</NutPopup>
</template>
<template v-else>
<NutCalendarItem
ref="calendarRef"
:visible="innerVisible"
:type="props.type"
:poppable="props.poppable"
:is-auto-back-fill="props.isAutoBackFill"
:title="props.title"
:default-value="props.defaultValue"
:start-date="props.startDate"
:end-date="props.endDate"
:start-text="props.startText"
:end-text="props.endText"
:confirm-text="props.confirmText"
:show-today="props.showToday"
:show-title="props.showTitle"
:show-sub-title="props.showSubTitle"
:to-date-animation="props.toDateAnimation"
:first-day-of-week="props.firstDayOfWeek"
:disabled-date="props.disabledDate"
:footer-slot="props.footerSlot"
:btn-slot="props.btnSlot"
@choose="choose"
@select="select"
@close="close"
>
<template v-if="slots.btn" #btn>
<slot name="btn" />
</template>
<template v-if="slots.day" #day="{ date }">
<slot name="day" :date="date" />
</template>
<template v-if="slots.topInfo" #topInfo="{ date }">
<slot name="topInfo" :date="date" />
</template>
<template v-if="slots.bottomInfo" #bottomInfo="{ date }">
<slot name="bottomInfo" :date="date" />
</template>
<template v-if="slots.footer" #footer="{ date }">
<slot name="footer" :date="date" />
</template>
</NutCalendarItem>
</template>
</view>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1 @@
@import "../popup/index";

View File

@@ -0,0 +1 @@
export * from './calendar'

View File

@@ -0,0 +1,99 @@
import type { ExtractPropTypes } from 'vue'
import { CHOOSE_EVENT, SELECT_EVENT } from '../_constants'
import { commonProps, getDay, makeNumberProp, makeStringProp, truthProp } from '../_utils'
export const calendaritemProps = {
...commonProps,
/**
* @description 是否可见
*/
visible: Boolean,
/**
* @description 类型,日期单选 `one`,区间选择 `range`,日期多选 `multiple`,周选择 `week`
*/
type: makeStringProp<'one' | 'range' | 'multiple' | 'week'>('one'),
/**
* @description 是否弹窗状态展示
*/
poppable: truthProp,
/**
* @description 自动回填
*/
isAutoBackFill: Boolean,
/**
* @description 显示标题
*/
title: makeStringProp('日期选择'),
/**
* @description 默认值,单个日期选择为 `string`,其他为 `string[]`
*/
defaultValue: {
type: [String, Array],
},
/**
* @description 开始日期
*/
startDate: makeStringProp(getDay(0)),
/**
* @description 结束日期
*/
endDate: makeStringProp(getDay(365)),
/**
* @description 范围选择,开始信息文案
*/
startText: makeStringProp('开始'),
/**
* @description 范围选择,结束信息文案
*/
endText: makeStringProp('结束'),
/**
* @description 底部确认按钮文案
*/
confirmText: makeStringProp('确认'),
/**
* @description 是否展示今天标记
*/
showToday: truthProp,
/**
* @description 是否在展示日历标题
*/
showTitle: truthProp,
/**
* @description 是否展示日期标题
*/
showSubTitle: truthProp,
/**
* @description 是否启动滚动动画
*/
toDateAnimation: truthProp,
/**
* @description 设置周起始日
*/
firstDayOfWeek: makeNumberProp(0),
/**
* @description 一个用来判断该日期是否被禁用的函数,接受一个 `年 - 月 - 日` 作为参数。 应该返回一个 Boolean 值。
* @default undefined
*/
disabledDate: Function,
/**
* @description 是否使用 footer 插槽,如果使用,此值必须为 true
*/
footerSlot: Boolean,
/**
* @description 是否使用 btn 插槽,如果使用,此值必须为 true
*/
btnSlot: Boolean,
}
export type CalendarItemProps = ExtractPropTypes<typeof calendaritemProps>
/* eslint-disable unused-imports/no-unused-vars */
export const calendaritemEmits = {
[CHOOSE_EVENT]: (value: string | object) => true,
[SELECT_EVENT]: (value: any) => true,
update: () => true,
close: () => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type CalendarItemEmits = typeof calendaritemEmits

View File

@@ -0,0 +1,860 @@
<script lang="ts" setup>
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
import { computed, defineComponent, onMounted, reactive, ref, useSlots, watch } from 'vue'
import { CHOOSE_EVENT, PREFIX, SELECT_EVENT } from '../_constants'
import {
compareDate,
date2Str,
formatResultDate,
getDay,
getMainClass,
getMonthDays,
getMonthPreDay,
getMonthWeek,
getNumTwoBit,
getWeekDate,
getWhatDay,
getYearWeek,
isEqual,
isH5,
} from '../_utils'
import requestAniFrame from '../_utils/raf'
import { useTranslate } from '../../locale'
import { calendaritemEmits, calendaritemProps } from './calendaritem'
import type { CalendarDateProp, CalendarTaroState, Day, MonthInfo, StringArr } from './types'
const props = defineProps(calendaritemProps)
const emit = defineEmits(calendaritemEmits)
const slots = useSlots()
const componentName = `${PREFIX}-calendar-item`
const { translate } = useTranslate(componentName)
const state: CalendarTaroState = reactive({
yearMonthTitle: '',
defaultRange: [],
containerHeight: '100%',
currDate: '',
propStartDate: '',
propEndDate: '',
unLoadPrev: false,
touchParams: {
startY: 0,
endY: 0,
startTime: 0,
endTime: 0,
lastY: 0,
lastTime: 0,
},
transformY: 0,
translateY: 0,
scrollDistance: 0,
defaultData: [],
chooseData: [],
monthsData: [],
dayPrefix: 'nut-calendar__day',
startData: '',
endData: '',
isRange: props.type === 'range',
timer: 0,
currentIndex: 0,
avgHeight: 0,
scrollTop: 0,
monthsNum: 0,
})
const classes = computed(() => {
return getMainClass(props, componentName, {
'nut-calendar--nopop': !props.poppable,
'nut-calendar--nofooter': props.isAutoBackFill,
})
})
// 新增:自定义周起始日
const weekdays = (translate('weekdays') as any).map((day: string, index: number) => ({
day,
weekend: index === 0 || index === 6,
}))
const weeks = ref([...weekdays.slice(props.firstDayOfWeek, 7), ...weekdays.slice(0, props.firstDayOfWeek)])
const months = ref<HTMLElement | null>(null)
const scalePx = ref(2)
const viewHeight = ref(0)
const compConthsData = computed(() => {
return state.monthsData.slice(state.defaultRange[0], state.defaultRange[1])
})
const scrollWithAnimation = ref(false)
// 日期转化成数组
function splitDate(date: string) {
return date.split('-')
}
// 判断是否为开始时间
function isStart(currDate: string) {
return isEqual(state.currDate[0], currDate)
}
// 判断是否为结束时间
function isEnd(currDate: string) {
return isEqual(state.currDate[1], currDate)
}
function isMultiple(currDate: string) {
if (state.currDate?.length > 0) {
return (state.currDate as StringArr)?.some((item: string) => {
return isEqual(item, currDate)
})
}
else {
return false
}
}
// 获取当前数据
function getCurrDate(day: Day, month: MonthInfo) {
return `${month.curData[0]}-${month.curData[1]}-${getNumTwoBit(+day.day)}`
}
// 获取样式
function getClass(day: Day, month: MonthInfo, index?: number) {
const res = []
if (
typeof index === 'number'
&& ((index + 1 + props.firstDayOfWeek) % 7 === 0 || (index + props.firstDayOfWeek) % 7 === 0)
) {
res.push('weekend')
}
const currDate = getCurrDate(day, month)
const { type } = props
if (day.type === 'curr') {
if (
isEqual(state.currDate as string, currDate)
|| ((type === 'range' || type === 'week') && (isStart(currDate) || isEnd(currDate)))
|| (type === 'multiple' && isMultiple(currDate))
) {
res.push(`${state.dayPrefix}--active`)
}
else if (
(state.propStartDate && compareDate(currDate, state.propStartDate))
|| (state.propEndDate && compareDate(state.propEndDate, currDate))
|| (props.disabledDate && props.disabledDate(currDate))
) {
res.push(`${state.dayPrefix}--disabled`)
}
else if (
(type === 'range' || type === 'week')
&& Array.isArray(state.currDate)
&& Object.values(state.currDate).length === 2
&& compareDate(state.currDate[0], currDate)
&& compareDate(currDate, state.currDate[1])
) {
res.push(`${state.dayPrefix}--choose`)
}
}
else {
res.push(`${state.dayPrefix}--disabled`)
}
return res
}
// 确认选择时触发
function confirm() {
const { type } = props
if ((type === 'range' && state.chooseData.length === 2) || type !== 'range') {
let selectData: any = state.chooseData.slice(0)
if (type === 'week') {
selectData = {
weekDate: [handleWeekDate(state.chooseData[0] as string[]), handleWeekDate(state.chooseData[1] as string[])],
}
}
emit(CHOOSE_EVENT, selectData)
if (props.poppable)
emit('update')
}
}
// 选中数据
function chooseDay(day: Day, month: MonthInfo, isFirst = false) {
if (!getClass(day, month).includes(`${state.dayPrefix}--disabled`)) {
const { type } = props
const [y, m] = month.curData
const days = [...month.curData]
days[2] = getNumTwoBit(Number(day.day))
days[3] = `${days[0]}-${days[1]}-${days[2]}`
days[4] = getWhatDay(+days[0], +days[1], +days[2])
if (type === 'multiple') {
if (state.currDate?.length > 0) {
let hasIndex: number | undefined;
(state.currDate as StringArr)?.forEach((item: string, index: number) => {
if (item === days[3])
hasIndex = index
})
if (isFirst) {
state.chooseData.push([...days])
}
else {
if (hasIndex !== undefined) {
(state.currDate as StringArr).splice(hasIndex, 1)
state.chooseData.splice(hasIndex, 1)
}
else {
(state.currDate as StringArr).push(days[3])
state.chooseData.push([...days])
}
}
}
else {
state.currDate = [days[3]]
state.chooseData = [[...days]]
}
}
else if (type === 'range') {
const curDataLength = Object.values(state.currDate).length
if (curDataLength === 2 || curDataLength === 0) {
state.currDate = [days[3]]
}
else {
if (compareDate(state.currDate[0], days[3]))
Array.isArray(state.currDate) && state.currDate.push(days[3])
else
Array.isArray(state.currDate) && state.currDate.unshift(days[3])
}
if (state.chooseData.length === 2 || !state.chooseData.length) {
state.chooseData = [[...days]]
}
else {
if (compareDate(state.chooseData[0][3], days[3]))
state.chooseData = [...state.chooseData, [...days]]
else
state.chooseData = [[...days], ...state.chooseData]
}
}
else if (type === 'week') {
const weekArr = getWeekDate(y, m, day.day, props.firstDayOfWeek)
if (state.propStartDate && compareDate(weekArr[0], state.propStartDate))
weekArr.splice(0, 1, state.propStartDate)
if (state.propEndDate && compareDate(state.propEndDate, weekArr[1]))
weekArr.splice(1, 1, state.propEndDate)
state.currDate = weekArr
state.chooseData = [formatResultDate(weekArr[0]), formatResultDate(weekArr[1])]
}
else {
state.currDate = days[3]
state.chooseData = [...days]
}
if (!isFirst) {
let selectData: any = state.chooseData
if (type === 'week') {
selectData = {
weekDate: [
handleWeekDate(state.chooseData[0] as string[]),
handleWeekDate(state.chooseData[1] as string[]),
],
}
}
// 点击日期 触发
emit(SELECT_EVENT, selectData)
if (props.isAutoBackFill || !props.poppable)
confirm()
}
}
}
function handleWeekDate(weekDate: string[]) {
const [y, m, d] = weekDate
return {
date: weekDate,
monthWeekNum: getMonthWeek(y, m, d, props.firstDayOfWeek),
yearWeekNum: getYearWeek(y, m, d),
}
}
// 获取当前月数据
function getCurrData(type: string) {
const monthData = type === 'prev' ? state.monthsData[0] : state.monthsData[state.monthsData.length - 1]
let year = Number.parseInt(monthData.curData[0])
let month = Number.parseInt(monthData.curData[1].toString().replace(/^0/, ''))
switch (type) {
case 'prev':
month === 1 && (year -= 1)
month = month === 1 ? 12 : --month
break
case 'next':
month === 12 && (year += 1)
month = month === 12 ? 1 : ++month
break
}
return [`${year}`, getNumTwoBit(month), `${getMonthDays(String(year), String(month))}`]
}
// 获取日期状态
function getDaysStatus(days: number, type: string, dateInfo: CalendarDateProp) {
// 修复当某个月的1号是周日时月份下方会空出来一行
const { year, month } = dateInfo
if (type === 'prev' && days >= 7)
days -= 7
return Array.from(Array.from({ length: days }), (v, k) => {
return {
day: String(k + 1),
type,
year,
month,
}
})
}
// 获取上一个月的最后一周天数,填充当月空白
function getPreDaysStatus(days: number, type: string, dateInfo: CalendarDateProp, preCurrMonthDays: number) {
// 新增:自定义周起始日
days = days - props.firstDayOfWeek
// 修复当某个月的1号是周日时月份下方会空出来一行
const { year, month } = dateInfo
if (type === 'prev' && days >= 7)
days -= 7
const months = Array.from(Array.from({ length: preCurrMonthDays }), (v, k) => {
return {
day: String(k + 1),
type,
year,
month,
}
})
return months.slice(preCurrMonthDays - days)
}
// 获取月数据
function getMonth(curData: string[], type: string) {
// 一号为周几
const preMonthDays = getMonthPreDay(+curData[0], +curData[1])
let preMonth = Number(curData[1]) - 1
let preYear = Number(curData[0])
if (preMonth <= 0) {
preMonth = 12
preYear += 1
}
// 当月天数与上个月天数
const currMonthDays = getMonthDays(String(curData[0]), String(curData[1]))
const preCurrMonthDays = getMonthDays(`${preYear}`, `${preMonth}`)
const title = {
year: curData[0],
month: curData[1],
}
const monthInfo: MonthInfo = {
curData,
title: translate('monthTitle', title.year, title.month),
monthData: [
...(getPreDaysStatus(
preMonthDays,
'prev',
{ month: String(preMonth), year: String(preYear) },
preCurrMonthDays,
) as Day[]),
...(getDaysStatus(currMonthDays, 'curr', title) as Day[]),
],
cssHeight: 0,
cssScrollHeight: 0,
}
let titleHeight, itemHeight
if (isH5) {
titleHeight = 46 * scalePx.value + 16 * scalePx.value * 2
itemHeight = 128 * scalePx.value
}
else {
titleHeight = Math.floor(46 * scalePx.value) + Math.floor(16 * scalePx.value) * 2
itemHeight = Math.floor(128 * scalePx.value)
}
monthInfo.cssHeight = titleHeight + (monthInfo.monthData.length > 35 ? itemHeight * 6 : itemHeight * 5)
let cssScrollHeight = 0
if (state.monthsData.length > 0) {
cssScrollHeight
= (state.monthsData[state.monthsData.length - 1] as MonthInfo).cssScrollHeight
+ (state.monthsData[state.monthsData.length - 1] as MonthInfo).cssHeight
}
monthInfo.cssScrollHeight = cssScrollHeight
if (type === 'next') {
// 判断当前日期 是否大于 最后一天
if (
!state.endData
|| !compareDate(
`${state.endData[0]}-${state.endData[1]}-${getMonthDays(state.endData[0], state.endData[1])}`,
`${curData[0]}-${curData[1]}-${curData[2]}`,
)
) {
state.monthsData.push(monthInfo)
}
}
else {
// 判断当前日期 是否小于 第一天
if (
!state.startData
|| !compareDate(
`${curData[0]}-${curData[1]}-${curData[2]}`,
`${state.startData[0]}-${state.startData[1]}-01`,
)
) {
state.monthsData.unshift(monthInfo)
}
else {
state.unLoadPrev = true
}
}
}
// 初始化数据
function initData() {
// 初始化开始结束数据
const propStartDate = props.startDate ? props.startDate : getDay(0)
const propEndDate = props.endDate ? props.endDate : getDay(365)
state.propStartDate = propStartDate
state.propEndDate = propEndDate
state.startData = splitDate(propStartDate)
state.endData = splitDate(propEndDate)
// 根据是否存在默认时间,初始化当前日期,
if (props.defaultValue || (Array.isArray(props.defaultValue) && (props.defaultValue as any[]).length > 0)) {
state.currDate
= props.type !== 'one' ? ([...props.defaultValue] as StringArr) : (props.defaultValue as string | StringArr)
}
// 判断时间范围内存在多少个月
const startDate = {
year: Number(state.startData[0]),
month: Number(state.startData[1]),
}
const endDate = {
year: Number(state.endData[0]),
month: Number(state.endData[1]),
}
let monthsNum = endDate.month - startDate.month
if (endDate.year - startDate.year > 0)
monthsNum = monthsNum + 12 * (endDate.year - startDate.year)
if (monthsNum <= 0)
monthsNum = 1
// 设置月份数据
getMonth(state.startData, 'next')
let i = 1
do
getMonth(getCurrData('next'), 'next')
while (i++ < monthsNum)
state.monthsNum = monthsNum
// 日期转化为数组,限制初始日期。判断时间范围
if (props.type === 'range' && Array.isArray(state.currDate)) {
if (state.currDate.length > 0) {
if (propStartDate && compareDate(state.currDate[0], propStartDate))
state.currDate.splice(0, 1, propStartDate)
if (propEndDate && compareDate(propEndDate, state.currDate[1]))
state.currDate.splice(1, 1, propEndDate)
state.defaultData = [...splitDate(state.currDate[0]), ...splitDate(state.currDate[1])]
}
}
else if (props.type === 'multiple' && Array.isArray(state.currDate)) {
if (state.currDate.length > 0) {
const defaultArr: string[] = []
const obj: any = {}
state.currDate.forEach((item: string) => {
if (
propStartDate
&& !compareDate(item, propStartDate)
&& propEndDate
&& !compareDate(propEndDate, item)
) {
if (!Object.hasOwnProperty.call(obj, item)) {
defaultArr.push(item)
obj[item] = item
}
}
})
state.currDate = [...defaultArr]
state.defaultData = [...splitDate(defaultArr[0])]
}
}
else if (props.type === 'week' && Array.isArray(state.currDate)) {
if (state.currDate.length > 0) {
const [y, m, d] = splitDate(state.currDate[0])
state.currDate = getWeekDate(y, m, d, props.firstDayOfWeek)
if (propStartDate && compareDate(state.currDate[0], propStartDate))
state.currDate.splice(0, 1, propStartDate)
if (propEndDate && compareDate(propEndDate, state.currDate[1]))
state.currDate.splice(1, 1, propEndDate)
state.defaultData = [...splitDate(state.currDate[0]), ...splitDate(state.currDate[1])]
}
}
else {
if (state.currDate) {
if (propStartDate && compareDate(state.currDate as string, propStartDate))
state.currDate = propStartDate
else if (propEndDate && !compareDate(state.currDate as string, propEndDate))
state.currDate = propEndDate
state.defaultData = [...splitDate(state.currDate as string)]
}
}
// 设置默认可见区域
let current = 0
let lastCurrent = 0
if (state.defaultData.length > 0) {
state.monthsData.forEach((item, index) => {
if (item.title === translate('monthTitle', state.defaultData[0], state.defaultData[1]))
current = index
if (props.type === 'range' || props.type === 'week') {
if (item.title === translate('monthTitle', state.defaultData[3], state.defaultData[4]))
lastCurrent = index
}
})
}
setDefaultRange(monthsNum, current)
state.currentIndex = current
state.yearMonthTitle = state.monthsData[state.currentIndex].title
if (state.defaultData.length > 0) {
// 设置当前选中日期
if (state.isRange) {
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
chooseDay({ day: state.defaultData[5], type: 'curr' }, state.monthsData[lastCurrent], true)
}
else if (props.type === 'week') {
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
}
else if (props.type === 'multiple') {
[...state.currDate].forEach((item: string) => {
const dateArr = splitDate(item)
let current = state.currentIndex
state.monthsData.forEach((item, index) => {
if (item.title === translate('monthTitle', dateArr[0], dateArr[1]))
current = index
})
chooseDay({ day: dateArr[2], type: 'curr' }, state.monthsData[current], true)
})
}
else {
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
}
}
const lastItem = state.monthsData[state.monthsData.length - 1]
const containerHeight = lastItem.cssHeight + lastItem.cssScrollHeight
state.containerHeight = `${containerHeight}px`
state.scrollTop = Math.ceil(state.monthsData[state.currentIndex].cssScrollHeight)
state.avgHeight = Math.floor(containerHeight / (monthsNum + 1))
if (months?.value)
viewHeight.value = months.value.clientHeight
}
function scrollToDate(date: string) {
if (compareDate(date, state.propStartDate))
date = state.propStartDate
else if (!compareDate(date, state.propEndDate))
date = state.propEndDate
const dateArr = splitDate(date)
state.monthsData.forEach((item, index) => {
if (item.title === translate('monthTitle', dateArr[0], dateArr[1])) {
// scrollTop 不会实时变更。当再次赋值时scrollTop无变化时不会触发滚动
state.scrollTop += 1
scrollWithAnimation.value = props.toDateAnimation
requestAniFrame(() => {
setTimeout(() => {
state.scrollTop = state.monthsData[index].cssScrollHeight
setTimeout(() => {
scrollWithAnimation.value = false
}, 200)
}, 10)
})
}
})
}
function initPosition() {
state.scrollTop = Math.ceil(state.monthsData[state.currentIndex].cssScrollHeight)
}
// 设置当前可见月份
function setDefaultRange(monthsNum: number, current: number) {
if (monthsNum >= 3) {
if (current > 0 && current < monthsNum)
state.defaultRange = [current - 1, current + 3]
else if (current === 0)
state.defaultRange = [current, current + 4]
else if (current === monthsNum)
state.defaultRange = [current - 2, current + 2]
}
else {
state.defaultRange = [0, monthsNum + 2]
}
state.translateY = state.monthsData[state.defaultRange[0]].cssScrollHeight
}
// 区间选择&&当前月&&选中态
function isActive(day: Day, month: MonthInfo) {
return (
(props.type === 'range' || props.type === 'week')
&& day.type === 'curr'
&& getClass(day, month).includes('nut-calendar__day--active')
)
}
// 是否有开始提示
function isStartTip(day: Day, month: MonthInfo) {
return isActive(day, month) && isStart(getCurrDate(day, month))
}
// 是否有结束提示
function isEndTip(day: Day, month: MonthInfo) {
if (state.currDate.length >= 2 && isEnd(getCurrDate(day, month)))
return isActive(day, month)
return false
}
// 开始结束时间是否相等
function rangeTip() {
if (state.currDate.length >= 2)
return isEqual(state.currDate[0], state.currDate[1])
}
// 是否有 当前日期
function isCurrDay(dateInfo: Day) {
const date = `${dateInfo.year}-${dateInfo.month}-${Number(dateInfo.day) < 10 ? `0${dateInfo.day}` : dateInfo.day}`
return isEqual(date, date2Str(new Date()))
}
// 滚动处理事件
function mothsViewScroll(e: ScrollViewOnScrollEvent) {
if (state.monthsData.length <= 1)
return
const currentScrollTop = e.detail.scrollTop
let current = Math.floor(currentScrollTop / state.avgHeight)
if (current === 0) {
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight)
current += 1
}
else if (current > 0 && current < state.monthsNum - 1) {
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight)
current += 1
if (currentScrollTop < state.monthsData[current].cssScrollHeight)
current -= 1
}
if (state.currentIndex !== current) {
state.currentIndex = current
setDefaultRange(state.monthsNum, current)
}
state.yearMonthTitle = state.monthsData[current].title
}
// 重新渲染
function resetRender() {
state.chooseData.splice(0)
state.monthsData.splice(0)
initData()
}
// 监听 默认值更改
watch(() => props.defaultValue, (value) => {
if (value) {
if (props.poppable) {
resetRender()
}
}
})
onMounted(() => {
// 初始化数据
uni.getSystemInfo({
success(res) {
let scale = 2
let toFixed = 3
if (isH5) {
toFixed = 5
const fontSize = document.documentElement.style.fontSize
scale = Number((Number.parseInt(fontSize) / 40).toFixed(toFixed))
}
else {
const screenWidth = res.screenWidth
scale = Number((screenWidth / 750).toFixed(toFixed))
}
scalePx.value = scale
initData()
},
})
})
defineExpose({
scrollToDate,
initPosition,
})
</script>
<script lang="ts">
export default defineComponent({
name: `${PREFIX}-calendar-item`,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle">
<view class="nut-calendar__header">
<view v-if="props.showTitle" class="nut-calendar__header-title">
{{ props.title || translate('title') }}
</view>
<view v-if="props.btnSlot" class="nut-calendar__header-slot">
<slot name="btn" />
</view>
<view v-if="props.showSubTitle" class="nut-calendar__header-subtitle">
{{ state.yearMonthTitle }}
</view>
<view class="nut-calendar__weekdays">
<view
v-for="(item, index) of weeks"
:key="index"
class="nut-calendar__weekday"
:class="{ weekend: item.weekend }"
>
{{ item.day }}
</view>
</view>
</view>
<scroll-view
ref="months"
class="nut-calendar__content"
:scroll-y="true"
:scroll-top="state.scrollTop"
:scroll-with-animation="scrollWithAnimation"
@scroll="mothsViewScroll"
>
<view class="nut-calendar__panel" :style="{ height: state.containerHeight }">
<view class="nut-calendar__body" :style="{ transform: `translateY(${state.translateY}px)` }">
<view v-for="(month, index) of compConthsData" :key="index" class="nut-calendar__month">
<view class="nut-calendar__month-title">
{{ month.title }}
</view>
<view class="nut-calendar__days">
<view
class="nut-calendar__days-item"
:class="{ 'nut-calendar__days-item--range': props.type === 'range' }"
>
<template v-for="(day, i) of month.monthData" :key="i">
<view
class="nut-calendar__day"
:class="getClass(day, month, i)"
@click="chooseDay(day, month)"
>
<!-- 日期显示slot -->
<view class="nut-calendar__day-value">
<!-- #ifdef MP -->
{{ day.type === 'curr' ? day.day : '' }}
<!-- #endif -->
<!-- #ifndef MP -->
<slot name="day" :date="day.type === 'curr' ? day : ''">
{{ day.type === 'curr' ? day.day : '' }}
</slot>
<!-- #endif -->
</view>
<!-- #ifdef H5 -->
<view v-if="slots.topInfo" class="nut-calendar__day-tips nut-calendar__day-tips--top">
<slot name="topInfo" :date="day.type === 'curr' ? day : ''" />
</view>
<view v-if="slots.bottomInfo" class="nut-calendar__day-tips nut-calendar__day-tips--bottom">
<slot name="bottomInfo" :date="day.type === 'curr' ? day : ''" />
</view>
<!-- #endif -->
<!-- #ifndef MP -->
<view
v-if="!slots.bottomInfo && props.showToday && isCurrDay(day)"
class="nut-calendar__day-tips--curr"
>
{{ translate('today') }}
</view>
<!-- #endif -->
<!-- #ifdef MP -->
<view v-if="props.showToday && isCurrDay(day)" class="nut-calendar__day-tips--curr">
{{ translate('today') }}
</view>
<!-- #endif -->
<view
v-if="isStartTip(day, month)"
class="nut-calendar__day-tip"
:class="{ 'nut-calendar__day-tips--top': rangeTip() }"
>
{{ props.startText || translate('start') }}
</view>
<view v-if="isEndTip(day, month)" class="nut-calendar__day-tip">
{{ props.endText || translate('end') }}
</view>
</view>
</template>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view v-if="props.poppable && !props.isAutoBackFill" class="nut-calendar__footer">
<slot v-if="props.footerSlot" name="footer" :date="state.chooseData" />
<view v-else class="nut-calendar__confirm" @click="confirm">
{{ props.confirmText || translate('confirm') }}
</view>
</view>
</view>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1,270 @@
.nut-theme-dark {
.nut-calendar {
$block: &;
&-item {
color: $dark-color;
background: $dark-background;
}
&__header {
color: $dark-color;
background: $dark-background;
}
&__content {
#{$block}__panel {
#{$block}__days {
#{$block}__day {
&--disabled {
color: $dark-calendar-disabled !important;
}
}
.calendar-month-day {
&-choose {
color: $calendar-choose-font-color;
background-color: $dark-calendar-choose-color;
}
}
}
}
}
&__footer {
color: $dark-color;
background: $dark-background2;
}
}
}
.nut-calendar {
$block: &;
&-item {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
overflow: hidden;
font-size: 16px;
color: #333;
background-color: $white;
}
&#{$block}--nopop {
#{$block}__header {
#{$block}__header-title {
font-size: $calendar-base-font;
}
}
}
.popup-box {
height: 100%;
}
::-webkit-scrollbar {
display: none;
}
&__header {
display: flex;
flex-direction: column;
padding-top: 1px;
text-align: center;
background-color: $white;
&-title {
font-size: $calendar-title-font;
font-weight: $calendar-title-font-weight;
line-height: 44px;
}
&-slot {
min-height: 24px;
}
&-subtitle {
padding: 7px 0;
font-size: $calendar-sub-title-font;
line-height: 22px;
}
#{$block}__weekdays {
display: flex;
align-items: center;
justify-content: space-around;
height: 36px;
border-radius: 0 0 12px 12px;
box-shadow: 0 4px 10px 0 rgba($color: #000, $alpha: 6%);
#{$block}__weekday {
&.weekend {
color: $calendar-day67-font-color;
}
}
}
}
&__content {
display: block;
flex: 1;
width: 100%;
overflow-y: auto;
#{$block}__panel {
position: relative;
box-sizing: border-box;
display: block;
width: 100%;
height: auto;
#{$block}__body {
display: block;
}
#{$block}__month {
display: flex;
flex-direction: column;
text-align: center;
}
view:nth-of-type(2) {
#{$block}__month-title {
padding-top: 0;
}
}
.calendar-loading-tip {
position: absolute;
top: -50px;
right: 0;
left: 0;
height: 50px;
font-size: $calendar-text-font;
line-height: 50px;
color: $text-color;
text-align: center;
}
#{$block}__month-title {
height: 23px;
margin: 8px 0;
font-size: $calendar-month-title-font-size;
line-height: 23px;
}
#{$block}__days {
overflow: hidden;
#{$block}__day {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
float: left;
width: 14.28%;
height: 64px;
font-weight: $calendar-day-font-weight;
&.weekend {
color: $calendar-day67-font-color;
}
#{$block}__day-tips {
position: absolute;
width: 100%;
}
#{$block}__day-tips--curr {
position: absolute;
bottom: 6px;
width: 100%;
font-size: 12px;
line-height: 14px;
}
#{$block}__day-tip {
position: absolute;
bottom: 6px;
width: 100%;
font-size: 12px;
line-height: 14px;
color: $calendar-primary-color;
}
#{$block}__day-tips--top {
top: 6px;
}
#{$block}__day-tips--bottom {
bottom: 6px;
}
&--active {
color: $white !important;
background-color: $calendar-primary-color;
border-radius: $calendar-day-active-border-radius;
#{$block}__day-tips {
visibility: hidden;
}
#{$block}__day-tips--curr {
visibility: hidden;
}
#{$block}__day-tip {
color: $white;
}
}
&--disabled {
color: $calendar-disable-color !important;
}
&--choose {
color: $calendar-choose-font-color;
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
background-color: $calendar-choose-color;
opacity: 0.09;
}
}
#{$block}__day-value {
padding: 2px 0;
font-size: $calendar-day-font;
}
}
}
}
}
&__footer {
display: flex;
width: 100%;
height: 62px;
background-color: $white;
#{$block}__confirm {
width: 100%;
height: 44px;
margin: 10px 18px;
line-height: 44px;
color: $white;
text-align: center;
background: $button-primary-background-color;
border-radius: 22px;
}
}
}

View File

@@ -0,0 +1 @@
export * from './calendaritem'

View File

@@ -0,0 +1,65 @@
export interface TouchParam {
startY: number
endY: number
startTime: number
endTime: number
lastY: number
lastTime: number
}
export type InputDate = string | string[]
export type StringArr = string[]
export interface CalendarState {
yearMonthTitle: string
currDate: string | string[]
propStartDate: string
propEndDate: string
currentIndex: number
unLoadPrev: boolean
touchParams: TouchParam
transformY: number
translateY: number
scrollDistance: number
defaultData: InputDate
chooseData: (string | string[])[]
monthsData: MonthInfo[]
dayPrefix: string
startData: InputDate
endData: InputDate
isRange: boolean
timer: number
avgHeight: number
monthsNum: number
defaultRange: number[]
}
export interface CalendarTaroState extends CalendarState {
scrollTop: number
containerHeight: string
}
export interface CalendarDateProp {
year: string
month: string
}
export interface Day {
day: string
type: string
year?: string
month?: string
}
export interface MonthInfo {
curData: string[] | string
title: string
monthData: Day[]
cssHeight: number
cssScrollHeight: number
}
export interface CalendarInst extends HTMLElement {
scrollToDate: (date: string) => void
initPosition: () => void
}

View File

@@ -0,0 +1,47 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeStringProp, truthProp } from '../_utils'
export const cardProps = {
...commonProps,
/**
* @description 左侧图片 `Url`
*/
imgUrl: makeStringProp(''),
/**
* @description 标题
*/
title: makeStringProp(''),
/**
* @description 商品价格
*/
price: makeStringProp(''),
/**
* @description 会员价格
*/
vipPrice: makeStringProp(''),
/**
* @description 店铺介绍
*/
shopDesc: makeStringProp(''),
/**
* @description 配送方式
*/
delivery: makeStringProp(''),
/**
* @description 店铺名称
*/
shopName: makeStringProp(''),
/**
* @description 是否需要价格展示
*/
isNeedPrice: truthProp,
}
export type CardProps = ExtractPropTypes<typeof cardProps>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import { computed, defineComponent } from 'vue'
import { PREFIX } from '../_constants'
import { getMainClass } from '../_utils'
import NutPrice from '../price/price.vue'
import NutTag from '../tag/tag.vue'
import { cardProps } from './card'
const props = defineProps(cardProps)
const classes = computed(() => {
return getMainClass(props, componentName)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-card`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<div :class="classes" :style="customStyle">
<div class="nut-card__left">
<image :src="imgUrl" alt="" />
</div>
<div class="nut-card__right">
<div class="nut-card__right__title">
{{ title }}
</div>
<slot name="prolist" />
<div v-if="isNeedPrice" class="nut-card__right__price">
<slot name="price">
<NutPrice v-if="price" :price="price" />
</slot>
<slot name="origin">
<view class="nut-card__right__price__origin">
<NutPrice v-if="vipPrice" :price="vipPrice" />
</view>
</slot>
</div>
<div class="nut-card__right__other">
<slot name="shopTag">
<NutTag type="danger">
{{ shopDesc }}
</NutTag>
<NutTag plain>
{{ delivery }}
</NutTag>
</slot>
</div>
<div class="nut-card__right__shop">
<slot name="shopName">
<div class="nut-card__right__shop__name">
{{ shopName }}
</div>
</slot>
<slot name="footer" />
</div>
</div>
</div>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,112 @@
@import '../price/index';
@import '../tag/index';
.nut-theme-dark {
.nut-card {
.nut-card__right {
color: $dark-color;
}
}
}
.nut-card {
display: flex;
width: 100%;
.nut-card__left {
flex-shrink: 0;
width: 120px;
height: 120px;
background-color: $card-left-background-color;
border-radius: $card-left-border-radius;
> image {
display: block;
width: 100%;
height: 100%;
}
}
.nut-card__right {
flex: 1;
padding: 0 10px 8px;
.nut-card__right__title {
display: -webkit-box;
overflow: hidden;
font-size: 14px;
line-height: 1.5;
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.nut-card__right__price {
display: flex;
align-items: center;
height: 18px;
margin-top: 9px;
line-height: 18px;
.nut-price {
.nut-price--symbol-large {
font-size: 12px;
}
.nut-price--large {
font-size: 18px;
}
.nut-price--decimal-large {
font-size: 12px;
}
}
.nut-card__right__price__origin {
:deep(.nut-price) {
margin-left: 2px;
color: #d2a448;
.nut-price--symbol-large {
font-size: 12px;
}
.nut-price--large {
font-size: 12px;
}
.nut-price--decimal-large {
font-size: 12px;
}
}
}
}
.nut-card__right__other {
display: flex;
align-items: center;
padding: 5px 0 2px;
.nut-tag {
padding: 0 2px;
margin-right: 5px;
font-size: $card-font-size-0;
border: none;
}
}
.nut-card__right__shop {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
.nut-card__right__shop__name {
font-size: 12px;
line-height: 1.5;
color: #999;
}
}
}
}

View File

@@ -0,0 +1 @@
export * from './card'

View File

@@ -0,0 +1,106 @@
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
import {
CHANGE_EVENT,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
UPDATE_MODEL_EVENT,
UPDATE_VISIBLE_EVENT,
} from '../_constants'
import { commonProps, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
import { popupProps } from '../popup/popup'
import type { CascaderOption, CascaderValue } from './types'
export const cascaderProps = {
...popupProps,
...commonProps,
/**
* @description 选中值,双向绑定
*/
modelValue: Array,
/**
* @description 显示选择层
*/
visible: Boolean,
/**
* @description 级联数据
*/
options: makeArrayProp<any>([]),
/**
* @description 是否开启动态加载
*/
lazy: Boolean,
/**
* @description 动态加载回调,开启动态加载时生效
*/
lazyLoad: Function,
/**
* @description 自定义 `options` 结构中 `value` 的字段
*/
valueKey: makeStringProp('value'),
/**
* @description 自定义 `options` 结构中 `text` 的字段
*/
textKey: makeStringProp('text'),
/**
* @description 自定义 `options` 结构中 `children` 的字段
*/
childrenKey: makeStringProp('children'),
/**
* @description 当 `options` 为可转换为树形结构的扁平结构时,配置转换规则
*/
convertConfig: Object,
/**
* @description 是否需要弹层展示(设置为 `false` 后,`title` 失效)
*/
poppable: truthProp,
/**
* @description 标题
*/
title: String,
/**
* @description 选中底部展示样式 可选值: 'line', 'smile'
*/
titleType: makeStringProp<'line' | 'card' | 'smile'>('line'),
/**
* @description 标签栏字体尺寸大小 可选值: 'large', 'normal', 'small'
*/
titleSize: makeStringProp<'large' | 'normal' | 'small'>('normal'),
/**
* @description 标签间隙
*/
titleGutter: makeNumericProp(0),
/**
* @description 是否省略过长的标题文字
*/
titleEllipsis: truthProp,
/**
* @description 自定义弹窗样式
*/
popStyle: {
type: [String, Object, Array] as PropType<StyleValue>,
default: '',
},
/**
* @description 遮罩显示时的背景是否锁定
*/
lockScroll: truthProp,
}
export type CascaderProps = ExtractPropTypes<typeof cascaderProps>
/* eslint-disable unused-imports/no-unused-vars */
export const cascaderEmits = {
[UPDATE_MODEL_EVENT]: (value: CascaderValue) => true,
[UPDATE_VISIBLE_EVENT]: (value: boolean) => true,
[CHANGE_EVENT]: (value: CascaderValue, nodes: CascaderOption[]) => true,
pathChange: (nodes: CascaderOption[]) => true,
[OPEN_EVENT]: () => true,
[OPENED_EVENT]: () => true,
[CLOSE_EVENT]: () => true,
[CLOSED_EVENT]: () => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type CascaderEmits = typeof cascaderEmits

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import { computed, defineComponent, ref, useSlots, watch } from 'vue'
import {
CHANGE_EVENT,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
PREFIX,
UPDATE_MODEL_EVENT,
UPDATE_VISIBLE_EVENT,
} from '../_constants'
import { getMainClass } from '../_utils'
import NutCascaderItem from '../cascaderitem/cascaderitem.vue'
import NutPopup from '../popup/popup.vue'
import { cascaderEmits, cascaderProps } from './cascader'
import type { CascaderOption, CascaderValue } from './types'
const props = defineProps(cascaderProps)
const emit = defineEmits(cascaderEmits)
const slots = useSlots()
const innerValue = ref(props.modelValue as CascaderValue)
const innerVisible = computed({
get() {
return props.visible
},
set(value) {
emit(UPDATE_VISIBLE_EVENT, value)
},
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
const popClasses = computed(() => {
return `${componentName}__popup ${props.popClass}`
})
const overlayClasses = computed(() => {
return `${componentName}__overlay ${props.overlayClass}`
})
function handleChange(value: CascaderValue, pathNodes: CascaderOption[]) {
innerValue.value = value
innerVisible.value = false
emit(UPDATE_MODEL_EVENT, value)
emit(CHANGE_EVENT, value, pathNodes)
}
function handlePathChange(pathNodes: CascaderOption[]) {
emit('pathChange', pathNodes)
}
function handleOpen() {
emit(OPEN_EVENT)
}
function handleOpened() {
emit(OPENED_EVENT)
}
function handleClose() {
emit(CLOSE_EVENT)
}
function handleClosed() {
emit(CLOSED_EVENT)
}
watch(() => props.modelValue, (value) => {
if (value !== innerValue.value) {
innerValue.value = value as CascaderValue
}
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-cascader`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle">
<template v-if="props.poppable">
<NutPopup
v-model:visible="innerVisible"
:custom-class="popClasses"
:custom-style="props.popStyle"
:overlay-class="overlayClasses"
:overlay-style="props.overlayStyle"
position="bottom"
round
:closeable="props.closeable"
:close-icon="props.closeIcon"
:close-icon-position="props.closeIconPosition"
:z-index="props.zIndex"
:lock-scroll="props.lockScroll"
:overlay="props.overlay"
:close-on-click-overlay="props.closeOnClickOverlay"
:destroy-on-close="false"
@open="handleOpen"
@opened="handleOpened"
@close="handleClose"
@closed="handleClosed"
>
<slot v-if="slots.title" name="title" />
<template v-else>
<rich-text v-if="props.title" class="nut-cascader__bar" :nodes="props.title" />
</template>
<NutCascaderItem
:model-value="innerValue"
:visible="innerVisible"
:options="props.options"
:lazy="props.lazy"
:lazy-load="props.lazyLoad"
:value-key="props.valueKey"
:text-key="props.textKey"
:children-key="props.childrenKey"
:convert-config="props.convertConfig"
:title-type="props.titleType"
:title-size="props.titleSize"
:title-gutter="props.titleGutter"
:title-ellipsis="props.titleEllipsis"
@change="handleChange"
@path-change="handlePathChange"
/>
</NutPopup>
</template>
<template v-else>
<NutCascaderItem
:model-value="innerValue"
:visible="innerVisible"
:options="props.options"
:lazy="props.lazy"
:lazy-load="props.lazyLoad"
:value-key="props.valueKey"
:text-key="props.textKey"
:children-key="props.childrenKey"
:convert-config="props.convertConfig"
:title-type="props.titleType"
:title-size="props.titleSize"
:title-gutter="props.titleGutter"
:title-ellipsis="props.titleEllipsis"
@change="handleChange"
@path-change="handlePathChange"
/>
</template>
</view>
</template>
<style lang="scss">
@import "./index";
@import "../popup/index";
</style>

View File

@@ -0,0 +1,80 @@
import type { CascaderConfig, CascaderOption, ConvertConfig } from './types'
export function formatTree(tree: CascaderOption[], parent: CascaderOption | null, config: CascaderConfig): CascaderOption[] {
return tree.map((node: CascaderOption) => {
const { value: valueKey = 'value', text: textKey = 'text', children: childrenKey = 'children' } = config
const { [valueKey]: value, [textKey]: text, [childrenKey]: children, ...others } = node
const newNode: CascaderOption = {
loading: false,
...others,
level: parent ? ((parent && parent.level) || 0) + 1 : 0,
value,
text,
children,
_parent: parent,
}
if (newNode.children && newNode.children.length)
newNode.children = formatTree(newNode.children, newNode, config)
return newNode
})
}
export function eachTree(tree: CascaderOption[], cb: (node: CascaderOption) => any): void {
let i = 0
let node: CascaderOption
/* eslint-disable no-cond-assign */
while ((node = tree[i++])) {
if (cb(node) === true)
break
if (node.children && node.children.length)
eachTree(node.children, cb)
}
}
const defaultConvertConfig = {
topId: null,
idKey: 'id',
pidKey: 'pid',
sortKey: '',
}
export function convertListToOptions(list: CascaderOption[], options: ConvertConfig): CascaderOption[] {
const mergedOptions = {
...defaultConvertConfig,
...(options || {}),
}
const { topId, idKey, pidKey, sortKey } = mergedOptions
let result: CascaderOption[] = []
let map: any = {}
list.forEach((node: CascaderOption) => {
node = { ...node }
const { [idKey]: id, [pidKey]: pid } = node
const children = (map[pid] = map[pid] || [])
if (!result.length && pid === topId)
result = children
children.push(node)
node.children = map[id] || (map[id] = [])
})
if (sortKey) {
Object.keys(map).forEach((i) => {
if (map[i].length > 1)
map[i].sort((a: CascaderOption, b: CascaderOption) => a[sortKey] - b[sortKey])
})
}
map = null
return result
}

View File

@@ -0,0 +1,110 @@
.nut-theme-dark {
.nut-cascader {
.nut-tabs__titles {
background: $dark-background3 !important;
}
&__bar {
color: $dark-color;
background: $dark-background2;
}
&-item {
&__inner {
color: $dark-color-gray;
}
}
}
}
.nut-cascader {
.nut-tab-pane {
padding: 0;
}
.nut-tabs__titles {
padding: $cascader-tabs-item-padding;
background: #fff;
}
&-item {
width: 100%;
font-size: $cascader-font-size;
line-height: $cascader-line-height;
$block: &;
&.nut-tabs {
&.horizontal {
.nut-tabs__titles {
.nut-tabs__titles-item {
flex: initial;
padding: $cascader-tabs-item-padding;
white-space: nowrap;
}
}
}
}
&__inner {
display: flex;
align-items: center;
padding: $cascader-item-padding;
margin: 0;
font-size: $cascader-item-font-size;
color: $cascader-item-color;
cursor: pointer;
}
&__title {
flex: 1;
}
&__icon-check {
margin-left: 10px;
visibility: hidden;
}
&__icon-loading {
margin-left: 10px;
}
&.active {
&:not(.disabled) {
color: $cascader-item-active-color;
}
#{$block}__icon-check {
color: $cascader-item-active-color;
visibility: visible;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
&__bar {
display: flex;
align-items: center;
justify-content: center;
padding: $cascader-bar-padding;
font-size: $cascader-bar-font-size;
font-weight: bold;
line-height: $cascader-bar-line-height;
color: $cascader-bar-color;
text-align: center;
}
&-pane {
display: block;
width: 100%;
height: 342px;
padding: 10px 0 0;
margin: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}

View File

@@ -0,0 +1 @@
export * from './cascader'

View File

@@ -0,0 +1,73 @@
import { eachTree, formatTree } from './helper'
import type { CascaderConfig, CascaderOption, CascaderValue } from './types'
class Tree {
nodes: CascaderOption[]
readonly config: CascaderConfig
constructor(nodes: CascaderOption[], config?: CascaderConfig) {
this.config = {
value: 'value',
text: 'text',
children: 'children',
...(config || {}),
}
this.nodes = formatTree(nodes, null, this.config)
}
updateChildren(nodes: CascaderOption[], parent: CascaderOption | null): void {
if (!parent)
this.nodes = formatTree(nodes, null, this.config)
else
parent.children = formatTree(nodes, parent, this.config)
}
// for test
getNodeByValue(value: CascaderOption['value']): CascaderOption | void {
let foundNode
eachTree(this.nodes, (node: CascaderOption) => {
if (node.value === value) {
foundNode = node
return true
}
})
return foundNode
}
getPathNodesByValue(value: CascaderValue): CascaderOption[] {
if (!value.length)
return []
const pathNodes = []
let currentNodes: CascaderOption[] | void = this.nodes
while (currentNodes && currentNodes.length) {
const foundNode: CascaderOption | void = currentNodes.find(node => node.value === value[node.level as number])
if (!foundNode)
break
pathNodes.push(foundNode)
currentNodes = foundNode.children
}
return pathNodes
}
isLeaf(node: CascaderOption, lazy: boolean): boolean {
const { leaf, children } = node
const hasChildren = Array.isArray(children) && Boolean(children.length)
return leaf == null ? !hasChildren && !lazy : leaf
}
hasChildren(node: CascaderOption, lazy: boolean): boolean {
if (lazy)
return Array.isArray(node.children) && Boolean(node.children.length)
return !this.isLeaf(node, lazy)
}
}
export default Tree

View File

@@ -0,0 +1,37 @@
export interface CascaderPane {
nodes: CascaderOption[]
selectedNode: CascaderOption | null
}
export interface CascaderConfig {
value?: string
text?: string
children?: string
}
export interface CascaderTabs {
title: string
paneKey: string
disabled: boolean
}
export interface CascaderOption {
text?: string
value?: number | string
disabled?: boolean
children?: CascaderOption[]
leaf?: boolean
level?: number
loading?: boolean
[key: PropertyKey]: any
}
export type CascaderValue = CascaderOption['value'][]
export interface ConvertConfig {
topId?: string | number | null
idKey?: string
pidKey?: string
sortKey?: string
}

View File

@@ -0,0 +1,72 @@
import type { ExtractPropTypes } from 'vue'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { commonProps, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
import type { CascaderOption, CascaderValue } from '../cascader/types'
export const cascaderitemProps = {
...commonProps,
/**
* @description 选中值,双向绑定
*/
modelValue: Array,
/**
* @description 显示选择层
*/
visible: Boolean,
/**
* @description 级联数据
*/
options: makeArrayProp<any>([]),
/**
* @description 是否开启动态加载
*/
lazy: Boolean,
/**
* @description 动态加载回调,开启动态加载时生效
*/
lazyLoad: Function,
/**
* @description 自定义 `options` 结构中 `value` 的字段
*/
valueKey: makeStringProp('value'),
/**
* @description 自定义 `options` 结构中 `text` 的字段
*/
textKey: makeStringProp('text'),
/**
* @description 自定义 `options` 结构中 `children` 的字段
*/
childrenKey: makeStringProp('children'),
/**
* @description 当 `options` 为可转换为树形结构的扁平结构时,配置转换规则
*/
convertConfig: Object,
/**
* @description 选中底部展示样式 可选值: 'line', 'smile'
*/
titleType: makeStringProp<'line' | 'card' | 'smile'>('line'),
/**
* @description 标签栏字体尺寸大小 可选值: 'large', 'normal', 'small'
*/
titleSize: makeStringProp<'large' | 'normal' | 'small'>('normal'),
/**
* @description 标签间隙
*/
titleGutter: makeNumericProp(0),
/**
* @description 是否省略过长的标题文字
*/
titleEllipsis: truthProp,
}
export type CascaderItemProps = ExtractPropTypes<typeof cascaderitemProps>
/* eslint-disable unused-imports/no-unused-vars */
export const cascaderitemEmits = {
[UPDATE_MODEL_EVENT]: (value: CascaderValue) => true,
[CHANGE_EVENT]: (value: CascaderValue, nodes: CascaderOption[]) => true,
pathChange: (value: CascaderOption[]) => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type CascaderItemEmits = typeof cascaderitemEmits

View File

@@ -0,0 +1,339 @@
<script lang="ts" setup>
import { computed, defineComponent, ref, watch } from 'vue'
import { CHANGE_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass } from '../_utils'
import { useTranslate } from '../../locale'
import { convertListToOptions } from '../cascader/helper'
import Tree from '../cascader/tree'
import type { CascaderOption, CascaderPane, CascaderTabs, CascaderValue, ConvertConfig } from '../cascader/types'
import NutIcon from '../icon/icon.vue'
import NutTabPane from '../tabpane/tabpane.vue'
import NutTabs from '../tabs/tabs.vue'
import { cascaderitemEmits, cascaderitemProps } from './cascaderitem'
const props = defineProps(cascaderitemProps)
const emit = defineEmits(cascaderitemEmits)
const classes = computed(() => {
return getMainClass(props, componentName)
})
const configs = computed(() => ({
lazy: props.lazy,
lazyLoad: props.lazyLoad,
valueKey: props.valueKey,
textKey: props.textKey,
childrenKey: props.childrenKey,
convertConfig: props.convertConfig,
}))
const tabsCursor = ref(0)
const initLoading = ref(false)
const innerValue = ref(props.modelValue as CascaderValue)
const tree = ref(new Tree([], {}))
const panes = ref<CascaderPane[]>([])
const isLazy = computed(() => configs.value.lazy && Boolean(configs.value.lazyLoad))
const lazyLoadMap = new Map()
let currentProcessNode: CascaderOption | null
async function init() {
lazyLoadMap.clear()
panes.value = []
tabsCursor.value = 0
initLoading.value = false
currentProcessNode = null
let { options } = props
if (configs.value.convertConfig) {
options = convertListToOptions(options as CascaderOption[], configs.value.convertConfig as ConvertConfig)
}
tree.value = new Tree(options as CascaderOption[], {
value: configs.value.valueKey,
text: configs.value.textKey,
children: configs.value.childrenKey,
})
if (isLazy.value && !tree.value.nodes.length) {
await invokeLazyLoad({
root: true,
loading: true,
text: '',
value: '',
})
}
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
syncValue()
}
const methods = {
// 选中一个节点,静默模式不触发事件
async handleNode(node: CascaderOption, silent?: boolean) {
const { disabled, loading } = node
if ((!silent && disabled) || !panes.value[tabsCursor.value])
return
if (tree.value.isLeaf(node, isLazy.value)) {
node.leaf = true
panes.value[tabsCursor.value].selectedNode = node
panes.value = panes.value.slice(0, (node.level as number) + 1)
if (!silent) {
const pathNodes = panes.value.map(pane => pane.selectedNode)
emitChange(pathNodes as CascaderOption[])
emit('pathChange', pathNodes as CascaderOption[])
}
return
}
if (tree.value.hasChildren(node, isLazy.value)) {
const level = (node.level as number) + 1
panes.value[tabsCursor.value].selectedNode = node
panes.value = panes.value.slice(0, level)
panes.value.push({
nodes: node.children || [],
selectedNode: null,
})
tabsCursor.value = level
if (!silent) {
const pathNodes = panes.value.map(pane => pane.selectedNode)
emit('pathChange', pathNodes as CascaderOption[])
}
return
}
currentProcessNode = node
if (loading)
return
await invokeLazyLoad(node)
if (currentProcessNode === node) {
panes.value[tabsCursor.value].selectedNode = node
methods.handleNode(node, silent)
}
},
handleTabClick(tab: CascaderTabs) {
currentProcessNode = null
tabsCursor.value = Number(tab.paneKey)
},
isSelected(pane: CascaderPane, node: CascaderOption) {
return pane?.selectedNode?.value === node.value
},
}
async function syncValue() {
const currentValue = innerValue.value
if (currentValue === undefined || !tree.value.nodes.length) {
return
}
if (currentValue.length === 0) {
tabsCursor.value = 0
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
return
}
let needToSync = currentValue
if (isLazy.value && Array.isArray(currentValue) && currentValue.length) {
needToSync = []
const parent = tree.value.nodes.find(node => node.value === currentValue[0])
if (parent) {
needToSync = [parent.value]
initLoading.value = true
const last = await currentValue.slice(1).reduce(async (p: Promise<CascaderOption | void>, value) => {
const parent = await p
await invokeLazyLoad(parent)
const node = parent?.children?.find(item => item.value === value)
if (node)
needToSync.push(value)
return Promise.resolve(node)
}, Promise.resolve(parent))
await invokeLazyLoad(last)
initLoading.value = false
}
}
if (needToSync.length && currentValue === props.modelValue) {
const pathNodes = tree.value.getPathNodesByValue(needToSync)
pathNodes.forEach((node, index) => {
tabsCursor.value = index
methods.handleNode(node, true)
})
}
}
async function invokeLazyLoad(node?: CascaderOption | void) {
if (!node)
return
if (!configs.value.lazyLoad) {
node.leaf = true
return
}
if (tree.value.isLeaf(node, isLazy.value) || tree.value.hasChildren(node, isLazy.value))
return
node.loading = true
const parent = node.root ? null : node
let lazyLoadPromise = lazyLoadMap.get(node)
if (!lazyLoadPromise) {
lazyLoadPromise = new Promise((resolve) => {
// 外部必须resolve
configs.value.lazyLoad?.(node, resolve)
})
lazyLoadMap.set(node, lazyLoadPromise)
}
const nodes: CascaderOption[] | void = await lazyLoadPromise
if (Array.isArray(nodes) && nodes.length > 0) {
tree.value.updateChildren(nodes, parent)
}
else {
// 如果加载完成后没有提供子节点,作为叶子节点处理
node.leaf = true
}
node.loading = false
lazyLoadMap.delete(node)
}
function emitChange(pathNodes: CascaderOption[]) {
const emitValue = pathNodes.map(node => node.value)
innerValue.value = emitValue
emit(UPDATE_MODEL_EVENT, emitValue)
emit(CHANGE_EVENT, emitValue, pathNodes)
}
function formatTabTitle(pane: CascaderPane) {
return pane.selectedNode ? pane.selectedNode.text : translate('select')
}
watch(() => [configs.value, props.options], () => {
init()
}, {
deep: true,
immediate: true,
})
watch(() => props.modelValue, (value) => {
if (value !== innerValue.value) {
innerValue.value = value as CascaderValue
syncValue()
}
})
watch(() => props.visible, (value) => {
// TODO: value为空时保留上次选择记录修复单元测试问题
if (value && Array.isArray(innerValue.value) && innerValue.value.length > 0) {
syncValue()
}
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-cascader-item`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutTabs
v-model="tabsCursor"
:custom-class="classes"
:custom-style="props.customStyle"
:type="props.titleType"
:size="props.titleSize"
:title-gutter="props.titleGutter"
:ellipsis="props.titleEllipsis"
title-scroll
@click="methods.handleTabClick"
>
<template v-if="!initLoading && panes.length">
<NutTabPane v-for="(pane, index) in panes" :key="index" :title="formatTabTitle(pane)">
<view class="nut-cascader-pane" role="menu">
<scroll-view style="height: 100%" :scroll-y="true">
<template v-for="node in pane.nodes" :key="node.value">
<view
class="nut-cascader-item__inner"
:class="{ active: methods.isSelected(pane, node), disabled: node.disabled }"
role="menuitemradio"
:aria-checked="methods.isSelected(pane, node)"
:aria-disabled="node.disabled || undefined"
@click="methods.handleNode(node, false)"
>
<view class="nut-cascader-item__title">
{{ node.text }}
</view>
<NutIcon
v-if="node.loading"
custom-class="nut-cascader-item__icon-loading"
loading
name="loading"
/>
<NutIcon
v-else
custom-class="nut-cascader-item__icon-check"
name="checklist"
/>
</view>
</template>
</scroll-view>
</view>
</NutTabPane>
</template>
<template v-else>
<NutTabPane title="Loading...">
<view class="nut-cascader-pane" />
</NutTabPane>
</template>
</NutTabs>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1,3 @@
@import "../cascader/index";
@import "../tabs/index";
@import "../tabpane/index";

Some files were not shown because too many files have changed in this diff Show More