This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+933
View File
@@ -0,0 +1,933 @@
## 1.1.182025-09-25
* [版本] 编译到app/web/h5请可以使用4.76+(鸿蒙还有部分组件有异常),微信小程序请务必使用4.81+
* [重要] 整体组件细节样式调整,颜色计算逻辑调整,使得整体更有精致感(请使用细腻的屏幕重新感受对比)。主题要更偏年轻化。,css库中添加了对应的状态颜色如text-error,保持与主题匹配
* [优化] 大部分示例demo页面重构为setup版本。优化文档显示。
* [重要] x-echart,重构为setup,同时废除属性opts,请使用ref方法setOptions来设置数据,经过测试4.76+上通过组件属性设置过多数据的参数会导致失效,我也莫名其妙。
* 同时图表已经升级到最新百度图表6.0.0版本,经过我观察样式和主题默认颜色有些许变化。升级时微信请重新复制demo页面中的echart.esm.min.js(原先是echart.min.js),非微信请复制hybrid到你目录中
* [重要] xtext修改单词highlight,因此你的highlighthighlightReghighlightStyle都要修正为这三个,请一定注意修改。
* [新增插件] x-chinesetopinyin-s可以将中文转换为拼音(含声调注音及不带注音的两个属性)(全平台支持)
* [新增插件] x-texttovoice-s 文本转语音播报功能(小程序不支持,其它平台全支持)
* [重要优化] 画布ICanvas API库已经对鸿蒙兼容适配了,增加了画布30篇文档使用及介绍,同时增加了示例案例:语音录制动画,海报绘制,自动图形排版,新增图形二维码。目前优化已经基本可以适配商业场景:如海报分享图,动画绘制等业务场景。
* [重大优化] x-table,现在支持子元素列之间的合并(它是向右合并吞并下一列数据实现,因此它的下列会自动隐藏不显示),
* 同时单元格高不再固定高改为最小高如果出现大量数据会自动断行(其它列自动上下撑开对齐本行高),同时添加了行多选checkbox功能
* [重要优化] x-tabbar优化了canvas绘制时的逻辑
* [优化] x-tree组件现在允许最后一级(非异步且没有子节点的场景时)点击选中切换状态。
* [优化] 全局config中增加了:fontColorfontDarkColor分别对应默认的文本色及默认的暗黑文本色。
* [优化] xfinger,对外暴露更多slot参数。
* [优化] xDrawer重构为setup版本,同时修复因为版本差异的web端嵌套的问题。
* [优化] xModal重构为setup版本
* [优化] xOverlay重构为setup版本
* [优化] xText重构为setup版本
* [优化] xRadio重构为setup版本,相关值类型改成为:string|boolean|number
* [优化] xRadioGroup重构为setup版本,相关值类型改成为:string|boolean|number
* [优化] xCheckbox重构为setup版本,相关值类型改成为:string|boolean|number
* [优化] xCheckboxGroup重构为setup版本,相关值类型改成为:string|boolean|number[]
* [优化] x-action-menu重构为setup版本,并且允许在不同平台嵌套使用
* [优化] x-action-modal重构为setup版本,并且允许在不同平台嵌套使用,修复多语言时默认为中文的问题
* [优化] x-alert重构为setup版本
* [优化] x-sheet重构为setup版本
* [优化] x-float-drawer重构为setup版本
* [优化] x-tag重构为setup版本
* [优化] x-tree-flat思维导图重构为setup版本,并在opts配置项中增加属性layout ?: 'horizontal' | 'vertical',用于左右,和上下排列(之前只有左右)
* [优化] x-text-cloud重构为setup版本(并已兼容鸿蒙Next),并设置新的散射样式。
* [优化] x-button重构为setup版本,loading icon大小同步图片大小一致
* [优化重构] x-float-drawer浮动面板优化重构,由于sdk版本导致在ios和微信端可能无法流畅滚动,因此重构了,已经在5个平台中测试较为流畅。
* [优化+修复] x-weekbar,优化体验问题,当设置了seekDay时,如果当前日期默认不在偏移范围内,可能会导致当前的日期不可可视范围内。
* 同时添加了同日历组件一样的状态属性,类型格式都是一模一样的。方便给日期底部添加对应状态提示。然后修复了拼写错误造成不能赋值,优化允许动态修改start,end
* [优化] x-search重构为setup版本,并新增属性showCancel
* [优化] x-table重构为setup,同时兼容一个sdk bug,即动态将list-item设置显示和隐藏会触sdk bug,大家要自己避免这个list-item的bug(后续可能官方 什么 修复).
* [优化] x-cell重构为setup版本
* [优化] x-code-input重构为setup版本
* [优化] x-countdown重构为setup版本
* [优化] x-empty重构为setup版本
* [优化] x-float-button重构为setup版本
* [优化] x-grid重构为setup版本
* [优化] x-grid-item重构为setup版本
* [优化] x-icon重构为setup版本
* [优化] x-radio-button重构为setup版本
* [优化] x-rate重构为setup版本
* [优化] x-rolling-number重构为setup版本
* [优化] x-badge重构为setup版本
* [优化] x-virtual-list,兼容一个ios sdk的一个bug,即ios在设置滚动为子协商时会无法滚动(仅ios有此sdk bug,后续可能官方 什么 修复)
* [优化] x-slider-children重构为setup版本
* [优化] x-slider-tree重构为setup版本
* [优化] x-msg-notice重构为setup版本,同时增加了上和底拖拉时的反向操作阻尼反馈,让体验更完美。更新了示例效果。同时增加了对pc的鼠标手势支持。
* [优化] x-notice重构为setup版本
* [优化] x-snackbar重构为setup版本
* [优化] x-more重构为setup版本
* [优化] x-loading重构为setup版本
* [优化] x-input-tag重构为setup版本
* [优化] x-image-group重构为setup版本
* [优化] x-code-input重构为setup版本
* [优化] x-card重构为setup版本
* [优化] x-keyboardx-keyboard-carx-keyboard-idcardx-keyboard-number重构为setup版本
* [优化] x-snackbar-item重构为setup版本,优化了动效。
* [优化] x-backtop重构为setup版本,同时增加了disabledPageScroll,scrollTop属性,以实现局部返回置顶功能,具体见demo实现案例。
* 【修复】日历相关组件在同时设置start和end会产生边界判断问题。
* 【修复】x-between-time未对外使用format格式同步数据。同时添加默认插件数据startValendVal,show
* 【优化】x-barcode对原生的条形码改为code-128标准条码见:[标准128](https://barcode.tec-it.com/en/Code128?data=ABC-abc-1234)
* [优化] x-qrcoder添加新的属性wifi字段,用于生成自动连接的wifiQr码,详见demo示例页面展示。
## 1.1.172025-09-03
* 【声明】本次更新中在1.1.16版本中提到的鸿蒙Next相关插件和组件,在本次版本中并未更新兼容。请关注后续版本。
* [适配] 本次版本适配了xSqliteS数据库插件,从1.1.17版本开始可以用于鸿蒙原生应用了。
* [优化] xDrawer动画放弃了官方的动画结束@transitionend事件,改用setTimeout,官方 的这个接口在不同机型和设备上有不同的差异,慎用。
* x-darg-item 写错了一个语法。会在鸿蒙编译的时候无法编译通过。并在插槽上添加数据:order,用于需要显示自身索引位置的场景。
* 优化demo页面展示,同步组件的使用方式。
* [优化] x-form新增支持嵌套字段校验如{a:{b:""}},需要form-item上属性filed="a/b",关于'.'连接符安卓sdk会在4.86+上支持
* [优化] x-tabbar,增加事件doubleClick。[issue](https://git.tmui.design//tmui/tmui4xIssue/issues/11)
* [修复] x-buttonloading图标没有跟随字体颜色。[issue](https://git.tmui.design//tmui/tmui4xIssue/issues/7)
* [修复] x-tabs,中激活动态字体大小与普通字体大小不一致时导致的定位偏移。见[issue](https://git.tmui.design//tmui/tmui4xIssue/issues/3)
* [修复] x-slider-menu暗黑切换项目背景在ios端有兼容性。见[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/12)
* [修复] x-drawer多语言设置时变量出错了。见[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/16)
* [优化] xDate新增函数getBetweenDate,用于取start,end之间的比较,如果在范围内返回本身,如果在范围外取start或者end,返回的值始终在起始之间。
* [修复] xBetweenTime ,限制start,end后,如果点快捷选择时会超出限制的时间区间。[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/18)
* [修复] xWeekbar在提供了start和end后未限制日期[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/20)
* [增强] xWeekbar新增属性seekDay,可以控制一周的第一天是任意一周的某一天(0-6)
* [修复+优化] 修复xIndexbar索引组件,使用flex:1时无法自动填充父容器(导致无法全屏或者局部),为组件增了属性:customSliderBar用于自定右侧菜单,新增插槽:top用于自定义顶部自由布局,当右边滑动时会自定定位到top插槽位置类似微信通讯录。
同时重新编写了demo索引页面,仿微信通讯录示例。
* [修复] x-money,值为0时的千分位问题[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/21)
* [优化] xGrid组件新增roundshowBorder等相关属性,以显示边线和圆角相关,子组件相应增加order,同时两组件重写为setup组合模式了。demo示例页面增加了更多的示例
* [增强] 相关日历组件增加了属性seekDay用于设定月面板中周的第一天是0-6(周一至周日)中的哪天为开始。比如设定6,表示以周日为起始第一天排序。
* [增强] 二维码,新增了一个属性pdRounded,用于控制是否让三个定位变成个性化圆形,同时mode增加了可选模式:vertical竖形胶囊和星星,看起来可爱一点。
* [增强] 日历组件增加了dateStatus设定日期状态指示,并更新了示例页面为组合式代码。
* [修复] betweentTime组件因多语言设置忽略了用户的format格式的输入导致输入框没有按用户的格式显示。[issue](https://git.tmui.design/tmui/tmui4xIssue/issues/23)
* [优化文档] 对技术文档重新梳理,并启用了新文档页面。
## 1.1.162025-8-17
* 已有项目请谨慎更新!!!,因兼容鸿蒙部分组件可能更改了结构。
* 【重要】请务必使用4.75sdk+,本次更新鸿蒙兼容,并不代表完全兼容,以下有部分组件还要继续完善,还有一些插件未提及可能也无法使用,但大部分是可以使用的。其它问题在后续中继续完善。
* 适配原生鸿蒙Next(5.1.1/5.0.3+ api 13+),由于鸿蒙的插件写成了har包,因此对应的Next平台的插件部分源码将不开放,其它平台遵循对vip开放原则。
* [不兼容鸿蒙组件]: 日历,swiper轮播,:这几个由于sdk问题和排查较久,先让我和官方沟通。
* [鸿蒙体验不佳]:x-slider-tree,x-watermark,x-text-cloud,x-barragex-sign-board:这几个由于sdk问题和排查较久,先让我和官方沟通。
* [不兼容鸿蒙插件]:x-mlkit-scannig-s,x-mlkit-scannig-u,x-camrea-u:这三个由于我没真机,暂没去兼容等我真机。
* [优化] x-betweent-teim增加属性widthCoverCenter,同xDrawer属性。
* [优化] x-cascader被重构,以兼容鸿蒙Next;
* [插件] x-svg-u 变更较大,不再需要导入组件,可以直接在页面上写标签使用,已变更为标准原生组件,请注意适配。
* [优化] 相机的插件和组件,针对性的为x5内核的浏览器(微信),优化,以防摄像头切换和自动悬浮全屏,脱离文档。
## 1.1.152025-07-16
* 【非常重要】本次更新是破坏式更新,主要是内置组件实现了多语言,并且需要安装多语言插件。app.use入口类型改成,结构改变,具体见demo或者入门安装文档那比对下。
* [新增] 新增多语言插件x-vuei18n-s,可集成到其它应用中,在tmmui4x项目集成时,一定要复制在运行插件目录,组件库自动安装集成。
如果是非tmui4x项目请按照插件内的文档自行集成。本插件用法与原官方插件使用基本一致,无学习成本,可参考组件使用。
* [修复] x-image微信端条件编译问题及不再使用下载接口(仅微信端)。
* [优化] x-tabs在4.71sdk下闪烁。官方已经在4.74sdk中修复
* [优化] x-echart在4.71 sdk web中出现init两次触发。
* [优化] x-pikcer4.71sdk 闪退(4.72修复)
* [优化] x-input 4.71sdk黑边,(4.72修复)
* [修复] 4.71sdk xEchart在web端事件被执行了两次。
* [优化] 两个日历组件x-calendar-multiple和x-calendar-view在hbx sdk4.74+上无法点击了,追踪的原因是sdk bug导致,但其它平台不受影响,为了兼容ios这边取消了视差动画效果,其它平台动画维持。
* [优化] x-slider兼容hbxsdk 4.74+,相同值同时或者反复赋值背景动态条无变化,还是旧的视觉。此bug在4.74上引发。
* [修复] sdk4.74+可能导致radio,checkbox组件无法选中的问题。
* [修复优化] x-picker-selected 当采用远程搜索数据时因为list可能为空,会导致选中数据被清空,现改成当用户一直在搜索变动list时,会将所有查询的数据缓存到本地供查询使用。解决赋值为list空时
* 以上仅对localSearch=false有效。
* [优化] x-qrcoder,添加logoBgColor背景
* [优化] picker,picker-city,picker-date,picker-selected为了更好的回显在默认触发插槽增加了label数据插槽数据,可以实时回显,以增加便利性。(与之前的vmodel:modelStr不冲突,这个是用来非插槽控制时便于回显)
* [优化] x-input增加属性isLink和rightIcon便于作为表单中配合其它组件作为静态展示显示,比如和picker配合点击触发,再配合上面的插槽数据回显,可有效减少变量的使用。
* [优化] x-cell右侧不显示任何信息时,取消间隙的设定。
* [修复] x-calendar-view,修复单选日历选中出错的问题,同时优化,不允许跨月点击,只允许用户翻页(滑动翻)选中当前面板日期
* [修复] xtips插件在执行关闭时未执行参数中close回调
* [优化] xmodal兼容修复contentPadding参数(向后兼容)
* [优化] weekbar组件增加事件swiperChange,滑动切换周期时,触发切换事件,并返回当前的7天时间数组
* [修复] xmodals,xtoastsxloadings插件,因为异步创建的全屏组件,导致后续如果连续打开,无法自动关闭上一个弹层。
* [优化] x-form增加ref函数checkAsyncVaildStatus
* [修复] x-popover修复在微信上事件丢失。
* [优化] x-swiper优化拖动的流畅度
## 1.1.142025-06-15
* 【非常重要】此版本仅可sdk4.71+(含)以上编译,如果你的项目在4.71以下,不要升级本版本,如果一定要升级请选择性升级不可全部。
* [新增] 新加了个xVirtualList超高性能虚拟列表数据,这将是处理列表的不二之选,在app端要比listView等官方组件性能强悍的多,助力安卓性能瓶颈。
* 首页探索页面已经采用了xVirtualList虚拟列表实现。对于大家处理数据列表的展示,固定高的列表展示非常顺滑
* [文档ai] 本站已在根目录存放了llms.txt,如果使用AI搜索可以直接提供链接https://xui.tmui.design/llms.txt文件喂给ai快速检索。
* [优化] 基于最新的4.71sdk,插件已经开始支持app.use(xui)或者app.use(xui{配置参数}),现在不需要在app.uvue中使用函数配置参数了,可以直接通过入口初始化了。
* [优化] x-form添加了ref方法clearValid用于清除验证状态
* [优化] x-row,x-col使用setup重写。
* [优化] 基于最新的4.71sdk优化并开放了x-calendar-multiplex-calendar-view日历的无限滑动切换功能,并且配置了readyOnly(默认开启,如果不要,可关闭以提升体验)以支援pad等10年前的低端机支持性能上的提升。
* [修复] xDate,修复31,29号等日期时可能造成异常错乱。从sdk4.71+开始,您可以自由的使用本xDate库了。
* [修复] xModalS支持html渲染后,未兼容安卓6及以下版本。同时添加按钮分割线样式,以应对不同的样式偏好设计。同时修复ios端渲染html后颜色设置失效。
* [修复] x-navbar未使用参数backErrorPath
* [兼容] x-mqtt-s在sdk4.71下无法编译,修改后此插件仅可在4.71+以上编译。
* [新增] x-input-number新增一个数字输入框组件,用于数字场景输入
* [优化] 加密插件x-cryptop-s增加了两个函数aesEncrypt2aesDecrypt2用于对传参及加密解密输入和输出全程为Uint8Array格式数据,解决转换的问题。
* 但由于IOS端在插件内直接使用Uint8Array处理数据会闪退(我可以确认是sdk bug),因此在传递参数是使用Arraybuffer,Uint8Array.buffer即可,加密和解密返回来的是number[],转换为Uint8Array使用Uint8Array.from(number[])
* 具体见加密和解密demo示例页面的操作。
* [修复] xDrag在拖时,如果停止拖放的位置在禁用的项目位置时,会覆盖禁用的项目。正确的应该是回退到原位置不允许覆盖。
* [优化] x-cascader单独开放了内部类型,修改了文档注释。顺便优化了下切换体验。
* [修复] xModalS,xloadingS插件内存管理。
* [优化] x-modal组件间距视觉改进。
* [优化] x-drawer在内容顶部添加了一个插槽:contentTop用于在顶部制作额外的营销类布局。见demo
* [优化] x-picker-selected开放了实时本地搜索(之前考虑会卡一直没放开)
* [变更] x-modal变更了动画效果
* [优化] x-picker系列组件开放了同Drawer的相同属性widthCoverCenter,以控制是否需要开启宽屏适配。
* [优化] x-picker和x-drawer开放了customWrapStyle属性用于自定义背景容器的外观可见两demo示例,以方便更多的设计外观。
* [优化] x-tabs增加了属性textStle及textActiveStyle文本样式。
* [优化] x-network-change,增加isNetworkAvailable和getNetworkType函数具体见插件文档。
* [优化] x-modal踢除了官方@transitionend事件,用timeout代替,近1年来此api接口经常在不同机型上概率性不执行,导致有些机型无法关闭弹层。如果此修改不影响后续,未来所有组件将逐步删除。
* [优化] x-markdown,新增对LaTex数学公式的支持,安卓,IOS,web直接支持,微信小程序请一定仔细查看文档使用才可支持。效果见demo示例
* [修复] x-collapse-item未注册click事件,导致重复触发。
* [修复] sdk不止从可起,x-popover微信端不管采用什么查询方法得到的悬浮节点bounds总是不准确,具体见文档说明。为了保证微信准确,内容最好不要动态更改宽和高内容(如果确实要变动可以vif或者刷新整体组件)。
* [优化] x-popover增加属性showTriangle及相应的triangle-colortriangle-dark-color显示指示三角及对应的颜色。默认不显示(兼容旧版本)
* [修复] x-drag在app端,如果跨行,拖放到禁用项目上,有机率会造成被放置到禁用项目上
* [优化] x-grid,x-grid-item分别增加了textDarkColor属性
* [修复] x-swiper自动轮播时,change事件未触发 。
* [优化] x-table添加属性hideHead用于控制是否显示头。
* [修改] 1.1.13中加入的字号响应,已经删除,经测试x对于computed函数在text内赋值性能影响太大。
* [优化] x-image逻辑尺寸app与非app单独条件编译分开,避免微信经常忘记加域名下载白名单。
## 1.1.132025-05-29
* [祝各位2025端午节快乐,家庭团圆幸福,阖家欢乐,此敬!]为了节日期间防止程序员们加班劳累特别更新本版本,增加一些便利属性,在最后两天好上线产品。
* [重要] 请使用hbx sdk4.53,4.57,4.65-4.66,4.67dev版本编译应用,其它版本不要偿试。(如果你从4.53升级出现编译不了,请卸载应用并全新安装)
* [优化] x-float-button优化显示时机,以兼容pad等低端机设备
* [优化] x-form新增了属性rules,要求属性:**Map<string,FORM_RULE[]>**,其中的string为form-item中的field,可以将整个表单的校验模块化统一组织管理提供
* 为了向下兼容,之前组件上的form-item上的rule同样有效,两则有一边命中校验的key即可校验。如果两边都提供将会合并校验为数组一同参与校验。
* 同时增加了属性watchValidStatus是否开启实时同步校验状态(非必要不建议开启),需要通过v-model:modelValid绑定获取实时的校验状态,用于外部指示校验按钮状态等场景
* [优化] x-form-item增加了属性contentStyle默认插槽的样式自定,特殊的情况可能需要直接布局,可以减少外部再套一个view节点的。
* [优化] x-upload-media优化了数据同步转换,防止响应式传递,同时增加了属性align用于项目左对齐还是右对齐。修改了相关样式。同时内部的xImage被替换成了官方的image因此没有了加载状态指示及错误图片指示了
* 更改原因:在表单中,如果过多自定节点组件会影响表单渲染性能。
* [优化] x-switch-slider增加属性eventNone以控制内容层是否响应事件。
* [修复] x-drag-item在ios下异常显示(其它平台无影响,主要是sdk平台差异导致)
* [优化] x-switch添加activeIconicon属性用于显示开关激活和未激活时的按钮图标,不提供不显示。
* [优化] xRequest,提供了loadToastText:加载时提示文本,successToastText:成功提示文本,errorToastText:出错时的提示文本,相关值属性
* [优化] 文档添加了微信分享文档操作方案,同时demo首页也添加了微信分享示例。
* [修复] x-collapse-item,我多写了个ref名称导致高异常。
* 其它的优化及修改。
## 1.1.122025-05-24
* [重要] 请使用hbx sdk4.53,4.57,4.65,4.67dev版本编译应用,其它版本不要偿试。(如果你从4.53升级出现编译不了,请卸载应用并全新安装)
* [重要] 升级,请重新复制hybrid文件夹内的文件,重新复制插件目录文件。
* [优化] xRequest请求优化了根据最新的sdk简化了逻辑判断,配置中新增了hostUrl(请求的时候可以随时配置,这样可以多域名,多模块请求),cacheTime:缓存时间默认1分钟
* useCache是否启用接口缓存(只对post,get进行缓存),最多在本地缓存20条接口,超过后远到近递减替换,以减轻字典类的接口重复的请求,直接从根上解决需要数据缓存的接口。
* [优化] xRadioButton,如果提供icon,title为空时,让其它间隙为0,以便支持纯图标选择样式组件,同时增加了textStyle自定文本样式。
* [修复] xPickerSelected组件在兼容微信时,丢失了底部操作条.
* [优化] xPickerSelected增加了一个属性isRadioMode:当设置multiple为false时,是否允许为单选唯一模式,即不允许取消唯一项,意味着一旦选中一项就无法清空或者取消.
* [优化] xModalS,ios,安卓,web,内容现在可以渲染html内容,不要太复杂.安卓和web会拦截a标签,ios目前拦截不了会直接跳转外链.
* [优化] xModal新增属性beforeClose属性,可用于在确认按钮之前执行异步函数,是否阻止关闭弹层.
* [优化] xDrawer新增属性beforeClose属性,可用于在确认按钮之前执行异步函数,是否阻止关闭弹层.
* [优化] xCheckbox增加round属性,用于控制圆角,默认为4
* [优化] demo首页删除了浮动,改为左侧滑菜单演示效果。
* [优化+修复] xDate,设置顺序调整,可能带来的不准确。以及修复历史遗留问题只能选中到1970之后。
* [优化] x-picker-selected为空数据时,可能出现的位置偏移。
* [修复] x-sqlite-s 修复安卓端执行插入数据时,可能会被执行两次。修复安卓/ios端不能使用应用的uni.env.USER_DATA_PATH目录及自定二级目录,在安卓端执行COUNT(*)时类型转换错误。
* [优化] x-collapse-item的异步获取问题。兼容app sdk4.65+,微信端兼容优化。
* [修复] x-file-s下微信小程序端类型爆错误
* [修复] x-scrollx下微信小程序端在hbx4.57+以上失效。
* [修复] x-avatar-group下微信小程序端在hbx4.57+以上爆红问题和异常error(但不影响使用,但看着难受)
* [优化] x-tag在微信小程序端样式延迟响应(其它端正常)
* [优化] x-calendar-multiple日历范围组件全新重构,已开放无限滑动切换,切换效果更炫,如果本次更新x-calendar-multiple反响好,下次就会依此对x-calendar相应的更新
* * 我已经请求了官方优化,请关注官方的4.67版本再对比性能目前4.57-4.66没有太大变化,如果过多样式,还是有点延迟,以上说的仅对安卓说明。非安卓比较流畅。并增加了头和尾插槽
* [修复] 修复x-tabbar异形外观时,镂空在安卓端会有0.5-1px的差异
* [优化] x-drag-item添加disabled属性,可以禁止某项拖动,并保证该项被固定顺序不会被替换,对于特殊的排序要求非常有用。* [优化] x-drag-item添加disabled属性,可以禁止某项拖动,并保证该项被固定顺序不会被替换,对于特殊的排序要求非常有用。
* [优化+修复] xoverlay,x-modal更新了在web端的显示机制,并修复了xoverlay在web端重复打开可能的报错显示问题。
* [修复] x-action-menu因兼容安卓sdk缺陷时,导致其它平台item-click被重复执行。
* [修复] rolling-number在不平台时,如果动画执行中更新,可能导致动画无法继续,导致数据更新与实际值不符。
* [优化] xToast,xLoadingS,xModal优化为全屏覆盖。
* [优化] x-swiper重构部分,及手势状态,让其在页面内滑动与页面滚动减少手势冲突,并且重新开放了item事件click,同时也保留了父级的click事件向后兼容。
* 同时增加了loop属性及事件dragLastEnd及显示showLastView,配合后,可以实现拉到最右侧拉不动时,显示最右侧的内容,并触发拖拉事件(仿头条首页推荐视频拉到最右时触发继续拉触发到视频页面)
* [优化] x-popover增加了上下边界检测,如果设置为下显示时,下空间不够会自动转为向上展示,其它情况类似。
* [优化] x-crypto-s加密安卓调整函数以支持兼容安卓5.0及更低版本安卓(pda设备好多比较低的版本)
* [优化] x-float-button新增事件longpress,用于支持长按时触发。
* [优化] x-sse-s 参数修改,支持header,并增加demo示例页面,请参照demo修改你的使用方式。
* [优化] x-float-drawer,标题高亮修改为仅触发标题时指示高亮,底部拖拉修复可能出现的错位,并设置为点击头就置顶滚动。
* [修复] x-text微信端在兼容本身sdk bug时,导致插槽事件丢失,无法触发click(通过label不受影响)
* [优化] x-echart在最新的4.57+sdk上带有函数的配置失效,为了应对不同平台频繁兼容问题,请参考我的demo对有函数的opts配置分平台按格式写。目前demo演示了app上带了函数格式化的配置。
* [优化] x-upload-fileweb端向后端传递文件名。
* [优化] x-color-view,优化了样式,新增showAlpha控制显示透明度。新增ref函数getAlphagetColorgetColorNoAlpha
* 其它优化和修复
## 1.1.112025-04-14
* [**本次优化和更改了常用插件,请已使用的用户谨慎升级并及时反馈兼容性(常用提示插件理论已经支持到了安卓1.0)**]
* [新增插件] 增加了个数据库插件x-sqlite-s,为Sqlite3的一个操作映射插件,具体见插件目录内的说明文档.
* [新增插件] 增加了人脸检测 xFacedetection,及活体检测。
* [插件优化] xModalS,安卓端弹层修改布局,现在可以覆盖官方的Dialog-page页面了,并且可以接受安卓的返回键及侧滑关闭弹层了.可以覆盖住官方的diaglo-page,做到真正的全覆盖
* [插件优化] xToast在安卓,ios,web布局已更新为宽自动,并设定最大宽(之前宽是固定),方便在平板手机内容超多时自动断行及限定最大宽,安卓端:可以覆盖住官方的diaglo-page,做到真正的全覆盖
* [插件优化] xLoadingS 安卓端现在可以覆盖住官方的diaglo-page,做到真正的全覆盖
* [修复] 修复xToastS事件中的close事件未执行(是我忘记执行了)
* [插件优化] x-vibrate-s现在ios端支持指定震动频率.
* [优化] xCountDown优化,增加了属性captcha,用于验证码倒计时模式,开启后任意地方多个地方使用组件共用一个倒计时值防止重复.
* [修复] x-openweb在ios16+上无法打开外链的问题.
* [修复] x-qrcoder上个版本升级时,定义未删除,导致没有引用模块的人无法打包(有引用的可以打包)。
* [修复] x-crypto-s加密微信端没有同步到其它端。
* [修改] xImageResize微信端没返回文件连接,web改成Base64格式数据,请自己作好条件编译上传。
* [优化] xFloatButton增加了Zindex层级属性。
* [优化] xBetweenTime,对属性quickDate增加一条规则p+x模式表示前x年,方便对数据历史记录的查询
* [优化] x-markdown,增加了对表格的解析及代码的解析,并增加了对表格的样式支持,注意:如果你提供的是html需要自己添加类型样式支持,这里仅是对markdown的解析
* [优化] xDate,xPickerData,xDateView等现支持非正常格式了如:YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
并可通过设置formatSyncValue为true来让format格式化后的格式支持到vmodel中,方便同步表单,xDate库增加了国际化语言设置函数见文档。
* [修复] xUploadMedia组件点击删除时,未触发同步更新的问题.修复,还有可能微信版本会上传两次(不一定存在,但本版本测试不存在了.)
* [修复] 由于hbx sdk4.51+以上造成text嵌套事件失效,导致x-link事件也失效,现更改了内部结构,绕开sdk bug
* [修复] xtabs,项目居中,由于兼容微信造成了app不居中.
* [修复] xTabbar,当设置is-canvas-render为false时,再关闭凸出选项造成背景色丢失.
* [修复] xSticky的change事件忘记携带当前状态了.
* [优化+修复] xImageResizer,新增属性compress压缩值0-1,微信现在开始返回文件地址(之前是base64),web返回base64(之前是Blob链接,会让外层无法上传),并完善了web端跨域出错的提示.
* [优化] xPopover增加了属性maskBgColor,用于控制背景,默认是透明.
* [优化] xTree,增强了parentSelectedFullChildren属性,打开后,现在会根据子节点选中状态,改变当前的三个状态:选中,未选,半选中(仅表示状态,并未选中)
* [优化修复] xbutton,xtag由于微信的版本库本身的bug,导致这两组件里面的文字会没变化,因此本次升级作了兼容处理。(也与官方验证过,确实是微信自身问题)
如果想单独控选中父就要设置为false,具体要见文档
* [优化] x-pull-refresh,组件,由于sdk bug设置了相关一个属性导致安卓机上下拉视觉体验不太好.现已删除恢复到默认状态.
* [优化] x-slider,x-slider-double增加了滑动条上,点击时也可以设置滑块值.
* [优化] xSwitchSlider增加了属性borderColor,borderDarkColor下边线的颜色配置.
* [优化] xSliderMenu添加了属性showToggleMenu,允许侧边菜单展开和收缩为边栏,同时添加插槽toggle用于指定义自己的展开收缩的样式,
* [优化] x-cascader本组件完全重构,增加了属性showCurrentBtn是否显示选中本级,并触发confirm及同步vmodel,最后一项时,也会触发相同事件,同时增加插槽header来完全自定义顶部导航.
list添加了指定icon图标,用于为侧边收缩是显示的图标指示.具体见demo, 同时添加menuPosition菜单位置,left或者right
* [优化] x-scrollx,增加属性showScrollBar,用来是否显示底部指示条.
* [修复] xswiper由于sdk的web 动画缺陷,会导致本组件在切换页面时,丢失相关参数及事件,本版本修复及兼容web相关sdk缺陷.
* [修复] xFloatBottom在web端点击事件失效.
* [优化] xModal,xDrawer增加了btnColor,底部操作按钮的主题色,默认空值取全局,提供时以提供为准。
* [优化] 所有弹层类picker补上disabled属性
* [优化] xtabas增加了click,用于点击时触发,同时修改了change逻辑,同个index时,不再重复触发.
* [优化] Icanvas中的属性rotateCenter增加了"topLeft"|"center"|"topRight"|"bottomLeft"|"bottomRight"等属性类型.
* [cli集成测试] 根目录中jet.test我更新了配置,现在可以测试整个项目了。
* [优化] xEchart图表,更新了opts字符串的解析函数。现在可以解析带函数的opts字符串,且较为复杂的函数
* [优化] xConfig配置中增加了全局统一的xInput的背景色inputBgColor,统一的palceStyle样式配置,方便统一为你的设计稿样式
* [修复] xModal中的按钮色未读取btnColor
* [优化] xSearch增加cancelFontColor,取消的文本颜色.
* [修复] x-image-group兼容了一个hbx sdk的bug可能在部分平台预览顺序不一致.
* [修复] x-popover在设置isClickClose=false失效的情况(也是早期兼容sdk后后期没有更改过来.)
* [优化] x-image,增加了previewSrc,用于预览的图片地址,如果为空,则采用src地址.
## 1.1.10 (2025-03-06)
[优化] 组件文档,已经更新新的解析引擎,文档出错率已经得到降低,为了文档更好,所有组件的相关注释也已更规范的修改+提示
并同时提供离线文档格式:markdown和html两种文件格式,用于大家用AI创建自己的知识库,更好的为你码代码,提高效率。
后期还会提供Pdf离线文档,用于大点的公司内部离线交流查阅组件文档(公派,劳务派遣),保密较高,只有内网查阅。
并同时后期还会提供相关UTS编译技能验证试卷用于认证当前员工是否适合uniappx的开发及是否需要培训的验证。
验证你有没有能力同时开发uniappx并兼容所有端。
[新增] xEdite,富文本编辑器,支持传入markdown,html进行内容编辑(建议遵循微信小程序规范,这样可以适配全部平台)
[优化] 优化x-overflay在微信上的动画表现
[优化] 优化x-slide-verify在微信上嵌套组件内时,可能的错误
[优化] 优化x-echart在微信上事件注册丢失问题
[修复] xToast 安卓端重新设定了动画参数,让打开显示的更自然.同时关闭逻辑改成了:当页面卸载或者超时跳转页面时,不会自动关闭上页的问题.
[修复] xLoadingS 修改了打开动画,显得更自然.同时当页面卸载或者不在当前页面时自动关闭.
[修复] xModalS 修改了打开动画,显得更自然.同时当页面卸载或者不在当前页面时自动关闭.
[修复] xTips 更新了动画,并修复了,如果路径页面,上个页面不会自动关闭的问题.
[修复] xTips,xToast,xLoadingS,xModalS 更新了部分动画,现在不管显示还是关闭都更加的自然
[兼容] x-popver组件在4.51+sdk的安卓上不显示.现在已全部优化,已适配4.51+以上的sdk
[优化] 优化了demo部分页面渲染时机,加快启动速度.
[优化] xTreeFlat组件在安卓上优化了渲染时机
[优化] xNavbar增加了属性maxWidth(仅web生效),height:状态栏高设定.
[优化] xPickerSelected,属性idKey设置为了any,不能为null,现在你的id可以为非null的任意值.
[优化] xBetweenTime,增加了事件dateClick
[优化] xWeekBar,增加了属性emptyValueSelected,默认为true(保持向下兼容),当为false时,modelValue也为空,默认不会选中日期.
[修复] xRequest修复了如果并发时,opts可能没有独立分开,导致可能错误.
[优化兼容] xMarkdown,(建议遵循微信小程序规范,这样可以适配全部平台)预览时增加如下功能:支持文档流式解析文档内容,支持动态高,支持内容复制,本次更新需要大家同时更新hybrid目录内容.
同时这个组件还增加了tagClick事件,用于触发webview内的a,img标签的事件,你可以监听并处理相关逻辑.
同时增加了nodeStyle和nodeDarkStyle请自行配置样式,默认样式我已经清除,为了匹配所有平台。
xMarkdown组件同时从1.1.10开始支持微信(tagClick不支持,只支持预览功能),同时app,web端删除了编辑功能,请使用全端的x-edite
[优化] xSignBorad组件在demo中增加了弹层内显示的示例.
[优化] xQrcoder,美化了点点,并新增定位颜色单独配置,及新增一个小方格样式,目前四种样式了。
[优化] sign-board,增加了笔触效果,更符合中国式的签名效果。
[修复] xButton对skin的监测。
[优化] xCrpotS加密插件进行了功能升级新增了几个加密函数,具体见demo源码和插件文档
[优化] xInput增加了trim是否自动清空首尾空格(默认为true)
[优化] xCroptS加密插件,修复了微信,web端的base64加密错误,同时新增了sha512,hmacsha512,以及生成rsa密钥对的函数,具体见插件文档.
[优化] xForm增加属性errorAlign用于控制出错时的文本对齐方式,left,center,right
[优化] xForm表单xFormItem的rule校验规则对象增加了一个trigger触发时机选项,默认是change,如果你要设置为blur的话,你一定要是要校验的对象组件是input否则
不触发blur就无法校验.
[优化] xFloatDrawer,在电脑端的滑动体验优化了下..同时逻辑层上已经加入了右滑关闭的逻辑,待官方事件优化即可开启.
[优化] xCell添加了相关属性margin用法同sheet,并在全局中添加了cellMargin,当cell为card时可通过全局修改边距.
[优化] xCalendarMultiple增加了属性rangColor范围选中时的区域间颜色设置(默认为主色的透明0.5)
[优化] xUploadMedia,给用户默认添加的文件对象增加了status,0表示待上传,要确保你的上传文件路径是正确的否则安卓会引发io错误,并且uni.upload是无法捕捉到fail事件中,会导致整个程序不可用
[修复] x-switch-slider 优化并修复相关问题,现在可以让其它在不同端滑动时更加的人性,使用体验更好,页面滚动与左滑不冲突了。(h5不行,因为官方的sdk未设置passive: false
[优化] x-finger优化了下。
[兼容] sdk4.51后不支持max-width省略在安卓上,帮xCell右宽直接固定了值,如果厉害要宽一点可以通过插槽或者属性配置rightWidth。通过插槽的话,这个属性会失效,需要你自己设置宽
[优化] 现在xImageResiz裁剪,导出的图片全量使用了xBase64file插件,因此裁剪导出后,全平台返回的是图片链接地址(不再分平台有的 filepath,有的是base64了)
[优化] xStepper,新增属性inputStyle可以覆盖输入框样式。新增属性:autoHideBtn,可以自行达到最小值时隐藏减按钮
## 1.1.9 (2025-02-16)
* **非常重要**改动较大,如果遇到小问题,请自行提交到coding上,按规范提,不要一直@我,发布大版本,有点小问题属正常.后续更新修正即可.不想升级的保持在1.1.8即可.
* 【非常重要】本次更新非常大,大多数插件对微信了适配,比如图片识别码也能在微信上实现适配(非微信官方弹摄像头,而是直接本地图片识别),更新力度非常大
* 带来的4端适配也是非常好的,本次更新耗费了我大量的时间及精力(适配前后花了约2个月,适配微信),因此本次升级后授权费用涨价50元,由之前的499涨至559元
* [新增组件]思维导图 xTreeFlat,这是一个高性能的即时渲染思维导图组件原生绘制,无惧性能
* [新增渲染库]ICanvas.uts,是一个强劲图形渲染库,原生绘制,无惧性能,事件系统独立每个元素都有自己的事件,还有独立的动画系统。
* [重要]x-qr-u,已经删除,请使用x-qrcoder代替,x-qroder已经完成纯原生绘制,因此不再需要依赖第三方插件。
* [重要]由于插件和组件(部分)改动较大,如果升级后出现编译错误的,请定位问题,查看文档和比对源码逐一修改,有些修改日志可能没有充分体现.
* [适配微信] 这里着重讲下差异 **如何适配到微信**
* 1. 默认的插槽如果含有插插槽数据的,在微信端直接在标签内写内容是会被直接忽略不显示正确的做法如下:
```vue
<template v-slot:default="xx">
正常的标签内容
</template>
```
因此我大部分默认的插槽数据会被删除,以方便大家直接兼容微信,而不必写上面的格式,但缺点是数据没了.但相对大家可以一步过渡到微信,只选伤害为次的方案.
当然有的组件可能我判断用的少,而不是大量使用的组件我会保留,那么你使用了,导致不显示了则需要如上格式写全才能显示.
* 1. text直接内部插槽会让内容失去响应,就是改变数据,页面不更新,解决方案是给text套view再写插槽,
* 影响我的xtext组件(已经作了条件编译,这里是提醒你们注意兼容sdk)
* 2. 有些组件为了兼容可能推了一些屎山代码,影响微信的性能,但不多,整体以兼容为主.
* 3. app专用的原生插件大部分无法在微信上使用,请自行做好兼容,做条件编译.
* 4. 小程序不支持动态插槽,只支持同名或者不同名的循环插槽,这意味着一些需要通过动态插槽来布局的组件将失效,仅支持内部默认样式
* 已知组件:x-table
* 5. 小程序不支持元素大小响应监测,只支持query page监测,因此组件内无法预测组件相对窗口的大小变动,因此无法支持UniResizeObserver,所以
* 你在app,web上组件能响应式,但在微信上无法响应式。
* 6. 插槽内,如果不套个根节点,而是直接循环多节点,微信小程序内会自动给插槽套个父节点View,导致布局异常,请注意sdk变化。
* 7. 前期已经做好的项目升级兼容微信,基本改动很少,就可直接无缝发布到微信小程序
* [重要]x-drage,由于兼容微信的原因事件touchstart,touchend,touchmove改为start,end,move,请注意兼容
* [优化]x-modal-s,优化三星S21-S23机型安卓端点击遮罩层不关闭的问题
* [优化]x-tabar可能存在0.5像素的差异.
* [优化]x-weekbar,修改为无限滑动切换周次。
* [优化]x-msg-notice,可以允许向上,向下,向左,向右的手势来关闭消息通知栏.
* [优化]瀑布流组件,demo示例我已经已更新请大家参考demo布局,以实现高性能无限加载体验(另:安卓已经采取多线程异步处理数据显示提高单个瞬间加载显示的流畅度)
## 1.1.8 (2025-01-08)
**祝各位2025/蛇 新春快乐**
* 建议使用sdk 4.36,4.44,4.45来使用本版本
* 已知sdk4.44+BUG: ios动效部分不流畅,安卓数字自动加.0(老bug复发)
* [新增]剪切板操作插件 x-clipboard-s
* [新增]插件x-animation-s,原生动画,可增强交互体验,可以实现3d等复杂动画(比如3d翻转等营销交互)
* [新增]插件x-sse-s,流数据读取.使用方法见代码仓库
* [重要更新]xStore新增了个函数setConfig(config as utsjson)可以统一设置全局所有属性,所有字段为可选
同时xConfig中增加了属性字段为theme:new Map<string,string>(['primary','#ff0000'])此字段可以覆盖内置所有同名主题的颜色,比如默认的primse,success,error等常用的主题颜色名称
* [优化]x-between-time,修改了样式细节,修复了上个版本导致标题塌陷.
* [修复]x-checkbox,印象中是从1.1.4开始改了非选中值为空值,demo和内部未同步,导致半选状态失效.
* [优化]xConfig,增加了全局属性closeIcon,不同人的设计稿可能对右关闭图标样式有要求,有需要线性有的需要面性图标,现在统一全局更改
* [优化]x-float-drawer开启滚动时的视差体验,目前可以与头条的体验相仿。
* [优化兼容]x-image-resizer兼容sdk 4.44安卓端,这个可能还后期如何官方修复了sdk bug可能还要再改,但目前在4.44安卓是正常了。
* [修复]x-col的精度在安卓上可能导致除不尽的列数出现断行。
* [修复]x-icon,可能旋转不在中心点上。
* [优化]x-search添加属性darkColor用于自定义暗黑背景。
* [优化]x-switch-slider,补充了些事件,demo增加了个列表循环手动状态管理的示例
* [修复]x-slider,x-slider-double,在设置max及step时,精度问题导致的一些问题.
* [修复]x-mlkit-scannig-s的web端函数名称错误
* [优化]x-drag,增加了touchstart,touchmove,touchend事件,属性:scrollDiff滚动距离,touchmove是在页面需要自动滚动时触发并且间隔200ms,
具体见demo,demo中增加了:自动滚动,局部拖动区域的演示,方便大家学习参考.得益于sdk4.36+起ios sdk已经能阻止页面滚动的冒泡,现在已经放开了触摸组件时允许滚动页面.
* [优化]x-dropdown-menu,增加了切换动画。
## 1.1.7 (2024-12-24)
* [重要]仅支持正式版本4.36+(alpha4.37+)运行,(不可使用alpha4.34,35,36)
* [新增]x-base642file-s插件,用于将图片base64数据转换为本地图片文件对象,三端兼容,web转换为Blob对象文件,ios,安卓为缓存文件路径.
* [新增]x-image-resizer组件,用于将图片裁剪为指定的比例尺寸图片,三端可用,可双指缩放操作.(安卓在4.36上暂无法截图使用,sdk未知bug,待官方沟通修复)
* [新增]x-color-view,新增一个用户界面操作友好,精致的颜色选择器,多模式选择,为你的app增彩.
* [新增]x-x-translate-s,谷歌大模型翻译,预下载后可以离线翻译.
* [新增]x-runnable-s,安卓,web端可用(ios官方限制不可用)。多线程执行插件,主要用于复杂,数据处理在不影响页面流畅度的情况下在后台开线程执行逻辑。为后续xCanvas库提供强劲动力。
* [新增]x-screenshot-s插件,可用于对应用页面进行保存,从而分享页面,对局部节点保存并分享,(分享需要使用我的另一个分享插件)
* [新增]x-zip-s,解压缩插件,主要用于比如对沙盒内的一些缓存,临时数据资料打包后统一上传,或者从服务器下载压缩配置文件统一解压到应用依赖。
* [新增]在1.1.7开始新增新的标准组件库依赖x-design,里面的组件开头为xd-xxx,从这版本开始凡是以xd-xx开头的标签统一为原生标准组件需要打包,或者搭配环境使用.
* [新增]xd-blur 模糊标准组件见x-design下的readme,主要功能:模糊标签内的内容本身,模糊它底下的背景(即磨砂背景效果),功能强大,可以应用模糊任意内部内容和任意它的父级(背景)
* [增强]重构了插件x-crypto-s,支持了大量的加密算法见文档,另外sm2,sm3,sm4本次未更新见下个版本更新.
* [增强]x-upload-media,早期因为官方不支持视频选择,现在官方支持了,因此我也扩展了下.现在可以上传视频或者图片,参数:mode:video,photo,及视频参数videoOps,具体见文档.
* [增强]x-camrea-u,相机增强了start,stop,pause视频录制功能,增加事件recoder,具体见demo,可以在录制视频的同时进行抓拍相片.停止或者拍照时现在不会关闭相机,需要你手动关闭,方便连续拍照
* 如果你想实现点击拍照,长按录制请根据提供的方法,结合套个view通过事件执行方法实现.
* [优化]x-tabbar因为hbx sdk的对canvas渲染的不稳定性,我提供了两个属性:firstRenderTimeoutisCanvasRender用于回退使用。请自行选择用哪个引擎渲染界面。
* [修复]image增加的插槽,不小心vif写错位置了.
* [优化]radio增加了onlyChecked属性,,出现这个属性时,group或者单个使用时将不允许取消选中,至少保留一项选择.
* [优化]x-radui-button增加了项目中disabled,用于控制某菜单是否要被禁用.同时增加了按钮click事件(不管是否禁用都会触发)
* [修复兼容]x-drawer动画在h5会被执行多次,造成事件被执行多次,由于这是底层事件各平台对齐原因。现把三平台对齐只执行一次。
* [修复]x-input的clearColor颜色忘记使用了。
* [优化]x-button删除_style,_class,没有意义的属性,可以直接在组件上写style,class.
* [兼容]x-tabbar兼容了4.36+
* [修复]lines在web端如果动态设置0时,不会生效.不清楚是sdk bug还是web规范.现在已经兼容修复了.
* [修复]x-picker-time时间不准确的问题.
* [修复]x-action-menu,修改跳转时机,避免触发sdk 底层渲染bug
* [优化]x-pull-refresh 增加了disabledPull,disabledBottom禁用下拉刷新和触底刷新.增加ref方法:setScrollTop,setScrollIntoView
* [优化]x-markdown背景整成了透明.
* [修复]x-float-drawer内容无法滚动的问题.
* [优化]x-float-bottom增加了thresholdBottom,thresholdTop底部和顶部的安全距离的单独控制。
* [优化修复]x-image组件,由于sdk image组件自带bug造成了我这组件在ios容易丢失图片,本次更新兼容了sdk bug,ios端不会再丢失图片了.
* [优化]x-slide-verify兼容pc操作.
* [修复]x-input,clear,与右图标上下不对齐的问题
* [优化]插件x-qr-u,增加logo设置,增加ref方法getQrcodePath,用于获取码的图片路径(web返回的是Bolb文件,安卓,ios返回的是缓存图片路径).
* [优化]x-drawer,增加了属性:offsetBottom,用于对底部的偏移,有些情况可能需要在带tabbar页面弹出。
* [优化]x-swiper,修改了反转播放逻辑,现在播放到最后一个时,直接设置为0从新播放,不再倒放.
* [修复]x-echart,不支持函数参数格式化,更新时,请大家重新复制hybrid到你项目中.
* [优化]x-icon,新增fontFamilycode属性用于自定义图标,当提供code字符时以code图标字符为准,name将失效。fontFamily可以定义为自己的图标字体,需要你自己先安装好字体
* [优化]x-ocr-s,ios,安卓添加本地路径识别函数localFilePathImageBuilder,可自己循环批量处理.
* [优化]x-inputtag,增加maxCount限制,及修复设定高时,不顶对齐.
* [优化]x-slider-menu,标题设置最小行高
* [优化]x-overlay增加了参数disabledAni控制禁用点击时,是否开启弹跳动画,默认开启.
* [优化]x-cell,提供了padding属性,可以自己改变内边跑,当然你也可以不用,直接在组件上写style是一样的.用法与sheet一样,并且修改默认值让card与非card间距等同.
* [优化]x-dropmenu组件优化了布局以应对更复杂的嵌套应用,增加了参数:zIndex(嵌套时需要用到层级),hidnMask(隐藏遮罩,试图在全局遮罩中嵌套时web端需要隐藏)
* [优化]x-float-drawer,增加了参数contentMargin,用于控制内容层的边跑.
* [优化]x-cehckbox,x-radio,添加属性:default-checked,只接受初始值,默认是否选中,不会影响选中的值,只影响选中状态.在group中不会起作用(与多个大厂比对过,确实合理,不然值会冲突)
* [优化]x-picker-selected,增加:disabledPull,disabledBottom,v-model:refresh,v-model:bottomR-refresh,增加事件:refreshbottomRefresh,用于支持搜索数据分页加载,触底和下拉刷新
* [优化] **x-tree更新如下:**
2. floaderIcon自定前缀打开和关闭图标/图片
3. showFloaderIcon显示或者隐藏上述
4. showChecked显示或者隐藏选择框
5. checkedIcon选中和未选中框图标/图片
item项目中增加字段:
1. showEdite用于控制是否编辑节点标题内容
2. showRemove显示删除按钮
3. showAdd显示添加子级功能
* [优化]x-more,增加unActiveColor,及自行判断要不要显示展开按钮:showMoreBtn
* [优化]indexbar,右侧字号改的更小,尺寸也更小了,以方便在更小尺寸上展示防止裁剪.
## 1.1.6 (2024-11-21)
* 仅限hbx sdk 4.32+使用
* [适配]sign-board 适配4.34sdk 使用
* [适配]x-echart图表 ios端在4.33+无效.现适配了4.34,可用了.ios端的格式类型与安卓不一样.一个是UTSJOSN,一个Object.
* [适配]x-markdown,ios端适配了4.33+
* [修复]between格式化写反了导致前两个快捷选错了。
* [修复]x-tabbar修复兼容4.31+ sdk bug
* [更新]图标库更新到4.5.0
* [修复]主题修复兼容4.31+ sdk bug
* [修复]x-ocr-s ,ios没对齐安卓,失败不会返回回调.
* [调整]调整x-keyboard-number位置,及补充车牌键盘字符。
* [调整]x-tabbar不同平台的兼容性,同时提供全局tabbar高的读取,xTabbarConfig.tabaarHeight
* [新增]新增x-mention提及组件,用于聊天或者需要提及符输入时有用.
* [修复]x-between-time如果切换过快会造成数值可能没有回正为正确值.
* [优化]x-float-drawer补充了beforeClose事件,另外增加了movestart,moveend,以及heightChange百分比.配合这些参数及dom操作可以满足达到头条那样的丝滑的反差差值缩放视频和评论大小的关系动画.
* [优化]x-table增加属性:refresh异步函数刷新时执行.增加事件@refresh刷新时触发.
* [优化]安卓端的扫码插件添加了几种常见条码识别类型,ios是全部类型,安卓因为有些机子性能差,不能全部加,不然识别慢。
* [优化]x-mlkit-scinnig-u:新增属性flash默认是否开启闪光灯.后续如果需要动态打开 和 关闭闪光请使用ref方法:setFlashModel(boolean)
* [优化]x-mlkit-scinnig-s界面中底部操作按钮中间增加了打开/关闭闪光灯的按钮,以便夜晚识别码的需求.
* [优化]x-button添加navigateMode,用于指定跳转类型.
* [优化]x-image,增加error,loading两个插槽,方便自行定义加载和错误的样式.
* [优化]x-picker-selected增加了插槽名item,并携带了数据名v-slot:item="{item}",类型是UTSJSON,并同时增加了属性itemHeight,用自定义自己设计排版项目样式.
* [优化增强]x-pull-refresh增加事件@scrollDirection和@scroll,前者是滚动方向改变时触发,后者是滚动时触发。
* [优化]x-float-drawer增加zIndex层级设置
* [优化]x-tabbar增加zIndex层级设置,增加list,增加activeIndex用于单独控制,不依赖全局,当采用本地值时,需要自行控制索引选中
* [修复]x-tabbar当暗黑切换时,没有重新渲染。
* [优化]picker系列组件:picker-city,-date,-selected,-time-modal-overlay等增加了zIndex层级属性设置默认全是1100
* [优化增强]x-slider-menu改动较大,但保持 了 向下兼容:左侧菜单栏目改成了虚拟列表提高性能.增加了layoutMode=default兼容旧版本,layoutMode=scroll时会开启右边循环左侧菜单
* 栏目的项目节点作为定位节点,会开启右侧滚动左侧菜单定位,左侧点击菜单时,右边内容自动定位滚动的效果.增加了itemSelectedStyle用来覆盖菜单项目的选中的样式,可以定义自己样式.增加了itemHeight控制菜单项目的高.
* 建议参考我的demo使用.
* [优化增强]x-slider-tree子分类全采用list-view虚拟列表.
* [优化]x-table增加了showScrollbar属性.同时隐藏了pc版本时多条的滚动条.
* [优化]所有picker弹层系列增加了cancelText,confirmText字段用于控制多语言文本或者定义自己的文本.
*
## 1.1.5 (2024-10-27)
* [重要更新!!!]x-form的rule加强了校验,由于你们可能需要让值为null,这牵涉到联合类型,因此你们的校验函数vaild(val:any)=>boolean要改成valid(val:any|null)=>{}
* 同时修改了FORM_RULE类型唯一影响的就是vaild你们要适配下,这个类型的所有参数改了可选,并且增加了max,min来快速校验值,同时增加了指定type类型校验:这对你们快速校验非常有用它增加了对:date,array,email,phone等快速验证.
* [优化]x-tabbar属性outIndex从此版本开始不作限制,可以任意索引凸起,并且增加了属性可以控制是否动态镂空,
* 而且还可以响应式尺寸镂空.通过static下的遮罩可以更改凹陷的造型.同时也可以使用背景图.此版本更新后样式个性极为灵活.(注:sdk 4.31 ios官方有bug连带这个特性在ios上有差异,请后续关注官方更新)
* [修复]xUploadFileMedia在ios端放开压缩后产生类型丢失,造成无法上传.
* [修复]日历相关相组件在app端绘制dot时位置错误.
* [修复]x-drag可能长按不移动时造成位置混乱.
* [轮播]指示点可能让底部事件失效。
* [修复]x-picker默认首次为空时默认第一项确认可能在安卓平台造成无法选中默认的第一项值.
* [增强]x-input-tag添加Postion属性用于输入标签在输入框内还是在外两种排版布局.并优化样式.
* [优化]x-search增加placeholderStyle属性用于控制输入框提示样式.
* [修复]xtabbar由于sdk中的computed 无法动态返回store的对象值造成内容改变时无法监测变动.因此换了个写法兼容sdk的问题.
* [修复]x-code-input双向绑定失败
* [修复]x-switch-slider在弹层内失效(由于节点被传递到外层导致内部无法查询节点.)
* [优化]x-btweent-tiem上方的快捷按钮设置为带选中状态,点击时高亮选中.滑动时间时会自动丢失选中状态.
* [修复]x-btweent-tiem上方的快捷按钮如果设置为空时,间隙丢失了.
* [优化]picker系列组件增加了modelStrJoin用于modelStr拼接时修改拼接符号.
* [优化]x-markdown增加了属性isHtml用于纯html渲染,直接内部赋值html,以避免特殊字符在安卓上触发sdk bug
* [优化]radiocheckbox增加了labelSpace间隙属性用于自由布局时,好控制间隙。添加了cell布局示例。
* [优化]x-tabs改变了移动方式,现在改为如果内容能滚动,选中的内容始终处于组件居中的位置.
* 同时废除了moveCount属性,增加了isItemCenter属性用于项目少时,想居中(配合前述可自动居中),增加了line-gradient用于设置渐变线条.
* [优化]picker,picker-view添加了unitsFontSize用于修改单位字号大小.
* [优化]x-wiper-item组件增加了round属性,用于单独控制圆角.
* [优化]x-qr-u,根据谷歌官方安卓端升级了码库为3.5.3(最新2024-1),并调整了配置减少内存占用,并且生成码时在多线程内执行防止UI阻塞
* [优化]x-waterfall瀑布流组件,采取了虚拟节点渲染,使用时请务必让组件在scrollview节点内(安卓和ios,必须要让组件处于滚动中可以是listview),web没有要求.web是自带的滚动处理虚拟节点.
* 本次更新后:安卓需要打包后才能测试出性能比之前提高了非常多的性能(至少提升100%),ios端提升也是非常明显(与web提升一样).web端提升更多(基本可以无限的下拉不限数量,也不卡再多也不怕.)
* [优化]x-between-time,添加了属性disabledClear,是否允许用户清空选中的数据。如果开启,那用户清空了选项后无法按确认键,也就是无法让数据为空。
* [新增]x-svg-u插件,用于显示svg文件。三端通用。请务必4.31打包。
* [优化]x-camrea-u,增加属性orientation以控制前置,后置摄像头,flash 闪光灯是否开启,web不支持这些属性
* [重要,不向下兼容]x-ocr-s升级了调用方式,使得安卓,ios用同样的方式调用,不再区别,统一使用callback,并且在回调中携带回了坐标,以便让大家通过坐标计算识别比例或者绘制位置.并且文本块统一为行返回(之前是文本块返回,但在源始数据中还是块和坐标)使用方法见文档或者demo
## 1.1.42024-09-23
* [优化]picker-date,date-view默认不同步格式化后的值(如果为空时)
* [修复]x-waterfall组件上个版本为了实现响应式瀑布流,可能导致ios端异常闪退。
* [增强]x-waterfall增加isResize属性用于关闭和开启响应监测能力。关闭后不会跟随尺寸响应。
* [新增插件]x-svg-u,用于显示svg图片或者图标.
* [修复]markdown动态渲染,追回内容时不会渲染.
* [修复]upload-file在ios被上传时会被执行两次.
* [优化]x-table标题和单元格的style中增加一个align属性,用于控制元素对齐方式(用自定插槽也可以自己实现并完全自定)flex-start,center,flex-end
* [优化]input增加focusBorder及全局配置增加inputFocusBorder来控制输入框聚集时候的边框样式来显示高亮.
* [修复]pull-refresh组件如果model=listview时内容没有切换为list-item相关.导致长列表性能差.切换为这个模式的时候你们的布局子项必须为list-item
## 1.1.3 (2024-09-15)
* **重要:本版本仅支持hbx sdk 4.27(群测版本或者4.27+) 0910,以下的sdk版本不支持使用.**
* [优化]x-drawer,适配无安全区域时的间隙(主要是web).
* [修复]file-s插件由于安卓(sdk 机制 原因)共享进程的原因,导致file-s结果集可能始终在上次页面中.导致离开当前页面后,再进来选中的结果执行不在当前进程内.
* [修复]date-view,picker-date初始值设置时可能与选中值不符,经format值也与初始值不符.
* [优化]x-image不再采用接口getImageInfo返回的缓存地址,由于官方迟迟无法修复bug,故采用回原始路径(可能会造成缓存与实际加载时触发两次加载,具体未测试.)
* [修复]uvuePx.min.css压缩工具压缩后丢失了条件编译。
* [优化]button增加了fontWeight属性,默认normal
* [优化]x-tabs添加了动态插槽,用于自行定制自己的标签样式.
* [优化]x-taost-s修改了ios上下对齐样式.兼容4.2.7
* [优化]x-loading-s兼容4.2.7
* [修复]x-mlkit-scannig-s ios异常。
* [修复]x-file-s在安卓端重复创建实例.
* [修复和优化]x-tabs增加属性moveCount,用来给你自行控制选中时的位置偏移个数,以满足你们需要选中项居中的需求,同时修复可能数据超多时,会被遮挡的问题。
* [修复]x-switch在ios端如果出现在弹层内可能异常的问题(sdk底层导致)
* [优化]x-runstart-logo,ios端将弹层注入到了app根节点.这样不管跳转到哪个页面弹层始终在最前面.
* [新增插件]增加消息提示插件x-tips-s
* [兼容]x-mqtt-s对最新的4.27进行了回调适配
* [兼容]x-network-change对最新的4.27进行了回调适配
* [更新]echart.min.js更新到了5.5.1请注意更新并复制hybrid目录到你的目录中。
* [更新]图标库已经更新到remixicon 4.3.0
* [试验]x-image增加属性lazy用于在scorllview根节点的页面进行懒加载,不可视范围内的不显示.请仅慎使用,不可在list-view中使用.
* [优化]键盘系列组件增加了属性hold用于是否保持键盘不收起.
* [优化]为了切实解决原生节点过多造成drawer阻塞动画问题,我增加了个lazy属性用于内容懒加载,参考大厂的做法,动画界面优先,内容延后展示.此属性默认是关闭的.如果你节点内容复杂建议开启.
* [优化]x的成熟,我已经放开了form,radioGroup,checkGroup内仅允许它的直接子节点,现在可以在这些父节点内随意嵌套其它组件进行更自由的布局.但不建议嵌套太深,影响性能.
* [优化]x-float-button在web端增加事件冒泡阻止.
## 1.1.2 (2024-09-11)
* **重要:本版本仅支持hbx sdk 4.27(群测版本或者4.27+) 0910,以下的sdk版本不支持使用.**
* [优化]x-drawer,适配无安全区域时的间隙(主要是web).
* [修复]file-s插件由于安卓(sdk 机制 原因)共享进程的原因,导致file-s结果集可能始终在上次页面中.导致离开当前页面后,再进来选中的结果执行不在当前进程内.
* [修复]date-view,picker-date初始值设置时可能与选中值不符,经format值也与初始值不符.
* [优化]x-image不再采用接口getImageInfo返回的缓存地址,由于官方迟迟无法修复bug,故采用回原始路径(可能会造成缓存与实际加载时触发两次加载,具体未测试.)
* [修复]uvuePx.min.css压缩工具压缩后丢失了条件编译。
* [优化]button增加了fontWeight属性,默认normal
* [优化]x-tabs添加了动态插槽,用于自行定制自己的标签样式.
* [优化]x-taost-s修改了ios上下对齐样式.兼容4.2.7
* [优化]x-loading-s兼容4.2.7
* [修复]x-mlkit-scannig-s ios异常。
* [修复]x-file-s在安卓端重复创建实例.
## 1.1.12024-09-04
* 【请务必使用hbx sdk4.26+ 使用本组件库,不能低于此版本,否则部分组件异常】
* 【4.26sdk bug预警】我已经提交给官方修复:[issue](https://issues.dcloud.net.cn/pages/issues/detail?id=7832),会导致我的轮播套了图片后失效。解决办法是套了image,或者x-image的
* 需要在标签上写style="pointer-events:none"来屏蔽Image上的事件,通过swiperClick事件来捕捉。
* **非常重要**源码用户和加密用户在main.uts中的引用安装方法已经改了,请使用4.25+的用户立即查看文档修改
* ```原源码用户:import {xui} from "@/uni_modules/tmx-ui" 需要改成 import {xui} from "@/uni_modules/tmx-ui/index.uts"```
* radio未选中值改成了''
* [重要修改]x-camera-s扫码插件, openCameraApi接口调用增加了第二个onlyCamre是否仅使用相机识别,不通过相册识别.请使用时传递此参数做好向下兼容,不传第二参数调用会报错.
* [修复]x-picker-item组件在复杂的嵌套弹层节点深时,宽会丢失原因不明,目前已经固定了宽.
* x-checkbox-group类型标识错误
* x-navbar静态时黑夜设置为透明时,首次不透明。
* x-radio,checkbox使用了安卓sdk不能使用的===导致可能选不中。
* x-text正则有问题,现已修复
* x-upload-media为了解决手势冲突问题,排序改回长按启动排序功能,一是为了防止手势冲突,二是解决容易误解拖动排序.
* x-slider-double,x-slider不知道从哪个版本的sdk开始就不行了,现已在4.23中适配
* dropdown-menu优化背景遮罩位置设计在菜单下方,当菜单想要透明时,可以避免遮罩在菜单底部.
* [新增]金额栅格 xMoney组件,用于金额格式化和显示,完整的保持精度避免3.99 = 4的情况。
* [新增插件]x-openweb,打开外链(使用默认浏览器打开)
* [新增组件]x-link可以打开应用页面或者外部链接.
* x-share-s插件解决uniapp项目无法在安卓上分享本地图片资源问题,同时兼容uniapp ios分享的问题。
* [新增插件]x-call-phone,拨打电话
* [新增插件]x-runstart-logo,用于在appvue中直接显示该插件以展示启动logo图,可以自己定义背景颜色,logo图,版权声明等属性,并配合我的xModalS以达到隐藏协议声明前禁止访问应用的布局.
* x-keyboard-number新增digit用于是否开启小数键盘或者整数键盘。
* x-navbar改成了setup模式组件。
* x-loading增加了hideText属性,用于隐藏文本。
* [优化]waterfall瀑布流增加了响应式处理,可以响应屏幕尺寸,如果你的图片需要高响应,建议你用rpx单位。
* [新增组件]x-slide-verify,滑块验证.
* [升级插件]x-mlkit-scannig-s安卓和ios升级为全屏扫码,ios支持相册选择图片识别.并兼容安卓5.0+
* [优化]扫码插件相册的识别率,升级ios扫码也支持相册 识别 码,至此三端安卓,ios,web已对齐.
* [新增插件]x-camrea-u相机组件,用于实时捕捉画面(止前功能相对简单,但至少可用),三端支持
* [修复]x-tabs中的lineColor不生效的问题.
* [新增插件]x-crypto-s 常用加密插件,三端可用.
* [修复]x-steps vmodel变量无效的问题.
* [优化]x-steps 新增属性reverse,用于状态反向展示.如果你先前了竖向的,要用回之前的倒序你需要设置reverse为true,请做好兼容.
* [优化]x-collapse现在开始已经支持了自适应高.
* [优化]x-navbar新增属性llWidth,来控制左边宽.之前是lrWidth控制两边宽的,以达到居中效果.
* [新增组件]x-weekbar,时间周组件,样式丰富,非常精美,能够适应不同设计要求.
* [优化]x-more 现在支持响应式,异步加载内容,自应内容高动画.
* [优化]x-keyboard-number键盘新增modenumber,password模式,可以以纯数字符串形式当作密码用不会限。
* [优化]x-calendar-view增加属性:hideHeaderdisabledSwiperdisabled,同时增加了切换日历的方向:左右或者上下(无限循环)。
* [新增组件]x-alert 警告提示框,样特别丰富且非常精美。
* [新增插件]x-network-change 用于监视网络状态变化,及时知道当前是否有网络连接.
* [更新]x-input新增同步官方的属性inputmode,holdKeyboard
* [优化]x-snackbar兼容响应式尺寸显示.并修改动画,视觉效果更为快速流畅,同时postion开放了6个位置,具体见文档.
* [优化]x-popover 修改动画,视觉效果更为快速流畅
* [优化]x-drawer增加了offsetTop用于偏移顶部的偏移量,zIndex用于弹层的层级。
* [优化]x-loading-s,现在把最小高100设置成了config.size,因此你可以设置弹层小点,但你同步得把iconSize弄小,不然不会正方形,因为高是最小高。
* [新增库] xTween库增强动画,试验性质必须4.26+ sdk版本
* 【修改优化】x-animation采用xTween库制作动画,删除了属性revert,因为name已经有成对的动画,可自行修改name反向播放。并且动画已由css动画改成了刷新回调动画。
* [优化] x-overlay 采用xTween库增强动画,之前这个组件是没有动画的.这是一个试验性质,如果出现问题,请及时联系.
* [兼容] x-barcode兼容4.26+ sdk,并修改了样式,文本底部对齐线条两端.
* [优化] x-image添加了模拟的click事件,原生的click事件已抛弃,原因在于原生 的click有移动事件中容易误触发。
* [优化] x-modal-s修改了代码,兼容uniapp应用。
* [优化] x-icon因为css动画的不可控导致一些需要旋转时部分机型异常。现在已经完全用xTween插件替代了动画,应该不会有异常了。
* [修复] x-image,属性mode写错了。
* [修复] 颜色对rgb的校验错误,导致不能设置rgb颜色。
* [视觉优化]x-button和x-input把圆角默认统一改成了12,同时主题配置中给Input配置了统一修改圆角的属性,方便修改默认全局的输入表单圆角.
* [视觉优化] x-cell之前改造时rpx没改成默认的px样式不统一,并且crad时,左右间隙改为了14相比增加2px
* [优化] 优化了主题颜色,修改一些组件的默认样式及动画细节,使得组件更为精致.
* [优化]x-tags增加对skin属性的动态响应。
* [优化]x-upload-file增加上传执行的函数beforeUpload,用过上传前的过滤或者验证类的需求场景.
* [优化]x-form-item增加showError属性用来控制和隐藏底部的错误提示。
* [优化]x-dropdown-item增加render属性,用于绕开一些组件触发sdk bug,具体使用说明见文档注解.如果你没有问题不用理会本属性.
* [优化]x-image-group组件因官方的函数uni.previewImage有bug导致预览时索引异常.
* [优化]x-file-s增加了filter类型过滤器,对应x-upload-file也增加了相应的filter,但限制和注意事项请看文档使用,这是一个权衡后的属性有限制和注意事项,请着情使用.
* x-upload-file增加了beforeUpload用于手动文件过滤,x-files-s,三端拉齐了一些函数,都需要执行remove,clear函数来清除和移除某文件.
* [修复]x-loading横向向没对齐
* [修复]x-tabbar项目选中中设置disabled没起效
* [新增组件]x-avatar-group,头像组,设置丰富,可以文本,图片,占位
* [新增插件]x-mqtt-s,用于连接webstockws,wss,订阅和推送消息(ios wss/tls需要证书)
* [优化修复]x-setps组件竖向可能的异常。并修改了相关视觉效果。
* [优化]x-radio-button增加了响应式布局,优化了圆角细节。
* [新增插件]x-window-grey,让当前页面变灰,如果想任意页面变灰,可以通过minx,然后在onShow中执行变灰操作.
* [优化]x-pull-refresh,由于listview,scrollview在最近的版本中ios已经问题不大因此开放了下拉刷新自定内容,同时新增了mode用于内部采用listview还是scrollview来渲染.
* [插件增强]x-camrea-u组件新增ref函数chooseImage,相册选取识别码.
* [优化]x-upload-media增加属性beforeComplete,用于每个图片成功前修改类型和结果值,达到人为判断是没权限还是什么让上传成功或者失败。
* [优化]x-slider及x-slider-double值的转化为1为单位,过滤掉小数
* [优化]x-table增加了rippleColor,和rowCellColor等间隔背景及非间隔的默认行背景。
* [新增库]xCavas预览版本
* [修复]x-drag最后一位横向移动时可能异常.
## 1.1.02024-08-1
* x-mlkit-scannig-s升级支持IOS12+扫码,WEB PC、H5实时扫码识别(无扫码点仅单个识别)
* x-mlkit-scannig-u升级支持IOS12+扫码嵌入式页面,同时支持WEB PC、H5实时扫码识别(无扫码点仅单个识别)
* x-qr-u 更新已经支持了web/h5,已经兼容了三端,想要快速的生成码,请使用本组件,而非webview版本的x-qrcoder。
* 【为了你能支持web,h5支持实时扫码】请同步更新static目录js文件。
* **重要: checkbox,checkboxGroup可能会影响你们的业务,id修改成了string类型**,另外js中""==0=="0"==false,因此你们的字符id不要以0开始,因为安卓不支持===
* 删除了插件 toast,loading 的原生行高属性,以兼容低版本安卓。
* x-float-drawer,删除了 preventDefault 属性以 免部分安卓机丢失事件
* x-picker-selected 在安卓端加大了 lazy 的延迟量,并增加 lazy-duration,设置为 100ms.用于部分低性能手机可能效果不明显的问题。
* x-swiper,事件冲突问题修复
* x-picker-date,默认值校验问题修复
* x-search,改善了动画视觉效果。在安卓由于input的sdk vmodel 5ms vue模型数据响应的问题会导致安卓丢字符,现已修改赋值方式绕开这个vue模型,解决安卓丢字符问题。
* picker,picker-city全部放开了默认文本回显示,由于要大量递归计算回显,表单过多,时请自行考虑性能问题。
* 修复switch-slider可能的事件丢失与手势冲突。
* x-navbar现在支持了动态悬浮时的动态渐变背景切换,为了实现此功能,布局已做了修改已兼容安卓。同时增加staticTransparent用于指示在静态顶部时,是否透明背景。请各位做好兼容修改。
* 控制背景时变得更强大。
* x-request,类型拼写错误更正,请大家更新时如何报错记得字母内更新下加个q
* code-input,新增了place-shape属性用于组件内占位闪烁的动画效果,空值可去掉。
* xDate增加了个函数方法fromBetweenLongTime用于返回离指定时间多久,比如:刚刚,1分钟前。。类推
* xDate增加了个函数方法:diffTime,与目标时间相差多少个单位比如两时间内共多少天,多少小时之类的。
* swiper增加showDot属性,同时修改了一些其它问题。
* 新增组件 x-date-view,使用方法与 x-picker-date一样,一个是内嵌的一个是弹层式的。
* xDate新增方法getQuarter,用于获取本年或者本时间所在的季度。
* x-calendar-view日历组件修改了视觉效果,并兼容了宽屏和响应式屏幕。现在app端也可横屏竖屏都完美了。
* 新增组件x-calendar-multiple日历组件,支持多选和范围选择,支持限定选择的天数。
* 修改了xColorUtil下的暗黑计算,影响了主题为thin的视觉效果。
* x-picker-selected增加了list监听回调。
* checkbox,checkboxGroup可能会影响你们的业务,我调整为字符串了。之前是number,主要考虑到id多以字符为主,而且数字没有实际意义,字符比较好命名。由于不支持联合类型,暂为字符。请及时修改
* ximage组件改写了加载逻辑使用了sdk最新的api修改以适应原本的sdk宽高的缺陷。(仅安卓和web有效,ios还是以前的)
* 修复x-loading-s,x-modal-s,x-toast-s在低端安卓几下不能关闭的问题,目前已经调至可支持到安卓4.2+版本(含),x-loading-s新增了size属性用于约定宽。
* xicon兼容新版本sdk4.23
* xbutton兼容了pc状态指示。
* x-modal增加了属性zIndex,用于调整层级。调整为默认1000
* 修改了x-skeleton启动动画的延迟量,不然有可能造成低版本的安卓动画无法启动。
* x-switch-slider布局菜单样式修改了,修复部分的安卓机型菜单宽度无法设置。
* x-form修复指针空异常。由于我是采用内部字段diff比较,以提高比对性能,但有时表单字段可能存在变化,可能之前有,后面没有,造成了指针为空。
* x-picker,优化当用户提供的id值为非法值时,进行强制纠正为默认第一个值。
* x-uploadfile因为官方的css不支持word-break,导致一些文件名全是英文和数字的无法断行。目前app采取1行省略号的方法。
* x-upload-media增加了sourceType选项。
* x-share升级支持IOS支持分享文件,视频,音频,图片到微信/QQ
* [新增组件]x-pagination翻页器,用于列表翻页展示使用。
* 修复image-group预览属性无法关闭
* x-upload-midia添加了压缩属性,同时修复了自动上传关闭后,无法通过ref手动上传。
* x-modal增加contentPadding,控制内容区域的间隙。
* x-qrcoder组件更换了qr库,请更新时记得更新hybrid目录。如果你是使用的x-qrcoder-u无需要变动.
* x-image,修改了暗黑时的相关样式为统一层级关系
* x-form-item增加了labelAlign对齐属性
* x-upload-media添加了上传按钮的位置可以放置在前或者后,同时修改了排序的视觉动画,改成了拖动修改顺序。
## 1.0.92024-07-15
* 请务必使用hbx 4.23 正式或者 alpha 版本。 不向下也不向上兼容。更新请重新复制 hybrid 目录 覆盖老的目录。
* [重要] **自带一些工具类库之前的xDate,xRequest,xAnimation**等等,请不要再使用原有的导入方式,请统一使用
* import {xDate } from "@/uni_modules/tmx-ui/index.uts"这种方式导入见demo。原有方式,在本版本中还未废除。预计在1.1.0中不再支持。因此你尽快改正。
* 因为我要支持加密模式。
* [重要优化] x-indexbar重构了,为了应对上千上万数据索引流畅废除了子组件indexbar-item,只保留了x-indexbar,并且使用动态插槽渲染数据,请根据demo用法重新调整你的页面.
* [新增]在工具类库能增加了tool,可以通过上述方式导出.比如防抖函数:tool.debounce,请见文档页面查看具体使用方法。
* [新增] 配置丰富限制少,x-loading-s用于在任意位置调用覆盖全屏的加载组件,完全可用来替代官方的uni.showLoading尽展现你的品牌。
* [新增] 配置丰富限制少,x-modal-s用于在任意位置调用覆盖全屏的加载组件,完全可用来替代官方的uni.showmodal尽展现你的品牌。
* [新增] 配置丰富限制少,x-toast-s用于在任意位置调用覆盖全屏的加载组件,完全可用来替代官方的uni.showToast尽展现你的品牌。
* [修复优化] x-upload-meidia组件预览索引/url有问题,由于sdk bug导致我的也有问题,改成了索引
* [优化] x-modal和darwer一样增加了延迟属性,解决ios渲染问题(sdk 底层导致,需要相关设置绕开)
* [修复] x-tabbar修复点击不跳转。
* [修复] 瀑布流进行了重构和性能优化,请务必对照demo重新使用,使用了setup方式,用户给vip用户一个setup组件使用和编写的一个示例参考。并增加了gutter间距属性。**请更新时对照demo重新优化和布局**
* [修复] switch-slider,float-button点击事件的问题。
* [修复] x-oveflay,同之前的弹层一样的问题在ios端由于底层渲染问题,导致可能打开后无法关闭。
* [优化] cell增加了labelSize属性.
* [优化增强] drawer,增加了属性:widthCoverCenter,用于适配pc,仅上下生效,让内容居中显示,不会铺满屏幕。
* [优化增强] picker系列组件,适配pc,不会撑满屏幕居中显示。
* [优化增强] x-keyboard系列组件,适配pc,不会撑满屏幕居中显示。
* [优化] slider优化了视觉
* [hybrid 修复] 修复若干插件的依赖web-view的hybrid中的jsbug请大家一定要更新这个文件夹,更新时,删除原有。直接复制本目录。
* [增强] xMarkdown富文本已经增强,高可以根据内容自适应。
* [增强] xMarkdown富文本已经可以进行内容的基础编辑,获取和设置请看demo如何使用。
* [优化] x-popover优化了弹出动画的流畅性。
* [适配] x-sign-board适配了4.22 sdk。不向下兼容
* [优化] x-button,x-tag 在skin=text模式时,修复在暗黑下的视觉效果。
* [优化] 修改x-radio-button默认字号大小。
* [适配] x-circle-progress 适配4.22 sdk的改变。不向下兼容
* [优化] x-keyboard-number,优化视觉效果,扩大确认按钮大小。新增属性:max,用于控制输入最大值,如果超过此值将无法输入。
* [新增] x-skeleton 骨架屏加载组件。自由度非常高。
* [优化] 调整了x-grid默认的高,提高视觉效果。
* [优化] 调整picker-data,picker-time个位数被充为两位数。首位填充0
* [优化视觉] x.drawer,x.picker系列的视觉样式。
* [优化视觉] x-input,修改密码时的图标间距。
* [新增] x-code-input,验证码输入框
* [适配] x-steps适配了sdk 4.21,因此不向下兼容必须4.21+
* [优化插件] x-file-s文件上传组件新增maxFilseSize,以此来过滤超过此大小的文件。且不被允许上传。
* [新增] x-scrollx组件,请于滚动指示.
* [优化] x-upload-media,新增autoStart和ref方法uplaod用于手动控制上传。
* [优化] x-navbar增加属性linear-gradient,用于定义渐变背景,使用方法见demo或者文档
* [优化] x-tabbar增加属性linear-gradient,用于定义渐变背景,使用方法见demo或者文档
* [优化] x-picker-selected增加属性lazyContent,以便在内容超多时,视觉效果更为流畅。
* [优化] x-input统一设置全局的光标颜色为主题色
* [优化] x-picker-date增加属性lazyContent,以便在内容超多时,视觉效果更为流畅。
* [优化] x-picker增加属性lazyContent,以便在内容超多时,视觉效果更为流畅。
* [优化] xradio,xcheckbox增加属性iconSize允许设置中间小图标大小。
* [优化] x-darwer,添加了属性swiperLenClose,默认为0,如果大于此0时,会触发手势,下下或者左右滑动下手势距离大于此值时会关闭弹层,建议要打开时设置为80-100比较合理
* [优化] x-picker,针对model-str,如果空值,将内部首次递归渲染回显文本。如果你后台返回,就不会计算。因此如果对性能有要求的请务必让后台在首次显示时先回显文本,这样会提高渲染性能。
* [修复] x-table,表宽计算有误。
* [优化] x-dropdown新增属性offsetTop,用于自己对菜单顶部的偏移和让位。
* [优化] x-picker放开一个原本的单位属性:cellUnits它和picker-view是同等用法的。用于给选项在顶部显示单位。
* [新增] x-keyboard-idcard 身份证键盘,会自动校验输入值,解析第二代身份证号是准确并在事件中携带校验值。
* [新增] x-keyboard-car 车牌键盘
* [优化] x-dropdown-item 允许动态更新标题字号颜色等。
* [优化] 调整了一些组件的默认圆角值
* [优化] 调整了button的默认round为空时,根据 默认的size来调整
* [优化] xEchart图表 在原有的ref函数cahrtActions增加第三个参数fun用于在调用click时的回调,请结合demo示例使用。
* [重要的修复] x-picker-selected我改变了vmodel类型改成了any[],因此如果你是vmodel来绑定数据不会有任何变化。但如果你是confirm来读取的会有问题你读取到地ids是any[]之前是number[]因此会报类型不匹配你应该读取到ids后再as number[]或者string[]来兼容
* [优化] x-form提供了一个ref函数valid,具体见文档,用于校验指定字段,空数组校验所有。
* [优化] x-search,新增了border及darkBorderColor属性,请根据文档使用。
* [优化] x-watermark没有最长边计算错误,导致宽屏时可能覆盖不全。
* [优化] x-cell增加了属性bottomBorderInsert,用于让底边线是贯穿还是向右靠齐。同时添加了avatarRound左侧图标/头像圆角,默认6
* [修复] x-collapse可能的逻辑错误
* [swiper] 重构,减少问题,并且适应了 pc 端,现在可以用鼠标滑动了。 并且新加了多种播放模式。
* [兼容] x-popover兼容4.23 sdk 的 bug
* [优化] x-picker-date,兼容用户可能的错误值,比如开始和结束设置后,在 modevalue 时又提供个不在范围内的值,造成混乱。
* 全新的文档更新啦 支持搜索 组件,标注源码位置方便查找。
* 一些其它的已知问题。
* 修改了 demo 示例
## 1.0.82024-06-07
* **本文件更新可能较多,请根据git拉取的代码比对修改,必须 sdk 4.19版本使用**
* **新的版本要求你重新复制hybrid和theme.json文件到根目录**
* [新增组件] x-markdown,允许markdown标记渲染和html文本渲染,编辑功能未开放。
* [新增api] 新增原生组件x-vibrate-s适配了ios,安卓,震动手机功能
* [新增api] 新增原生组件x-wechat-s适配了安卓,ios未适配,通过本应用拉起微信客户端指定的小程序,插件见源码。
* [新增api] 新增原生组件x-share-s适配了ios,安卓,用于任意文件文本等分享到其它应用。具体见:[分享功能](https://ext.dcloud.net.cn/plugin?id=18494)
* [增强] 原生组件x-file-s已经兼容适配了ios
* [新增组件] x-view-tofull动态全屏,效果见demo,比较酷炫
* [修复]xform校验兼容可能的类型错误,input请按照demo只能是string类型,但type可以写官方的number,让用户只输入数字。但类型需要定义为string
* [修复]所有弹出来picker由于官方本身的sdk机制推出的pageJson,我适配时出了bug,导致弹出后,再关闭导致页面可以下拉刷新了。
* [优化增强] x-form,由于官方不断完善,本组件demo已经更新对于数据绑定的vmodel数据模型已由原来的utsobjson改为了object as any,因此你前台的数据模型可以使用自己定义的结构或者utsjsonobj
* 已大幅度优化了数据模型的绑定,之前的限制已经解除,更接近了原生太的绑定了。
* [优化] xnavbar,属性activeBgColor,默认设置为空了,默认取全局的color,如果自己填写了就以填写的为准。
* [优化] input左图标颜色,iconColor,默认设置为空了,默认取全局的color,如果自己填写了就以填写的为准。
* [修复] xformitem demo演示页面属性showBottomBorder拼写成了showBottomBoder写错了,同时属性判断有问题。
* [优化] demo首页不再使用官方的listview,这个官方组件表现的不尽人意,兼容性有待考验。
* [优化] darg,在app端增加了启动排序时触发手机震动,并改成了长按触发。
* [优化] switchSlider,让滑动页面和左滑不冲突,体验更良好。
* [优化] xRequest请求失败时把statusCode赋值为errCode。
* [修复] cell的hover效果消失了。
* [优化] xnavbar,添加了showNavBack属性,是否隐藏返回按钮。
* [优化] x-float-drawer增加了scroll容器的事件协商,不会干扰滚动内容。同时增加了头点击时展开或者关闭的功能。
* [优化] x-tabs子项目超多进,会让原生的安卓无法执行节点查询系sdk bug,我已经作 了兼容来绕开该问题。
* [优化] x-input,添加了showChartCount属性用来控制是否要显示字符统计。
* [优化] x-image,改动较大,主要是做了响应式处理,因为image原生是不支持%,auto,我是模拟支持了,因此如果出现异常请把你的布局代码提交到conding.net上
* [修复] x-calendar,修复了在web平台点击本日时,不会选中当日的bug.日历格式把个位数补充为0
* [优化] xReauest增加了一个参数successStatusCode,用于定义服务器自己成功返回的code,默认是200
* [优化] ios,web,前台和后台切换时,可能导致轮播的定时器异常混乱,导致轮播图片异常。已作兼容。
* [优化] xuploadMedia添加了completechange事件。
* [优化] x-cell添加了bottomBorderColor,用于自定边线颜色。
* [优化] x-xCascader标题过多时增加了左右滚动。
* [优化] ios端暗黑弹层调整,因兼容sdk的 pullrefresh导致暗黑切换异常。
* [优化] radio,checkbox适当增加了4px大小 。
* [优化] switch调整了其宽和高,默认size高与input对齐,排版时更美观。
* [增强] x-image-vip增加 属性:isScanQr,开启后,可长按图片识别二维码,类似微信的长按功能。(普通授权用户无法使用)
* [修复] x-notice,如果切换到下一页时,由于sdk在web回收机制导致返回时功能失效。现已修复。
* [优化+增强] x-navbar添加了事件fiexdChange,与顶部与差比0-1,0表示在顶,1表示超过了自身高。使用方法见demo示例,展现了元素之间差值计算
* [优化] x-formitem,x-form增加showLabel属性,用于隐藏标题。
* [增强] x-files-s,x-upload-file对应的原生文件和组件文件选择器,已经适配了ios系统11+(含),现在三端已经对齐可在不同平台选择任意文件上传了。
* [增强] x-ehcart 图表新增ref函数cahrtActions,用于执行echart对象任意函数方法,具体见文档。同时适配响应式尺寸。现支持横屏和和竖屏了。
* [优化] x-sign-board,现在适配响应式尺寸,支持横屏和竖屏切换。
* [优化] x-upload-media,现在适配响应式尺寸,支持横屏和竖屏切换。
* [优化] x-table,支持具名指定单元格插槽及行插槽,用于复杂的布局(使用插槽时,不可排序),现在你可以在单格内放置按钮,输入框等一切布局元素了。
* [优化] x-image-group,增加了round圆角属性。
* [优化] x-popover,增加了打开动画.
* 其它未记录到的变化,请根据git对比差异。
*
## 1.0.72024-05-07
* **升级说明:改动较大,请按照文档或者demo重新配置相关插件路径**
* 1.已适配IOS端
* 2.已全局适配字号设置,可全局动态修改字号大小(方便适配老人版本)
* 3.已适配暗黑效果(IOs目前存在点问题待官方修复)
* 4.已自动适配横屏,竖屏,宽屏,电脑端,可下载demo自动旋转屏幕查看效果及打开电脑web查看效果
* 5.部分原生插件适配了ios端
* **至此 安卓,WEB|H5IOS多端已完成适配**
* 接下下我将适配剩余的插件到ios,web端,及维护,细节优化,及扩展更多插件及功能。
-------------
* 【重要】[xOcrS离线识别](https://ext.dcloud.net.cn/plugin?id=17557)文本识别插件已经支持ios12.0(含)+
* 【重要】[xQrU码生成](https://ext.dcloud.net.cn/plugin?id=16470)已经支持IOS9.0(含)+,高效,批量无延迟生成码
* [新增] 新增文件选择器[在此围观](https://ext.dcloud.net.cn/plugin?id=17624),tmui4.0种子用户已经附赠在源码中(无需购买),请更新版本后直接按照文档引用即可。如果需要查看效果请下载本应用demo,导航到原生插件栏目体验。
* [新增] x-vibrate-s震动api,让手机震动(安卓,ios),web不支持。
* [扫码插件] 原先的scning-u改成了嵌入式扫码可自行像页面布局一样装饰你的相机扫码功能。
* [新增扫码插件] 原先的scning-s为api试扫码功能与上面的嵌入式不一样。这个是通过api方式调用。可以在任意Uts位置调用识别。
* [新增] x-waterfall瀑布流组件。
* [增强] x-table增加一个属性multiRowFloat用于是否开启多行固定表头的功能(滚动指定行时自动吸顶在顶部)。
* [增强] x-rate,增加属性half,打半星的功能。允许打半分。
* [优化] x-picker-selected,新增modelStr回显已选文本,请使用v-model:model-str来同步显示。并修改了样式。
* [优化] x-slider-menu添加属性showScrollbar是否显示滚动条
* [优化] x-form表单的默认校验兼容性(兼容sdk的bug)
* [修复] x-request请求库中的某个bug问题。
* [修复] xDate时间不能早于1950年的问题。及小时的问题
* [优化] xtabs支持屏幕响应变化。
* [优化] x-slider-tree调整,返回时应该不触发change的。
* [修改] x-float-button关闭了首次的默认动画效果。
* [修复] x-image,在计算占位时可能的设置宽高有问题导致意外的为0宽(缓存加载时安卓端易出现。)
* [修复] xtabs在安卓及ios端时(web正常),首次进入渲染修改底部线条位置和宽时不会有效,经过测试界面上是有相关宽和属性但就是不显示可能与sdk或者其它因素导致。目前通过vif切换刷新解决。
* [优化] x-tabbar样式优化和修改。
* [优化] x-picker-view安卓端适配4.13sdk
* [优化] x-search增加了取消显示和隐藏的缩放动画
* [优化] x-popover在h5端可能的位置偏移。
* [优化] x-more增加了click事件,用于点击打开/关闭是触发,方便写事件是否允许打开查看详情内,比如付费解锁内容/登录查阅内容场景。
* 其它的修复优化
## 1.0.62024-04-10
* 【重要】更新后请使用4.06+ sdk正式版本,不可使用4.11 dev或者4.11 aplpha版本(4.11 bug 太多官方在修复中)
* 【重要】新增xOcrS Api插件用于图片文字识别,支持网络,本地,相册,拍照识别功能。[在此围观](https://ext.dcloud.net.cn/plugin?id=17557)
* tmui4.0种子用户已经附赠在源码中(无需购买),请更新版本后直接按照文档引用即可。如果需要查看效果请下载本应用demo,导航到原生插件栏目体验。
* [优化] x-floatDrawer 在safri web浏览器上可能的尺寸不正确。
* [优化] x-picker,x-picker-view添加了model-str用于选中项回显显示文本。同时修复了历史遗留问题(4.02之前sdk的问题导致)
* [优化] 调整了xbadge的样式。
* [优化] xrequest把参数showLoadToastshowSuccessToastshowErrorToast提取了全局配置中,可通过静态方法setShowToast统一设置,使得全局请求统一行为,局部可以覆盖全局。
* [优化] pickder,picker-data,time等添加confirm事件,超长文本断行问题。
* [优化] inputTexarea,把默认的confirmType改成空值。否则会不能断行。
* [修复] formItem属性showBottomBorder并写错误已改正,已使用的请改下,看文档复制下。
* [新增] 新增xTable表格组件。
* [优化] 部分组件类型统一放置在interface中,方便用户自己导入使用类型。
* [优化] x-sticky之前兼容老的sdk后,忘记在新sdk中改回
* [优化] x-cell,rightLabel做了限制修改,同时添加了title插槽
* [新增] x-devtool开发组件,主要是记录请求的一些记录。开发者可以将此组件预埋在某个页面,通过后台请求是否打开 和显示开发模式。这样在app发布后,通过远程调试也可打开请求记录等一些实质的数据。
* [优化修复] x-float-draweronlyHeader为true后在pc端可能造成拖动不了。
* [优化] x-float-button兼容pc电脑端。同时改进了设计窗口尺寸方案。
* [优化] x-tabs,增加了titlePadding两边文字间隙属性。同时修复之前兼容旧sdk问题,本次更新不兼容旧sdk请4.06+
* [新增] x-row,x-col组件,用于常规布局使用。
* [新增] 城市选择器 xPickerCity
* [优化] ximage添加了round圆角属性。
* [优化] xinput增加了inputPadding属性用于控制输入框的padding值。
* 其它代码的优化和修复。
## 1.0.52024-04-03
* [优化] x-drag list类型改成了UTSJSONObject[],这样可以不限制你的list类型。
* [优化和修复] action-menu,新增cellClickClose属性可以控制选项是否能关闭弹层。同时修复设置了maxHeight时,头尾不见了。
* [新增] 增加了xRequest 请求库
* [优化]switch-slider,优化了误触时的左右滑动距离。
* [优化] 时间类组件,默认格式化后给加上了10进位占位0,并且默认如果为非正常时间值(比如空,或者 不合规范的值)将不再同步更新掉,而是不操作和纠正。
* [优化] x-tag优先height,而不是size,因此默认size为空
* [优化] modal,drawer加了滑动阻止,之前忘记加了。
* [优化] x-icon增加属性duration,动画时间的控制。
* [优化] xInput,补充了一些官方的事件和属性,增加了texarea文本域
* [优化] xSwiper,默认把动画时长 改小了。如果默认为1数量,禁用自动轮播。
* [增强] xSwiper,添加了space属性,用于控制两边露出的距离,形成卡片试轮播。
* [新增] xcard组件
* 其它代码的优化和修复
## 1.0.42024-03-28
* **重要:升级后请务必uniapp x sdk4.05+版本**这个版本修复了我提到了查询延迟导致部分组件兼容影响视觉感观。
* **组件适配了web端,也就是说可以用来写电脑端,也可以写移动端。**
* [新增] 新增组件x-drag自由拖拽排序组件,自由度高布局不限制。
* [新增] 搜索列表组件picker-selected,可支持本地数据搜索或异步加载列表搜索
* [xAnimate] 新增属性isDescPlay,所有按顺序加入的动画元素,将顺序执行动画,而不是一起执行,
* 新增enterFrame渲染回调函数,可用于实时动画操作或者绘图操作。类似canvas渲染,可作一些动画交互,视觉体验的增强
[效果预览](https://xui.tmui.design/h5/pages/index/xanimate)
* [rate] 增加了交互动画
* [overlay] 在禁用遮罩关闭时,增加了交互动画弹跳效果以视觉方式让用户知道无法关闭。并支持了嵌套,需要sdk4.02+
* [x-upload-media] 优化长按拖动图片排序时在web间易误触发预览图片。
* [switch-slider] 暂时关闭默认插槽数据传递,原因是4.04bug导致待官方修复放开数据。
* [indexbar] 优化了下时间段和样式,把节流时间调低。
* [modal] 在禁用遮罩关闭时,增加了交互动画弹跳效果,并支持了嵌套,需要sdk4.02+
* [x-action-menu] 样式调整,行加高,遮罩颜色 加深
* [x-sign-board] 兼容了pc浏览器,方便在pc端使用。
* [x-keyboard-number] 确认键改成了中文字符,不使用图标,方便国人。
* [增强] 所有弹出层等一些界面支持响应式屏幕,以便支持pc及宽屏ipad
* [优化] 优化indexbar性能
* [优化] 优化slider-double支持电脑,及修复意外的布局错误。
* [优化] 优化button,字号的判断错误,默认设置为空,取size的字号。
* [xDate] 优化日期处理的时间,提升性能,性能比对是能提高30ms左右
* [优化] x-calendar-view日历组件,提升了响应时间,比之前快50%,之前大概触摸到结束响应时间(如果遇到自定样式多时)大概在250-350ms之间,造成部分手机不跟手。优化后大概是60-110ms之间。
* 在此也提醒各位:数据操作中尽量不要使用findex,indexOfincludes等判断和筛选。应该使用基础的for然后break;
* 4.05的sdk发布,对应的作了一些兼容。
## 1.0.32024-03-18
* [非常重要] **本次升级需要要4.02+uniappx sdk支持,并适配了web版本。改动较多且较大,日志不反映实际更改,只列出重要的更改**
* [更新事项] 更新时,请注意同步更新hybrid目录文件。
* [优化] x-action-menu新增space属性,可以控制是否让两边和底部留下空白。默认为true
* [优化] x-sign-board删除了组件的引用,使用自己的事件。
* [新增x动画库] xAnimate ,核心功能:支持官方所支持的css属性动画,支持联合属性动画和单独控制,支持事件回调(运行时,结束时,开始时),它并非是css animation动画
* 而是我写的js动画,并且在web,安卓端采用原生的刷新机制(并非settimeout实现)因此保证了超高的性能优势。并且让你做一些交互动画时异常简单。效果请下载demo或者线上看web。进入首页
* 拉到底部进入xAnimate页面查看你的手机效果对齐流畅度。具体用法请参考demo页面使用及文档参数
* [增强] x-finger新增pinch捏合缩放,roate两指旋转事件,通过事件可获取当前的缩放和旋转信息。
* [新增] x-calendar,日历面板,app端使用canvas绘制,web端使用节点绘制
* [优化增强]picker系列组件,统一采用虚拟列表,因此可以支持大量数据,性能更强。
* [优化增强]极联无限级选择器,已优化使用虚拟列表,可支持无限大数据量,性能更强。
* [优化增强] sliderTree,列表全部改为虚拟列表,可支撑大量数据。
* [优化增强] uploadMedia上传组件,已支持拖拽排序,激活条件是长按图片且未在上传时激活排序。主要是方便产品的封面图顺序排列更为方便。
* [优化增强] drawer,增强disabledScroll属性,可以用来禁用(内容超长时)滚动,以阻uniappx sdk的自身bug:1,套input不会上推键盘,2,view move事件打断。
* [优化] pickview下pickviewitem里面有两个版本,目前使用的是官方的pick-view封装,如果觉得不好用,可以使用我自己编写的pick-view,只要内容互换就行,自编的可控度高。
## 1.0.22024-01-25
【新增】:x-mlkit-scannig-u原生扫码组件,支持多码识别。采用谷歌机器学习库识别速度非常快。具体见demo自己体验效果,也可以根据需求自行修改定制(当前交互类微信)[插件地址](https://ext.dcloud.net.cn/plugin?id=16468)
【新增】:x-qr-u原生生成码组件,与原有的x-qrcoder不同,qrcoder是webview实现的。本qr-u是原生库,效率流畅无比,支持13种码制格式。
修改了部分样式[插件地址](https://ext.dcloud.net.cn/plugin?name=x-qr-u)
【优化】:针对真机,修改改部分样式及字体大小等。
【声明】:当前sdk3.99 uniappx还不适合应用于生产,得让官方修复bug,下个版本应该有所好转。
**特别注意:带-u的是原生组件插件带-s是原生插件api,类目中x-core为原生UTs实现的基类**
虽然购买能获得源码,但原生插件类组件,要求门槛较高,购买可以边学习边生产使用,可自行研究修改。view组件是正常的应用布局使用。
## 1.0.12024-01-16
试用能下载源码,请直接微信找我要源码
## 1.0.02024-01-16
tmui4.0 xui 1.0.0初发版本号。约82个组件。
@@ -0,0 +1,694 @@
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick, onUpdated } from "vue"
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, setTextColorLightByDark } from "../../core/util/xCoreColorUtil.uts"
import { toFillMarginAr, checkIsCssUnit, getUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
import { XACTION_MENU_ITEM_INFO } from "../../interface.uts"
/**
* 内部使用:动作菜单项目类型
*/
type XACTION_MENU_ITEM_INFO_PRIVATE = {
iconSize: string,
fontSize: string,
iconColor: string,
fontColor: string,
icon: string,
disabled: boolean,
id: string,
text: string,
}
/**
* @name 动作菜单面板 xActionMenu
* @page /pages/index/action-menu
* @category 反馈组件
* @description 从底部弹出来的操作菜单。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xActionMenu" })
const i18n = xConfig.i18n
const proxy = getCurrentInstance()?.proxy ?? null;
defineSlots<{
trigger(props: { show: boolean }): any
}>()
const emits = defineEmits([
/**
* 取消时触发
*/
'cancel',
/**
* 点击遮罩事件
*/
'click',
/**
* 关闭是触发
*/
'close',
/**
* 打开时触发
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 等同v-model:show
*/
'update:show',
/**
* 项目被点击时触发
* @param {Number} index - 项目索引
*/
'item-click'
])
export type xActionMenuPropsType = {
/**
* 自定义遮罩样式
*/
customStyle: string,
/**
* 标题,请选择
*/
title: string,
/**
* 是否显示标题
*/
showTitle: boolean,
/**
* 是否显示关闭
*/
showClose: boolean,
/**
* 遮罩是否允许点击被关闭
*/
overlayClick: boolean,
/**
* 选项点击时,是否允许关闭弹层。
*/
cellClickClose: boolean,
/**
* 显示可v-model:show双向绑定
*/
show: boolean,
/**
* 显示取消按钮
*/
showCancel: boolean,
/**
* 动画时间
*/
duration: number,
/**
* 打开方向为上和下时的圆角
* 空值时,取全局配置的圆角。注意是取drawer的圆角,统一弹层的圆角
*/
round: string,
/**
* 弹层最大的高度值,默认为屏幕的可视高
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: string,
/**
* 菜单条目
*/
list: XACTION_MENU_ITEM_INFO[],
/**
* 弹层的层,两边是否留空白间隙,包括底部。
*/
space: boolean,
/**
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
* 此值仅在ios下生效。
*/
watiDuration: number
}
const props = withDefaults(defineProps<xActionMenuPropsType>(), {
customStyle: "",
title: "",
showTitle: true,
showClose: false,
overlayClick: true,
cellClickClose: true,
show: false,
showCancel: true,
duration: 350,
round: "",
maxHeight: "",
list: [] as XACTION_MENU_ITEM_INFO[],
space: true,
watiDuration: 120
})
// 响应式数据
const _width = ref(0)
const _height = ref(0)
const showOverflay = ref(false)
const showOverflayRef = ref<UniElement | null>(null)
const xActionMenuWrapContentRef = ref<UniElement | null>(null)
const actioning = ref(false)
const status = ref("")
const id = ref("xActionMenu" + getUid())
const wrapId = ref("xActionMenuWrap" + getUid())
const first = ref(true)
const tid = ref(0)
const windtop = ref(0)
const nowClickIndex = ref(-1)
const clienEventType = ref('')
// #ifdef H5
const teleportElH5 = ref("uni-app")
const teleportTarget = ref<string | null>(null)
const getTeleportTarget = () => {
try {
if(status.value == ''||status.value=='close') return 'uni-app'
// 优先尝试 uni-page
if (document.querySelector('uni-page')) {
return 'uni-page'
}
// 优先尝试 uni-app
if (document.querySelector('uni-app')) {
return 'uni-app'
}
// 备用方案:尝试 app
if (document.querySelector('#app')) {
return '#app'
}
// 最后备用:body
return 'body'
} catch (error) {
console.warn('Failed to get teleport target:', error)
return 'body'
}
}
// 检查teleport目标是否可用
const isTeleportTargetValid = (target: string) => {
try {
return !!document.querySelector(target)
} catch {
return false
}
}
// #endif
// 计算属性
function getFontSize(size: string): string {
let fontSize = checkIsCssUnit(size, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 14
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
}
const _customStyle = computed((): string => props.customStyle)
const _show = computed((): boolean => props.show)
const _showClose = computed((): boolean => props.showClose)
const _duration = computed((): number => props.duration)
const _cellClickClose = computed((): boolean => props.cellClickClose)
const _showTitle = computed((): boolean => props.showTitle)
const _space = computed((): boolean => props.space)
const _showCancel = computed((): boolean => props.showCancel)
const _title = computed((): string => {
if (props.title == '') return i18n.t("tmui4x.actionMenu.title")
return props.title
})
const _round = computed((): string => {
let round = props.round;
if (round == "") {
round = xConfig.drawerRadius
}
let radius = checkIsCssUnit(round, xConfig.unit);
if (!_space.value) {
return `${radius} ${radius} 0px 0px`
}
return `${radius}`
})
const _maxHeight = computed((): string => {
if (props.maxHeight == "") return "80%"
return checkIsCssUnit(props.maxHeight, xConfig.unit);
})
const _animationFun = computed((): string => xConfig.animationFun)
const __height = computed((): string => {
let h = '100%';
// #ifdef WEB
h = `calc(100% - ${windtop.value}px)`
// #endif
return h;
})
const _bgColor = computed((): string => {
return xConfig.dark == 'dark' ? xConfig.sheetDarkColor : "#f5f5f5"
})
const _cellBgColor = computed((): string => {
return xConfig.dark == 'dark' ? xConfig.inputDarkColor : "#ffffff"
})
const _list = computed((): XACTION_MENU_ITEM_INFO_PRIVATE[] => {
return props.list.map((el: XACTION_MENU_ITEM_INFO): XACTION_MENU_ITEM_INFO_PRIVATE => {
let psd = el.disabled == null ? false : (el.disabled!)
let defaultSize = getFontSize('16');
let temiconcolor = el.iconColor != null ? getDefaultColor(el.iconColor as string) : '#333333';
let temfontcolor = el.iconColor != null ? getDefaultColor(el.iconColor as string) : '#333333';
let iconColor = xConfig.dark == 'dark' ? setTextColorLightByDark(temiconcolor) : temiconcolor;
let fontColor = xConfig.dark == 'dark' ? setTextColorLightByDark(temfontcolor) : temfontcolor;
return {
iconSize: el.iconSize != null ? (getFontSize(el.iconSize!)) : defaultSize,
icon: el.icon != null ? (el.icon!) : '',
fontSize: el.fontSize != null ? (getFontSize(el.fontSize!)) : defaultSize,
iconColor: iconColor,
fontColor: fontColor,
disabled: psd,
id: el.id,
text: el.text,
} as XACTION_MENU_ITEM_INFO_PRIVATE
})
})
// 方法
function setStyleAni() {
let watiDuration = 60;
// #ifdef APP-IOS
watiDuration = props.watiDuration
// #endif
try {
if (status.value == 'open') {
showOverflay.value = true;
clearTimeout(tid.value)
tid.value = setTimeout(function () {
if (showOverflayRef.value == null || xActionMenuWrapContentRef.value == null) return;
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
xActionMenuWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
showOverflayRef.value!.style.setProperty('opacity', '1')
xActionMenuWrapContentRef.value!.style.setProperty('transform', `translate(0%,${_space.value ? -24 : 0}rpx)`)
}, watiDuration);
} else if (status.value == 'close') {
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
xActionMenuWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
showOverflayRef.value!.style.setProperty('opacity', '0')
xActionMenuWrapContentRef.value!.style.setProperty('transform', `translate(0%,100%)`)
}
} catch (e) {
//TODO handle the exception
console.error("xActionMenu Error:",e)
}
}
function closeAlert() {
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
// #ifdef APP-IOS
actioning.value = false;
// #endif
if (actioning.value || status.value == 'close') return;
actioning.value = true;
status.value = 'close'
/**
* 关闭前执行
*/
emits('beforeClose')
setStyleAni();
}
function showAlert() {
if (actioning.value) return;
if (status.value == 'open') return;
showOverflay.value = true;
actioning.value = true;
status.value = 'open'
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
/**
* 打开前执行
*/
emits('beforeOpen')
setStyleAni();
}
function onEnd() {
actioning.value = false;
if (status.value == 'close') {
showOverflay.value = false;
/**
* 关闭时执行
*/
emits('close')
/**
* 等同v-model:show
*/
emits('update:show', false)
if (clienEventType.value == 'click') {
/**
* 项目被点击。由于安卓端动画关闭前触发,会触发view渲染异常.导致动画失败.从而造成,下个页面返回时,上页无法执行结束.
* @param index {number} 当前项目索引。
*/
emits('item-click', nowClickIndex.value)
clienEventType.value = ''
}
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
} else {
/**
* 打开执行的事件
*/
emits('open')
nowClickIndex.value = -1
}
}
function overTouch(evt: UniTouchEvent) {
// #ifdef WEB
evt.preventDefault()
// #endif
// #ifdef APP
evt.stopPropagation()
// #endif
}
function onCancel() {
closeAlert();
emits('cancel')
}
function itemClick(index: number, disabled: boolean) {
if (actioning.value || disabled || status.value == 'close') return;
nowClickIndex.value = index;
if (!_cellClickClose.value) return;
clienEventType.value = 'click'
closeAlert()
}
function overflayMoveTouch(evt: TouchEvent) {
evt.preventDefault();
}
function onClickOverflowy(evt: Event) {
evt.stopPropagation()
emits("click")
if (!props.overlayClick) return;
onCancel();
}
function openDrawer() {
showAlert();
}
// 监听器
watch((): boolean => props.show, (newval: boolean) => {
if (newval) {
showAlert()
} else {
closeAlert()
}
})
// 生命周期
onMounted(() => {
let sys = uni.getWindowInfo()
// #ifndef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight;
windtop.value = sys.windowTop;
// #endif
// #ifdef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight + 44;
// #endif
// #ifdef H5
nextTick(() => {
teleportTarget.value = getTeleportTarget()
})
// #endif
if (_show.value) {
tid.value = setTimeout(() => {
showAlert();
}, 50)
}
})
// 监听页面变化,重新获取teleport目标
onUpdated(() => {
// #ifdef H5
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
teleportTarget.value = getTeleportTarget()
}
// #endif
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
defineExpose({
/** 打开 **/
open: () => showAlert(),
/** 关闭 **/
close: () => closeAlert()
})
</script>
<template>
<view>
<view @click="openDrawer" >
<!--
@slot 标签触发显示遮罩,免于使用变量控制
@prop {Boolean} show - 当前是否已显示
-->
<slot name="trigger" :show="_show"></slot>
</view>
<!-- #ifdef H5 -->
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<root-portal>
<!-- #endif -->
<view
@click="onClickOverflowy"
@touchmove="overTouch"
v-if="showOverflay"
:id="id"
ref="showOverflayRef"
class="xActionMenuWrap xActionMenuWrap_bottom"
:style="[{width:'100%',top:windtop+'px',height:__height,'transition-timing-function':_animationFun},_customStyle]">
<!-- @touchmove="overflayMoveTouch" -->
<view
@transitionend="onEnd"
@click.stop=""
:class="[_space?'onOpenSpace':'']"
class="xActionMenuWrapContent xActionMenuWrapContent_bottom"
ref="xActionMenuWrapContentRef"
:id="wrapId" :style="{
borderRadius:_round,
'transition-timing-function':_animationFun,
backgroundColor:_bgColor
}">
<x-icon v-if="_showClose" class="xActionMenuXclose" @click="closeAlert" color="#dcdcdc" font-size="21"
name="close-circle-fill"></x-icon>
<view>
<view v-if="_showTitle" class="xActionMenuTitleBox" :style="{backgroundColor:_cellBgColor}">
<!--
@slot 标题插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="title" :show="_show">
<text class="xActionMenutitleBox">{{_title}}</text>
</slot>
</view>
</view>
<view class="xActionMenuWrapContentBox" :style="{maxHeight:_maxHeight!=''?_maxHeight:'100%'}">
<scroll-view :style="
{
flex:1,backgroundColor:_bgColor
}
" :scroll-y="true" :rebound="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="default" >
<view v-for="(item,index) in _list" :key="index" @click="itemClick(index,item.disabled)"
:style="{backgroundColor:_cellBgColor,opacity:item.disabled?'0.5':'1'}"
class="xActionMenuItem" :hover-class="item.disabled?'':'xActionMenuHover'"
hover-stay-time="100" hover-start-time="10">
<x-icon v-if="item.icon!=''" :color="item.iconColor" :name="item.icon"
:style="{'margin-right': '10rpx'}"></x-icon>
<text class="xActionText" :style="{fontSize:item.fontSize,color:item.fontColor}">{{item.text}}</text>
</view>
</slot>
</scroll-view>
</view>
<view @click="onCancel" hover-class="xActionMenuHover" :style="{backgroundColor:_cellBgColor}" hover-stay-time="100" hover-start-time="10"
v-if="_showCancel" class="xActionMenuFooter">
<x-text font-size='16' class="xActionMenuFooterText">
<!-- 取消 -->
{{i18n.t("tmui4x.cancel")}}
</x-text>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
</root-portal>
<!-- #endif -->
<!-- #ifdef H5 -->
</teleport>
<!-- #endif -->
</view>
</template>
<style>
.xActionText{
lines: 1;
}
.xActionMenuWrapContentBox {
display: flex;
flex-direction: column;
flex: 1;
}
.xActionMenuItem {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 50px;
margin-bottom: 1px;
}
.xActionMenuFooter {
width: 100%;
margin-top: 8px;
/* background-color: #ffffff; */
height: 50px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xActionMenuHover {
/* background-color: #f7f7f7; */
}
.xActionMenuFooterText {
font-size: 16px;
color: #333333;
text-align: center;
}
.xActionMenuXclose {
position: absolute;
right: 12px;
top: 6px;
z-index: 100;
}
.xActionMenuTitleBox {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 4px;
}
.xActionMenutitleBox {
max-width: 175px;
overflow: hidden;
lines: 1;
text-overflow: ellipsis;
font-size: 14px;
color: #888888;
}
.xActionMenuWrap_bottom {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end;
}
.xActionMenuWrapContent {
transition-duration: 350ms;
transition-property: transform;
/* background-color: #f5f5f5; */
display: flex;
flex-direction: column;
max-width: 500px;
flex: 1;
}
.onOpenSpace {
margin: 0 16px;
}
.xActionMenuWrapContent_bottom {
transform: translate(0%, 100%);
/* #ifndef APP */
overflow: hidden;
/* #endif */
}
.xActionMenuWrap {
background-color: rgba(0, 0, 0, 0.35);
opacity: 0;
position: fixed;
z-index: 1100;
left: 0;
top: 0px;
/* #ifndef APP-HARMONY */
transition-duration: 350ms;
/* #endif */
/* #ifdef APP-HARMONY */
transition-duration: 0ms;
/* #endif */
transition-property: opacity;
}
</style>
@@ -0,0 +1,589 @@
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick, onUpdated } from "vue"
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
/**
* @name 底部对话框 xActionModal
* @page /pages/index/action-modal
* @category 反馈组件
* @description 样式与darawer不一样,风格更为圆润精致,适于提醒框,阅读对话框等场景。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xActionModal" })
const i18n = xConfig.i18n
const proxy = getCurrentInstance()?.proxy ?? null;
defineSlots<{
trigger(props: { show: boolean }): any
title(props: { show: boolean }): any
default(props: { show: boolean }): any
footer(props: { show: boolean }): any
}>()
const emits = defineEmits([
/**
* 底部按钮被点击时触发
*/
'confirm',
/**
* 点击遮罩事件
*/
'click',
/**
* 关闭是触发
*/
'close',
/**
* 打开时触发
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 等同v-model:show
*/
'update:show'
])
export type xActionModalPropsType = {
/**
* 自定义遮罩样式
*/
customStyle: string,
/**
* 标题
*/
title: string,
/**
* 是否显示底部关闭按钮
*/
showTitle: boolean,
/**
* 是否显示关闭
*/
showClose: boolean,
/**
* 遮罩是否允许点击被关闭
*/
overlayClick: boolean,
/**
* 显示可v-model:show双向绑定
*/
show: boolean,
/**
* 显示取消按钮
*/
showConfirm: boolean,
/**
* 动画时间
*/
duration: number,
/**
* 打开方向为上和下时的圆角
* 空值时,取全局配置的圆角。注意是取drawer的圆角,统一弹层的圆角
*/
round: string,
/**
* 弹层最大的高度值,默认为屏幕的可视高
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: string,
/**
* 弹层的背景
*/
bgColor: string,
/**
* 弹层的暗黑背景,如果为空取sheetDarkColor
*/
darkBgColor: string,
/**
* 空值取全局主题值。
*/
btnColor: string,
/**
* 确认按钮的文本
*/
btnText: string,
/**
* 空值最自动计算文本色。
*/
btnFontColor: string,
/**
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
* 此值仅在ios下生效。
*/
watiDuration: number
}
const props = withDefaults(defineProps<xActionModalPropsType>(), {
customStyle: "",
title: "",
showTitle: true,
showClose: false,
overlayClick: true,
show: false,
showConfirm: true,
duration: 350,
round: "",
maxHeight: "",
bgColor: "white",
darkBgColor: "",
btnColor: "",
btnText: "",
btnFontColor: "",
watiDuration: 120
})
// 响应式数据
const _width = ref(0)
const _height = ref(0)
const showOverflay = ref(false)
const showOverflayRef = ref<UniElement | null>(null)
const xActionModalWrapContentRef = ref<UniElement | null>(null)
const actioning = ref(false)
const status = ref("")
const id = ref("xActionModal" + getUid())
const wrapId = ref("xActionModalWrap" + getUid())
const tid = ref(0)
const windtop = ref(0)
const pageOninit = ref(false)
const isOpenedDefault = ref(false)
// #ifdef H5
const teleportElH5 = ref("uni-app")
const teleportTarget = ref<string | null>(null)
const getTeleportTarget = () => {
try {
if(status.value == ''||status.value=='close') return 'uni-app'
// 优先尝试 uni-page
if (document.querySelector('uni-page')) {
return 'uni-page'
}
// 优先尝试 uni-app
if (document.querySelector('uni-app')) {
return 'uni-app'
}
// 备用方案:尝试 app
if (document.querySelector('#app')) {
return '#app'
}
// 最后备用:body
return 'body'
} catch (error) {
console.warn('Failed to get teleport target:', error)
return 'body'
}
}
// 检查teleport目标是否可用
const isTeleportTargetValid = (target: string) => {
try {
return !!document.querySelector(target)
} catch {
return false
}
}
// #endif
// 计算属性
const _customStyle = computed((): string => props.customStyle)
const _show = computed((): boolean => props.show)
const _showClose = computed((): boolean => props.showClose)
const _duration = computed((): number => props.duration)
const _showTitle = computed((): boolean => props.showTitle)
const _showConfirm = computed((): boolean => props.showConfirm)
const _btnFontColor = computed((): string => {
return getDefaultColor(props.btnFontColor)
})
const _btnColor = computed((): string => {
if (props.btnColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(props.btnColor)
})
const _btnText = computed((): string => {
if (props.btnText == '') return i18n.t("tmui4x.actionModal.btnText")
return props.btnText
})
const _title = computed((): string => {
if (props.title == '') return i18n.t("tmui4x.actionModal.title")
return props.title
})
const _round = computed((): string => {
let round = props.round;
if (round == "") {
round = xConfig.drawerRadius
}
let radius = checkIsCssUnit(round, xConfig.unit);
return `${radius}`
})
const _bgColor = computed((): string => {
if (xConfig.dark == 'dark') {
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(props.bgColor)
})
const _maxHeight = computed((): string => {
if (props.maxHeight == "") return "80%"
return checkIsCssUnit(props.maxHeight, xConfig.unit);
})
const _animationFun = computed((): string => xConfig.animationFun)
const __height = computed((): string => {
let h = '100%';
// #ifdef WEB
h = `calc(100% - ${windtop.value}px)`
// #endif
return h;
})
// 方法 - 按依赖顺序定义,被调用的函数在前面
function setStyleAni() {
try {
let watiDuration = 60;
// #ifdef APP-IOS
watiDuration = props.watiDuration
// #endif
if (status.value == 'open') {
showOverflay.value = true;
clearTimeout(tid.value)
tid.value = setTimeout(function () {
if (showOverflayRef.value == null || xActionModalWrapContentRef.value == null) return;
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
xActionModalWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
showOverflayRef.value!.style.setProperty('opacity', '1')
xActionModalWrapContentRef.value!.style.setProperty('transform', `translate(0%,-24rpx)`)
}, watiDuration);
} else if (status.value == 'close') {
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
xActionModalWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
showOverflayRef.value!.style.setProperty('opacity', '0')
xActionModalWrapContentRef.value!.style.setProperty('transform', `translate(0%,100%)`)
}
} catch (e) {
//TODO handle the exception
console.error("xActionModal Error:", e)
}
}
function onEnd() {
actioning.value = false;
if (status.value == 'close') {
showOverflay.value = false;
/**
* 关闭时执行
*/
emits('close')
/**
* 等同v-model:show
*/
emits('update:show', false)
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
} else {
/**
* 打开执行的事件
*/
emits('open')
}
}
function closeAlert() {
if (actioning.value || status.value == 'close') return;
actioning.value = true;
status.value = 'close'
/**
* 关闭前执行
*/
emits('beforeClose')
setStyleAni();
}
function showAlert() {
if (actioning.value) return;
if (status.value == 'open') return;
showOverflay.value = true;
actioning.value = true;
status.value = 'open'
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
/**
* 打开前执行
*/
emits('beforeOpen')
setStyleAni();
}
function openDrawer() {
showAlert();
}
function onConfirm() {
closeAlert()
emits('confirm')
}
function onClickOverflowy(evt: Event) {
evt.stopPropagation()
/**
* 点击遮罩事件
*/
emits("click")
if (!props.overlayClick) return;
closeAlert();
}
function overflayMoveTouch(evt: TouchEvent) {
evt.preventDefault();
}
function overTouch(evt: UniTouchEvent) {
// #ifdef WEB
evt.preventDefault()
// #endif
// #ifdef APP
evt.stopPropagation()
// #endif
}
// 监听器
watch((): boolean => props.show, (newval: boolean) => {
if (newval) {
showAlert()
} else {
closeAlert()
}
})
// 生命周期
onMounted(() => {
function oninitready() {
let sys = uni.getWindowInfo()
_width.value = sys.windowWidth
_height.value = sys.windowHeight;
windtop.value = sys.windowTop;
if (_show.value) {
showAlert();
}
}
oninitready()
// #ifdef H5
nextTick(() => {
teleportTarget.value = getTeleportTarget()
})
// #endif
})
// 监听页面变化,重新获取teleport目标
onUpdated(() => {
// #ifdef H5
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
teleportTarget.value = getTeleportTarget()
}
// #endif
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
defineExpose({
/** 打开 **/
open: () => showAlert(),
/** 关闭 **/
close: () => closeAlert()
})
</script>
<template>
<view>
<view @click="openDrawer">
<!--
@slot 标签触发显示遮罩,免于使用变量控制
@prop {Boolean} show - 当前是否已显示
-->
<slot name="trigger" :show="_show"></slot>
</view>
<!-- #ifdef H5 -->
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<root-portal>
<!-- #endif -->
<view
@click="onClickOverflowy"
@touchmove="overTouch"
v-if="showOverflay"
@transitionend="onEnd"
:id="id"
ref="showOverflayRef"
class="xActionModalWrap xActionModalWrap_bottom"
:style="[{width:'100%',height:__height,top:windtop+'px','transition-timing-function':_animationFun},_customStyle]">
<!-- @touchmove="overflayMoveTouch" -->
<view
@click.stop=""
class="xActionModalWrapContent xActionModalWrapContent_bottom"
ref="xActionModalWrapContentRef"
:id="wrapId"
:style="{
borderRadius:_round,
maxHeight:_maxHeight!=''?_maxHeight:'100%',
backgroundColor:_bgColor,
'transition-timing-function':_animationFun
}">
<x-icon v-if="_showClose" class="xActionModalXclose" @click="closeAlert" color="#dcdcdc" font-size="24"
name="close-circle-fill"></x-icon>
<view>
<view v-if="_showTitle" class="xActionModalTitleBox">
<!--
@slot 标题插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="title" :show="_show">
<text class="xActionModaltitleBox">{{_title}}</text>
</slot>
</view>
</view>
<scroll-view style="flex:1;" :scroll-y="true" :rebound="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="default">
</slot>
</scroll-view>
<view v-if="_showConfirm" class="xActionModalFooter">
<!--
@slot 页脚按钮插槽
-->
<slot name="footer">
<x-button @click="onConfirm" :block="true" :color="_btnColor" :round="_round"
:font-color="_btnFontColor">{{_btnText}}</x-button>
</slot>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
</root-portal>
<!-- #endif -->
<!-- #ifdef H5 -->
</teleport>
<!-- #endif -->
</view>
</template>
<style>
.xActionModalItem {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 44px;
margin-bottom: 1px;
}
.xActionModalFooter {
margin: 16px;
}
.xActionModalFooterText {
font-size: 15px;
color: #333333;
text-align: center;
}
.xActionModalXclose {
position: absolute;
right: 12px;
top: 11px;
z-index: 100;
}
.xActionModalTitleBox {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 4px;
}
.xActionModaltitleBox {
max-width: 350rpx;
overflow: hidden;
lines: 1;
text-overflow: ellipsis;
font-size: 16px;
color: #888888;
}
.xActionModalWrap_bottom {
display: flex;
flex-direction: column;
justify-content: flex-end;
/* #ifndef MP-WEIXIN */
align-items: center;
/* #endif */
}
.xActionModalWrapContent {
transition-duration: 350ms;
transition-property: transform;
display: flex;
flex-direction: column;
margin: 0 16px;
max-width: 500px;
/* width:100%; */
}
.xActionModalWrapContent_bottom {
transform: translate(0%, 100%);
}
.xActionModalWrap {
background-color: rgba(0, 0, 0, 0.35);
/* #ifndef APP-HARMONY */
transition-duration: 350ms;
/* #endif */
/* #ifdef APP-HARMONY */
transition-duration: 0ms;
/* #endif */
opacity: 0;
position: fixed;
z-index: 400;
left: 0;
top: 0px;
transition-property: opacity;
}
</style>
@@ -0,0 +1,373 @@
<template>
<view v-if="showAlert" @click="onclickbar" class="xAlert" :style="_styleMap.bgStyle">
<!--
@slot 左边图标插槽
-->
<slot name="left">
<view class="xAlertLeft">
<x-icon :font-size="props.iconSize" :color="(_styleMap.textStyle.get('color')! as string)"
:dark-color="(_styleMap.textStyle.get('color')! as string)" :name="_iconName"></x-icon>
</view>
</slot>
<view class="xAlertContent">
<text :style="_styleMap.textStyle">
<!--
@slot 默认插槽内容
-->
<slot></slot>
</text>
</view>
<view @click="closeAlert" v-if="_showClose" class="xAlertRight">
<x-icon :font-size="props.iconSize" :color="(_styleMap.textStyle.get('color')! as string)"
:dark-color="(_styleMap.textStyle.get('color')! as string)" :name="props.closeIcon"></x-icon>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted } from "vue"
import { xDate } from "../../core/util/xDate.uts"
import { checkIsCssUnit, getUnit, fillArrayCssValue, fillArrayCssValueByround, fillArrayCssValueBycolor } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, getDefaultColorObj, getThinColorObj } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type styleMapType = {
textStyle: Map<string, any>,
bgStyle: Map<string, any>
}
type xAlertStatusType = "primary"|"warn"|"success"|"error"|"info"
/**
* @name 警告 xAlert
* @page /pages/index/alert
* @category 展示组件
* @description 样式丰富常用警告提醒
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xAlert" })
defineSlots<{
left(): any
default(): any
}>()
const emits = defineEmits([
/**
* 关闭时触发
*/
"close",
/**
* 组件被点击时触发
*/
"click"
])
export type xAlertPropsType = {
/**
* 类型
* warn:警告
* success:成功
* error:错误
* info:信息
* primary:正常主题
*/
status: xAlertStatusType,
/**
* 警告图标,不填写取status默认图标
* 填写以填写为准
*/
icon: string,
/**
* 警告图标大小
*/
iconSize: string,
/**
* 关闭图标
*/
closeIcon: string,
/**
* 显示还是隐藏关闭按钮
*/
showClose: boolean,
/**
* 文字大小
*/
fontSize: string,
/**
* 主题色,如果不填写以status为准
*/
color: string,
/**
* 文字颜色,如果不填写以status为准
*/
fontColor: string,
/**
* 暗黑主题颜色,如果不填写自动计算
*/
darkColor: string,
/**
* 暗黑文字颜色,如果不填写自动计算
*/
fontDarkColor: string,
/**
* 它是建立在你没有提供color时才有效。
* 如果提供了color是以你color为背景最终色。
* thin浅色模式,
* normal标准背景色
*/
skin: string,
/**
* 圆角
* 数组数字时
* [全部]
* [顶左,顶右,底右,底左]
* [顶左,底右]
* [顶左,顶右,底右]
* 空数组时取全局值
*/
round: string[],
/**
* 边线
* 数组数字时
* 数组数字时
* [全部]
* [左,上,右,下]
* [左右,上下]
* [左,上,右]
* 空数组时取全局值
*/
border: string[],
/**
* 边框颜色
* 格式同border边线。
* 空数组时取全局值
*/
borderColor: string[],
/**
* 如果不填写,自动计算
*/
darkBorderColor: string[],
/**
* 边线类型,默认solid,可以为none
*/
borderStyle: string,
/**
* 间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
* 空数组时取全局值
*/
margin: string[],
/**
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
* 空数组时取全局值
*/
padding: string[],
/**
* 自定义高度,可以是数字,单位或者百分比,auto
*/
height: string,
/**
* 宽,单位合法即可数字,字符串带单位,百分比,auto
*/
width: string
}
const props = withDefaults(defineProps<xAlertPropsType>(), {
status: "primary",
icon: "",
iconSize: "20",
closeIcon: "close-line",
showClose: true,
fontSize: "15",
color: "",
fontColor: "",
darkColor: "",
fontDarkColor: "",
skin: "thin",
round:():string[] => [] as string[],
border: ():string[] => [] as string[],
borderColor: ():string[] => [] as string[],
darkBorderColor: ():string[] => [] as string[],
borderStyle: 'solid',
margin: ():string[] => ['16', '0', '16', '16'] as string[],
padding: ():string[] => ['16', '12'] as string[],
height: "auto",
width: "auto"
})
// 响应式数据
const showAlert = ref(true)
// 计算属性
const _color = computed((): string => {
let tcolor = props.color;
if (tcolor == '') {
tcolor = props.status;
}
if (xConfig.dark == 'dark' && props.darkColor != '') {
tcolor = props.darkColor
}
let color = getDefaultColor(tcolor)
return color;
})
const _fontColor = computed((): string => {
let tcolor = props.fontColor;
if (xConfig.dark == 'dark' && props.fontDarkColor != '') {
tcolor = props.fontDarkColor
}
return tcolor;
})
const _isDark = computed((): boolean => xConfig.dark == 'dark')
const _showClose = computed((): boolean => props.showClose)
const _fontSize = computed((): string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _margin = computed((): string => {
if (props.margin.length == 0) {
let par = fillArrayCssValue(xConfig.sheetMargin)
if (par.length == 0) return "0px 0px 0px 0px";
return par.join(" ")
}
let ar: string[] = fillArrayCssValue(props.margin as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _padding = computed((): string => {
if (props.padding.length == 0) {
let par = fillArrayCssValue(xConfig.sheetMargin)
if (par.length == 0) return "0px 0px 0px 0px";
return par.join(" ")
}
let ar: string[] = fillArrayCssValue(props.padding as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _round = computed((): string => {
if (props.round.length == 0) {
let par = fillArrayCssValueByround(xConfig.sheetRadius)
if (par.length == 0) return "0px 0px 0px 0px";
return par.join(" ")
}
let ar: string[] = fillArrayCssValueByround(props.round as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _border = computed((): string => {
let ar: string[] = fillArrayCssValue(props.border as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _borderColor = computed((): string => {
let bordercolor = props.borderColor as string[];
if (xConfig.dark == 'dark') {
bordercolor = props.darkBorderColor.length == 0 ? xConfig.sheetDarkBorderColor : props.darkBorderColor
}
let ar: string[] = fillArrayCssValueBycolor(bordercolor as string[])
if (ar.length == 0) return "transparent transparent transparent transparent";
return ar.join(" ")
})
const _iconName = computed((): string => {
const iconsmap = new Map<string, string>([
['warn', 'alert-line'],
['success', 'check-double-line'],
['error', 'close-circle-line'],
['info', 'information-2-line'],
['primary', 'notification-line'],
])
let dicon = iconsmap.get(props.status)
return props.icon == '' ? (dicon == null ? '' : dicon!) : props.icon
})
const _styleMap = computed((): styleMapType => {
let bgStylemap = new Map<string, any>()
let textStylemap = new Map<string, any>()
let colorObj = getDefaultColorObj(_color.value, _color.value)
if (props.skin == 'thin') {
colorObj = getThinColorObj(_color.value, _color.value, _isDark.value)
}
let defaultObj: UTSJSONObject = colorObj.getJSON("default")!
bgStylemap.set("backgroundColor", defaultObj.getString("background")!)
bgStylemap.set("margin", _margin.value)
bgStylemap.set("padding", _padding.value)
bgStylemap.set("border-radius", _round.value)
bgStylemap.set("border-width", _border.value)
bgStylemap.set("height", checkIsCssUnit(props.height, xConfig.unit))
bgStylemap.set("width", checkIsCssUnit(props.width, xConfig.unit))
bgStylemap.set("border-style", props.borderStyle)
if (props.borderColor.length > 0) {
bgStylemap.set("border-color", _borderColor.value)
} else {
bgStylemap.set("border-color", getDefaultColor(_color.value))
}
if (_fontColor.value == '') {
textStylemap.set("color", defaultObj.getString("fontColor")!)
} else {
textStylemap.set("color", _fontColor.value)
}
textStylemap.set("font-size", _fontSize.value)
return {
bgStyle: bgStylemap,
textStyle: textStylemap
} as styleMapType;
})
// 方法
function closeAlert(): void {
emits('close')
showAlert.value = false
}
function onclickbar(): void {
emits('click')
}
</script>
<style>
.xAlert {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.xAlertContent {
flex: 1;
}
.xAlertLeft {
margin-right: 8rpx;
}
.xAlertLeft {
flex-shrink: 0;
padding-right: 6px;
}
.xAlertRight {
flex-shrink: 0;
padding-left: 20px;
}
</style>
@@ -0,0 +1,304 @@
<script lang="ts">
import { PropType } from "vue";
import { getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xTween } from "@/uni_modules/tmx-ui/index.uts"
import { xTweenAnimate, xTweenEventCallFunType} from "@/uni_modules/tmx-ui/interface.uts"
/**
* @name 动画 xAnimation
* @page /pages/index/animation
* @description 动画组件
* @category 其它组件
* @constant 平台兼容
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
*/
export default {
data() {
return {
element: null as Element | null,
id: "xAnimation" + getUid() as String,
playStatus: 'complete' as 'reset' | 'playing' | 'complete',
playFag: '',
tid: 0
}
},
emits: [
/**
* 播放前执行
*/
'beforePlay',
/**
* 播放完成执行
*/
'complete',
/**
* 播放时触发
*/
'play',
/**
* 同步控制播放参数
* 等同v-model:control
*/
'update:control',
/**
* 当前播放的状态
* 等同vmodel:status
* 它是单向输出的
*/
'update:status'],
props: {
/**
* 是否自动播放
*/
autoPlay: {
type: Boolean,
default: false
},
/**
* 动画播放的时间+50+5
*/
duration: {
type: Number,
default: 350
},
/**
* 动画名称
*/
name: {
type: String as PropType<'fadeIn' | 'fadeOut' | 'zoomIn' | 'zoomOut' | 'left' | 'right' | 'top' | 'bottom'>,
default: 'fadeIn'
},
/**
* 是否允许反转,如果允许反转
* 播放当前动画后。再点播放,会反方向播放动画。接着再播放又是正常,这样反复。
*/
revert: {
type: Boolean,
default: true
},
/**
* 自动监测是否播放。如果标志为play就开始播放动画,但
* 如果动画在播放中这个值不会有任何响应,而且需要v-model:playing用法来双向绑定读取此属性
* 你可以通过播放事件来确定修改此值。
*
*/
control: {
type: String as PropType<'play' | 'default'>,
default: 'default'
},
/**
* 播放状态,只能用来读取此值,不可更改,使用时请v-model:status来读取动态的状态值
*
*/
status: {
type: String as PropType<'reset' | 'playing' | 'complete'>,
default: 'complete'
},
},
watch: {
control() {
if (this.playStatus == 'playing' || this.control != 'play') {
this.$emit('update:control', 'default')
return;
}
this.play()
}
},
computed: {
_animationFun() : string {
return xConfig.animationFun
}
},
mounted() {
let t = this;
this.resetStatusInitType();
this.tid = setTimeout(function () {
t.autoPlaySetFlex()
if (t.autoPlay) {
t.play();
}
}, 50);
},
beforeUnmount() {
clearTimeout(this.tid)
},
methods: {
resetStatusInitType() {
// ||this.playStatus == 'reset'
this.element = this.$refs["xAnimation"] as UniElement
if (this.element == null) return;
this.playStatus = 'reset'
this.$emit('update:status', 'reset')
// if(this.name=='fadeIn'||this.name=='fadeOut'){
// this.element!.style.setProperty("transition-property",'opacity')
// }else{
// this.element!.style.setProperty("transition-property",'transform')
// }
if (this.playFag == '') {
this.element!.style.setProperty("transition-duration", '0ms')
if (this.name == 'fadeIn') {
this.element!.style.setProperty("opacity", 0)
}
if (this.name == 'fadeOut') {
this.element!.style.setProperty("opacity", 1)
}
if (this.name == 'zoomIn') {
this.element!.style.setProperty("opacity", 0)
this.element!.style.setProperty("transform", 'scale(0.65)')
}
if (this.name == 'zoomOut') {
this.element!.style.setProperty("opacity", 1)
this.element!.style.setProperty("transform", 'scale(1)')
}
if (this.name == 'left') {
this.element!.style.setProperty("transform", 'translateX(1000%)')
}
if (this.name == 'right') {
this.element!.style.setProperty("transform", 'translateX(-100%)')
}
if (this.name == 'top') {
this.element!.style.setProperty("transform", 'translateY(-100%)')
}
if (this.name == 'bottom') {
this.element!.style.setProperty("transform", 'translateY(100%)')
}
}
},
autoPlaySetFlex() {
this.element = this.$refs["xAnimation"] as UniElement
if (this.element == null) return;
this.element!.style.setProperty("display", 'flex')
},
/**
* 播放动画
* @public
*/
play() {
this.element = this.$refs["xAnimation"] as UniElement
if (this.element == null || this.playStatus == 'playing') return;
if (this.playStatus != 'reset') {
this.resetStatusInitType()
}
/**
* 播放前触发
*/
this.$emit('beforePlay')
this.playStatus = 'playing'
this.$emit('update:status', 'playing')
let t = this;
clearTimeout(this.tid)
//需要一个反应时间。
this.tid = setTimeout(function () {
/**
* 播放时触发
*/
t.$emit('play')
if (t.playFag == '') {
t.element!.style.setProperty("transition-duration", t.duration.toString() + 'ms')
if (t.name == 'fadeIn') {
t.element!.style.setProperty("opacity", 1)
}
if (t.name == 'fadeOut') {
t.element!.style.setProperty("opacity", 0)
}
if (t.name == 'zoomIn') {
t.element!.style.setProperty("transform", 'scale(1)')
t.element!.style.setProperty("opacity", 1)
}
if (t.name == 'zoomOut') {
t.element!.style.setProperty("transform", 'scale(0.65)')
t.element!.style.setProperty("opacity", 0)
}
if (t.name == 'left' || t.name == 'right') {
t.element!.style.setProperty("transform", 'translateX(0%)')
}
if (t.name == 'top' || t.name == 'bottom') {
t.element!.style.setProperty("transform", 'translateY(0%)')
}
} else {
t.element!.style.setProperty("display", 'flex')
t.element!.style.setProperty("transition-duration", t.duration.toString() + 'ms')
if (t.name == 'fadeIn') {
t.element!.style.setProperty("opacity", 0)
}
if (t.name == 'fadeOut') {
t.element!.style.setProperty("opacity", 1)
}
if (t.name == 'zoomIn') {
t.element!.style.setProperty("transform", 'scale(0.65)')
t.element!.style.setProperty("opacity", 0)
}
if (t.name == 'zoomOut') {
t.element!.style.setProperty("transform", 'scale(1)')
t.element!.style.setProperty("opacity", 1)
}
if (t.name == 'left') {
t.element!.style.setProperty("transform", 'translateX(100%)')
}
if (t.name == 'right') {
t.element!.style.setProperty("transform", 'translateX(-100%)')
}
if (t.name == 'top') {
t.element!.style.setProperty("transform", 'translateY(-100%)')
}
if (t.name == 'bottom') {
t.element!.style.setProperty("transform", 'translateY(100%)')
}
}
}, 5);
},
playEnd() {
if (this.playStatus != 'reset') {
this.playStatus = 'complete'
/**
* 播放完成触发
*/
this.$emit('complete')
this.$emit('update:status', 'complete')
this.$emit('update:control', 'default');
if (this.revert) {
if (this.playFag == '') {
this.playFag = 'revert'
} else {
this.playFag = ''
}
}
}
}
}
}
</script>
<template>
<view ref="xAnimation" @transitionend="playEnd" class="xAnimation" :id="id"
:style="{'transition-timing-function':_animationFun}">
<!--
默认插槽
@prop {object} status - 状态 {state:'reset'|'playing'|'complete',flag:'play'|'default'}
-->
<slot :status="{state:playStatus,flag:playFag}"></slot>
</view>
</template>
<style>
.xAnimation {
display: none;
transition-property: opacity, transform;
transition-duration: 350ms;
}
</style>
@@ -0,0 +1,321 @@
<script lang="ts">
import { PropType } from "vue";
import { getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xTween } from "@/uni_modules/tmx-ui/index.uts"
import { XANIMATE_OPIONS, xTweenAnimate, xTweenCallbackFunType, xTweenEventCallFunType} from "@/uni_modules/tmx-ui/interface.uts"
/**
* @name 动画 xAnimation
* @page /pages/index/animation
* @description 动画组件
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
element: null as Element | null,
id: "xAnimation" + getUid() as String,
playStatus: 'complete' as 'reset' | 'playing' | 'complete',
playFag: '',
tid: 0,
tw:new xTween()
}
},
emits: [
/**
* 播放前执行
*/
'beforePlay',
/**
* 播放完成执行
*/
'complete',
/**
* 播放时触发
*/
'play',
/**
* 同步控制播放参数
* 等同v-model:control
*/
'update:control',
/**
* 当前播放的状态
* 等同vmodel:status
* 它是单向输出的
*/
'update:status'],
props: {
/**
* 是否自动播放
*/
autoPlay: {
type: Boolean,
default: false
},
/**
* 动画播放的时间+50+5
*/
duration: {
type: Number,
default: 350
},
/**
* 动画名称
*/
name: {
type: String,
default: 'fadeIn'
},
/**
* 缓动动画名称
*/
ease: {
type: String,
default: 'tmxEase'
},
/**
* 自动监测是否播放。如果标志为play就开始播放动画,但
* 如果动画在播放中这个值不会有任何响应,而且需要v-model:playing用法来双向绑定读取此属性
* 你可以通过播放事件来确定修改此值。
*
*/
control: {
type: String as PropType<'play' | 'default'>,
default: 'default'
},
/**
* 播放状态,只能用来读取此值,不可更改,使用时请v-model:status来读取动态的状态值
*
*/
status: {
type: String as PropType<'reset' | 'playing' | 'complete'>,
default: 'complete'
},
},
watch: {
control() {
if (this.playStatus == 'playing' || this.control != 'play') {
this.$emit('update:control', 'default')
return;
}
this.play();
},
name(){
this.resetStatusInitType();
this.autoPlaySetFlex()
if (this.autoPlay) {
this.play();
}
}
},
computed: {
_animationFun() : string {
return xConfig.animationFun
}
},
mounted() {
this.tw.startRender()
this.resetStatusInitType();
this.autoPlaySetFlex()
if (this.autoPlay) {
this.play();
}
},
beforeUnmount() {
this.tw.destroy()
clearTimeout(this.tid)
},
methods: {
resetStatusInitType() {
// ||this.playStatus == 'reset'
let element = this.$refs["xAnimation"] as UniElement
if (element == null) return;
this.playStatus = 'reset'
this.$emit('update:status', 'reset')
if (this.playFag == '') {
if (this.name == 'fadeIn') {
element.style.setProperty("opacity", 0)
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
}
if (this.name == 'fadeOut') {
element.style.setProperty("opacity", 1)
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
}
if (this.name == 'zoomIn') {
element.style.setProperty("opacity", 0)
element.style.setProperty("transform", 'scale(0) translateX(0%) translateY(0%)')
}
if (this.name == 'zoomOut') {
element.style.setProperty("opacity", 1)
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
}
if (this.name == 'leftIn') {
element.style.setProperty("transform", 'translateX(-100%) scale(1) translateY(0%)')
}
if (this.name == 'leftOut') {
element.style.setProperty("transform", 'translateX(0%) scale(1) translateY(0%)')
}
if (this.name == 'rightIn') {
element.style.setProperty("opacity", 0)
element.style.setProperty("transform", 'translateX(100%) scale(1) translateY(0%)')
}
if (this.name == 'rightOut') {
element.style.setProperty("opacity", 1)
element.style.setProperty("transform", 'translateX(0%) scale(1) translateY(0%)')
}
if (this.name == 'topIn') {
element.style.setProperty("opacity", 0)
element.style.setProperty("transform", 'translateY(-100%) scale(1) translateX(0%)')
}
if (this.name == 'topOut') {
element.style.setProperty("opacity", 1)
element.style.setProperty("transform", 'translateY(0%) scale(1) translateX(0%)')
}
if (this.name == 'bottomIn') {
element.style.setProperty("opacity", 0)
element.style.setProperty("transform", 'translateY(-100%) scale(1) translateX(0%)')
}
if (this.name == 'bottomOut') {
element.style.setProperty("opacity", 1)
element.style.setProperty("transform", 'translateY(0%) scale(1) translateX(0%)')
}
}
},
autoPlaySetFlex() {
let element = this.$refs["xAnimation"] as UniElement
if (element == null) return;
element.style.setProperty("display", 'flex')
},
/**
* 播放动画
* @public
*/
play(){
let _this = this;
let element = this.$refs["xAnimation"] as UniElement
if (element == null || this.playStatus == 'playing') return;
if (this.playStatus != 'reset') {
this.resetStatusInitType()
}
/**
* 播放前触发
*/
this.$emit('beforePlay')
this.playStatus = 'playing'
this.$emit('update:status', 'playing')
let atr = this.name
this.tw.stop()
this.tw.removeAnimate()
this.tw
.addAnimate({
duration:this.duration,
ease:this.ease,
enter:(item : xTweenEventCallFunType) =>{
if(atr == 'fadeIn'){
element.style.setProperty('opacity',(item.progress).toString())
}else if(atr == 'fadeOut'){
element.style.setProperty('opacity',(1-item.progress).toString())
}else if(atr == 'zoomIn'){
element.style.setProperty("opacity", item.progress)
element.style.setProperty("transform", `scale(${item.progress})`)
}else if(atr == 'zoomOut'){
element.style.setProperty("opacity", (1-item.progress))
element.style.setProperty("transform", `scale(${1-item.progress})`)
}else if(atr == 'leftIn'){
let left = (1-item.progress)*100*-1
element.style.setProperty('opacity',(item.progress).toString())
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
}else if(atr == 'leftOut'){
let left = (item.progress)*100*-1
element.style.setProperty('opacity',(1-item.progress).toString())
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
}else if(atr == 'rightIn'){
let left = (1-item.progress)*100
element.style.setProperty('opacity',(item.progress).toString())
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
}else if(atr == 'rightOut'){
let left = (item.progress)*100
element.style.setProperty('opacity',(1-item.progress).toString())
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
}else if(atr == 'topIn'){
let left = (1-item.progress)*100*-1
element.style.setProperty('opacity',(item.progress).toString())
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
}else if(atr == 'topOut'){
let left = (item.progress)*100*-1
element.style.setProperty('opacity',(1-item.progress).toString())
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
}else if(atr == 'bottomIn'){
let left = (1-item.progress)*100
element.style.setProperty('opacity',(item.progress).toString())
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
}else if(atr == 'bottomOut'){
let left = (item.progress)*100
element.style.setProperty('opacity',(1-item.progress).toString())
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
}
},
complete: (_ : xTweenEventCallFunType) => {
_this.playStatus = 'complete'
/**
* 播放完成触发
*/
_this.$emit('complete')
_this.$emit('update:status', 'complete')
_this.$emit('update:control', 'default');
}
} as xTweenAnimate)
this.tw.play()
}
}
}
</script>
<template>
<view ref="xAnimation" class="xAnimation" :id="id">
<!-- #ifdef APP||WEB -->
<!--
@slot 默认插槽,下面的数据微信没有
@prop {object} status - 状态 {state:'reset'|'playing'|'complete',flag:'play'|'default'}
-->
<slot :status="{state:playStatus,flag:playFag}"></slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<slot ></slot>
<!-- #endif -->
</view>
</template>
<style>
.xAnimation {
display: none;
flex-direction: column;
/* transition-property: opacity, transform;
transition-duration: 350ms; */
}
</style>
@@ -0,0 +1,430 @@
<template>
<view class="xAvatarGroup" ref="xAvatarGroup" :class="[_flat?'xAvatarGroupWrap':'xAvatarGroupNoWrap']">
<template v-for="(item,index) in _list" :key="index">
<image @click="itemClick(item,index)" v-if="item.isImg" :style="{
width:_size,
height:_size,
borderRadius:_round,
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter
}" class="xAvatarGroupImage" :src="item.name" :mode="props.model"></image>
<view @click="itemClick(item,index)" v-else :style="{
width:_size,
height:_size,
borderRadius:_round,
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter,
backgroundColor:item.bgColor
}" class="xAvatarGroupImage xAvatarGroupImageBytext">
<x-text v-if="item.name!=''" :font-size="props.fontSize"
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor">
{{item.name}}
</x-text>
<x-icon v-if="item.name==''" :font-size="props.fontSize"
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor"
:name="props.placeIcon"></x-icon>
</view>
</template>
<!--
@slot more更多插槽,如果使用了这个插槽moreClick事件会丢失,请自己写在自己的布局上。
-->
<slot name="more">
<view @click="moreClick" class="xAvatarGroupImageMore" :style="{
width:_size,
height:_size,
borderRadius:_round,
backgroundColor:_BgColor
}" v-if="_showCount">
<x-text :font-size="props.fontSize" :color="props.fontColor"
:dark-color="props.darkFontColor">{{_count==0?(_list.length>99?99:_list.length)+'+':(_count>99?99:_count)+'+'}}</x-text>
</view>
</slot>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { type PropType } from "vue"
import { getDefaultColor, hslaToCss } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xProvitae } from "../../config/xConfig.uts"
type XAVATARGROUPITEMTYPE = {
name : string,
isImg : boolean,
bgColor : string
}
/**
* @name 头像组 xAvatarGroup
* @page /pages/index/avatar-group
* @category 展示组件
* @description 平铺和堆叠方式。如果想要单头像建议使用:xSheet+xImage配合,+xBadge达到效果,因此我不再提供单头像组件,没有意义。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xAvatarGroup" })
let resizeObserver = null as UniResizeObserver | null
const xAvatarGroup = ref<UniElement | null>(null)
const emits = defineEmits([
/**
* 头像被点击时
* @param {number} index 当前索引
* @param {string} src 当前图片地址
*/
"click",
/**
* 最后一个数字头像more被点击时
*/
"moreClick"
])
const props = defineProps({
/**
* 头像列表,也可以是文本数组,也可以是空字符串数组
*/
list: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
/**
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
*/
size: {
type: String,
default: '32'
},
/**
* 最多显示几个头像。
*/
maxCount: {
type: Number,
default: 5
},
/**
* 圆角
*/
round: {
type: String,
default: '16'
},
/**
* 平铺或者堆叠时的间隙或者前推差值。
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
*/
gutter: {
type: String,
default: '16'
},
/**
* 显示类型见:
* https://doc.dcloud.net.cn/uni-app-x/component/image.html#%E5%B1%9E%E6%80%A7
*/
model: {
type: String,
default: "scaleToFill"
},
/**
* 显示在最后一个时,显示的数字。如果为0取list的长度
*/
count: {
type: Number,
default: 0
},
/**
* 是否显示最后一个数字头像
*/
showCount: {
type: Boolean,
default: true
},
/**
* 是否平铺,如果否就是堆叠。是就是正常排列。
*/
flat: {
type: Boolean,
default: false
},
/**
* 如果为文本头像时的背景
*/
bgColor: {
type: String,
default: "#f5f5f5"
},
/**
* 如果为文本头像时的暗黑背景
* 空时默认取inputDarkBgcolor
*/
darkBgColor: {
type: String,
default: ""
},
/**
* 如果为文本头像时的文字颜色
*/
fontColor: {
type: String,
default: "#a6a6a6"
},
/**
* 如果为文本头像时的暗黑背景
* 空时默认取inputDarkBgcolor
*/
darkFontColor: {
type: String,
default: "#ffffff"
},
/**
* 字号
*/
fontSize: {
type: String,
default: "14"
},
/**
* 文本头像时,是否随机背景色
*/
randomBgColor: {
type: Boolean,
default: false
},
/**
* 如果当图片或者文本为空时的图片占位符
* 可以是图片地址或者图标名称
*/
placeIcon: {
type: String,
default: 'user-3-fill'
}
})
const _isFileImg = (name : string) : boolean => {
if (name.lastIndexOf(".") > -1 ||
name.indexOf("ftp:") > -1 ||
name.indexOf("https:") > -1 ||
name.indexOf("http:") > -1 ||
name.indexOf("data:image") > -1
) {
return true;
}
return false;
}
const _randomBgColor = computed(() : boolean => {
return props.randomBgColor
})
const _BgColor = computed(() : string => {
let bgcolor = props.bgColor;
if (xConfig.dark == 'dark') {
bgcolor = props.darkBgColor;
if (bgcolor == '') {
bgcolor = xConfig.inputDarkColor
}
}
return getDefaultColor(bgcolor)
})
const UpperEn = new Map<string, string>([
['a', 'A'],
['b', 'B'],
['c', 'C'],
['d', 'D'],
['e', 'E'],
['f', 'F'],
['g', 'G'],
['h', 'H'],
['i', 'I'],
['j', 'J'],
['k', 'K'],
['l', 'L'],
['m', 'M'],
['n', 'N'],
['o', 'O'],
['p', 'P'],
['q', 'Q'],
['r', 'R'],
['s', 'S'],
['t', 'T'],
['u', 'U'],
['v', 'V'],
['w', 'W'],
['x', 'X'],
['y', 'Y'],
['z', 'Z']
]);
const _list = computed(() : XAVATARGROUPITEMTYPE[] => {
let nowlist = props.list.slice(0)
if (props.maxCount > 0) {
nowlist = nowlist.slice(0, props.maxCount)
}
let compeltedList = [] as XAVATARGROUPITEMTYPE[];
for (let i = 0; i < nowlist.length; i++) {
let name = nowlist[i]
let isImg = _isFileImg(name);
let bgColor = ''
if (!isImg) {
name = name.slice(0, 1)
if (UpperEn.get(name) != null) {
name = UpperEn.get(name)!
}
let h = Math.random() * 360
let s = 78
let l = 62
bgColor = hslaToCss({ h: parseInt(h.toFixed(0)), s, l, a: 1 } as UTSJSONObject)
}
compeltedList.push({
name,
isImg,
bgColor: props.randomBgColor ? bgColor : _BgColor.value
} as XAVATARGROUPITEMTYPE)
}
return compeltedList;
})
const _size = computed(() : string => checkIsCssUnit(props.size, xConfig.unit))
const _sizeBypx = computed(() : number => {
let rpx = checkIsCssUnit(props.size, xConfig.unit);
let unit = getUnit(rpx)
let zhi = parseFloat(rpx)
if (unit == 'rpx') {
zhi = uni.rpx2px(parseFloat(rpx))
}
return zhi
})
const _showCount = computed(() : boolean => props.showCount)
const _round = computed(() : string => checkIsCssUnit(props.round, xConfig.unit))
const _flat = computed(() : boolean => props.flat)
const _gutter = computed(() : string => {
if (props.flat) {
return checkIsCssUnit(props.gutter, xConfig.unit)
}
return '-' + checkIsCssUnit(props.gutter, xConfig.unit)
})
const _gutterBypx = computed(() : number => {
let rpx = checkIsCssUnit(props.gutter, xConfig.unit);
let unit = getUnit(rpx)
let zhi = parseFloat(rpx)
if (unit == 'rpx') {
zhi = uni.rpx2px(parseFloat(rpx))
}
return zhi
})
const _count = computed(() : number => props.count)
const totalWidth = ref(0)
const column = ref(1)
const row = ref(0)
let tid = 0
const calcColumn = (width : number) => {
totalWidth.value = width;
if (_sizeBypx.value > 0) {
column.value = Math.floor((totalWidth.value + _gutterBypx.value) / (_sizeBypx.value + _gutterBypx.value * (props.flat ? 1 : -1)));
}
if (column.value > 0 && _list.value.length > 0) {
row.value = Math.ceil(props.list.length / column.value)
}
}
const getNodeinfo = () => {
uni.createSelectorQuery()
.in(getCurrentInstance()?.proxy)
.select(".xAvatarGroup")
.boundingClientRect((res : any) => {
let node = res as NodeInfo
calcColumn(node.width!)
})
.exec()
}
const createrObr = () => {
// #ifndef MP
let ele = xAvatarGroup.value as UniElement | null;
if (ele == null) return;
if (resizeObserver == null) {
resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
entries.forEach(entry => {
if (entry.target == ele) {
clearTimeout(tid)
tid = setTimeout(function () {
let bund = ele!.getBoundingClientRect()
calcColumn(bund.width)
}, 150);
}
})
})
}
resizeObserver!.observe(ele!)
// #endif
}
const removeObr = () => {
// #ifndef MP
resizeObserver?.disconnect()
// #endif
}
const itemClick = (item : XAVATARGROUPITEMTYPE, index : number) => {
emits('click', item.name, index)
}
const moreClick = () => {
emits('moreClick')
}
onMounted(() => {
getNodeinfo()
// #ifndef MP
createrObr();
// #endif
})
onUnmounted(() => {
clearTimeout(tid)
// #ifndef MP
removeObr()
// #endif
})
</script>
<style>
.xAvatarGroup {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.xAvatarGroupImage {
position: relative;
/* transition-duration: 200ms;
transition-property: margin-right, margin-bottom;
transition-timing-function: linear; */
}
.xAvatarGroupImageMore {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 5;
}
.xAvatarGroupImageBytext {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xAvatarGroupWrap {
flex-wrap: wrap;
}
.xAvatarGroupNoWrap {
flex-wrap: nowrap;
}
</style>
@@ -0,0 +1,192 @@
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, rpx2px } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xProvitae } from "../../config/xConfig.uts"
/**
* @name 返回顶部 xBacktop
* @page /pages/index/backtop
* @category 导航组件
* @description 在uvue页面中,根节点一定是scroll-view并且设置为flex:1才可滚动到顶部。
* 如果你想局部放到scroll-view组件中,你需要scroll事件中的top传递到属性scrollTop上并启用局部滚动置顶
* 如果你想改变或者自定位置,你可以直接在组件上写style来覆盖定位。详见:https://doc.dcloud.net.cn/uni-app-x/api/page-scroll-to.html
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xBacktop" })
type xBacktopPropsType = {
/**
* 圆角,空值时取全局的drawr圆角。
*/
round: string,
/**
* 向下滚动页面时,如果超过此值显示返回顶部按钮。
*/
offset: number,
/**
* 背景,支持渐变值如:linear-gradient(to left, #FFED46, #FF7EC7)
* 默认空值,取全局主题值。
*/
bgColor: string,
/**
* 高度
*/
width: string,
/**
* 宽度
*/
height: string,
/**
* 图标颜色。
*/
color: string,
/**
* 图标
*/
icon: string,
/**
* 图标大小
*/
iconSize: string,
/**
* 如果你想让本组件放置到局部的scroll中时,你外部scrollview通过scroll事件取得距离顶部的位置传递到此。
*/
scrollTop: number,
/**
* 禁用页面级根节点滚动后,可以通过scrollTop来实现局部置顶功能。
*/
disabledPageScroll:boolean
}
const props = withDefaults(defineProps<xBacktopPropsType>(), {
round: "50px",
offset: 100,
bgColor: "",
width: "50px",
height: "50px",
color: "white",
icon: "skip-up-fill",
iconSize: "30",
disabledPageScroll:false,
scrollTop:0,
})
const emits = defineEmits<{
/**
* 点击时触发
*/
(e: 'click'): void
}>()
// 响应式数据
const opened = ref<boolean>(false)
// 计算属性scrollTop
const _stop = computed((): number => {
if(props.disabledPageScroll){
return props.scrollTop
}
return xProvitae.scrollTop;
})
const _icon = computed((): string => props.icon)
const _iconSize = computed((): string => props.iconSize)
const _round = computed((): string => checkIsCssUnit(props.round, xConfig.unit))
const _width = computed((): number => {
let p = parseInt(props.width)
if (props.width.lastIndexOf('rpx') > -1) {
p = rpx2px(p)
}
return Math.floor(p)
})
const _height = computed((): number => {
let p = parseInt(props.height)
if (props.height.lastIndexOf('rpx') > -1) {
p = rpx2px(p)
}
return Math.floor(p)
})
const _styleMap = computed((): Map<string, any> => {
const styleMap = new Map<string, any>()
styleMap.set('width', _width.value.toString() + 'px')
styleMap.set('height', _height.value.toString() + 'px')
styleMap.set('borderRadius', _round.value)
if (props.bgColor.indexOf('linear-gradient') > -1) {
styleMap.set('backgroundImage', props.bgColor)
} else {
const color = props.bgColor == "" ? getDefaultColor(xConfig.color) : getDefaultColor(props.bgColor)
styleMap.set('backgroundColor', color)
}
if(props.disabledPageScroll){
styleMap.set('position', 'absolute')
}else{
styleMap.set('position', 'fixed')
}
styleMap.set('transform', opened.value ? 'scale(1)' : 'scale(0)')
styleMap.set('opacity', opened.value ? '1' : '0')
return styleMap
})
const _color = computed((): string => getDefaultColor(props.color))
// 监听器
watch(_stop, (newValue: number) => {
opened.value = newValue >= props.offset
})
// 方法
function onclick(): void {
/**
* 点击组件时触发。
*/
emits('click')
if(props.disabledPageScroll) return;
/**
* h5端的uni.pageScrollTo有bug,延迟厉害,性能差,不如原生快。
*/
// #ifdef WEB
window.scrollTo({ top: 0, behavior: 'smooth' })
// #endif
// #ifndef WEB
uni.pageScrollTo({
scrollTop: 0,
duration: 650
})
// #endif
}
</script>
<template>
<view @click="onclick" class="xBackTop" :style="_styleMap">
<!--
@slot 默认图标插槽
-->
<slot>
<x-icon :font-size="_iconSize" :name="_icon" :color="_color"></x-icon>
</slot>
</view>
</template>
<style scoped>
.xBackTop {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
right: 16px;
bottom: 24px;
transition-duration: 350ms;
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
transform: scale(0);
opacity: 1;
/* box-shadow: 0 5px 24px rgba(0, 0, 0, 0.06); */
}
</style>
@@ -0,0 +1,267 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick, getCurrentInstance } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type positionType = "right" | "left" | "bottomLeft" | "bottomRight" | 'top' | 'bottom'
/**
* @name 角标 xBadge
* @page /pages/index/badge
* @category 展示组件
* @description 角标
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xBadge"})
const proxy = getCurrentInstance()?.proxy??null;
type xBadgePropsType = {
/**
* 文字大小,可数字或者带单位
*/
fontSize: string,
/**
* 背景颜色,合法和颜色值及主题名称
*/
bgColor: string,
/**
* 文字颜色,合法和颜色值及主题名称
*/
fontColor: string,
/**
* 是否显示为点,优先级小于count,label
*/
dot: boolean,
/**
* 是否显示为文本数字,优先级小于label
*/
count: number,
/**
* 为数字时大于此值显示+号
*/
maxCount: number,
/**
* 是否显示为文本,优先级最大
*/
label: string,
/**
* 位置
*/
position: positionType,
/**
* 偏移
*/
offset: number[]
}
const props = withDefaults(defineProps<xBadgePropsType>(), {
fontSize: "9",
bgColor: "error",
fontColor: "white",
dot: true,
count: 0,
maxCount: 99,
label: "",
position: "right",
offset: ():number[] => [0, 0] as number[]
})
// 响应式数据
const padding = ref<string>('4px 4px')
const test = ref<Map<string, string>>(new Map<string, string>([['border', '2px solid red'], ['background-color', 'green']]))
// 方法
function getNodeInfo(): void {
uni.createSelectorQuery().in(proxy)
.select(".xBadge-countAndLabel")
.boundingClientRect().exec((ret) => {
if(ret.length==0) return;
let nodeinfo = ret[0] as NodeInfo
if(nodeinfo==null) return;
let width = nodeinfo.width as number
let height = nodeinfo.height as number
let max = Math.max(width, height)
let px = Math.ceil(max / 2);
// let py = max / 2;
padding.value = `${px}px ${px}px`;
})
}
// 计算属性
const _offset = computed((): number[] => {
return props.offset;
})
const _isDot = computed((): boolean => {
if (props.label != "" || props.count > 0 || !props.dot) return false;
return true;
})
const _fontColor = computed((): string => {
return getDefaultColor(props.fontColor)
})
const _fontSize = computed((): string => {
return checkIsCssUnit(props.fontSize, xConfig.unit)
})
const _label = computed((): string => {
if (props.label != "") return props.label;
if (props.count > 0 && props.count <= props.maxCount) return props.count.toString();
if (props.count <= 0) return ""
return props.maxCount.toString() + "+"
})
const _cStyles = computed((): Map<string, string>[] => {
let trs = ''
if (props.position == 'right') {
trs = 'translate(50%, -50%)'
} else if (props.position == 'left') {
trs = 'translate(-50%, -50%)'
} else if (props.position == 'bottomLeft') {
trs = 'translate(-50%, 50%)'
} else if (props.position == 'bottomRight') {
trs = 'translate(50%, 50%)'
} else if (props.position == 'top') {
trs = 'translate(0%, -50%)'
} else if (props.position == 'bottom') {
trs = 'translate(0%, 50%)'
}
let top = ''
let bottom = ''
let left = ''
let right = ''
if (props.position == 'top') {
top = '0px'
left = 'auto'
right = 'auto'
} else if (props.position == 'bottom') {
bottom = '0px'
left = 'auto'
right = 'auto'
} else if (props.position == 'right') {
top = _offset.value[1].toString() + 'px'
right = _offset.value[0].toString() + 'px'
} else if (props.position == 'left') {
top = '0px'
left = '0px'
} else if (props.position == 'bottomLeft') {
bottom = '0px'
left = '0px'
} else if (props.position == 'bottomRight') {
bottom = '0px'
right = '0px'
}
let dotMapCs = new Map<string, string>()
dotMapCs.set("background", getDefaultColor(props.bgColor))
dotMapCs.set("left", left)
dotMapCs.set("right", right)
dotMapCs.set("top", top)
dotMapCs.set("bottom", bottom)
dotMapCs.set("transform", trs)
let labelMapCs = new Map<string, string>()
labelMapCs.set("background", getDefaultColor(props.bgColor))
labelMapCs.set("left", left)
labelMapCs.set("right", right)
labelMapCs.set("top", top)
labelMapCs.set("bottom", bottom)
labelMapCs.set("transform", trs)
labelMapCs.set("visibility", _label.value == "" ? "hidden" : "visible")
nextTick(() => {
getNodeInfo();
})
return [dotMapCs, labelMapCs] as Map<string, string>[]
})
// 监听器
watch([
(): string => props.label,
(): string => props.position,
(): number => props.count,
(): number[] => props.offset,
], (): void => {
nextTick(() => {
getNodeInfo();
})
})
// 生命周期
onMounted((): void => {
getNodeInfo();
})
</script>
<template>
<view class="xBadge" :style='{padding:padding}'>
<view class="xBadgeWrap">
<text :style='_cStyles[0]!' class="xBadge-dot" :class="[_isDot?'noneShow':'nonex']"></text>
<view id="xBadge-countAndLabel" class="xBadge-countAndLabel" :class="[_isDot?'nonex':'noneShow']"
:style='_cStyles[1]!'>
<text class="xBadge-countAndLabelText"
:style='{color:_fontColor,fontSize:_fontSize}'>{{_label}}</text>
</view>
<!--
@slot 默认内容区域,你的正常内容放置在标签内
-->
<slot></slot>
</view>
</view>
</template>
<style>
.xBadge {
overflow: visible;
}
.xBadge-countAndLabel {
position: absolute;
z-index: 3;
border-radius: 100px;
/* min-width: 16px; */
padding: 0rpx 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.xBadge-countAndLabelText {
line-height: 1.5;
text-align: center;
}
.xBadge-dot {
position: absolute;
width: 6px;
height: 6px;
border-radius: 18px;
z-index: 3;
opacity: 0;
}
.noneShow {
opacity: 1;
}
.nonex {
opacity: 0;
}
.xBadgeWrap {
flex-direction: column;
justify-content: center;
align-items: center;
overflow: visible;
position: relative;
}
</style>
@@ -0,0 +1,70 @@
class code128b {
// 输入数据
data = ""
// Code 128 模式表:索引0..106 -> 二进制模块串(1=黑,0=白),来自业界通用表
// 参考:StartB=104 -> 11010010000, Stop=106 -> 1100011101011
private static PATTERNS = [
"11011001100","11001101100","11001100110","10010011000","10010001100","10001001100",
"10011001000","10011000100","10001100100","11001001000","11001000100","11000100100",
"10110011100","10011011100","10011001110","10111001100","10011101100","10011100110",
"11001110010","11001011100","11001001110","11011100100","11001110100","11101101110",
"11101001100","11100101100","11100100110","11101100100","11100110100","11100110010",
"11011011000","11011000110","11000110110","10100011000","10001011000","10001000110",
"10110001000","10001101000","10001100010","11010001000","11000101000","11000100010",
"10110111000","10110001110","10001101110","10111011000","10111000110","10001110110",
"11101110110","11010001110","11000101110","11011101000","11011100010","11011101110",
"11101011000","11101000110","11100010110","11101101000","11101100010","11100011010",
"11101111010","11001000010","11110001010","10100110000","10100001100","10010110000",
"10010000110","10000101100","10000100110","10110010000","10110000100","10011010000",
"10011000010","10000110100","10000110010","11000010010","11001010000","11110111010",
"11000010100","10001111010","10100111100","10010111100","10010011110","10111100100",
"10011110100","10011110010","11110100100","11110010100","11110010010","11011011110",
"11011110110","11110110110","10101111000","10100011110","10001011110","10111101000",
"10111100010","11110101000","11110100010","10111011110","10111101110","11101011110",
"11110101110","11010000100","11010010000","11010011100","1100011101011"
]
// Code Set B: 字符到值(0..95 => ASCII 32..127
private mapCharToValueB(ch:string):number {
const code = ch.charCodeAt(0)!;
if (code < 32 || code > 127) {
return -1
}
return code - 32
}
constructor(text:string) {
this.data = text
}
encode():string {
if (this.data.length == 0) return ""
const values = [] as number[]
for (let i = 0; i < this.data.length; i++) {
values.push(this.mapCharToValueB(this.data.charAt(i)))
}
// 起始码 CodeB=104
const startVal = 104
let checksum = startVal
for (let i = 0; i < values.length; i++) {
checksum += values[i] * (i + 1)
}
checksum = checksum % 103
// 组合:StartB + 数据 + 校验 + Stop
const bits = [] as string[]
bits.push(code128b.PATTERNS[startVal])
for (let i = 0; i < values.length; i++) {
bits.push(code128b.PATTERNS[values[i]])
}
bits.push(code128b.PATTERNS[checksum])
// Stop=106
bits.push(code128b.PATTERNS[106])
return bits.join("")
}
}
export { code128b };
@@ -0,0 +1,87 @@
/**
* 商品条码严格按照国际通用码
* @author tmzdy
* @description 资料参考:https://zhuanlan.zhihu.com/p/120676811
*/
class ean13 {
//编码数据
data=""
//护码
safeCode = "101"
middleCode = "01010"
//编码方式,我们中国是6:LGGGLL
EAN13_STRUCTURE = [
'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL'
];
constructor(text:string) {
let data = text;
if (data.search(/^[0-9]{12}$/)!== -1 as number ) {
data += (this.checksum(data)).toString();
}
this.data = data.toUpperCase();
}
//校验码。
checksum(codestr:string):number{
const res = codestr.substring(0, 12).split('')
const resNumber = res.map((n:string):number => parseInt(n))
const jg=resNumber.reduce((sum:number, a:number, idx:number):number => (idx % 2) > 0 ? sum + a * 3 : sum + a, 0);
return (10 - (jg % 10)) % 10;
}
//按钮编码规则提取逻辑值,
getEan13Str(key:string):string[]{
if(key=="L") return [
'0001101', '0011001', '0010011', '0111101', '0100011',
'0110001', '0101111', '0111011', '0110111', '0001011'
]
if(key=="G") return [
'0100111', '0110011', '0011011', '0100001', '0011101',
'0111001', '0000101', '0010001', '0001001', '0010111'
]
if(key=="R") return [
'1110010', '1100110', '1101100', '1000010', '1011100',
'1001110', '1010000', '1000100', '1001000', '1110100'
]
return [] as string[];
}
encodeByGuzhe(dataStr:string, structure:string):string{
let encoded = dataStr.split('')
let atrartys = [] as string[][]
for(let i=0;i<encoded.length;i++){
let chart = structure.substring(i,i+1)
atrartys.push(this.getEan13Str(chart))
}
let encoded_str = atrartys.map((val:string[], idx:number):string => {
let stur = dataStr.substring(idx,idx+1)
return val[parseInt(stur)]
});
return encoded_str.join("")
};
encode():string[] {
// 取编码方式。
let codeFang = this.EAN13_STRUCTURE[parseInt(this.data.substring(0,1))]
//取后面的12位数字
let data = this.data.substring(1,13)
let leftCode = data.substring(0,6)
let rightCode = data.substring(6,13)
let left_code_str = this.encodeByGuzhe(leftCode,codeFang);
//右资料码按规则是物料码+校验码
let right_code_str = this.encodeByGuzhe(rightCode,"RRRRRR");
// 组合码制
//护码+左资料码+中间码+右资料码+护码
// let totalcode = this.safeCode+left_code_str+this.middleCode+right_code_str+this.safeCode
return [this.safeCode,left_code_str,this.middleCode,right_code_str,this.safeCode]
}
}
export { ean13 };
@@ -0,0 +1,245 @@
<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { code128b } from "./utsbarcode/code128.uts"
import { ean13 } from "./utsbarcode/ean13.uts"
/**
* @name 条码 xBarcode
* @page /pages/index/barcode
* @category 其它组件
* @description 本条码暂只开发了cdoebar,ean13两种。ean13是国际通用码,也是国内商品的码,
* 我严格按照编码规则进行开发,所以提供商品码时,一定要准确。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: ('xCircleProgeress-' + getUid()) as string,
boxWidth: 0,
boxHeight: 0,
}
},
props: {
/**
* 窗口宽
*/
width: {
type: String,
default: 'auto'
},
/**
* 宽器高,这将影响条码的高度
*/
height: {
type: String,
default: '140px'
},
/**
* 上下间隙,单位是px
*/
pading: {
type: Number,
default: 20
},
/**
* 条码颜色
*/
color: {
type: String,
default: "black"
},
/**
* 目前我仅开发两种常见的国内格式
* codebar正常的数字字符条码
* ean13国际通用物品编码,也是国内的商品码以69开头。
*/
encode: {
type: String as PropType<"codebar" | "ean13" | "code128">,
default: "ean13"
},
/**
* 条码内容
*/
text: {
type: String,
default: ""
},
},
computed: {
_width() : string {
return checkIsCssUnit(this.width, 'rpx')
},
_height() : string {
return checkIsCssUnit(this.height, 'rpx')
},
_color() : string {
return getDefaultColor(this.color);
},
_text() : string {
return this.text;
},
},
watch: {
text(newValue : string) {
if (newValue == "") return;
this.getNodeInfo();
}
},
mounted() {
if (this.text == "") return;
// #ifndef APP-HARMONY
this.getNodeInfo();
// #endif
// #ifdef APP-HARMONY
let t = this;
setTimeout(function() {
t.getNodeInfo();
}, 120);
// #endif
},
methods: {
getNodeInfo() {
let _this = this;
uni.createSelectorQuery().in(this)
.select(".xBarcode")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
this.boxWidth = nodeinfo.width!
this.boxHeight = nodeinfo.height!
// #ifdef APP-IOS || WEB
_this.dreawer()
// #endif
// #ifdef APP-ANDROID||APP-HARMONY
setTimeout(function() {
_this.dreawer()
}, 50);
// #endif
})
},
dreawer() {
this.clear()
let canvas = this.$refs['xBarcode'] as UniCanvasElement
let ctx = canvas.getContext('2d')!
// 处理高清屏逻辑
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
let ratio = 1;
let code = ""
let eanCode = [] as string[]
if (this.encode == "ean13") {
eanCode = new ean13(this._text).encode()
code = eanCode.join("")
} else if (this.encode == "codebar" || this.encode == "code128") {
code = new code128b(this._text).encode()
}
let strCode = code.split("");
let linewidth = 2;
let totalWidth = strCode.length * (linewidth);
let barheight = this.boxHeight - this.pading * 2;
let start_x = (this.boxWidth - totalWidth) / 2
let start_y = this.pading
ctx!.beginPath()
ctx!.fillStyle = this._color
if (this.encode == "codebar" || this.encode == "code128") {
ctx.font = `${16 * ratio}px Arial`;
let texts = this._text.split("")
let textwidth = ctx.measureText(this._text).width;
let space = (totalWidth - textwidth)/(texts.length-1)
// let textwidth = ctx.measureText(this._text).width + (texts.length-1) * 6
// 绘制数字。
let sx = (this.boxWidth-totalWidth)/2;
for (let a0 = 0; a0 < texts.length; a0++) {
let sxx = (ctx.measureText(texts[a0]).width+space) * a0 + sx
ctx!.fillText(texts[a0], sxx * ratio, (start_y + barheight) * ratio)
}
for (let i = 0; i < strCode.length; i++) {
if (strCode[i] == "1") {
ctx!.fillRect((i * linewidth + start_x + linewidth) * ratio, start_y * ratio, linewidth * ratio, (barheight - 24) * ratio)
}
}
} else if (this.encode == "ean13") {
let k = 0;
ctx.font = `${13 * ratio}px Arial`;
ctx!.fillText(this._text.substring(0, 1), (start_x - 10) * ratio, (start_y + barheight) * ratio)
for (let j = 0; j < eanCode.length; j++) {
let item = eanCode[j];
let itemcodeas = item.split("")
let offsetHeigt = (j == 0 || j == 2 || j == 4) ? 0 : 12
// 绘制数字。
if (j == 0) {
let ncsr = this._text.substring(1, 7).split("")
for (let a0 = 0; a0 < ncsr.length; a0++) {
ctx!.fillText(ncsr[a0], (start_x + 10 + (a0 * 13) + linewidth) * ratio, (start_y + barheight) * ratio)
}
}
if (j == 3) {
let ncsr = this._text.substring(7).split("")
for (let a0 = 0; a0 < ncsr.length; a0++) {
ctx!.fillText(ncsr[a0], ((this.boxWidth / 2) + 10 + (a0 * 13) + linewidth) * ratio, (start_y + barheight) * ratio)
}
}
for (let i = 0; i < itemcodeas.length; i++) {
if (itemcodeas[i] == "1") {
ctx!.fillRect(
(k * linewidth + start_x + linewidth) * ratio,
start_y * ratio,
linewidth * ratio,
(barheight - offsetHeigt) * ratio)
}
++k;
}
}
}
ctx.fill()
},
clear() {
let canvas = this.$refs['xBarcode'] as UniCanvasElement
let ctx = canvas.getContext('2d')!
// 处理高清屏逻辑
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
let ratio = 1;
try{
ctx?.reset()
}catch(e){
//TODO handle the exception
}
ctx.fillStyle = 'rgba(0,0,0,0)'
ctx.fillRect(0, 0, this.boxWidth, this.boxHeight)
ctx.fill()
}
},
}
</script>
<template>
<canvas class="xBarcode" ref="xBarcode" :id="id" :style="{width:_width,height:_height}"></canvas>
</template>
<style scoped>
</style>
@@ -0,0 +1,126 @@
<script lang="ts">
import { checkIsCssUnit, getUid } from '../../core/util/xCoreUtil.uts'
import { BARRAGE_ITEM_AR } from "../x-barrage/interface.uts"
import { PropType } from "vue"
/**
*
* @name 弹幕子节点 BarrageItem
* @page /pages/index/barrage
* @category 反馈组件
* @description 弹幕内部私有组件,不要引用。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: 'xBarrage-' + getUid(),
real_w: 0,
real_h: 0,
totalWidth: 10000,
dur: 0,
endX: 0,
rightX: 0,
status: 'none',
}
},
props: {
/**
* 速度
*/
speed: {
type: Number,
default: 30
},
label: {
type: String,
default: ""
},
/**
* 速度
*/
boxWidth: {
type: Number,
default: 0
},
index: {
type: Number,
default: 0
},
data: {
type: Array as PropType<BARRAGE_ITEM_AR[]>,
default: () : BARRAGE_ITEM_AR[] => [] as BARRAGE_ITEM_AR[]
}
},
mounted() {
this.getNodes();
},
methods: {
getNodes() {
if (this.status == 'play') return;
uni.createSelectorQuery().in(this)
.select(".xBarrageItemTextContentBox")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
this.real_w = nodeinfo.width!;
this.real_h = nodeinfo.height!;
let syw = this.boxWidth + this.real_w;
this.totalWidth = syw
this.dur = Math.ceil(syw / this.speed) * 1000;
this.endX = this.boxWidth + this.real_w
this.status = 'play'
})
},
}
}
</script>
<template>
<view class="xBarrageItemTextContent" :style="{width:totalWidth+'px'}">
<view class="xBarrageItemTextContentBox" :style="{
'transition-duration':dur+'ms',
'transform':`translateX(-${endX}px)`,
'transition-delay':index*800,
}">
<text v-for="(item) in data" :key="item.id" class="xBarrageItemText">{{item.label}}</text>
</view>
</view>
</template>
<style scoped>
.xBarrageItemTextContent {
position: absolute;
left: 0px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.xBarrageItemTextContentBox {
transition-property: transform;
transition-timing-function: linear;
/* transform: translateX(100%); */
display: flex;
flex-direction: row;
}
.xBarrageItemText {
padding: 0 10px;
height: 24px;
background-color: rgba(0, 0, 0, 0.64);
color: white;
font-size: 14px;
text-overflow: ellipsis;
max-width: 110px;
border-radius: 20px;
line-height: 24px;
text-align: center;
margin-right: 20px;
}
</style>
@@ -0,0 +1,8 @@
export type BARRAGE_ITEM_AR = {
id:string,
top:string,
parentIndex:number,
chirenIndex:number,
label:string,
delay:number
}
@@ -0,0 +1,122 @@
<script lang="ts">
import { checkIsCssUnit, getUid, splitArrayByGroup } from '../../core/util/xCoreUtil.uts'
// import barrageText from "./barrage-text.uvue"
import { BARRAGE_ITEM_AR } from "./interface.uts"
/**
*
* @name 弹幕 Barrage
* @page /pages/index/barrage
* @category 反馈组件
* @description 弹幕,当前版本比较初级。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
// components: {
// 'barrage-text': barrageText
// },
data() {
return {
id: 'xBarrage-' + getUid(),
real_w: 0,
real_h: 0,
datas: [] as BARRAGE_ITEM_AR[][]
}
},
props: {
/**
* 弹幕的总高度。如果你的容器高小于此高会被裁切。
*/
layerHeight: {
type: String,
default: ""
},
/**
* 字符数组
*/
list: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
}
},
computed: {
_list() : string[] {
return this.list
},
_height() : string {
if (this.layerHeight == "") return '100%'
return checkIsCssUnit(this.layerHeight, 'rpx')
}
},
watch: {
list() {
this.chuliDatas();
}
},
mounted() {
this.getNodes();
},
methods: {
chuliDatas() {
let maxLines = Math.floor(this.real_h / 34);
let ps = splitArrayByGroup(this.list, maxLines)
let mbl = ps.map((el : string[], index : number) : BARRAGE_ITEM_AR[] => {
return el.map((item : string, chirenIndex : number) : BARRAGE_ITEM_AR => {
let tels = {
id: 'xBarrage_item_id' + getUid(),
top: (index * 24 + 10).toString() + 'px',
label: item,
parentIndex: index,
chirenIndex,
delay: index * 500 + (chirenIndex) * 1500
} as BARRAGE_ITEM_AR
return tels
})
})
this.datas = mbl
},
getIds(index : number) : string {
return 'xBarrageBOx-' + getUid() + '-' + (index).toString()
},
getNodes() {
uni.createSelectorQuery().in(this)
.select("#" + this.id)
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
this.real_w = nodeinfo.width!;
this.real_h = nodeinfo.height!;
this.chuliDatas();
})
},
}
}
</script>
<template>
<view>
<!--
@slot 默认内容插槽,内容的高度至少要大于弹幕整体的layerHeight高度。
-->
<slot></slot>
<view class="xBarrageWrap" :id="id" :style="{height:_height}">
<x-barrage-item v-for="(item2,index2) in datas" :key='getIds(index2)' :index="index2" :boxWidth="real_w"
:data="item2" :style="{
top:(index2*34+10).toString()+'px',
}"></x-barrage-item>
</view>
</view>
</template>
<style scoped>
.xBarrageWrap {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
@@ -0,0 +1,736 @@
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
import { type PropType } from "vue"
import { getUid, setPagePullRefresh, getPagePullRefresh ,checkIsCssUnit} from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xDate, xDateTypeTime, createDate } from "../../core/util/xDate.uts"
import { xConfig } from "../../config/xConfig.uts"
import { PICKER_ITEM_INFO } from "../../interface.uts"
import { DateTimeFormatOrNull} from "@/uni_modules/x-vuei18n-s/interface.uts"
type coverValueType = {
value : string[],
str : string
}
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
/**
* @name 时间区间选择 xBetweenTime
* @description 快速的时间区间选择器,方便时间选择自动判断前后时间大小并校正。
* @page /pages/index/between-time
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xBetweenTime"})
const i18n = xConfig.i18n
const proxy = getCurrentInstance()?.proxy??null;
defineSlots<{
default(props: { show: boolean, startVal: string, endVal: string }): any
}>()
const emits = defineEmits([
/**
* 取消时触发
*/
'cancel',
/**
* 确认触发
* @param {string} date 当前选中时间id值
*/
'confirm',
/**
* 滑动变换时触发
* @param {string} date - 当前选中时间
*/
'change',
/**
* 快速日期被选中时触发
* @param {UTSJSONObject<{text:string,value:string[]}>} item - 当前选中时间组
*/
'dateClick',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 经格式化后的值。等同v-model:model-str
*/
'update:modelStr',
'update:modelValue'
])
type xBetweenTimePropsType = {
/**
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
* 否则报错,无法运行。
*/
modelValue: string[],
/**
* 当前时间经过format格式化后输出的值。
* 此值不会处理输入,只输出显示。
*/
modelStr: string,
/**
* 当前打开的状态。
* 等同v-model:model-show
*/
modelShow: boolean,
/**
* 顶部标题
* 空默认为:请选择时间(根据语言不同也不同)
* 如果你提供了值,组件内的多语言失效以你设定的为准
*/
title: string,
/**
* 开始时间,请提供正确的时间格式
*/
start: string,
/**
* 结束时间,请提供正确的时间格式
*/
end: string,
/**
* 精确到的级别,这里只是展示,具体的返回值还是完整的值。
* year:年
* month:年月
* day:年月日
* hour:年月日小时
* minute:年月日小时分钟
* second:年月日小时分钟秒
*/
type: ModelType,
/**
* 输出时间格式,只对v-model:modelStr及输入框展示有效YYYY-MM-DD,为空时由当前语文决定
* 因此它可能不是一个标准时间,比如YY SS ,所以不能作为modelValue使用
* 有效格式:
* YYYY年
* MM月
* DD日
* hh小时
* mm分钟
* ss秒
*/
format: string,
/**
* 上方的单位名称
* 默认为:['年', '月', '日', '时', '分', '秒']
*/
cellUnits: string[],
/**
* 快速时间区间选择,如果直接填写数字字符,会以你提供的数字最近多少来天来算。
* d:本日
* w:本周
* m:本月
* y:本年
* q:本季度
* 7:最近7天,后面的依此类推,数字的就是最近xx天。
* px:前x年,p+[x]数字依此类推,表示前x年,如:p1,p2...
* 如果提供以下json结构,则以你自定的为准。
* UTSJSONObject:{title:'本学年',start:'2025-1-1',end:'2025-12-31'} as UTSJSONObject
*/
quickDate: Array<any>,
/**
* 是否懒加载内部内容。
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
* 要兼容微信,必须设置为true,非微信可以为false
*/
lazyContent: boolean,
/**
* 如果你的快捷选择较多可能会让高度不足,需要自行设置下高。
*/
drawerSize: string,
/**
* 是否禁用清除按钮,默认不禁用,允许用户清空选择。点确认,以清空选项数据
*/
disabledClear: boolean,
/**
* 是否禁用弹出
*/
disabled: boolean,
/**
* 宽屏时是否让内容剧中显示
* 并限制其宽为屏幕宽,只展示中间内容以适应宽屏。
*/
widthCoverCenter: boolean
}
const props = withDefaults(defineProps<xBetweenTimePropsType>(), {
modelValue: () : string[] => [] as string[],
modelStr: "",
modelShow: false,
title: "",
start: "",
end: "",
type: "day",
format: "",
cellUnits: () : string[] => [] as string[],
quickDate: () : Array<any> => ['d', 'w', 'm', 'y', 'q'] as Array<any>,
lazyContent: true,
drawerSize: '540px',
disabledClear: false,
disabled: false,
widthCoverCenter: true
})
// 响应式数据
let startValue = new xDate();
let endValue = new xDate();
startValue.subtraction(1, 'y')
const show = ref(false)
const nowValue = ref(['', ''] as string[])
const nowModelValue = ref(['', ''] as string[])
const startDate = ref(startValue)
const endDate = ref(endValue)
const changeIndex = ref(0)
const yanchiDuration = ref(false)
const quicklist = ref([] as coverValueType[])
const quicklistSelectedStr = ref('')
const tid = ref(1)
// 计算属性
const _formatValStr = computed((): string[] => {
let estrt = nowValue.value?.[0]??'';
let eend = nowValue.value?.[1]??'';
let selfformat = props.format==''?'YYYY-MM-DD':props.format
let start = estrt == '' ? '' : (new xDate(estrt)).format(selfformat)
let ebd = eend == '' ? '' : (new xDate(eend)).format(selfformat)
return [start,ebd] as string[]
})
const _lazyContent = computed((): boolean => props.lazyContent)
const _disabledClear = computed((): boolean => props.disabledClear)
const _start_date = computed((): xDate => {
if (props.start == "") return startDate.value
return new xDate(props.start)
})
const _end_date = computed((): xDate => {
if (props.end == "") return endDate.value
return new xDate(props.end)
})
const _start_date_str = computed((): string => _start_date.value.format())
const _end_date_str = computed((): string => _end_date.value.format())
const _start_date_str_format = computed((): string => {
// '开始时间'
if (nowValue.value[0] == '') return xConfig.i18n.t("tmui4x.betweentTime.start")
// const date = (new xDate(nowValue.value[0])).format(props.format);
if(props.format!=''){
return (new xDate(nowValue.value[0])).format(props.format);
}
return xConfig.i18n.d(nowValue.value[0],null,{year:"numeric",month:"numeric",day:"numeric"} as DateTimeFormatOrNull)
})
const _end_date_str_format = computed((): string => {
// '结束时间'
if (nowValue.value[1] == '') return xConfig.i18n.t("tmui4x.betweentTime.end")
// (new xDate(nowValue.value[1])).format(props.format)
if(props.format!=''){
return (new xDate(nowValue.value[1])).format(props.format)
}
return xConfig.i18n.d(nowValue.value[1],null,{year:"numeric",month:"numeric",day:"numeric"} as DateTimeFormatOrNull)
})
const _backgroundColor = computed((): string => {
if (xConfig.dark == 'dark') {
return getDefaultColor(xConfig.inputDarkColor)
}
return getDefaultColor(xConfig.inputBgColor)
})
const _borderColor = computed((): string => {
if (xConfig.dark == 'dark') {
return getDefaultColor(xConfig.borderDarkColor)
}
return getDefaultColor(xConfig.inputBgColor)
})
const _activeBorderColor = computed((): string => getDefaultColor(xConfig.color))
const _placeStyle = computed((): string => {
if (xConfig.dark == 'dark') {
return "color:#c7c7c7;"
}
return "color:#838383;"
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') {
return "#efefef"
}
return "#333"
})
const _isDark = computed((): boolean => xConfig.dark == 'dark')
const _checkPass = computed((): boolean => nowValue.value.some((el:string):boolean=>el==''))
const _disabled = computed((): boolean => props.disabled)
const _cellUnits = computed((): string[] => {
if(props.cellUnits.length==0){
return [
xConfig.i18n.t("tmui4x.pickerDate.year"),
xConfig.i18n.t("tmui4x.pickerDate.month"),
xConfig.i18n.t("tmui4x.pickerDate.day"),
xConfig.i18n.t("tmui4x.pickerDate.hour"),
xConfig.i18n.t("tmui4x.pickerDate.minute"),
xConfig.i18n.t("tmui4x.pickerDate.second"),
]
}
return props.cellUnits;
})
function getTypes() : xDateTypeTime {
if (props.type == 'year') {
return 'y' as xDateTypeTime
} else if (props.type == 'month') {
return 'm' as xDateTypeTime
} else if (props.type == 'day') {
return 'd' as xDateTypeTime
} else if (props.type == 'hour') {
return 'h' as xDateTypeTime
} else if (props.type == 'minute') {
return 'M' as xDateTypeTime
} else if (props.type == 'second') {
return 's' as xDateTypeTime
}
return 's' as xDateTypeTime
}
function coverStrVal() : string {
let estrt = nowValue.value[0];
let eend = nowValue.value[1];
let selfformat = props.format==''?'YYYY-MM-DD':props.format
let start = estrt == '' ? '' : (new xDate(estrt)).format(selfformat)
let ebd = eend == '' ? '' : (new xDate(eend)).format(selfformat)
return start + '~' + ebd
}
function sorDateVaild(str : string[]) : string[] {
let types = getTypes()
str.sort((a : string, b : string) : number => {
return new xDate(a).getTime(types) - new xDate(b).getTime(types)
})
return str;
}
function validTimeDate(val : string[]) : string[] {
// let defaulttime = new xDate().format()
let str = ['', '']
if (val.length >= 1) {
str[0] = val[0]!
}
if (val.length >= 2) {
str[1] = val[1]!
}
return sorDateVaild(str)
}
function tongbuModelStr(){
let str = coverStrVal()
/**
* 经格式化后的值。等同v-model:model-str
*/
emits('update:modelStr', str == '~' ? '' : str);
}
function getQuickDateType() : coverValueType[] {
let typelist = [] as coverValueType[];
let list:Array<any> = props.quickDate;
if (list.length == 0) return typelist;
let _date = new xDate()
let startFDate = "YYYY/MM/DD 00:00:00"
let endFDate = "YYYY/MM/DD 23:59:59"
let _start = new xDate(new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
let _end = new xDate(new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
_date = new xDate(_start)
for (let i = 0; i < list.length; i++) {
let item : any = list[i];
if(typeof item == 'string'){
if(item.substring(0,1)=='p'){
let dshi = parseInt(item.substring(1))! as number
let date = _date.getClone()
let start = _start
date.subtraction(dshi, 'y')
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
let end = date.format()
// '前' + dshi + '年'
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle3",dshi) } as coverValueType)
}else if (!isNaN(parseInt(item))) {
let dshi = parseInt(item)! as number;
let date = _date.getClone()
let start = _start
date.subtraction(dshi, 'd')
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
let end = date.format()
// '最近' + item + '天'
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle2",dshi) } as coverValueType)
} else {
let dshi = 0;
let date = new xDate(_end)
let startFDate = "YYYY/MM/DD 00:00:00"
let endFDate = "YYYY/MM/DD 23:59:59"
let start = date.format(startFDate)
let end = date.format(endFDate)
// "本日"
let desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",0)
if (item == 'w') {
start = date.getDateStartOf('w').format(startFDate)
end = date.getDateEndOf('w').format(endFDate)
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",1);//"本周"
} else if (item == 'm') {
start = date.getDateStartOf('m').format(startFDate)
end = date.getDateEndOf('m').format(endFDate)
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",2);//"本月"
} else if (item == 'y') {
start = date.getDateStartOf('y').format(startFDate)
end = date.getDateEndOf('y').format(endFDate)
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",3);//"本年"
} else if (item == 'q') {
let nowq = date.getQuarter('')
let itemqatar = nowq[0]
start = itemqatar.start
end = itemqatar.end
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",4);//"本季度"
}
typelist.push({ value: [start, end], str: desc } as coverValueType)
}
}else if(typeof item == 'number'){
let dshi = item as number;
let date = _date.getClone()
let start = _start
date.subtraction(dshi, 'd')
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
let end = date.format()
// '最近' + item + '天'
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle2",dshi) } as coverValueType)
}
else if(item instanceof UTSJSONObject){
let title = item.getString('title')
let v_start = item.getString('start')
let v_end = item.getString('end')
if(title!=null&&v_start!=null&&v_end!=null){
typelist.push({ value: [v_start, v_end], str: title } as coverValueType)
}
}
}
return typelist;
}
function cancelResetDataCol() {
nowValue.value = nowModelValue.value.slice(0)
}
function clearDate(){
let dst = ['',''] as string[]
nowValue.value = dst
}
function dateChangeView(datestr : string) {
clearTimeout(tid.value)
tid.value = setTimeout(function() {
quicklistSelectedStr.value = ''
let nowvalu = nowValue.value.slice(0)
nowvalu[changeIndex.value] = datestr;
let strStart = nowvalu[0]
let strEnd = nowvalu[1]
let types = getTypes()
if (strEnd != '' && strStart != '') {
if (changeIndex.value == 0) {
if (new xDate(strStart).isBetweenOf(new xDate(strEnd), '>', types)) {
strEnd = strStart
}
} else if (changeIndex.value == 1) {
if (new xDate(strEnd).isBetweenOf(new xDate(strStart), '<', types)) {
strStart = strEnd
}
}
}
nowValue.value = [strStart, strEnd]
}, 50);
}
function tagsClick(item : coverValueType) {
nowValue.value = item.value
quicklistSelectedStr.value = item.str
emits('dateClick',{text:item.str,value:item.value.slice(0)} as UTSJSONObject)
}
function inputClick(index : number) {
changeIndex.value = index;
let nowvalu = nowValue.value.slice(0)
let strStart = nowvalu[0]
let strEnd = nowvalu[1]
if (changeIndex.value == 0) {
if (strStart == '') {
// strStart = strEnd != '' ? strEnd : (endDate.value.format())
let tempStart = new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')
strStart = new xDate(tempStart).format()
nowValue.value = [strStart, strEnd]
}
} else if (changeIndex.value == 1) {
if (strEnd == '') {
// strEnd = strStart != '' ? strStart : (endDate.value.format())
let tempEnd = new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')
strEnd = new xDate(tempEnd).format()
nowValue.value = [strStart, strEnd]
}
}
}
function openShow() {
if(_disabled.value) return;
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', true)
}
function onClose() {
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', false)
cancelResetDataCol()
if (_lazyContent.value) {
yanchiDuration.value = false
}
}
function onOpen() {
yanchiDuration.value = true
}
function onCancel() {
emits('cancel')
cancelResetDataCol()
}
function onConfirm() {
let nowval = nowModelValue.value.slice(0)
let str = coverStrVal()
let tmdate = [] as string[];
if(nowValue.value[0]!=''&&nowValue.value[1]!=''){
tmdate = nowValue.value.slice(0)
nowModelValue.value = tmdate
}
/**
* 点击确认时同步。等同v-model
*/
emits('update:modelValue', tmdate);
/**
* 经格式化后的值。等同v-model:model-str
*/
emits('update:modelStr', str == '~' ? '' : str);
emits('confirm', tmdate);
}
// 监听器
watch((): string[] => props.modelValue, (newvalue : string[]) => {
let sortvalue = sorDateVaild(validTimeDate(newvalue))
let newvaluestr = sortvalue.join('');
if (newvaluestr == nowModelValue.value.join('')) return;
nowValue.value = sortvalue;
nowModelValue.value = sortvalue;
tongbuModelStr()
quicklistSelectedStr.value = ''
})
watch((): boolean => props.modelShow, (newValue : boolean) => {
if (newValue == show.value) return;
show.value = newValue
})
watch((): Array<any> => props.quickDate, (newvalue : Array<any>) => {
quicklist.value = getQuickDateType()
})
// 生命周期
onMounted(() => {
yanchiDuration.value = _lazyContent.value ? false : true
let str = validTimeDate(props.modelValue)
nowValue.value = str;
nowModelValue.value = str;
quicklist.value = getQuickDateType()
tongbuModelStr()
})
defineExpose({
/** 打开选择器 **/
open: () => openShow(),
/** 关闭选择器 **/
close: () => onClose(),
/** 清空选择 **/
clear: () => clearDate()
})
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@prop {boolean} show - 控制打开关闭状态
@prop {boolean} startVal - 选日期的开始值,可能为空值
@prop {boolean} endVal - 选日期的结束值,可能为空值
-->
<slot :show="show" :startVal="_formatValStr[0]" :endVal="_formatValStr[1]"></slot>
</view>
<x-drawer :disabledConfirm="_checkPass&&_disabledClear" @open="onOpen" :widthCoverCenter="widthCoverCenter" :disabledScroll="true" @close="onClose"
@confirm="onConfirm" @cancel="onCancel" :showFooter="true" v-model:show="show"
:size="drawerSize" :showClose="false">
<template v-slot:title>
<view class="xPickerClear" :style="{height:'50px'}">
<x-text>{{title!=''?title:i18n!.t("tmui4x.betweentTime.title")}}</x-text>
<!-- 清空 -->
<x-text v-if="!_disabledClear" @click="clearDate" style="opacity: 0.5;">{{i18n!.t("tmui4x.clear")}}</x-text>
</view>
</template>
<view class="xPickerDateWrap">
<view v-if="quicklist.length>0" class="xPickerDateWrapQuickTags">
<x-tag font-size="14" :font-color="(_isDark?'white':'')" @click="tagsClick(item)" :skin='quicklistSelectedStr==item.str?"normal":"thin"' :round="8" size="large"
style="margin-right:10px;margin-bottom: 5px;" v-for="(item,index) in quicklist" :key="index">{{item.str}}</x-tag>
</view>
<x-divider></x-divider>
<view style="height: 8px;"></view>
<view class="xPickerDateWrapQuickInput">
<view class="xPickerInputMaskerParent" @click="inputClick(0)" :style="{flex:'1',height:'40px'}">
<input :placeholder-style="_placeStyle"
:style="{color:changeIndex==0?_activeBorderColor:_fontColor,
border: `2px solid ${changeIndex==0?_activeBorderColor:_backgroundColor}`,
backgroundColor:_backgroundColor,
fontSize:'15px'
}"
class="xPickerInput" :value="_start_date_str_format" />
<view class="xPickerInputMasker"></view>
</view>
<!-- 至 -->
<x-text style="width: 64px;text-align: center;">
{{i18n!.t("tmui4x.betweentTime.splite")}}
</x-text>
<view class="xPickerInputMaskerParent" @click="inputClick(1)" :style="{flex:'1',height:'40px'}">
<input :placeholder-style="_placeStyle"
:style="{color:changeIndex==1?_activeBorderColor:_fontColor,
border: `2px solid ${changeIndex==1?_activeBorderColor:_backgroundColor}`,
backgroundColor:_backgroundColor,
fontSize:'15px'
}"
class="xPickerInput" :value="_end_date_str_format" />
<view class="xPickerInputMasker"></view>
</view>
</view>
<x-divider style="margin-bottom: 8px;"></x-divider>
<x-date-view v-if="yanchiDuration" @change="dateChangeView" :format="format" :type="type" :cell-units="_cellUnits"
:start="_start_date_str" :end="_end_date_str" :model-value='nowValue[changeIndex]'></x-date-view>
</view>
<x-loading v-if="!yanchiDuration"></x-loading>
</x-drawer>
</template>
<style scoped>
.xPickerClear{
padding: 0 20px;
/* height:50px; */
display: flex;flex-direction: row;
justify-content: space-between;
align-items: center;
}
.xPickerDateText {
text-align: left;
transition-duration: 350ms;
transition-timing-function: linear;
transition-property: transform, opacity;
transform: translateY(100%) scale(0);
opacity: 0;
}
.xPickerDateWrapQuickInputPlackeTips {
flex: 1;
display: flex;
flex-direction: row;
pointer-events: none;
}
.xPickerInputMaskerParent{
position: relative;
}
.xPickerInputMasker{
position: absolute;
width:100%;
height:100%;
}
.xPickerInput {
border-radius: 40px;
height: 100%;
padding: 0 10px;
/* font-size: 16px; */
flex: 1;
pointer-events: none;
text-align: center;
}
.xPickerDateWrapQuickInput {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.xPickerDateWrapQuickTags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 8px;
}
.xPickerDateWrap {}
</style>
@@ -0,0 +1,584 @@
<script lang="ts" setup>
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, getOutlineColorObj } from "../../core/util/xCoreColorUtil.uts"
import { toFillMarginAr, checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { PropType, computed, watch, onMounted, getCurrentInstance, ref, type Ref } from 'vue'
/**
* @name 按钮 xButton
* @page /pages/index/button
* @category 常用组件
* @description 圆角,主题可通过配置统一设置或者动态全局设置,使设计风格统一并保持一致性。让你的风格独一无二。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: 'xButton' })
type SkinType = "default" | "secondary" | "text" | "outline" | "dashed" | "thin"
type SizeType = "mini" | "large" | "normal" | "small"
export type XButtonProps = {
/** 主题颜色,空取全局 */
color : string,
/** 暗黑主题颜色,空取全局 */
darkColor : string,
/** 自定背景色 */
bgColor : string,
/** 渐变[方向,颜色1,颜色2] */
linearGradient : string[],
/** 字号颜色 */
fontColor : string,
/** 暗黑字号颜色 */
fontDarkColor : string,
/** 字号大小 */
fontSize : string,
/** 圆角,空取全局 */
round : string,
/** 边线大小 */
border : number,
/** 投影[x,y,大小] */
shadow : number[],
/** 边线颜色 */
borderColor : string,
/** 样式主题"default" | "secondary" | "text" | "outline" | "dashed" | "thin" */
skin : SkinType,
/** 按钮图标 */
icon : string,
/** 是否是纯按钮图标 */
iconBtn : boolean,
/** 图标大小 */
iconSize : string,
/** 按钮大小"mini" | "large" | "normal" | "small" */
size : SizeType,
/** 跳转链接 */
url : string,
/** 跳转方式,同官方 */
navigateMode : string,
/** 是否禁用 */
disabled : boolean,
/** 加载状态 */
loading : boolean,
/** 高 */
height : string,
/** 宽 */
width : string,
/** 是否占据整行 */
block : boolean,
/** 是否作为x-form提交表单用于触发提交表单 */
formType : 'form' | '',
/** 行高 */
lineHeight : string,
/** 加粗 */
fontWeight : string,
/** 开放类型,同官方 */
openType : string,
lang : string,
sessionFrom : string,
sendMessageTitle : string,
sendMessagePath : string,
sendMessageImg : string,
appParameter : string,
showMessageCard : boolean,
phoneNumberNoQuotaToast : boolean
}
const props = withDefaults(defineProps<XButtonProps>(), {
color: '',
darkColor: '',
bgColor: '',
linearGradient: ():string[] => [] as string[],
fontColor: '',
fontDarkColor: '',
fontSize: '',
round: '',
border: 0.5,
shadow: ():number[] => [] as number[],
borderColor: '',
skin: 'default' as SkinType,
icon: '',
iconBtn: false,
iconSize: '',
size: 'normal' as SizeType,
url: '',
navigateMode: 'navigateTo',
disabled: false,
loading: false,
height: '',
width: '',
block: false,
formType: '' as 'form' | '',
lineHeight: '1.4',
fontWeight: 'normal',
openType: '',
lang: 'en',
sessionFrom: '',
sendMessageTitle: '',
sendMessagePath: '',
sendMessageImg: '',
appParameter: '',
showMessageCard: false,
phoneNumberNoQuotaToast: true
})
const emits = defineEmits([
'click',
'getuserinfo',
'contact',
'getphonenumber',
'getrealtimephonenumber',
'error',
'opensetting',
'launchapp',
'chooseavatar',
'chooseaddress',
'chooseinvoicetitle',
'addgroupapp',
'subscribe',
'login',
'agreeprivacyauthorization'
])
const instance = getCurrentInstance()
const proxy = instance?.proxy ?? null
const _set_border_color = ref(`1px solid rgba(0,0,0,0)`) as Ref<string>
const _set_background_color = ref(`rgba(0,0,0,0)`) as Ref<string>
const _set_background_img = ref(``) as Ref<string>
const _set_font_color = ref(`#ffffff`) as Ref<string>
const _isHover = ref(false) as Ref<boolean>
const boxShadow = ref('0 0px 0px rgba(0,0,0,0)') as Ref<string>
const _fontWeight = computed(() : string => props.fontWeight)
const _color = computed(() : string => {
let color = props.color
if (xConfig.dark == 'dark' && props.darkColor != '') {
color = props.darkColor
} else {
if (color == '') {
color = xConfig.color
}
}
return color
})
const _fontSize = computed(() : string => {
let fontSize = props.fontSize
if (fontSize == '') {
if (props.size == 'mini') fontSize = '12'
if (props.size == 'small') fontSize = '14'
if (props.size == 'normal') fontSize = '16'
if (props.size == 'large') fontSize = '18'
}
fontSize = checkIsCssUnit(fontSize, xConfig.unit)
if (xConfig.fontScale == 1) return fontSize
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) sizeNumber = 16
return (sizeNumber * xConfig.fontScale).toString() + getUnit(props.fontSize)
})
const _iconSize = computed(() : string => {
if (props.iconSize != '') {
let fontSize = checkIsCssUnit(props.iconSize, xConfig.unit)
if (xConfig.fontScale == 1) return fontSize
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) sizeNumber = 16
return (sizeNumber * xConfig.fontScale).toString() + getUnit(props.fontSize)
}
return _fontSize.value
})
const _disabled = computed(() : boolean => props.disabled)
const _icon = computed(() : string => props.icon)
const _loading = computed(() : boolean => props.loading)
const _radius = computed(() : string => {
let radius = props.round
if (radius == '') {
radius = xConfig.buttonRadius
if (props.size == 'mini') radius = '6'
if (props.size == 'small') radius = '8'
}
return checkIsCssUnit(radius, xConfig.unit)
})
const _border = computed(() : string => checkIsCssUnit(props.border.toString(), xConfig.unit))
const _iconBtn = computed(() : boolean => props.iconBtn)
const _height = computed(() : string => {
if (props.height != '') return checkIsCssUnit(props.height, xConfig.unit)
if (props.size == 'mini') return checkIsCssUnit('24', xConfig.unit)
if (props.size == 'small') return checkIsCssUnit('32', xConfig.unit)
if (props.size == 'normal') return checkIsCssUnit('46', xConfig.unit)
if (props.size == 'large') return checkIsCssUnit('56', xConfig.unit)
return checkIsCssUnit(props.height == '' ? '44' : props.height, xConfig.unit)
})
const _width = computed(() : string => {
if (_iconBtn.value) return _height.value
if (props.block) return '100%'
if (props.width != '') return checkIsCssUnit(props.width, xConfig.unit)
if (props.size == 'mini') return checkIsCssUnit('46', xConfig.unit)
if (props.size == 'small') return checkIsCssUnit('60', xConfig.unit)
if (props.size == 'normal') return checkIsCssUnit('98', xConfig.unit)
if (props.size == 'large') return checkIsCssUnit('128', xConfig.unit)
return checkIsCssUnit(props.width, xConfig.unit)
})
const _shadow = computed(() : number[] => {
if (props.shadow.length == 0) return [0, 0] as number[]
if (props.shadow.length == 1) return [props.shadow[0], props.shadow[0]] as number[]
return props.shadow
})
const _styleMap = computed(() : Map<string, any> => {
const styleMap = new Map<string, any>()
styleMap.set('width', _width.value)
styleMap.set('height', _height.value)
styleMap.set('border', _set_border_color.value)
styleMap.set('backgroundColor', _set_background_color.value)
if (_set_background_img.value != '') {
styleMap.set('backgroundImage', _set_background_img.value)
}
styleMap.set('borderRadius', _radius.value)
let opacity = '1'
if (_disabled.value || _loading.value) {
opacity = '0.5'
}
styleMap.set('opacity', opacity)
return styleMap
})
function findParent(parent : VueComponent | null) : VueComponent | null {
if (parent == null) return null;
// #ifdef WEB||APP-IOS||MP-WEIXIN
if (parent.$parent?.$options?.name?.indexOf('xForm') > -1) return parent.$parent;
// #endif
// #ifdef APP-HARMONY
if (parent.$parent?.$options?.name?.indexOf('xForm') > -1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
if (parent.$parent instanceof XFormComponentPublicInstance) return parent.$parent;
// #endif
let parents = findParent(parent.$parent)
// #ifdef WEB||APP-IOS||MP-WEIXIN
if (parents?.$options?.name?.indexOf('xForm') > -1) return parents;
// #endif
// #ifdef APP-HARMONY
if (parents?.$options?.name?.indexOf('xForm') > -1) return parents;
// #endif
// #ifdef APP-ANDROID
if (parents instanceof XFormComponentPublicInstance) return parents;
// #endif
return null;
}
function formSubmit() {
let pelement = findParent(proxy);
if (pelement == null) return;
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
parent.submit();
}
function customStyles(hover : boolean) {
let dePrimarycolor = getDefaultColor(xConfig.color);
let color = getDefaultColor(_color.value);
let hoverColor = props.color == 'info' ? getDefaultColor(xConfig.color) : color
let colorInit : UTSJSONObject = getDefaultColorObj(color, hoverColor);
let borderStyle = "solid"
if (props.skin == 'text') {
colorInit = getTextColorObj(color, hoverColor, xConfig.dark == 'dark')
}
if (props.skin == 'thin') {
colorInit = getThinColorObj(color, hoverColor, xConfig.dark == 'dark')
}
if (props.skin == 'outline' || props.skin == 'dashed') {
colorInit = getOutlineColorObj(color, hoverColor, xConfig.dark == 'dark')
}
if (props.skin == 'dashed') {
borderStyle = 'dashed'
}
let defaultObj : UTSJSONObject = colorInit.getJSON("default")!
let defaultActive : UTSJSONObject = colorInit.getJSON("active")!
let borderWidth = checkIsCssUnit(props.border.toString(), 'rpx')
let dbordercolor = getDefaultColor(props.borderColor)
let background = getDefaultColor(props.bgColor)
let fontcolor = getDefaultColor(props.fontColor)
let realColor = fontcolor
let realBackground = background
let realBackImg = ''
let shadowX = _shadow.value[0].toString();
let shadowY = _shadow.value[1].toString();
if (shadowX != shadowX && _shadow.value[0] != 0) {
boxShadow.value = `0 ${shadowX}px ${shadowY}px ${_iconBtn.value ? 'rgba(0,0,0,0)' : defaultObj.getString("shadow")!}`
}
_set_border_color.value = `${borderWidth} ${borderStyle} ${dbordercolor == "" ? defaultObj.getString("borderColor")! : dbordercolor}`
realBackground = background == "" ? defaultObj.getString("background")! : background
if (props.color == 'info') {
realColor = dePrimarycolor;
} else {
realColor = fontcolor == "" ? defaultObj.getString("fontColor")! : fontcolor
}
if (hover) {
if (shadowX != shadowX && _shadow.value[0] != 0) {
boxShadow.value = `0 ${shadowX}px ${shadowY}px ${_iconBtn.value ? 'rgba(0,0,0,0)' : defaultActive.getString("shadow")!}`
}
_set_border_color.value = `${borderWidth} ${borderStyle} ${dbordercolor == "" ? defaultActive.getString("borderColor")! : dbordercolor}`
realBackground = background == "" ? defaultActive.getString("background")! : background
realColor = fontcolor == "" ? defaultActive.getString("fontColor")! : fontcolor
}
if (props.linearGradient.length > 0) {
let dirs = props.linearGradient[0]
if (props.linearGradient[0] == 'top') {
dirs = 'to top'
} else if (props.linearGradient[0] == 'bottom') {
dirs = 'to bottom'
} else if (props.linearGradient[0] == 'left') {
dirs = 'to left'
} else if (props.linearGradient[0] == 'right') {
dirs = 'to right'
}
realBackground = ``
realBackImg = `linear-gradient(${dirs},${props.linearGradient[1]},${props.linearGradient[2]})`
}
if (props.fontDarkColor != '' && xConfig.dark == 'dark') {
realColor = getDefaultColor(props.fontDarkColor)
}
_set_background_color.value = realBackground
_set_background_img.value = realBackImg
_set_font_color.value = realColor
}
function touchStart() {
if (_disabled.value || _loading.value) return;
customStyles(true);
_isHover.value = true;
}
function touchCacel() {
if (_disabled.value || _loading.value) return;
customStyles(false);
_isHover.value = false
}
function touchEnd() {
if (_disabled.value || _loading.value) return;
customStyles(false);
_isHover.value = false
}
function clickListen(e : UniPointerEvent) {
if (!_disabled.value && !_loading.value) {
emits('click', e)
if (props.formType == 'form') {
formSubmit();
}
}
if (_disabled.value == false && props.url != "" && _loading.value == false) {
if (props.navigateMode == 'navigateTo') {
uni.navigateTo({
url: props.url,
fail: (error) => {
console.error(error)
}
})
} else if (props.navigateMode == 'redirectTo') {
uni.redirectTo({
url: props.url,
fail: (error) => {
console.error(error)
}
})
} else if (props.navigateMode == 'switchTab') {
uni.switchTab({
url: props.url,
fail: (error) => {
console.error(error)
}
})
} else if (props.navigateMode == 'reLaunch') {
uni.reLaunch({
url: props.url,
fail: (error) => {
console.error(error)
}
})
} else if (props.navigateMode == 'navigateBack') {
uni.navigateBack({
fail: (error) => {
console.error(error)
}
})
}
}
}
// #ifdef MP-WEIXIN
function getuserinfo(event : UniEvent) { emits('getuserinfo', event) }
function contact(event : UniEvent) { emits('contact', event) }
function getphonenumber(event : UniEvent) { emits('getphonenumber', event) }
function getrealtimephonenumber(event : UniEvent) { emits('getrealtimephonenumber', event) }
function error(event : UniEvent) { emits('error', event) }
function opensetting(event : UniEvent) { emits('opensetting', event) }
function launchapp(event : UniEvent) { emits('launchapp', event) }
function chooseavatar(event : UniEvent) { emits('chooseavatar', event) }
function chooseaddress(event : UniEvent) { emits('chooseaddress', event) }
function chooseinvoicetitle(event : UniEvent) { emits('chooseinvoicetitle', event) }
function addgroupapp(event : UniEvent) { emits('addgroupapp', event) }
function subscribe(event : UniEvent) { emits('subscribe', event) }
function login(event : UniEvent) { emits('login', event) }
function agreeprivacyauthorization(event : UniEvent) { emits('agreeprivacyauthorization', event) }
// #endif
watch([
():any => _color.value,
():any => props.bgColor,
():any => props.borderColor,
():any => props.skin,
():any => props.fontColor,
],
() => {
customStyles(false)
})
onMounted(() => {
customStyles(false)
})
</script>
<template>
<view <!-- #ifdef APP||H5 -->
@touchcancel="touchCacel"
@touchend="touchEnd"
@touchstart="touchStart"
@click="clickListen"
<!-- #endif -->
<!-- #ifdef WEB -->
@mousedown="touchStart"
@mouseup="touchEnd"
@mouseleave="touchEnd"
<!-- #endif -->
:style="_styleMap" class="parentButton" :class="[_disabled||_loading?'noDrag':'']">
<view :class="['xButton',_loading?'load':'']">
<x-icon v-if="_icon!=''&&!_loading" :style="{marginRight:_iconBtn?'0px':' 3px'}" :font-size="_iconSize"
:color="_set_font_color" :name="_icon"></x-icon>
<x-icon v-if="_loading" :color="_set_font_color" :font-size="_iconSize" :spin="true" name="loader-fill"></x-icon>
<!-- #ifdef MP -->
<view v-if="!_iconBtn"
:style="{fontWeight:_fontWeight,'color':_set_font_color,fontSize:_fontSize,lineHeight:lineHeight}">
<!--
@slot 默认插槽
-->
<slot></slot>
</view>
<!-- #endif -->
<!-- #ifndef MP -->
<text v-if="!_iconBtn"
:style="{fontWeight:_fontWeight,'color':_set_font_color,fontSize:_fontSize,lineHeight:lineHeight}">
<!--
@slot 默认插槽
-->
<slot></slot>
</text>
<!-- #endif -->
</view>
<!-- #ifdef MP-WEIXIN -->
<button :disabled="_disabled||_loading" :open-type="openType" :lang="lang" :sessionFrom="sessionFrom"
:sendMessageTitle="sendMessageTitle" :sendMessagePath="sendMessagePath" :sendMessageImg="sendMessageImg"
:appParameter="appParameter" :showMessageCard="showMessageCard"
:phoneNumberNoQuotaToast="phoneNumberNoQuotaToast" @touchcancel="touchCacel" @touchend="touchEnd"
@touchstart="touchStart" @click="clickListen" @getuserinfo="getuserinfo" @contact="contact"
@getphonenumber="getphonenumber" @getrealtimephonenumber="getrealtimephonenumber" @error="error"
@opensetting="opensetting" @launchapp="launchapp" @chooseavatar="chooseavatar"
@chooseaddress="chooseaddress" @chooseinvoicetitle="chooseinvoicetitle" @addgroupapp="addgroupapp"
@subscribe="subscribe" @login="login" @agreeprivacyauthorization="agreeprivacyauthorization"
class="xButtonReal">
</button>
<!-- #endif -->
</view>
</template>
<style scoped lang="scss">
.parentButton {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
/* #ifdef WEB*/
box-sizing: border-box;
cursor: pointer;
/* #endif */
}
/* #ifdef WEB*/
.parentButton.noDrag {
cursor: no-drop;
}
.parentButton:hover {}
.parentButton:active {}
/* #endif */
/* #ifdef MP-WEIXIN */
.xButtonReal {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
border: none;
background: transparent !important;
border-width: 0;
margin: 0;
padding: 0;
pointer-events: all;
&::after {
border: none;
border-width: 0;
}
}
/* #endif */
.xButton {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loadingMask {
position: absolute;
background-color: rgba(200, 200, 200, 0.6);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
/* #ifndef APP */
box-sizing: border-box;
width: 100%;
height: 100%;
/* #endif */
/* #ifdef APP*/
box-sizing: border-box;
width: 100%;
height: 100%;
/* #endif */
}
</style>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
</script>
<template>
<view class="xCalendarView">
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
</script>
<template>
<view class="xCalendarView">
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
</script>
<template>
<view class="xCalendarView">
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
</script>
<template>
<view class="xCalendarView">
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,389 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
import { xCalendar } from "./xCalendar"
import { xDateArrayItem,xDateArrayItemType,xCalendarArgs,xCalendarMode } from "../x-calendar-view/interface.uts"
// #ifdef APP
import { calendarDraw } from "./calendarDraw"
const proxy = getCurrentInstance()?.proxy;
const xCalendarViewItemRef = ref<UniElement|null>(null)
let calendarDom:calendarDraw|null = null;
// #endif
const i18n = xConfig.i18n;
type xCalendarMultiplePropsType = {
/**
* 同步当前时间v-model
* 不想受控:model-value
*/
modelValue : string[],
/**
* 范围选择模式
* day:天数多选,通过multipleMax可以设置允许选择的天数
* range:天数的范围选择,起始和终止
* week:按周次选择范围
* quarter:按季度选择范围
* year:按年
*/
model: xCalendarMode,
/**
* 多选模式时,允许选择的最大天数。
*/
multipleMax : number,
/**
* 禁用的日期字符串如"2023-12-12"
* 它与下面的startend不冲突。
*/
disabledDays: string[],
/**
* 允许选择的开始日期
*/
startDate: string,
/**
* 允许选择的结束日期
*/
endDate: string,
/**
* 设置指定日期的样式
* 数据类型见:xCalendarDateStyle_type
*/
dateStyle: xCalendarDateStyle_type[],
/**
* 同步vmodel时格式化模板
*/
format: string,
/**
* 选中的主题色,默认空值,取全局主题色
* 如果提供了dateStyle,以dateStyle为准
*/
color: string,
/**
* 默认的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontColor:string,
/**
* 默认的暗黑文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontDarkColor:string,
/**
* 默认选中时的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
activeFontColor:string,
/**
* 范围选中时,范围中间的选中颜色,
* 如果为空,为color的透明度0.5;
*/
rangColor:string,
rangFontColor:string,
currentDate:string,
/**
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
*/
seekDay:number,
/**
* 给日期设定状态
* 类型为:xCalendarDateStyleStatusType[]
*/
dateStatus:xCalendarDateStyleStatusType[]
}
const emit = defineEmits(['change','click'])
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
modelValue: [] as string[],
currentDate:'',
model:'day' as xCalendarMode,
multipleMax: -1,
disabledDays:[] as string[],
startDate:'1900-1-1',
endDate:'2025-5-13',
dateStyle:[] as xCalendarDateStyle_type[],
format:'YYYY-MM-DD',
color:'',
fontColor:'#333333',
fontDarkColor:'#ffffff',
activeFontColor:'#ffffff',
rangColor:'',
rangFontColor:'',
seekDay:0,
dateStatus:[] as xCalendarDateStyleStatusType[]
})
const calendar = new xCalendar()
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
const _rangColor = computed(()=>{
let color = props.rangColor == ''?xConfig.color:props.rangColor
let rgba = hexToRgb(getDefaultColor(color));
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${props.rangColor==''?0.2:1})`
})
const _modelValue = computed(():string[]=> props.modelValue )
const _model = computed(():xCalendarMode=> props.model )
function splitArray<T>(ar : Array<T>, len : number) : Array<Array<T>> {
const result : Array<Array<T>> = [];
for (let i = 0; i < ar.length; i += len) {
result.push(ar.slice(i, i + len));
}
return result
}
const _fontSize = computed(():string=> checkIsCssUnit('16',''))
const dateArrayList = computed(():xDateArrayItemType[][]=>{
const primaryColor = getDefaultColor(props.color==''?xConfig.color:props.color);
const dates = calendar.getCalendar(
props.seekDay,
props.model,
props.currentDate,
props.modelValue,
props.startDate!=''?new Date(props.startDate.replace(/-/g,'/')):null,
props.endDate!=''?new Date(props.endDate.replace(/-/g,'/')):null,
{
color:primaryColor,
fontColor:getDefaultColor(xConfig.dark=='dark'?props.fontDarkColor:props.fontColor),
activeFontColor:getDefaultColor(props.activeFontColor),
rangColor:_rangColor.value,
rangFontColor:props.rangFontColor==''?primaryColor:getDefaultColor(props.rangFontColor)
} as xCalendarArgs,props.dateStyle,props.disabledDays);
return splitArray<xDateArrayItemType>(dates,7)
})
function showLabel(item:xDateArrayItemType):string {
if(item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',2)
if(item.isInstart&&!item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',0)
if(!item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',1)
return ""
}
function dateClick(item:xDateArrayItemType){
if(item.disabled) return;
emit('click',item)
}
function checkDataIsInDateStatus(date:string|null):string{
if(date ==''||date == null) return '';
for(let k =0 ;k <_dateStatus.value.length;k++){
let itemStatus = _dateStatus.value[k]
let dates = itemStatus?.date??[];
let start = itemStatus?.between?.start??''
let end = itemStatus?.between?.end??''
let betweenColor = itemStatus?.between?.color??''
let notDates = itemStatus?.between?.notDate??[]
let nowDate = new xDate(date);
let isInBetweenDate = false;
if(start!=''&&end!=''){
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
let isNotDate = false
for(let i=0;i<notDates.length;i++){
let item = notDates[i];
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
isNotDate = true;
break;
}
}
isInBetweenDate = isBetween && !isNotDate
}
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
let selfColor = ''
for(let i=0;i<dates.length;i++){
let item = dates[i];
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
selfColor = item.color==''?'primary':item.color
break;
}
}
if(selfColor!=''){
return getDefaultColor(selfColor)
}
}
return ""
}
// #ifdef APP
function draw():Promise<any>{
return new Promise(()=>{
calendarDom?.draw(dateArrayList.value,_model.value,_dateStatus.value)
})
}
function canvsClick(e:UniPointerEvent){
xCalendarViewItemRef.value?.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
let cellHeight = 50;
let cellWidth = rect.width / 7;
let top = rect.top;
let left = rect.left;
let x = e.clientX - left;
let y = e.clientY - top;
let col = Math.floor(x/cellWidth);
let row = Math.floor(y/cellHeight);
let item = dateArrayList.value[row][col];
function testThread():Promise<any>{
return new Promise(()=>{
dateClick(item)
})
}
testThread()
})
.catch((er)=>{
console.error(er)
})
}
watch(():any => dateArrayList.value,()=>{
draw()
})
// #endif
onMounted(()=>{
// #ifdef APP
calendarDom = new calendarDraw(
xCalendarViewItemRef.value,
proxy,
[i18n.t('tmui4x.calendar.rangStatus',0),i18n.t('tmui4x.calendar.rangStatus',1),i18n.t('tmui4x.calendar.rangStatus',2)]
)
nextTick(()=>{
draw()
})
// #endif
})
</script>
<template>
<view class="xCalendarViewItem"
ref="xCalendarViewItemRef"
<!-- #ifdef APP -->
@click="canvsClick"
<!-- #endif -->
>
<!-- #ifndef APP -->
<view class="xCalendarViewItemCol" v-for="(children,index) in dateArrayList" :key="index">
<view class="xCalendarViewItemColItem"
@click="dateClick(item)"
v-for="(item,index2) in (children as xDateArrayItemType[])" :key="index2">
<view class="xCalendarViewItemColItemBox"
:style="{
backgroundColor:item.style.dstyle.backgroundColor,
opacity:item.style.dstyle.opacity,
}"
>
<view v-if="item.style.dot.dot"
class="xCalendarViewItemDot"
:class="[item.style.dot.dotLabel==''?'xCalendarViewItemDotNolabel':'']"
:style="{
color:item.style.dot.dotLabelColor,
backgroundColor:item.style.dot.dotColor
}"
>
{{item.style.dot.dotLabel}}
</view>
<text class="xCalendarViewItemColDate"
:style="{
color:item.style.dstyle.fontColor,
fontSize:_fontSize
}"
>
{{item.date.day}}
</text>
<text class="xCalendarViewItemColLabel"
:style="{
color:item.style.dstyle.fontColor
}"
>{{showLabel(item)||item.style.dstyle.label}}</text>
<view class="xCalendarViewStatus" :style="{backgroundColor:checkDataIsInDateStatus(item.date.date)}" v-if="checkDataIsInDateStatus(item.date.date)!=''"></view>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
<style scoped lang="scss">
.xCalendarViewStatus{
width:4px;
height:4px;
border-radius: 2px;
position: absolute;
bottom: 1px;
}
// #ifdef APP
.xCalendarViewItem{
width:100%;
height:100%;
}
// #endif
// #ifndef APP
.xCalendarViewItemDot{
padding:2px 4px;
min-width:18px;
min-height:18px;
font-size:10px;
border-radius:9px;
position: absolute;
right:0px;
top:0px;
&.xCalendarViewItemDotNolabel{
padding:0;
min-width:8px;
min-height:8px;
font-size:10px;
border-radius:9px;
}
}
.xCalendarViewItem{
width:100%;
height:100%;
.xCalendarViewItemCol{
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: center;
height: 50px;
.xCalendarViewItemColItem{
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
height: 100%;
width:14.285%;
.xCalendarViewItemColItemBox{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
border-radius: 25px;
overflow: visible;
}
.xCalendarViewItemColDate{
text-align: center;
height: 27px;
line-height: 27px;
margin-top: -4px;
// font-weight: bold;
// font-size: 16px;
display: block;
}
.xCalendarViewItemColLabel{
text-align: center;
font-size: 10px;
margin-top: -4px;
display: block;
min-height:10px;
}
}
}
}
// #endif
</style>
@@ -0,0 +1,189 @@
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import {xCalendarDateStyleStatusType} from "@/uni_modules/tmx-ui/interface.uts"
import { xDate } from "../../core/util/xDate.uts"
export class calendarDraw {
ele : UniElement | null = null
proxy : any | null = null
cellHeight = 50;
sapce = 5
model : xCalendarMode = 'day'
_fontSize : string = checkIsCssUnit('16', xConfig.unit)
_dateStatus : xCalendarDateStyleStatusType[] | null = null;
dateCnStrs:string[] = ["开始","结束","本日"]
constructor(target : UniElement | null, proxyx : any | null,dateCn:string[]) {
this.ele = target
this.proxy = proxyx
this.dateCnStrs = dateCn
}
checkDataIsInDateStatus(date:string|null):string{
if(date ==''||date == null || this._dateStatus == null) return '';
for(let k =0 ;k <this._dateStatus.length;k++){
let itemStatus = this._dateStatus[k]
let dates = itemStatus?.date??[];
let start = itemStatus?.between?.start??''
let end = itemStatus?.between?.end??''
let betweenColor = itemStatus?.between?.color??''
let notDates = itemStatus?.between?.notDate??[]
let nowDate = new xDate(date);
let isInBetweenDate = false;
if(start!=''&&end!=''){
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
let isNotDate = false
for(let i=0;i<notDates.length;i++){
let item = notDates[i];
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
isNotDate = true;
break;
}
}
isInBetweenDate = isBetween && !isNotDate
}
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
let selfColor = ''
for(let i=0;i<dates.length;i++){
let item = dates[i];
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
selfColor = item.color==''?'primary':item.color
break;
}
}
if(selfColor!=''){
return getDefaultColor(selfColor)
}
}
return ""
}
showLabel(item : xDateArrayItemType) : string {
if (item.isInstart && item.isInEnd && this.model == 'range') return this.dateCnStrs[2]
if (item.isInstart && !item.isInEnd && this.model == 'range') return this.dateCnStrs[0]
if (!item.isInstart && item.isInEnd && this.model == 'range') return this.dateCnStrs[1]
return ""
}
getColor(color : string, alpha : number) : string {
if (alpha == 1) return color;
let rgba = hexToRgb(getDefaultColor(color));
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${alpha})`
}
_draw(element : UniElement, Grect : DOMRect, list : xDateArrayItemType[][]) {
const ctx = element.getDrawableContext()!;
ctx.reset()
ctx.fillStyle = 'rgba(0,0,0,0)'
ctx.fillRect(0, 0, Grect.width, Grect.height)
ctx.textAlign = 'center'
const _realCellWidth = Grect.width / 7
const _h = Math.min(_realCellWidth, 50) - this.sapce * 2
for (let i = 0; i < list.length; i++) {
const children = list[i]
for (let col = 0; col < children.length; col++) {
let item = children[col];
let dstyle = item.style
let xy_x = _realCellWidth * col + _realCellWidth / 2;
let xy_y = this.cellHeight * i + this.cellHeight / 2;
// 绘制选中的背景
ctx.fillStyle = this.getColor(dstyle.dstyle.backgroundColor, (item.disabled || !item.inCurrentMonth) && !item.isInstart && !item.isInEnd ? 0.3 : 1);
if (dstyle.dstyle.backgroundColor != 'transparent') {
ctx.beginPath()
ctx.arc(xy_x, xy_y, _h / 2, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
}
// 绘制右角标。
if (dstyle.dot.dot) {
if (dstyle.dot.dotLabel == '') {
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2-10;
let lasty = _realCellWidth * (i) + 10
ctx.fillStyle = dstyle.dot.dotColor
ctx.beginPath()
ctx.arc(lastx, lasty, 4, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
} else {
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 5;
let lasty = _realCellWidth * (i) + 10
ctx.fillStyle = dstyle.dot.dotColor;
ctx.beginPath()
ctx.arc(lastx, lasty, 10, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
ctx.font = "10px"
ctx.fillStyle = dstyle.dot.dotLabelColor
// #ifdef APP-ANDROID || APP-HARMONY
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 3);
// #endif
// #ifdef APP-IOS
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 5);
// #endif
}
}
// 绘制文字
const text = item.date.day.toString();
ctx.fillStyle = this.getColor(dstyle.dstyle.fontColor, (item.disabled || !item.inCurrentMonth) ? 0.7 : 1);
ctx.font = this._fontSize
const textHeight = 34;
const _t_x = _realCellWidth * col + _realCellWidth / 2
let _t_y = this.cellHeight * i + this.cellHeight / 2 + 2;
// #ifdef APP-IOS
_t_y = this.cellHeight * i + this.cellHeight / 2 + 6;
// #endif
ctx.fillText(text, _t_x, _t_y);
let label = this.showLabel(item)
label = label == '' ? dstyle.dstyle.label : label
// 绘制底部的label
if (label != '') {
let labely = this.cellHeight * i + this.cellHeight - 9;
// #ifdef APP-ANDROID || APP-HARMONY
labely = this.cellHeight * i + this.cellHeight - 12;
// #endif
ctx.font = "8px"
ctx.fillText(label, _t_x, labely);
}
// 绘制状态小点点。
let statusColor = this.checkDataIsInDateStatus(item.date.date);
if(statusColor!=''){
ctx.fillStyle = statusColor
ctx.beginPath()
let statusY = this.cellHeight * i + this.cellHeight - this.sapce - 3;
ctx.arc(_t_x,statusY,2,0,Math.PI*2)
ctx.closePath()
ctx.fill()
}
}
}
ctx.update()
}
draw(list : xDateArrayItemType[][], modelv : xCalendarMode = 'day',status:xCalendarDateStyleStatusType[]|null = null) {
let _this = this;
_this.model = modelv;
_this._dateStatus = status;
this.ele?.getBoundingClientRectAsync()
?.then((rect : DOMRect) => {
_this._draw(_this.ele!, rect, list)
})
.catch((er) => {
console.error(er)
})
}
}
@@ -0,0 +1,750 @@
<script lang="ts" setup>
import { type PropType, getCurrentInstance } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig, getI18n } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type, xCalendarDateStyleStatusType } from "../../interface.uts"
import { xCalendar } from "./xCalendar"
import calendarMultipleUvue from "./calendar-multiple.uvue"
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
const i18n = xConfig.i18n
type xCalendarMultiplePropsType = {
/**
* 同步当前时间v-model
* 不想受控:model-value
*/
modelValue : string[],
/**
* 范围选择模式
* day:天数多选,通过multipleMax可以设置允许选择的天数
* range:天数的范围选择,起始和终止
* week:按周次选择范围(未开放)
* quarter:按季度选择范围(未开放)
* year:按年(未开放)
*/
model : "day" | "range" | "week" | "quarter" | "year",
/**
* 多选模式时,允许选择的最大天数。
*/
multipleMax : number,
/**
* 禁用的日期字符串如"2023-12-12"
* 它与下面的startend不冲突。
*/
disabledDays : string[],
/**
* 允许选择的开始日期
*/
startDate : string,
/**
* 是否上下切换日历
*/
vertical : boolean,
/**
* 允许选择的结束日期
*/
endDate : string,
/**
* 当前显示的月份,默认以modalValue中的第一项为初始月
* 如果为空,显示本月,可以控制这里切换显示的日期
*/
currentDate : string,
/**
* 设置指定日期的样式
* 数据类型见:xCalendarDateStyle_type
*/
dateStyle : xCalendarDateStyle_type[],
/**
* 同步vmodel时格式化模板
*/
format : string,
/**
* 选中的主题色,默认空值,取全局主题色
* 如果提供了dateStyle,以dateStyle为准
*/
color : string,
/**
* 默认的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontColor : string,
/**
* 默认的暗黑文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontDarkColor : string,
/**
* 默认选中时的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
activeFontColor : string,
/**
* 范围选中时,范围中间的选中颜色,
* 如果为空,为color的透明度0.5;
*/
rangColor : string,
rangFontColor : string,
/**
* 头的背景颜色,默认为透明
*/
headBgColor : string,
/**
* 头的文字颜色,提供了后暗黑失效会以这个为准。
*/
headFontColor : string,
/**
* 头部自定义样式。
*/
headStyle : string,
/**
* 循环渲染时,是否只渲染当前面板(如果你在pad等10年前的低端机上渲染日历有压力请打开此值为true)
* 关闭后可以提升滑动体验。
*/
renderOnly : boolean,
/**
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
*/
seekDay : number,
/**
* 给日期设定状态
* 类型为:xCalendarDateStyleStatusType[]
*/
dateStatus : xCalendarDateStyleStatusType[]
}
/**
* @name 多选日历 xCalendarMultiple
* @description 可以单选和多选,无限循环滚动,目前4.67dev以下版本使用可能会有性能风险,请关注后续官方针对性的优化。
* @page /pages/index/calendar-multiple
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| - | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xCalendarMultiple" })
const calendar = new xCalendar()
const emit = defineEmits([
/**
* 时间变化时触发
* @param {string[]} value - 当前变化的日期
*/
'change',
/**
* 当前日历面板的日期被点击时触发
* @param {string} value - 当前被点击的日期
*/
'click',
/**
* 当前激活面板月份改变时触发(就是当前看到的月份面板)
* @param {string} value - 当前激活面板的日期
*/
'currentChange',
/**
* 同步当前的选中的日期绑定
* @param {string[]} value - 当前选中日期
*/
'update:modelValue',
/**
* 同步当前查看的月份日期,请以日期形式提供值
* @param {string} value - 当前查看的月分日期
*/
'update:currentDate'
])
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
modelValue: () : string[] => [] as string[],
model: 'day',
multipleMax: -1,
disabledDays: () : string[] => [] as string[],
startDate: '1900-1-1',
endDate: '2100-1-1',
dateStyle: () : xCalendarDateStyle_type[] => [] as xCalendarDateStyle_type[],
format: 'YYYY-MM-DD',
color: '',
fontColor: '#333333',
fontDarkColor: '#ffffff',
activeFontColor: '#ffffff',
rangColor: '',
rangFontColor: '',
headBgColor: 'transparent',
headFontColor: '',
headStyle: '',
currentDate: '',
vertical: false,
renderOnly: true,
seekDay: 0,
dateStatus: () : xCalendarDateStyleStatusType[] => [] as xCalendarDateStyleStatusType[]
})
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
const weeksCn = computed(() : string[] => {
// ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
// 根据 seekDay 属性调整周名称的顺序
const weekNames = [
i18n!.t("tmui4x.calendar.week", 0),
i18n!.t("tmui4x.calendar.week", 1),
i18n!.t("tmui4x.calendar.week", 2),
i18n!.t("tmui4x.calendar.week", 3),
i18n!.t("tmui4x.calendar.week", 4),
i18n!.t("tmui4x.calendar.week", 5),
i18n!.t("tmui4x.calendar.week", 6),
]
// 如果 seekDay 为 0(默认周一),直接返回原数组
if (props.seekDay == 0) {
return weekNames
}
// 根据 seekDay 重新排列数组
const result : string[] = []
for (let i = 0; i < 7; i++) {
const index = (i + props.seekDay) % 7
result.push(weekNames[index])
}
return result
})
const _headBgColor = computed(() : string => getDefaultColor(props.headBgColor))
const _headFontColor = computed(() : string => {
if (props.headFontColor == '') return '#333333'
return getDefaultColor(props.headFontColor)
})
const _modelValue = ref(props.modelValue)
const _currentDate = ref(new xDate(props.currentDate).format("YYYY-MM-DD"))
const _currentDateSwipersIndex = ref(0)
const _currentDateSwipers = ref<string[]>([])
const _startDate = computed(() : string => props.startDate)
const _endDate = computed(() : string => props.endDate)
const _currentDateLabel = computed(() : string => {
let ars = _currentDate.value.split('-');
// `${ars[0]}年${ars[1]}月`
return i18n.t('tmui4x.calendar.titleCurrentMonth', [ars[0], ars[1]])
})
const _currentYear = ref(new xDate(props.currentDate).getYear())
let _modelValueDate = computed(() : Date[] => {
return _modelValue.value.map((d : string) : Date => {
return new Date(d.replace(/-/g, '/'))
})
})
const _tipsText = computed(() : string => {
if (props.model == 'day') {
// `已选择${_modelValue.value.length}日` : '未选择日期'
return _modelValue.value.length > 0 ? i18n.t('tmui4x.calendar.selectedStatus', 0, { count: _modelValue.value.length }) : i18n.t('tmui4x.calendar.selectedStatus', 1)
} else if (props.model == 'range') {
if (_modelValue.value.length == 0) return i18n.t('tmui4x.calendar.selectedStatus', 1);
if (_modelValue.value.length == 1) return i18n.t('tmui4x.calendar.selectedStatus', 3);
if (_modelValue.value.length > 1) {
let start = _modelValueDate.value[0].getTime()
let end = _modelValueDate.value[1].getTime()
let diff = Math.abs(start - end);
let diffDay = diff / (24 * 60 * 60 * 1000)
if (start - end > 0) return i18n.t('tmui4x.calendar.selectedStatus', 1);
return i18n.t('tmui4x.calendar.selectedStatus', 0, { count: (diffDay + 1) })
}
}
// '未选择日期'
return i18n.t('tmui4x.calendar.selectedStatus', 1);
})
const _monthBgColor = computed(() : string => xConfig.dark == 'dark' ? xConfig.sheetDarkColor : '#ffffff')
const _color = computed(() : string => props.color == '' ? getDefaultColor(xConfig.color) : getDefaultColor(props.color))
const showPanel = ref(false)
function dateClick(item : xDateArrayItemType) {
const isInIndex = calendar.isInRangeDateByIndex(new Date(item.date.date), _modelValueDate.value, props.model)
console.log(item.date.date)
emit('click', item.date.date)
let dates = _modelValue.value.slice(0)
function showToastFun() {
// `最大${props.multipleMax}天`
uni.showToast({
title: i18n.t('tmui4x.calendar.tips', props.multipleMax),
icon: 'none'
})
}
if (props.model == 'day') {
if (isInIndex == -1) {
if (props.multipleMax <= dates.length && props.multipleMax > -1) {
showToastFun()
return;
}
dates.push(item.date.date)
} else {
dates.splice(isInIndex, 1)
}
} else if (props.model == 'range') {
if (isInIndex == -1) {
if (dates.length == 0) {
dates = [item.date.date] as string[]
} else if (dates.length == 1) {
dates.push(item.date.date)
} else if (dates.length > 1) {
dates = [item.date.date] as string[]
}
} else if (isInIndex == 0) {
if (dates.length == 1) {
dates = [] as string[]
} else if (dates.length > 1) {
dates = [item.date.date] as string[]
}
} else if (isInIndex == 1) {
dates = [item.date.date] as string[]
}
if (dates.length >= 2) {
let adates = dates.map((d : string) : Date => {
return new Date(d.replace(/-/g, '/'))
})
let start = adates[0].getTime()
let end = adates[1].getTime()
let diff = Math.abs(start - end);
let diffDay = diff / (24 * 60 * 60 * 1000)
if (props.multipleMax <= diffDay && props.multipleMax > -1) {
showToastFun()
return;
}
if (start > end) {
dates = [dates[1], dates[0]] as string[]
}
}
}
_modelValue.value = dates
emit('update:modelValue', _modelValue.value)
if ((_modelValue.value.length >= 2 || _modelValue.value.length == 0) && props.model == 'range') {
emit('change', _modelValue.value)
} else if (props.model == 'day') {
emit('change', _modelValue.value)
}
}
function getSwiperListCurrentDates(nowCurrentDate : string) : string[] {
let index = _currentDateSwipersIndex.value
let currentData = new xDate(nowCurrentDate)
let xd = currentData.getClone().setDateOf(1, 'd').format('YYYY-MM-DD')
let start = currentData.getClone().setDateOf(1, 'd').subtraction(1, 'm').format('YYYY-MM-DD')
let end = currentData.getClone().setDateOf(1, 'd').add(1, 'm').format('YYYY-MM-DD')
let datas = [xd, end, start]
if (_currentDateSwipers.value.length == 0) return datas;
if (index == 0) {
datas = [xd, end, start]
} else if (index == 1) {
datas = [start, xd, end]
} else if (index == 2) {
datas = [end, start, xd]
}
return datas;
}
function clear() {
if (_modelValue.value.length == 0) return;
_modelValue.value = [] as string[]
emit('update:modelValue', _modelValue.value)
emit('change', _modelValue.value)
}
function nowMonth() {
let nowdate = new xDate()
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function stepperChangeYear(eyear : number) {
let nowdate = new xDate(_currentDate.value)
nowdate.setDateOf(eyear, 'y')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function changeMonth(eyear : number) {
let nowdate = new xDate(_currentDate.value)
nowdate.setDateOf(eyear, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
showPanel.value = false;
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
}
function nextMonth() {
let nowdate = new xDate(_currentDate.value)
nowdate.add(1, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function prevMonth() {
let nowdate = new xDate(_currentDate.value)
nowdate.subtraction(1, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function swiperChange(evt : UniSwiperChangeEvent) {
_currentDateSwipersIndex.value = evt.detail.current;
nextTick(() => {
_currentDate.value = _currentDateSwipers.value[evt.detail.current]
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
let nowdate = new xDate(_currentDate.value)
_currentYear.value = nowdate.getYear()
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
})
}
watch(() : string[] => props.modelValue, (newVal : string[]) => {
_modelValue.value = newVal.slice(0);
})
watch(() : string => props.currentDate, (newVal : string) => {
let nowdate = new xDate(newVal).setDateOf(1, 'd')
if (nowdate.format("YYYY-MM-DD") == _currentDate.value) return;
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipersIndex.value = 0;
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
// console.log(_currentDate.value)
})
onMounted(() => {
if (props.modelValue.length > 0 && props.currentDate == '') {
let nowdate = new xDate(props.modelValue[0])
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
}
nextTick(() => {
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('update:currentDate', _currentDate.value)
})
})
function getmonth(item : string) : number {
let vls = item.split("-");
return parseInt(vls[1])
}
defineExpose({
/**
* 下月
*/
next() {
nextMonth();
},
/**
* 上月
*/
prev() {
prevMonth();
},
/**
* 设置日历返回到本月
*/
setCurrentMonth() {
nowMonth()
},
/**
* 清空选择
*/
clear() {
clear()
}
})
</script>
<template>
<view class="xCalendarView">
<!--
@slot 日历头,隐藏使用空插槽,将隐藏,如果想自定,请通过ref函数来翻页控制日历走向。
-->
<slot name="header">
<view class="xCalendarViewDataHeaderWrap"
:class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']"
:style="[{backgroundColor:_headBgColor},headStyle]">
<view class="xCalendarViewHeader" style="padding: 0 12px">
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
<!-- 2024年5月 -->
<x-text :color="_headFontColor" font-size="21">{{_currentDateLabel}}</x-text>
<x-icon :color="_headFontColor" font-size="21" name="arrow-down-s-fill"></x-icon>
</view>
<view class="xCalendarViewHeaderRight">
<!-- <view @click="prevMonth" class="px-10 py-15">
<x-icon font-size="21" :color="_headFontColor" name="arrow-up-s-fill">上月</x-icon>
</view>
<view @click="nextMonth" class="px-10 py-15">
<x-icon font-size="21" :color="_headFontColor" name="arrow-down-s-fill">下月</x-icon>
</view> -->
<!-- 清空 -->
<x-text @click="clear" :color="_headFontColor"
style="padding: 10px 20px;">{{i18n.t('tmui4x.clear')}}</x-text>
<!-- 本月 -->
<x-text @click="nowMonth" :color="_headFontColor"
style="padding: 10px 0px;">{{i18n.t('tmui4x.calendar.currentMonthTitle')}}</x-text>
</view>
</view>
<view class="xCalendarViewDataHeader">
<view class="xCalendarViewDataHeaderItem" v-for="(item,index) in 7" :key="item">
<x-text :color="_headFontColor" font-size="12">{{weeksCn[index]}}</x-text>
</view>
</view>
</view>
</slot>
<view class="xCalendarViewSpace"></view>
<view :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']" class="xCalendarViewWrap">
<swiper v-if="_currentDateSwipers.length>0" :vertical="props.vertical" @change="swiperChange"
:current="_currentDateSwipersIndex" :circular="true" style="width:100%;height:100%">
<swiper-item v-for="(item,index) in _currentDateSwipers" :key="index" style="width:100%;height:100%;">
<view style="width:100%;height:100%;position: relative;">
<view class="xCalendarViewContentBox">
<calendar-multiple-uvue v-if="index == _currentDateSwipersIndex||!props.renderOnly"
@click="dateClick" :currentDate="item" :dateStatus="_dateStatus"
:seekDay="props.seekDay" :modelValue="props.modelValue" :model="props.model"
:multipleMax="props.multipleMax" :disabledDays="props.disabledDays"
:startDate="_startDate" :endDate="_endDate" :dateStyle="props.dateStyle"
:format="props.format" :color="props.color" :fontColor="props.fontColor"
:fontDarkColor="props.fontDarkColor" :activeFontColor="props.activeFontColor"
:rangColor="props.rangColor" :rangFontColor="props.rangFontColor">
</calendar-multiple-uvue>
</view>
<view class="xCalendarViewNum">
<text class="xCalendarViewNumText">{{getmonth(item)}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
<view class="xCalendarViewSpace"></view>
<!--
@slot 日历尾部
-->
<slot name="footer">
<view class="xCalendarViewFooter">
<x-text color="#707070" font-size="14">{{_tipsText}}</x-text>
</view>
</slot>
<!-- 月和年 -->
<view :class="[showPanel?'xCalendarViewMonthOn':'xCalendarViewMonthOff']" class="xCalendarViewMonth"
:style="{backgroundColor:_monthBgColor}">
<view class="xCalendarViewHeader" style="padding: 0 12px">
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
<!-- 2024年5月 -->
<x-text :color="_color" font-size="21">{{_currentDateLabel}}</x-text>
<x-icon :color="_color" font-size="21" name="arrow-up-s-fill"></x-icon>
</view>
<view class="xCalendarViewHeaderRight">
<!-- #ifdef APP-ANDROID -->
<x-stepper @change="stepperChangeYear" v-model="(_currentYear as number)" :min="1900" :max="5000"
width="120"></x-stepper>
<!-- #endif -->
<!-- #ifndef APP-ANDROID -->
<x-stepper @change="stepperChangeYear" v-model="_currentYear" :min="1900" :max="5000"
width="120"></x-stepper>
<!-- #endif -->
</view>
</view>
<view class="xCalendarViewMonthWrap" style="flex:1">
<view @click="changeMonth(index)" class="xCalendarViewMonthItem" v-for="(item,index) in 12"
:key="index">
<!-- <x-text font-size="18">{{item}}月</x-text> -->
<x-text font-size="18">{{i18n.t('tmui4x.calendar.monthCountSelected',item)}}</x-text>
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
$headerHeight: 50px;
$weekheaderHeight: 40px;
$footerHeight: 40px;
$spaceHeight: 5px;
$cellHeight: 50px;
$minBodyHeight: (
50px * 6) + $headerHeight + $weekheaderHeight + $footerHeight + ($spaceHeight * 2
);
.xCalendarViewMonth {
position: absolute;
z-index: 3;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
// #ifdef APP-IOS
opacity: 0;
transform: scale(0, 0);
width: 0;
height: 0;
// #endif
// #ifndef APP-IOS
opacity: 0;
transform: scale(0, 0);
// #endif
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
transform: scale(1, 1);
}
.xCalendarViewMonthWrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.xCalendarViewMonthItem {
width: 33.3333%;
height: 25%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
}
.xCalendarViewWrap {
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
opacity: 0;
// #ifndef APP-IOS
transform: scale(2, 2);
// #endif
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
// #ifndef APP-IOS
transform: scale(1, 1);
// #endif
}
position: relative;
width: 100%;
flex: 1;
.xCalendarViewContentBox {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 3;
}
.xCalendarViewNum {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.xCalendarViewNumText {
font-size: 200px;
color: rgba(125, 125, 125, 0.1);
font-weight: bold;
}
}
}
.xCalendarViewDataHeaderWrap {
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
opacity: 0;
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
}
}
.xCalendarViewSpace {
height: $spaceHeight;
}
.xCalendarViewHeader {
height: $headerHeight;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.xCalendarViewHeaderLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xCalendarViewHeaderRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
}
.xCalendarViewDataHeader {
height: $weekheaderHeight;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.xCalendarViewDataHeaderItem {
width: 14.285%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
.xCalendarView {
position: relative;
display: flex;
flex-direction: column;
min-height: $minBodyHeight;
}
.xCalendarViewFooter {
height: $footerHeight;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>
@@ -0,0 +1,243 @@
import { xDate,createDate } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
import { xDateArrayItemType,xCalendarArgs,dateStyleDot,dateStyleBg,dateStyleType,xCalendarMode } from "../x-calendar-view/interface.uts"
export class xCalendar {
date : xDate;
calendar:xDateArrayItemType[] = [];
constructor(currentDate : string | number | Date | null = null) {
this.date = new xDate(currentDate)
}
isInCurrentMonth(current:Date,target:Date):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
// let d1 = current.getDate()
let y2 = target.getFullYear()
let m2 = target.getMonth()
// let d2 = target.getDate()
return y1 == y2 && m1 == m2
}
isInRangeDate(current:Date,targets:Date[],mode:xCalendarMode):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
if(mode == 'day'){
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
return true
}
}
}else if(mode == 'range'){
if(targets.length<2) return false;
let start = targets[0]
let end = targets[targets.length-1]
return current.getTime()>start.getTime()&&current.getTime()<end.getTime()
}
return false
}
isInRangeDateByIndex(current:Date,targets:Date[],mode:xCalendarMode):number{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
if(mode == 'day'){
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
return i
}
}
}else if(mode == 'range'){
if(targets.length<2) return -1;
let start = targets[0]
let end = targets[targets.length-1]
if(current.getTime()>start.getTime()&&current.getTime()<end.getTime()) return -1
if(current.getTime()==start.getTime()) return 0
if(current.getTime()==end.getTime()) return 1
}
return -1
}
isDisabled(current:Date,start:Date|null,end:Date|null,targets:Date[]):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
let disabled = false
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
disabled = true;
break;
}
}
if(start!=null && end!=null){
if(start.getTime() <= end.getTime()){
// 正常的日期范围:start <= current <= end
disabled = disabled || (start.getTime() > current.getTime() || end.getTime() < current.getTime())
}
// 如果start > end,则忽略范围限制,只应用disabledDays
} else {
// 只有一个边界时,正常应用
if(start!=null){
disabled = disabled || start.getTime() > current.getTime()
}
if(end!=null){
disabled = disabled || end.getTime() < current.getTime()
}
}
return disabled
}
isInStart(current:Date,targets:Date[]):boolean{
if(targets.length==0) return false;
return targets[0].getTime() == current.getTime()
}
isInEnd(current:Date,targets:Date[]):boolean{
if(targets.length<2) return false;
return targets[targets.length-1].getTime() == current.getTime()
}
diffDays(start:Date,end:Date):number{
return end.getTime()-start.getTime()
}
getDateStyle(current:Date,defaultStyle:xCalendarArgs,disabled:boolean,inMonth:boolean,inRange:boolean,isInStart:boolean,isInEnd:boolean,dateStyle:xCalendarDateStyle_type[],mode:xCalendarMode):dateStyleType{
let nowdatestyleIndex = dateStyle.findIndex((d:xCalendarDateStyle_type):boolean => {
return new Date(d.date.replace(/-/g,'/')).getTime() == current.getTime()
})
let item:xCalendarDateStyle_type|null = nowdatestyleIndex==-1?null:dateStyle[nowdatestyleIndex]
const label = (item?.label??'') as string;
let fontColor = (item?.fontColor??defaultStyle.fontColor) as string;
fontColor = isInStart||isInEnd||(mode!='range'&&inRange) ? defaultStyle.activeFontColor:(inRange?defaultStyle.rangFontColor:fontColor)
let bgColor = (item?.color??'transparent') as string;
bgColor = isInStart||isInEnd||(mode!='range'&&inRange) ? defaultStyle.color : (inRange?defaultStyle.rangColor:bgColor)
const bgstyle = {
/** 底部文本 */
label : label,
/** 日期文字颜色 */
fontColor : fontColor,
backgroundColor : bgColor,
opacity : disabled||!inMonth?0.5:1
} as dateStyleBg
const dotstyle = {
/** 是否显示右角标 */
dot : item?.dot??false,
/** 右角标背景颜色 */
dotColor : item?.dotColor??defaultStyle.color,
/** 右角标文字颜色 */
dotLabelColor : item?.dotLabelColor??'#ffffff',
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
dotLabel : item?.dotLabel??'',
} as dateStyleDot
return {
dot : dotstyle,
dstyle : bgstyle
} as dateStyleType
}
getCalendar(
seekDay:number,
mode:xCalendarMode,
currentDate : string | number | Date | null = null,
selectedDate:string[],
start:Date|null,
end:Date|null,
defaultStyle:xCalendarArgs,
dateStyle:xCalendarDateStyle_type[] = [],
disabledDays:string[] = [],
isPadding:boolean = true):xDateArrayItemType[]{
const nowCutime = Date.now()
let nowdate = (currentDate == null?this.date:new xDate(currentDate)) as xDate;
const dateAr = nowdate.getDaysOf('m')
let dates = [] as xDateDayInfoType[]
if(isPadding){
// 使用 seekDay 参数控制月份第一天的周偏移量
// seekDay: 0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
// 注意:week 值实际是 0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
// 需要转换为 0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日的映射
let firstDayOfMonth = dateAr[0]
let firstDayWeek = firstDayOfMonth.week
// 将 week 值转换为 0=周一, 1=周二, ..., 6=周日的映射
// 原始:0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
// 目标:0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
let mappedWeek = (firstDayWeek + 6) % 7 // 将周日(0)映射为6,周一(1)映射为0
// 计算需要向前填充的天数
let beforeNum = 0
if (seekDay === 0) {
// 周一开始:mappedWeek 为 0 时不需要填充
beforeNum = mappedWeek
} else {
// 其他日期开始:计算到目标起始日的偏移量
beforeNum = (mappedWeek - seekDay + 7) % 7
}
// 如果 beforeNum 为 0,说明第一天正好是目标起始日,不需要填充
if (beforeNum > 0) {
const beforeDates = new xDate(firstDayOfMonth.date).getDaysOfNum(beforeNum,'before')
dates = [...beforeDates,...dateAr]
} else {
dates = [...dateAr]
}
if(dates.length<42){
//补齐最后一周的内容
const lastDate = new xDate(dates[dates.length-1].date);
let lastWeek = lastDate.getDaysOfNum(42-dates.length,'after')
dates = [...dates,...lastWeek]
}
}else{
dates = dateAr
}
let selectedTargets = selectedDate.map((d:string):Date =>{
return new Date(d.replace(/-/g,'/'))
})
let disabledDaysAs = disabledDays.map((d:string):Date =>{
return new Date(d.replace(/-/g,'/'))
})
const current = nowdate.date
const list = [] as xDateArrayItemType[]
for(let i=0;i<dates.length;i++){
let item = dates[i]
let checkDate = new Date(item.date);
const inmonth = this.isInCurrentMonth(checkDate,current);
const inRange = this.isInRangeDate(checkDate,selectedTargets,mode);
const disabled = this.isDisabled(checkDate,start,end,disabledDaysAs)
const isInstart = this.isInStart(checkDate,selectedTargets)
const isInEnd = this.isInEnd(checkDate,selectedTargets)
const astyle = this.getDateStyle(checkDate,defaultStyle,disabled,inmonth,inRange,isInstart,isInEnd,dateStyle,mode)
list.push({
date : item,
disabled : disabled,
inCurrentMonth : inmonth,
inRange : inRange,
isInstart : isInstart,
isInEnd : isInEnd,
style : astyle
})
}
// console.log(`执行组时间:${Date.now()-nowCutime}毫秒,循环数组:${dates.length}`)
return list
}
}
@@ -0,0 +1,385 @@
<script lang="ts" setup>
import { type PropType,getCurrentInstance } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
import { xCalendar } from "./xCalendar"
import { xDateArrayItem,xDateArrayItemType,xCalendarArgs,xCalendarMode } from "../x-calendar-view/interface.uts"
const i18n = xConfig.i18n;
// #ifdef APP
import { calendarDraw } from "./calendarDraw"
const proxy = getCurrentInstance()?.proxy;
const xCalendarViewItemRef = ref<UniElement|null>(null)
let calendarDom:calendarDraw|null = null;
// #endif
type xCalendarMultiplePropsType = {
/**
* 同步当前时间v-model
* 不想受控:model-value
*/
modelValue : string,
/**
* 范围选择模式
* day:天数多选,通过multipleMax可以设置允许选择的天数
* range:天数的范围选择,起始和终止
* week:按周次选择范围
* quarter:按季度选择范围
* year:按年
*/
model: xCalendarMode,
/**
* 禁用的日期字符串如"2023-12-12"
* 它与下面的startend不冲突。
*/
disabledDays: string[],
/**
* 允许选择的开始日期
*/
startDate: string,
/**
* 允许选择的结束日期
*/
endDate: string,
/**
* 设置指定日期的样式
* 数据类型见:xCalendarDateStyle_type
*/
dateStyle: xCalendarDateStyle_type[],
/**
* 同步vmodel时格式化模板
*/
format: string,
/**
* 选中的主题色,默认空值,取全局主题色
* 如果提供了dateStyle,以dateStyle为准
*/
color: string,
/**
* 默认的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontColor:string,
/**
* 默认的暗黑文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontDarkColor:string,
/**
* 默认选中时的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
activeFontColor:string,
/**
* 范围选中时,范围中间的选中颜色,
* 如果为空,为color的透明度0.5;
*/
rangColor:string,
rangFontColor:string,
currentDate:string,
/**
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
*/
seekDay:number,
/**
* 给日期设定状态类型为:xCalendarDateStyleStatusProps
*/
dateStatus:xCalendarDateStyleStatusType[],
}
const emit = defineEmits(['change','click'])
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
modelValue: "",
currentDate:'',
model:'day' as xCalendarMode,
disabledDays:[] as string[],
startDate:'1900-1-1',
endDate:'2025-5-13',
dateStyle:[] as xCalendarDateStyle_type[],
format:'YYYY-MM-DD',
color:'',
fontColor:'#333333',
fontDarkColor:'#ffffff',
activeFontColor:'#ffffff',
rangColor:'',
rangFontColor:'',
seekDay:0,
dateStatus:[] as xCalendarDateStyleStatusType[]
})
const calendar = new xCalendar()
const _rangColor = computed(()=>{
let color = props.rangColor == ''?xConfig.color:props.rangColor
let rgba = hexToRgb(getDefaultColor(color));
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${props.rangColor==''?0.2:1})`
})
const _modelValue = computed(():string=> props.modelValue )
const _model = computed(():xCalendarMode=> props.model )
function splitArray<T>(ar : Array<T>, len : number) : Array<Array<T>> {
const result : Array<Array<T>> = [];
for (let i = 0; i < ar.length; i += len) {
result.push(ar.slice(i, i + len));
}
return result
}
const _fontSize = computed(():string=> checkIsCssUnit('16',''))
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
const dateArrayList = computed(():xDateArrayItemType[][]=>{
const primaryColor = getDefaultColor(props.color==''?xConfig.color:props.color);
const dates = calendar.getCalendar(
props.seekDay,
props.model,
props.currentDate,
props.modelValue,
props.startDate!=''?new Date(props.startDate.replace(/-/g,'/')):null,
props.endDate!=''?new Date(props.endDate.replace(/-/g,'/')):null,
{
color:primaryColor,
fontColor:getDefaultColor(xConfig.dark=='dark'?props.fontDarkColor:props.fontColor),
activeFontColor:getDefaultColor(props.activeFontColor),
rangColor:_rangColor.value,
rangFontColor:props.rangFontColor==''?primaryColor:getDefaultColor(props.rangFontColor)
} as xCalendarArgs,
props.dateStyle,
props.disabledDays
);
return splitArray<xDateArrayItemType>(dates,7)
})
function showLabel(item:xDateArrayItemType):string {
if(item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',2);//'本日'
if(item.isInstart&&!item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',0);//'开始'
if(!item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',1);//'结束'
return ""
}
function dateClick(item:xDateArrayItemType){
if(item.disabled) return;
if(!calendar.isInCurrentMonth(new Date(item.date.date),new Date(props.currentDate))) return;
emit('click',item)
}
function checkDataIsInDateStatus(date:string|null):string{
if(date ==''||date == null) return '';
for(let k =0 ;k <_dateStatus.value.length;k++){
let itemStatus = _dateStatus.value[k]
let dates = itemStatus?.date??[];
let start = itemStatus?.between?.start??''
let end = itemStatus?.between?.end??''
let betweenColor = itemStatus?.between?.color??''
let notDates = itemStatus?.between?.notDate??[]
let nowDate = new xDate(date);
let isInBetweenDate = false;
if(start!=''&&end!=''){
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
let isNotDate = false
for(let i=0;i<notDates.length;i++){
let item = notDates[i];
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
isNotDate = true;
break;
}
}
isInBetweenDate = isBetween && !isNotDate
}
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
let selfColor = ''
for(let i=0;i<dates.length;i++){
let item = dates[i];
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
selfColor = item.color==''?'primary':item.color
break;
}
}
if(selfColor!=''){
return getDefaultColor(selfColor)
}
}
return ""
}
// #ifdef APP
function draw():Promise<any>{
return new Promise(()=>{
calendarDom?.draw(dateArrayList.value,_model.value,_dateStatus.value)
})
}
function canvsClick(e:UniPointerEvent){
xCalendarViewItemRef.value?.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
let cellHeight = 50;
let cellWidth = rect.width / 7;
let top = rect.top;
let left = rect.left;
let x = e.clientX - left;
let y = e.clientY - top;
let col = Math.floor(x/cellWidth);
let row = Math.floor(y/cellHeight);
let item = dateArrayList.value[row][col];
function testThread():Promise<any>{
return new Promise(()=>{
dateClick(item)
})
}
testThread()
})
.catch((er)=>{
console.error(er)
})
}
watch([():any => dateArrayList.value,():any => _dateStatus.value],()=>{
draw()
})
// #endif
onMounted(()=>{
// #ifdef APP
calendarDom = new calendarDraw(xCalendarViewItemRef.value,proxy)
nextTick(()=>{
draw()
})
// #endif
})
</script>
<template>
<view class="xCalendarViewItem"
ref="xCalendarViewItemRef"
<!-- #ifdef APP -->
@click="canvsClick"
<!-- #endif -->
>
<!-- #ifndef APP -->
<view class="xCalendarViewItemCol" v-for="(children,index) in dateArrayList" :key="index">
<view class="xCalendarViewItemColItem"
@click="dateClick(item)"
v-for="(item,index2) in (children as xDateArrayItemType[])" :key="index2">
<view class="xCalendarViewItemColItemBox"
:style="{
backgroundColor:item.style.dstyle.backgroundColor,
opacity:item.style.dstyle.opacity,
}"
>
<view v-if="item.style.dot.dot"
class="xCalendarViewItemDot"
:class="[item.style.dot.dotLabel==''?'xCalendarViewItemDotNolabel':'']"
:style="{
color:item.style.dot.dotLabelColor,
backgroundColor:item.style.dot.dotColor
}"
>
{{item.style.dot.dotLabel}}
</view>
<text class="xCalendarViewItemColDate"
:style="{
color:item.style.dstyle.fontColor,
fontSize:_fontSize
}"
>
{{item.date.day}}
</text>
<text class="xCalendarViewItemColLabel"
:style="{
color:item.style.dstyle.fontColor
}"
>{{showLabel(item)||item.style.dstyle.label}}</text>
<view class="xCalendarViewStatus" :style="{backgroundColor:checkDataIsInDateStatus(item.date.date)}" v-if="checkDataIsInDateStatus(item.date.date)!=''"></view>
</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
<style scoped lang="scss">
.xCalendarViewStatus{
width:4px;
height:4px;
border-radius: 2px;
position: absolute;
bottom: 1px;
}
// #ifdef APP
.xCalendarViewItem{
width:100%;
height:100%;
}
// #endif
// #ifndef APP
.xCalendarViewItemDot{
padding:2px 4px;
min-width:18px;
min-height:18px;
font-size:10px;
border-radius:9px;
position: absolute;
right:0px;
top:0px;
&.xCalendarViewItemDotNolabel{
padding:0;
min-width:8px;
min-height:8px;
font-size:10px;
border-radius:9px;
}
}
.xCalendarViewItem{
width:100%;
height:100%;
.xCalendarViewItemCol{
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: center;
height: 50px;
.xCalendarViewItemColItem{
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
height: 100%;
width:14.285%;
.xCalendarViewItemColItemBox{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
border-radius: 25px;
overflow: visible;
}
.xCalendarViewItemColDate{
text-align: center;
height: 27px;
line-height: 27px;
margin-top: -4px;
// font-weight: bold;
// font-size: 16px;
display: block;
}
.xCalendarViewItemColLabel{
text-align: center;
font-size: 10px;
margin-top: -4px;
display: block;
min-height:10px;
}
}
}
}
// #endif
</style>
@@ -0,0 +1,185 @@
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import {xCalendarDateStyleStatusType} from "@/uni_modules/tmx-ui/interface.uts"
import { xDate } from "../../core/util/xDate.uts"
export class calendarDraw {
ele : UniElement | null = null
proxy : any | null = null
cellHeight = 50;
sapce = 5
model : xCalendarMode = 'day'
_fontSize : string = checkIsCssUnit('16', xConfig.unit)
_dateStatus : xCalendarDateStyleStatusType[] | null = null;
constructor(target : UniElement | null, proxyx : any | null) {
this.ele = target
this.proxy = proxyx
}
checkDataIsInDateStatus(date:string|null):string{
if(date ==''||date == null || this._dateStatus == null) return '';
for(let k =0 ;k <this._dateStatus.length;k++){
let itemStatus = this._dateStatus[k]
let dates = itemStatus?.date??[];
let start = itemStatus?.between?.start??''
let end = itemStatus?.between?.end??''
let betweenColor = itemStatus?.between?.color??''
let notDates = itemStatus?.between?.notDate??[]
let nowDate = new xDate(date);
let isInBetweenDate = false;
if(start!=''&&end!=''){
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
let isNotDate = false
for(let i=0;i<notDates.length;i++){
let item = notDates[i];
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
isNotDate = true;
break;
}
}
isInBetweenDate = isBetween && !isNotDate
}
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
let selfColor = ''
for(let i=0;i<dates.length;i++){
let item = dates[i];
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
selfColor = item.color==''?'primary':item.color
break;
}
}
if(selfColor!=''){
return getDefaultColor(selfColor)
}
}
return ""
}
showLabel(item : xDateArrayItemType) : string {
if (item.isInstart && item.isInEnd && this.model == 'range') return '本日'
if (item.isInstart && !item.isInEnd && this.model == 'range') return '开始'
if (!item.isInstart && item.isInEnd && this.model == 'range') return '结束'
return ""
}
getColor(color : string, alpha : number) : string {
if (alpha == 1) return color;
let rgba = hexToRgb(getDefaultColor(color));
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${alpha})`
}
_draw(element : UniElement, Grect : DOMRect, list : xDateArrayItemType[][]) {
const ctx = element.getDrawableContext()!;
ctx.reset()
ctx.fillStyle = 'rgba(0,0,0,0)'
ctx.fillRect(0, 0, Grect.width, Grect.height)
ctx.textAlign = 'center'
const _realCellWidth = Grect.width / 7
const _h = Math.min(_realCellWidth, 50) - this.sapce * 2
for (let i = 0; i < list.length; i++) {
const children = list[i]
for (let col = 0; col < children.length; col++) {
let item = children[col];
let dstyle = item.style
let xy_x = _realCellWidth * col + _realCellWidth / 2;
let xy_y = this.cellHeight * i + this.cellHeight / 2;
// 绘制选中的背景
ctx.fillStyle = this.getColor(dstyle.dstyle.backgroundColor, (item.disabled || !item.inCurrentMonth) && !item.isInstart && !item.isInEnd ? 0.3 : 1);
if (dstyle.dstyle.backgroundColor != 'transparent') {
ctx.beginPath()
ctx.arc(xy_x, xy_y, _h / 2, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
}
// 绘制右角标。
if (dstyle.dot.dot) {
if (dstyle.dot.dotLabel == '') {
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 5;
let lasty = _realCellWidth * (i) + 5
ctx.fillStyle = dstyle.dot.dotColor
ctx.beginPath()
ctx.arc(lastx, lasty, 4, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
} else {
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 10;
let lasty = _realCellWidth * (i) + 10
ctx.fillStyle = dstyle.dot.dotColor;
ctx.beginPath()
ctx.arc(lastx, lasty, 10, 0, Math.PI * 2)
ctx.closePath()
ctx.fill()
ctx.font = "10px"
ctx.fillStyle = dstyle.dot.dotLabelColor
// #ifdef APP-ANDROID || APP-HARMONY
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 3);
// #endif
// #ifdef APP-IOS
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 5);
// #endif
}
}
// 绘制文字
const text = item.date.day.toString();
ctx.fillStyle = this.getColor(dstyle.dstyle.fontColor, (item.disabled || !item.inCurrentMonth) ? 0.7 : 1);
ctx.font = this._fontSize
const textHeight = 34;
const _t_x = _realCellWidth * col + _realCellWidth / 2
let _t_y = this.cellHeight * i + this.cellHeight / 2 + 2;
// #ifdef APP-IOS
_t_y = this.cellHeight * i + this.cellHeight / 2 + 6;
// #endif
ctx.fillText(text, _t_x, _t_y);
let label = this.showLabel(item)
label = label == '' ? dstyle.dstyle.label : label
// 绘制底部的label
if (label != '') {
let labely = this.cellHeight * i + this.cellHeight - 9;
// #ifdef APP-ANDROID || APP-HARMONY
labely = this.cellHeight * i + this.cellHeight - 12;
// #endif
ctx.font = "8px"
ctx.fillText(label, _t_x, labely);
}
// 绘制状态小点点。
let statusColor = this.checkDataIsInDateStatus(item.date.date);
if(statusColor!=''){
ctx.fillStyle = statusColor
ctx.beginPath()
let statusY = this.cellHeight * i + this.cellHeight - this.sapce - 3;
ctx.arc(_t_x,statusY,2,0,Math.PI*2)
ctx.closePath()
ctx.fill()
}
}
}
ctx.update()
}
draw(list : xDateArrayItemType[][], modelv : xCalendarMode = 'day',status:xCalendarDateStyleStatusType[]|null = null) {
let _this = this;
_this.model = modelv;
_this._dateStatus = status;
this.ele?.getBoundingClientRectAsync()
?.then((rect : DOMRect) => {
_this._draw(_this.ele!, rect, list)
})
.catch((er) => {
console.error(er)
})
}
}
@@ -0,0 +1,99 @@
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
import { xDate } from "../../core/util/xDate.uts"
export type xDateArrayItem = {
date : xDateDayInfoType,
//是否禁用
isDisabled : boolean,
//是否在当前月份
isInCureentMonth : boolean,
//是否已经选中
isSelected : boolean,
//样式
style : dateStyleType
}
export type xDateArrayItemType = {
date : xDateDayInfoType,
//是否禁用
disabled : boolean,
//是否在当前月份
inCurrentMonth : boolean,
//是否已经选中
inRange : boolean,
isInstart : boolean,
isInEnd : boolean,
//样式
style : dateStyleType
}
/**
* 日历单个日期的样式对象
*/
export type xCalendarDateStyle_real_type = {
/** 是否显示右角标 */
dot : boolean,
/** 右角标背景颜色 */
dotColor : string,
/** 右角标文字颜色 */
dotLabelColor : string,
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
dotLabel : string,
/** 底部文本 */
label : string,
/** 背景颜色 */
color : string,
/** 日期文字颜色 */
fontColor : string,
date : string
}
export type dateStyleDot = {
/** 是否显示右角标 */
dot : boolean,
/** 右角标背景颜色 */
dotColor : string,
/** 右角标文字颜色 */
dotLabelColor : string,
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
dotLabel : string,
}
export type dateStyleBg = {
/** 底部文本 */
label : string,
/** 日期文字颜色 */
fontColor : string,
backgroundColor : string,
opacity : number
}
export type dateStyleType = {
dot : dateStyleDot,
dstyle : dateStyleBg
}
export type xCalendarMode = "day" | "range" | "week" | "quarter" | "year"
export type BODY_SIZE_TYPE = {
width : number,
height : number
}
export type xCalendarArgs = {
color:string,
fontColor:string,
activeFontColor:string,
rangColor:string
rangFontColor:string,
}
export type xCalendarViewUpdateType = {
ar : xDateArrayItem[],
disabledDays : string[],
selectedDate : xDate | null,
nowDate : xDate,
start : xDate,
end : xDate,
color : string,
dateStyle : xCalendarDateStyle_real_type[]
}
export type GRID_SIZE = {
width : 0,
height : 0
}
@@ -0,0 +1,665 @@
<script lang="ts" setup>
import { type PropType,getCurrentInstance } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
import { xCalendar } from "./xCalendar"
import calendarMultipleUvue from "./calendar-multiple.uvue"
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
const i18n = xConfig.i18n;
type xCalendarMultiplePropsType = {
/**
* 同步当前时间v-model
* 不想受控:model-value
*/
modelValue : string,
/**
* day:固定此值
*/
model : "day" | "range" | "week" | "quarter" | "year",
/**
* 禁用的日期字符串如"2023-12-12"
* 它与下面的startend不冲突。
*/
disabledDays : string[],
/**
* 是否禁用用户交互,相当于展示日期。
*/
disabled:boolean,
/**
* 是否上下切换日历
*/
vertical:boolean,
/**
* 允许选择的开始日期
*/
startDate : string,
/**
* 允许选择的结束日期
*/
endDate : string,
/**
* 设置指定日期的样式
* 数据类型见:xCalendarDateStyle_type
*/
dateStyle : xCalendarDateStyle_type[],
/**
* 同步vmodel时格式化模板
*/
format : string,
/**
* 选中的主题色,默认空值,取全局主题色
* 如果提供了dateStyle,以dateStyle为准
*/
color : string,
/**
* 默认的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontColor : string,
/**
* 默认的暗黑文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
fontDarkColor : string,
/**
* 默认选中时的文字颜色
* 如果提供了dateStyle,以dateStyle为准
*/
activeFontColor : string,
/**
* 范围选中时,范围中间的选中颜色,
* 如果为空,为color的透明度0.5;
*/
rangColor : string,
rangFontColor : string,
/**
* 头的背景颜色,默认为透明
*/
headBgColor : string,
/**
* 头的文字颜色,提供了后暗黑失效会以这个为准。
*/
headFontColor : string,
/**
* 头部自定义样式。
*/
headStyle : string,
/**
* 循环渲染时,是否只渲染当前面板(如果你在pad等10年前的低端机上渲染日历有压力请打开此值为true)
* 关闭后可以提升滑动体验。
*/
renderOnly:boolean,
/**
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
*/
seekDay:number,
/**
* 给日期设定状态
* 类型为:xCalendarDateStyleStatusType
*/
dateStatus:xCalendarDateStyleStatusType[]
}
/**
* @name 日历 xCalendar
* @page /pages/index/calendar-view
* @category 表单组件
* @description 日历面板,支持指定日期新式设置,角标,底部文本设置等暂不同时支持多选,因为不支持联合类型后期需要分开组件使用。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| - | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCalendarMultiple"})
const calendar = new xCalendar()
const emit = defineEmits([
/**
* 时间变化时触发
* @param {string[]} value - 当前变化的日期
*/
'change',
/**
* 当前日历面板的日期被点击时触发
* @param {string} value - 当前被点击的日期
*/
'click',
/**
* 当前激活面板月份改变时触发(就是当前看到的月份面板)
* @param {string} value - 当前激活面板的日期
*/
'currentChange',
/**
* 同步当前的选中的日期绑定
* @param {string[]} value - 当前选中日期
*/
'update:modelValue',
/**
* 同步当前查看的月份日期,请以日期形式提供值
* @param {string} value - 当前查看的月分日期
*/
'update:currentDate'
])
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
modelValue: "",
model: 'day',
disabledDays:():string[] => [] as string[],
disabled:false,
vertical:false,
startDate: '1900-1-1',
endDate: '2100-1-1',
dateStyle: ():xCalendarDateStyle_type[] => [] as xCalendarDateStyle_type[],
format: 'YYYY-MM-DD',
color: '',
fontColor: '#333333',
fontDarkColor: '#ffffff',
activeFontColor: '#ffffff',
rangColor: '',
rangFontColor: '',
headBgColor: 'transparent',
headFontColor: '',
headStyle: '',
renderOnly:true,
seekDay:0,
dateStatus:():xCalendarDateStyleStatusType[] => [] as xCalendarDateStyleStatusType[]
})
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
const weeksCn = computed(() : string[] => {
// ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
// 根据 seekDay 属性调整周名称的顺序
const weekNames = [
i18n!.t("tmui4x.calendar.week",0),
i18n!.t("tmui4x.calendar.week",1),
i18n!.t("tmui4x.calendar.week",2),
i18n!.t("tmui4x.calendar.week",3),
i18n!.t("tmui4x.calendar.week",4),
i18n!.t("tmui4x.calendar.week",5),
i18n!.t("tmui4x.calendar.week",6),
]
// 如果 seekDay 为 0(默认周一),直接返回原数组
if (props.seekDay == 0) {
return weekNames
}
// 根据 seekDay 重新排列数组
const result: string[] = []
for (let i = 0; i < 7; i++) {
const index = (i + props.seekDay) % 7
result.push(weekNames[index])
}
return result
})
const _headBgColor = computed(() : string => getDefaultColor(props.headBgColor))
const _headFontColor = computed(() : string => {
if (props.headFontColor == '') return '#333333'
return getDefaultColor(props.headFontColor)
})
const _modelValue = ref(props.modelValue)
const _currentDate = ref(new xDate(props.modelValue).format("YYYY-MM-DD"))
const _currentDateSwipersIndex = ref(0)
const _currentDateSwipers = ref<string[]>([])
const _currentDateLabel = computed(() : string => {
let ars = _currentDate.value.split('-');
// `${ars[0]}年${ars[1]}月`
return i18n.t('tmui4x.calendar.titleCurrentMonth',[ars[0],ars[1]])
})
const _currentYear = ref(new xDate(props.modelValue).getYear())
let _modelValueDate = computed(() : Date => {
if(_modelValue.value=="") return new Date(_currentDate.value.replace(/-/g, '/'));
return new Date(_modelValue.value.replace(/-/g, '/'))
})
const _tipsText = computed(() : string => {
// return _modelValue.value!='' ? `已选择` : '未选择日期'
return _modelValue.value!='' ? i18n.t('tmui4x.calendar.selectedStatus',2) : i18n.t('tmui4x.calendar.selectedStatus',1)
})
const _monthBgColor = computed(() : string => xConfig.dark == 'dark' ? xConfig.sheetDarkColor : '#ffffff')
const _color = computed(() : string => props.color == '' ? getDefaultColor(xConfig.color) : getDefaultColor(props.color))
const showPanel = ref(false)
function dateClick(item : xDateArrayItemType) {
const isInselected = calendar.isInCurrente(new Date(item.date.date), _modelValueDate.value)
emit('click', item.date.date)
if(props.disabled) return;
let dates = ''
if(_modelValue.value==''){
dates = item.date.date
}else{
dates = isInselected?'':item.date.date
}
const nowFormat = new xDate(dates).format(props.format)
_modelValue.value = dates
emit('update:modelValue',dates==''?'':nowFormat)
emit('change', dates==''?'':nowFormat)
}
function getSwiperListCurrentDates(nowCurrentDate:string):string[]{
let index = _currentDateSwipersIndex.value
let currentData = new xDate(nowCurrentDate)
let xd = currentData.getClone().setDateOf(1,'d').format('YYYY-MM-DD')
let start = currentData.getClone().setDateOf(1,'d').subtraction(1,'m').format('YYYY-MM-DD')
let end = currentData.getClone().setDateOf(1,'d').add(1,'m').format('YYYY-MM-DD')
let datas = [xd,end,start]
if(_currentDateSwipers.value.length == 0) return datas;
if(index==0){
datas = [xd,end,start]
}else if(index==1){
datas = [start,xd,end]
}else if(index==2){
datas = [end,start,xd]
}
return datas;
}
function clear() {
if (_modelValue.value == "") return;
_modelValue.value = ""
emit('update:modelValue', _modelValue.value)
emit('change', _modelValue.value)
}
function nowMonth() {
let nowdate = new xDate()
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function stepperChangeYear(eyear : number) {
let nowdate = new xDate(_currentDate.value)
nowdate.setDateOf(eyear, 'y')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function changeMonth(eyear : number) {
let nowdate = new xDate(_currentDate.value)
nowdate.setDateOf(eyear, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
showPanel.value = false;
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
}
function nextMonth() {
let nowdate = new xDate(_currentDate.value)
nowdate.add(1, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function prevMonth() {
let nowdate = new xDate(_currentDate.value)
nowdate.subtraction(1, 'm')
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
}
function swiperChange(evt:UniSwiperChangeEvent){
_currentDateSwipersIndex.value = evt.detail.current;
nextTick(()=>{
_currentDate.value = _currentDateSwipers.value[ evt.detail.current]
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
let nowdate = new xDate(_currentDate.value)
_currentYear.value = nowdate.getYear()
emit('currentChange', _currentDate.value)
emit('update:currentDate', _currentDate.value)
})
}
watch(() : string => props.modelValue, (newVal:string) => {
_modelValue.value = props.modelValue;
if(newVal!=""){
let nowdate = new xDate(newVal).setDateOf(1,'d')
let nowcurrentDate = nowdate
if(nowcurrentDate.format("YYYY-MM-DD") == _currentDate.value) return;
_currentDate.value = nowcurrentDate.format("YYYY-MM-DD")
_currentYear.value = nowcurrentDate.getYear()
_currentDateSwipersIndex.value = 0;
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
}
})
onMounted(() => {
if (props.modelValue !="") {
let nowdate = new xDate(props.modelValue)
_currentDate.value = nowdate.format("YYYY-MM-DD")
_currentYear.value = nowdate.getYear()
}
nextTick(()=>{
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
emit('update:currentDate', _currentDate.value)
})
})
function getmonth(item:string):number{
let vls = item.split("-");
return parseInt(vls[1])
}
defineExpose({
/**
* 下月
*/
next(){
nextMonth();
},
/**
* 上月
*/
prev(){
prevMonth();
},
/**
* 设置日历返回到本月
*/
setCurrentMonth(){
nowMonth()
},
/**
* 清空选择
*/
clear(){
clear()
}
})
</script>
<template>
<view class="xCalendarView">
<!--
@slot 日历头,隐藏使用空插槽,将隐藏,如果想自定,请通过ref函数来翻页控制日历走向。
-->
<slot name="header">
<view class="xCalendarViewDataHeaderWrap" :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']"
:style="[{backgroundColor:_headBgColor},headStyle]">
<view class="xCalendarViewHeader" style="padding: 0 12px">
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
<!-- 2024年5月 -->
<x-text :color="_headFontColor" font-size="21">{{_currentDateLabel}}</x-text>
<x-icon :color="_headFontColor" font-size="21" name="arrow-down-s-fill"></x-icon>
</view>
<view class="xCalendarViewHeaderRight">
<!-- <view @click="prevMonth" class="px-10 py-15">
<x-icon font-size="21" :color="_headFontColor" name="arrow-up-s-fill">上月</x-icon>
</view>
<view @click="nextMonth" class="px-10 py-15">
<x-icon font-size="21" :color="_headFontColor" name="arrow-down-s-fill">下月</x-icon>
</view> -->
<!-- 清空 -->
<x-text @click="clear" :color="_headFontColor" style="padding: 10px 20px;">{{i18n.t('tmui4x.clear')}}</x-text>
<!-- 本月 -->
<x-text @click="nowMonth" :color="_headFontColor" style="padding: 10px 0px;">{{i18n.t('tmui4x.calendar.currentMonthTitle')}}</x-text>
</view>
</view>
<view class="xCalendarViewDataHeader">
<view class="xCalendarViewDataHeaderItem" v-for="(item,index) in 7" :key="item">
<!-- {{weeksCn[index]}} -->
<x-text :color="_headFontColor" font-size="12">{{weeksCn[index]}}</x-text>
</view>
</view>
</view>
</slot>
<view class="xCalendarViewSpace"></view>
<view :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']" class="xCalendarViewWrap">
<swiper :vertical="props.vertical" @change="swiperChange" :current="_currentDateSwipersIndex" v-if="_currentDateSwipers.length>0" :circular="true" style="width:100%;height:100%">
<swiper-item v-for="(item,index) in _currentDateSwipers" :key="index" style="width:100%;height:100%;">
<view style="width:100%;height:100%;position: relative;">
<view class="xCalendarViewContentBox">
<!-- 打开下方,可以在更低端机上提高2/3的性能 -->
<calendar-multiple-uvue v-if="index == _currentDateSwipersIndex||!props.renderOnly" @click="dateClick" :currentDate="item"
:dateStatus="_dateStatus"
:seekDay="props.seekDay"
:modelValue="props.modelValue" :model="props.model"
:disabledDays="props.disabledDays" :startDate="props.startDate" :endDate="props.endDate"
:dateStyle="props.dateStyle" :format="props.format" :color="props.color"
:fontColor="props.fontColor" :fontDarkColor="props.fontDarkColor"
:activeFontColor="props.activeFontColor" :rangColor="props.rangColor"
:rangFontColor="props.rangFontColor">
</calendar-multiple-uvue>
</view>
<view class="xCalendarViewNum">
<text class="xCalendarViewNumText">{{getmonth(item)}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
<view class="xCalendarViewSpace"></view>
<!--
@slot 日历尾部
-->
<slot name="footer">
<view class="xCalendarViewFooter">
<x-text color="#707070" font-size="14">{{_tipsText}}</x-text>
</view>
</slot>
<!-- 月和年 -->
<view :class="[showPanel?'xCalendarViewMonthOn':'xCalendarViewMonthOff']" class="xCalendarViewMonth"
:style="{backgroundColor:_monthBgColor}">
<view class="xCalendarViewHeader" style="padding: 0 12px">
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
<!-- 2024年5月 -->
<x-text :color="_color" font-size="21">{{_currentDateLabel}}</x-text>
<x-icon :color="_color" font-size="21" name="arrow-up-s-fill"></x-icon>
</view>
<view class="xCalendarViewHeaderRight">
<x-stepper @change="stepperChangeYear" v-model="(_currentYear as number)" :min="1900" :max="5000"
width="120"></x-stepper>
</view>
</view>
<view class="xCalendarViewMonthWrap" style="flex:1">
<view @click="changeMonth(index)" class="xCalendarViewMonthItem" v-for="(item,index) in 12"
:key="index">
<!-- <x-text font-size="18">{{item}}月</x-text> -->
<x-text font-size="18">{{i18n.t('tmui4x.calendar.monthCountSelected',item)}}</x-text>
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
$headerHeight: 50px;
$weekheaderHeight: 40px;
$footerHeight: 40px;
$spaceHeight: 5px;
$cellHeight: 50px;
$minBodyHeight: (
50px * 6) + $headerHeight + $weekheaderHeight + $footerHeight + ($spaceHeight * 2
);
.xCalendarViewMonth {
position: absolute;
z-index: 3;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
// #ifdef APP-IOS
opacity: 0;
transform: scale(0, 0);
width:0;
height:0;
// #endif
// #ifndef APP-IOS
opacity: 0;
transform: scale(0, 0);
// #endif
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
transform: scale(1, 1);
}
.xCalendarViewMonthWrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.xCalendarViewMonthItem {
width: 33.3333%;
height: 25%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
}
.xCalendarViewWrap {
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
opacity: 0;
// #ifndef APP-IOS
transform: scale(2, 2);
// #endif
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
// #ifndef APP-IOS
transform: scale(1, 1);
// #endif
}
position: relative;
width: 100%;
flex: 1;
.xCalendarViewContentBox {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 3;
}
.xCalendarViewNum {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.xCalendarViewNumText {
font-size: 200px;
color: rgba(125, 125, 125, 0.1);
font-weight: bold;
}
}
}
.xCalendarViewDataHeaderWrap {
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
transition-duration: 0.3s;
&.xCalendarViewMonthOff {
pointer-events: none;
opacity: 0;
}
&.xCalendarViewMonthOn {
pointer-events: auto;
opacity: 1;
}
}
.xCalendarViewSpace {
height: $spaceHeight;
}
.xCalendarViewHeader {
height: $headerHeight;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.xCalendarViewHeaderLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xCalendarViewHeaderRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
}
.xCalendarViewDataHeader {
height: $weekheaderHeight;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.xCalendarViewDataHeaderItem {
width: 14.285%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
.xCalendarView {
position: relative;
display: flex;
flex-direction: column;
min-height: $minBodyHeight;
}
.xCalendarViewFooter {
height: $footerHeight;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>
@@ -0,0 +1,237 @@
import { xDate } from "../../core/util/xDate.uts"
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
import { xDateArrayItemType,xCalendarArgs,dateStyleDot,dateStyleBg,dateStyleType,xCalendarMode } from "../x-calendar-view/interface.uts"
export class xCalendar {
date : xDate;
calendar:xDateArrayItemType[] = [];
constructor(currentDate : string | number | Date | null = null) {
this.date = new xDate(currentDate)
}
isInCurrentMonth(current:Date,target:Date):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
// let d1 = current.getDate()
let y2 = target.getFullYear()
let m2 = target.getMonth()
// let d2 = target.getDate()
return y1 == y2 && m1 == m2
}
isInpanel(current:Date|string):boolean{
console.log(this.calendar)
if(this.calendar.length==0) return false;
let s = new xDate(this.calendar[0].date.date);
let e = new xDate(this.calendar[this.calendar.length-1].date.date);
let c = new xDate(current);
return c.getTime('d')<=e.getTime('d')&&c.getTime('d')>=s.getTime('d')
}
isInRangeDate(current:Date,targets:Date[],mode:xCalendarMode):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
if(mode == 'day'){
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
return true
}
}
}else if(mode == 'range'){
if(targets.length<2) return false;
let start = targets[0]
let end = targets[targets.length-1]
return current.getTime()>start.getTime()&&current.getTime()<end.getTime()
}
return false
}
isInRangeDateByIndex(current:Date,targets:Date[],mode:xCalendarMode):number{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
if(mode == 'day'){
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
return i
}
}
}else if(mode == 'range'){
if(targets.length<2) return -1;
let start = targets[0]
let end = targets[targets.length-1]
if(current.getTime()>start.getTime()&&current.getTime()<end.getTime()) return -1
if(current.getTime()==start.getTime()) return 0
if(current.getTime()==end.getTime()) return 1
}
return -1
}
isInCurrente(current:Date,targets:Date):boolean{
return targets.getTime() == current.getTime()
}
isDisabled(current:Date,start:Date|null,end:Date|null,targets:Date[]):boolean{
let y1 = current.getFullYear()
let m1 = current.getMonth()
let d1 = current.getDate()
let disabled = false
for(let i=0;i<targets.length;i++){
let target = targets[i]
let y2 = target.getFullYear()
let m2 = target.getMonth()
let d2 = target.getDate()
if(y1 == y2 && m1 == m2 && d1 == d2){
disabled = true;
break;
}
}
if(start!=null && end!=null){
if(start.getTime() <= end.getTime()){
disabled = disabled || (start.getTime() > current.getTime() || end.getTime() < current.getTime())
}
} else {
if(start!=null){
disabled = disabled || start.getTime() > current.getTime()
}
if(end!=null){
disabled = disabled || end.getTime() < current.getTime()
}
}
return disabled
}
isInStart(current:Date,targets:Date[]):boolean{
if(targets.length==0) return false;
return targets[0].getTime() == current.getTime()
}
isInEnd(current:Date,targets:Date[]):boolean{
if(targets.length<2) return false;
return targets[targets.length-1].getTime() == current.getTime()
}
diffDays(start:Date,end:Date):number{
return end.getTime()-start.getTime()
}
getDateStyle(current:Date,defaultStyle:xCalendarArgs,disabled:boolean,inMonth:boolean,inRange:boolean,isInStart:boolean,isInEnd:boolean,dateStyle:xCalendarDateStyle_type[],mode:xCalendarMode):dateStyleType{
let nowdatestyleIndex = dateStyle.findIndex((d:xCalendarDateStyle_type):boolean => {
return new Date(d.date.replace(/-/g,'/')).getTime() == current.getTime()
})
let item:xCalendarDateStyle_type|null = nowdatestyleIndex==-1?null:dateStyle[nowdatestyleIndex]
const label = (item?.label??'') as string;
let fontColor = (item?.fontColor??defaultStyle.fontColor) as string;
fontColor = inRange ? defaultStyle.activeFontColor:(inRange?defaultStyle.rangFontColor:fontColor)
let bgColor = (item?.color??'transparent') as string;
bgColor = inRange ? defaultStyle.color : (inRange?defaultStyle.rangColor:bgColor)
const bgstyle = {
/** 底部文本 */
label : label,
/** 日期文字颜色 */
fontColor : fontColor,
backgroundColor : bgColor,
opacity : disabled||!inMonth?0.5:1
} as dateStyleBg
const dotstyle = {
/** 是否显示右角标 */
dot : item?.dot??false,
/** 右角标背景颜色 */
dotColor : item?.dotColor??defaultStyle.color,
/** 右角标文字颜色 */
dotLabelColor : item?.dotLabelColor??'#ffffff',
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
dotLabel : item?.dotLabel??'',
} as dateStyleDot
return {
dot : dotstyle,
dstyle : bgstyle
} as dateStyleType
}
getCalendar(
seekDay:number,
mode:xCalendarMode,
currentDate : string | number | Date | null = null,
selectedDate:string,
start:Date|null,
end:Date|null,
defaultStyle:xCalendarArgs,
dateStyle:xCalendarDateStyle_type[] = [],
disabledDays:string[] = [],
isPadding:boolean = true
):xDateArrayItemType[]{
const nowCutime = Date.now()
let nowdate = (currentDate == null?this.date:new xDate(currentDate)) as xDate;
const dateAr = nowdate.getDaysOf('m')
let dates = [] as xDateDayInfoType[]
if(isPadding){
let firstDayOfMonth = dateAr[0]
let firstDayWeek = firstDayOfMonth.week
// 将 week 值转换为 0=周一, 1=周二, ..., 6=周日的映射
// 原始:0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
// 目标:0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
// 将周日(0)映射为6,周一(1)映射为0
let mappedWeek = (firstDayWeek + 6) % 7
// 计算需要向前填充的天数
let beforeNum = 0
if (seekDay === 0) {
// 周一开始:mappedWeek 为 0 时不需要填充
beforeNum = mappedWeek
} else {
// 其他日期开始:计算到目标起始日的偏移量
beforeNum = (mappedWeek - seekDay + 7) % 7
}
// 如果 beforeNum 为 0,说明第一天正好是目标起始日,不需要填充
if (beforeNum > 0) {
const beforeDates = new xDate(firstDayOfMonth.date).getDaysOfNum(beforeNum,'before')
dates = [...beforeDates,...dateAr]
} else {
dates = [...dateAr]
}
// 补齐到完整的6周(42天)
if(dates.length < 42){
//补齐最后一周的内容
let lastWeek = new xDate(dates[dates.length-1].date).getDaysOfNum(42-dates.length,'after')
dates = [...dates,...lastWeek]
}
}else{
dates = dateAr
}
let selectedTargets = selectedDate==""?new Date():new Date(selectedDate.replace(/-/g,'/'))
let disabledDaysAs = disabledDays.map((d:string):Date =>{
return new Date(d.replace(/-/g,'/'))
})
const current = nowdate.date
const list = [] as xDateArrayItemType[]
for(let i=0;i<dates.length;i++){
let item = dates[i]
let checkDate = new Date(item.date);
const inmonth = this.isInCurrentMonth(checkDate,current);
const inRange = this.isInCurrente(checkDate,selectedTargets);
const disabled = this.isDisabled(checkDate,start,end,disabledDaysAs)
const isInstart = false
const isInEnd = false
const astyle = this.getDateStyle(checkDate,defaultStyle,disabled,inmonth,inRange,isInstart,isInEnd,dateStyle,mode)
list.push({
date : item,
disabled : disabled,
inCurrentMonth : inmonth,
inRange : inRange,
isInstart : isInstart,
isInEnd : isInEnd,
style : astyle
})
}
this.calendar = list;
return list
}
}
@@ -0,0 +1,228 @@
<script lang="ts" setup>
import { computed } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
*
* @name 卡片 xCard
* @page /pages/index/card
* @category 展示组件
* @description 圆角,主题可统一全局配置风格。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCard"})
const emits = defineEmits([
/** 卡片被点击 */
'click',
/** 右边状态小图标被点击 */
'status',
/** 底部按钮被点击 */
'action'
])
type BtnSizeType = "mini" | "small" | "normal" | "large"
type xCardPropsType = {
/** 内容的内边距。 */
padding: string,
/** 标题文字大小 */
titleSize: string,
/** 按钮颜色 */
btnColor: string,
/** 标题颜色 */
color: string,
/** 背景颜色 */
bgColor: string,
/** 暗黑背景颜色,如果为空,取sheetDarkColor */
darkBgColor: string,
/** 底部按钮数组。如果不满意风格布局请使用插槽footer来布局 */
btns: string[],
/** 副标题 */
subtitle: string,
/** 标题 */
title: string,
/** 右边的小图标,如果你是想显示状态,日期请使用对应插槽 */
statusIcon: string,
/** 中间内容。如果有大量内容请直接在默认插槽(标签内)内布局 */
content: string,
/** 头部图片地址。 */
image: string,
/** 头图片高度 */
imageHeight: string,
/** 圆角请不要动态更改此会,默认为空,取全局设置的风格值。 */
round: string,
/** 请不要动态更改些投影值,截止4.75+鸿蒙无法使用投影 */
shadow: string,
/** 按钮尺寸 */
btnSize: BtnSizeType
}
const props = withDefaults(defineProps<xCardPropsType>(), {
padding: "16",
titleSize: "18",
btnColor: "",
color: "#333333",
bgColor: "#ffffff",
darkBgColor: "",
btns: () : string[] => [] as string[],
subtitle: "",
title: "",
statusIcon: "more-fill",
content: "",
image: "",
imageHeight: "150",
round: "",
shadow: "0 3px 10px rgba(0, 0, 0, 0.05)",
btnSize: "small" as BtnSizeType
})
// 计算属性
const _image = computed((): string => props.image)
const _round = computed((): string => {
if (props.round == "") return checkIsCssUnit(xConfig.cardRound, xConfig.unit)
return checkIsCssUnit(props.round, xConfig.unit)
})
const _imageHeight = computed((): string => checkIsCssUnit(props.imageHeight, xConfig.unit))
const _padding = computed((): string => checkIsCssUnit(props.padding, xConfig.unit))
const _titleSize = computed((): string => props.titleSize)
const _color = computed((): string => getDefaultColor(props.color))
const _bgColor = computed((): string => {
if(xConfig.dark=='dark'){
if(props.darkBgColor!='') return getDefaultColor(props.darkBgColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(props.bgColor)
})
const _btnColor = computed((): string => {
if (props.btnColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(props.btnColor)
})
const _btns = computed((): string[] => props.btns)
const _subtitle = computed((): string => props.subtitle)
const _title = computed((): string => props.title)
const _statusIcon = computed((): string => props.statusIcon)
const _content = computed((): string => props.content)
// 方法
function actionClick(index : number) {
emits('action', index)
}
function statusClick() {
emits('status')
}
function onClick() {
emits('click')
}
</script>
<template>
<!-- boxShadow: shadow, -->
<view @click="onClick" class="xCard"
<!-- #ifndef APP-HARMONY -->
:style="{borderRadius:_round,boxShadow: shadow,backgroundColor:_bgColor}"
<!-- #endif -->
<!-- #ifdef APP-HARMONY -->
:style="{borderRadius:_round,backgroundColor:_bgColor}"
<!-- #endif -->
>
<!--
@slot 图片插槽
-->
<slot name="image">
<image :style="{width:'100%',height:_imageHeight,borderRadius:`${_round} ${_round} 0px 0px`}" :src="_image"
v-if="_image!=''"></image>
</slot>
<view :style="{padding:_padding}">
<view class="xCardHeader">
<!--
@slot 标题插槽
-->
<slot name="title">
<view v-if="_title!=''">
<x-text :color="_color" :font-size="_titleSize" class="xCardTitle">{{_title}}</x-text>
</view>
</slot>
<!--
@slot 状态右边小图标插槽
-->
<slot name="statusIcon">
<view @click.stop="statusClick" v-if="_statusIcon!=''" style="padding: 0rpx 0rpx 0rpx 16px;">
<x-icon :font-size="_titleSize" :name="_statusIcon"></x-icon>
</view>
</slot>
</view>
<view class="xCardSubtitle">
<!--
@slot 副标题插槽
-->
<slot name="subtitle">
<x-text font-size="12" v-if="_subtitle!=''" class="xCardSubtitleText">
{{subtitle}}
</x-text>
</slot>
</view>
<!--
@slot 默认内容插槽
-->
<slot>
<x-text :color="_color" font-size="16" v-if="_content!=''" class="xCardContent">
{{_content}}
</x-text>
</slot>
<!--
@slot 底部插槽
-->
<slot name="footer">
<view class="xCardFooter" v-if="_btns.length>0">
<x-button round="6" @click.stop="actionClick(index)" v-for="(item,index) in _btns" :key="index"
:style="{marginRight:(index!=_btns.length-1)?'12px':'0px'}" :color="_btnColor"
:size="btnSize">{{item}}</x-button>
</view>
</slot>
</view>
</view>
</template>
<style scoped>
.xCardContent {
font-size: 14px;
line-height: 1.4;
opacity: 0.8;
}
.xCardFooter {
display: flex;
justify-content: flex-end;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
margin-top: 16px;
}
.xCardSubtitle {}
.xCardSubtitleText {
font-size: 13px;
opacity: 0.7;
padding: 12rpx 0rpx 24rpx 0rpx;
}
.xCardHeader {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.xCardTitle {
font-weight: bold;
}
.xCard {
/* box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); */
}
</style>
@@ -0,0 +1,18 @@
/**
* 侧边分类数据
*/
export type SLIDER_TREE_ITEM = {
id : string,
title : string,
children : SLIDER_TREE_ITEM[],
disabled : boolean,
/** 当前选中的id数组 */
selected : string[],
checked:boolean
}
export type NODES_PATH_MENU_TYPE = {
indexPath:number[],
ids:string[],
pathData:SLIDER_TREE_ITEM[]
}
@@ -0,0 +1,125 @@
import { CASCADER_TREE_ITEM, CASCADER_PATH_MENU_TYPE } from "../../interface.uts"
/**
* 当前树有多少个被选中了。
*/
export function getTreeSelectedNum(item : CASCADER_TREE_ITEM[], target : Set<string>) : number {
let inx = 0;
function jshz(tree : CASCADER_TREE_ITEM[]) {
for (let i = 0; i < tree.length; i++) {
let item = tree[i]
if (item.children.length > 0) {
jshz(item.children)
} else if (target.has(item.id)) {
inx += 1
}
}
}
jshz(item)
return inx;
}
/**
* 根据目标节点,过滤掉父节点。
* @param item
* @param target
*/
export function filterParentNode(item : CASCADER_TREE_ITEM[], target : Set<string>) : string[] {
function jshz(tree : CASCADER_TREE_ITEM[]) : string[] {
let arr = [] as string[]
for (let i = 0; i < tree.length; i++) {
let item = tree[i]
if (item.children.length > 0) {
arr = arr.concat(jshz(item.children))
} else if (target.has(item.id)) {
arr.push(item.id)
}
}
return arr;
}
return jshz(item)
}
/**
* 是否是父节点
* @param item
* @param target
*/
export function isParent(item : CASCADER_TREE_ITEM[], target : string) : boolean {
let parent = false;
function jshz(tree : CASCADER_TREE_ITEM[]) {
if (parent) {
return;
}
for (let i = 0; i < tree.length; i++) {
let item = tree[i]
if (item.id == target && item.children.length > 0) {
parent = true;
} else if (item.id != target && item.children.length > 0) {
jshz(item.children)
}
}
}
jshz(item)
return parent
}
/** 通过节点获取当前节点的索引路径和ids数组 */
export function getIndexPathAndIds(tree : CASCADER_TREE_ITEM[], targetId : string) : string[] {
const path : string[] = [];
function dfs(node : CASCADER_TREE_ITEM[]) : boolean {
for (let i = 0; i < node.length; i++) {
let item = node[i];
path.push(item.id);
if (item.id === targetId) {
return true;
}
if (dfs(item.children)) {
return true;
}
path.pop();
}
return false;
}
if (dfs(tree)) {
return path;
} else {
return [] as string[];
}
}
/** 通过节点获取当前节点的索引路径和ids数组 */
export function getTreeNodesPath(tree : CASCADER_TREE_ITEM[], targetId : string) : CASCADER_PATH_MENU_TYPE|null {
const path : string[] = [];
const indexPaths:number[] = [];
const pathData:CASCADER_TREE_ITEM[] = [];
function dfs(node : CASCADER_TREE_ITEM[]) : boolean {
for (let i = 0; i < node.length; i++) {
let item = node[i];
path.push(item.id);
indexPaths.push(i)
pathData.push(item)
if (item.id === targetId) {
return true;
}
if (dfs(item.children)) {
return true;
}
path.pop();
indexPaths.pop();
pathData.pop();
}
return false;
}
if (dfs(tree)) {
return {
indexPath:indexPaths,
ids:path,
pathData
} as CASCADER_PATH_MENU_TYPE;
}
return null
}
@@ -0,0 +1,754 @@
<script lang="ts">
import { PropType, toRaw } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CASCADER_ITEM_INFO,CASCADER_TREE_ITEM } from "../../interface.uts"
import { isParent, getTreeNodesPath, getTreeSelectedNum } from "./util.uts"
/**
* @name 极联器 xCascader
* @page /pages/index/cascader
* @category 表单组件
* @description 极联选择器,单选模式。
* @constant 平台兼容
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
*/
export default {
data() {
return {
//当前的层级
activeId: "",
selectedsIds: [] as string[],
//当前的层级索引
selectedsIdsItem: [] as CASCADER_TREE_ITEM[],
menuBarList: [] as CASCADER_TREE_ITEM[],
activeIndex: [] as number[],
changshowpenl: "0",
showCchangshowpenl: true,
swiperIndex: 0,
_nowListSwiper:[] as CASCADER_TREE_ITEM[][],
isAndriod:(uni.getSystemInfoSync().platform == 'android') as boolean,
// 添加切换状态管理,防止闪烁
isTransitioning: false,
nextSwiperIndex: 0,
tid:20
}
},
emits: [
/**
* 选中触发时变化,只要路径变化了就会触发
* @param {String[]} ids - 当前id路径值
*/
'change',
/**
* 点击项目时触发
* @param {CASCADER_TREE_ITEM} item - 项目数据
* @param {number} parentIndex - 父index
* @param {number} childrenIndex - 当前子index
*/
'cellClick',
/**
* 最后一项时触发,或者选择本级时触发
* @param {String} id - 最后一级选中的值
* @param {String[]} ids - 完整的路径id值
*/
'confirm',
/**
* 等同v-model,或者选择本级时触发
* @param {String} id - 当前id
*/
'update:modelValue'
],
props: {
/**
* 宽,不可为auto。
*/
width: {
type: String,
default: "100%"
},
/**
* 高是必填,不可为auto。
*/
height: {
type: String,
default: "150"
},
/**
* 选项项目未选中的文字颜色
*/
itemTextColor: {
type: String,
default: "#333333"
},
/**
* 选项项目未选中的暗黑文字颜色,空值是取白色
*/
darkItemTextColor: {
type: String,
default: ""
},
/**
* 选项项目选中的文字颜色,空值取全局主题
*/
itemActiveColor: {
type: String,
default: ""
},
/**
* 内容区域背景颜色
*/
sliderContentBgColor: {
type: String,
default: "transparent"
},
/**
* 提供的数据结构
*/
list: {
type: Array as PropType<CASCADER_ITEM_INFO[]>,
default: () : CASCADER_ITEM_INFO[] => [] as CASCADER_ITEM_INFO[]
},
/**
* 当前选中项的id
*/
modelValue: {
type: String,
default: ""
},
/**
* 每级是否允许多选
* 暂不开放,如需多选请参考组件slider-tree。
*/
multiple: {
type: Boolean,
default: false
},
/**
* 项目文字大小
*/
fontSize: {
type: String,
default: "18"
},
/**
* 是否在有下级的项目上显示选择本级按钮.
* 当用户选中了本级时就同选择最后一项一样会触发confirm及同步vmodel值
*/
showCurrentBtn: {
type: Boolean,
default: false
},
},
computed: {
_showCurrentBtn() : boolean {
return this.showCurrentBtn
},
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit);
},
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit);
},
_fontSize() : string {
let fontSize = checkIsCssUnit(this.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
},
_itemTextColor() : string {
let color = this.itemTextColor;
if (xConfig.dark == 'dark') {
color = this.darkItemTextColor != "" ? this.darkItemTextColor : "#ffffff"
}
return getDefaultColor(color);
},
_itemActiveColor() : string {
return this.itemActiveColor != "" ? getDefaultColor(this.itemActiveColor) : getDefaultColor(xConfig.color);
},
_sliderContentBgColor() : string {
return getDefaultColor(this.sliderContentBgColor);
},
_multiple() : boolean {
return this.multiple
},
_list() : CASCADER_TREE_ITEM[] {
let list = this.list as CASCADER_ITEM_INFO[];
function addOptionalFieldsToTree(tree : CASCADER_ITEM_INFO[]) : void {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
node.disabled = node.disabled == null ? false : node.disabled! as boolean;
node.selected = node.selected == null ? [] : node.selected! as string[];
node.children = node.children == null ? ([] as CASCADER_ITEM_INFO[]) : node.children! as CASCADER_ITEM_INFO[];
if ((node.children!).length > 0) {
addOptionalFieldsToTree(node.children! as CASCADER_ITEM_INFO[]);
}
}
}
function addOptionalFieldsToTreeClolone(tree : CASCADER_ITEM_INFO[]) : CASCADER_TREE_ITEM[] {
let nowlist = [] as CASCADER_TREE_ITEM[]
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
node.disabled = node.disabled == null ? false : node.disabled! as boolean;
node.selected = node.selected == null ? [] : node.selected! as string[];
node.children = node.children == null ? ([] as CASCADER_ITEM_INFO[]) : node.children! as CASCADER_ITEM_INFO[];
let item = {
id: node.id,
title: node.title,
disabled: node.disabled!,
selected: node.selected!,
children: [] as CASCADER_TREE_ITEM[],
checked: false
} as CASCADER_TREE_ITEM
if ((node.children!).length > 0) {
item.children = addOptionalFieldsToTreeClolone(node.children! as CASCADER_ITEM_INFO[]);
}
nowlist.push(item)
}
return nowlist
}
addOptionalFieldsToTree(list as CASCADER_ITEM_INFO[]);
return addOptionalFieldsToTreeClolone(list)
},
_borderColor() : string {
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return '#f5f5f5'
},
_nextChildren() : CASCADER_TREE_ITEM[] {
if (this.menuBarList.length == 0) return this._list
let lastChildren = this.menuBarList[this.menuBarList.length -1]
if(lastChildren.children.length==0) return [] as CASCADER_TREE_ITEM[];
return lastChildren.children
},
},
mounted() {
this.oninit();
},
beforeUnmount() {
clearTimeout(this.tid)
},
watch: {
modelValue(newval : string) {
let nowid = ""
if (this.selectedsIds.length > 0) {
nowid = this.selectedsIds[this.selectedsIds.length - 1]
}
if (nowid == newval) return;
this.oninit();
}
},
methods: {
getFontSize(k:string):string{
return checkIsCssUnit(k,xConfig.unit)
},
getNowListSwiper(){
if (this.menuBarList.length == 0){
// 当菜单为空时,显示根级数据
this._nowListSwiper = [this._list]
return
}
let lastId = this.menuBarList[this.menuBarList.length -1].id;
let temlist = [] as CASCADER_TREE_ITEM[][]
try {
temlist = this.findIdToMenuAry(lastId,this._list)
} catch (error) {
//TODO handle the exception
console.error(error)
// 出错时回退到根级数据
temlist = [this._list]
}
this._nowListSwiper = temlist
},
// 根据id返回第一级到当前id级的数据列表
findIdToMenuAry(id: string | number, trees: CASCADER_TREE_ITEM[]): CASCADER_TREE_ITEM[][] {
// 存储最终结果:每个层级的所有节点(带checked状态)
const result: CASCADER_TREE_ITEM[][] = [];
// 存储从根到目标节点的路径
const selectedPath: CASCADER_TREE_ITEM[] = [];
// 标记是否找到目标节点
let found = false;
// 递归查找目标节点并构建路径
function findPath(node: CASCADER_TREE_ITEM, path: CASCADER_TREE_ITEM[] = []): boolean {
// 当前节点加入临时路径
const currentPath = [...path, node] as CASCADER_TREE_ITEM[];
// 找到目标节点
if (node.id == id) {
// 复制路径到selectedPath
selectedPath.push(...currentPath);
found = true;
return true;
}
// 递归搜索子节点
if (node.children.length>0) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (findPath(child, currentPath)) {
return true;
}
}
}
return false;
}
for (let i = 0; i < trees.length; i++) {
const tree = trees[i]
// 直接检查第一级节点
if (tree.id == id) {
selectedPath.push(tree);
found = true;
break;
}
// 如果第一级不匹配,继续递归查找
else if (findPath(tree)) {
break;
}
}
// 如果找到目标节点,构建结果数组
if (found) {
// 构建每一层级的节点列表
let currentLevel = trees;
let currentParent = null as null|CASCADER_TREE_ITEM;
// 遍历路径中的每个节点
for (let i = 0; i < selectedPath.length; i++) {
const pathNode = selectedPath[i];
// 为当前层级的所有节点添加checked状态
const levelWithChecked = currentLevel
// 添加到结果数组
result.push(levelWithChecked);
// 更新下一层级的父节点和子节点列表
if (i < selectedPath.length - 1) {
currentParent = currentLevel.find((node:CASCADER_TREE_ITEM):boolean => node.id == pathNode.id);
currentLevel = currentParent?.children??[]
}
}
return result;
}
return [trees];
},
//初始默认选中一个id值。如果为空的话。
oninit() {
const trees = toRaw(this._list) as CASCADER_TREE_ITEM[]
let rulst = getTreeNodesPath(trees, this.modelValue)
if (rulst != null) {
let indexpath = rulst.indexPath!
this.menuBarList = rulst.pathData
this.selectedsIds = rulst.ids
this.activeIndex = indexpath
this.changshowpenl = getUid() as string;
this.swiperIndex = this.menuBarList.length - 1
}
this.getNowListSwiper()
},
change(isCurrent:boolean) {
let idis = this.selectedsIds.slice(0);
let empty = ""
// 单项模式下,父节点不更新对外更新值,只取最后一个确定的值。
if (idis.length > 0) {
empty = idis[idis.length - 1]
}
if (empty != "" && (this._nextChildren.length==0||isCurrent)) {
/**
* 更新当前的值,等同v-model
*/
this.$emit('update:modelValue', empty)
/**
* 最后一项时触发.
*/
this.$emit('confirm',empty,idis)
}
this.$emit('change', idis)
},
swiperChange(detail:UniSwiperChangeEvent){
this.swiperIndex = detail.detail.current;
},
/**
* isCurrent:是否选中本级,不进行下级跳转
*/
nextOnClick(item : CASCADER_TREE_ITEM, parentIndex : number,childrenIndex:number, isNext : boolean,isCurrent:boolean) {
if (item.disabled || this.isTransitioning) return;
// 设置过渡状态,防止快速点击造成的闪烁
this.isTransitioning = true;
if(isNext){
this.menuBarList.push(item)
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
// 先更新数据,再切换swiper
this.getNowListSwiper()
if(item.children.length>0 && !isCurrent){
// 使用nextTick确保数据更新完成后再切换
this.$nextTick(() => {
this.swiperIndex += 1
// 延迟重置过渡状态
const _this =this;
this.tid = setTimeout(() => {
_this.isTransitioning = false
}, 200)
})
}else{
if(!isCurrent){
// #ifdef APP-IOS
this.swiperIndex = parentIndex
// #endif
}
this.isTransitioning = false
}
this.$emit("cellClick",item,parentIndex-1,childrenIndex)
this.change(isCurrent)
return;
}
this.menuBarList = this.menuBarList.slice(0,this.swiperIndex+1)
if(item.children.length>0){
if(this.swiperIndex == this.menuBarList.length-1){
this.menuBarList.splice(this.menuBarList.length-1,1,item)
}else{
this.menuBarList.push(item)
}
// 先更新数据
this.getNowListSwiper()
if(!isCurrent){
// 使用nextTick确保数据更新完成
this.$nextTick(() => {
this.swiperIndex += 1
const _this =this;
this.tid = setTimeout(() => {
_this.isTransitioning = false
}, 200)
})
} else {
this.isTransitioning = false
}
}else{
if(parentIndex>this.menuBarList.length-1){
this.menuBarList.push(item)
}else{
this.menuBarList.splice(this.menuBarList.length-1,1,item)
}
this.isTransitioning = false
}
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
this.$emit("cellClick",item,parentIndex,childrenIndex)
this.change(isCurrent)
},
menuBarClick(index : number) {
if(index==-1 || this.isTransitioning) return;
// 设置过渡状态
this.isTransitioning = true;
if(index==0){
this.menuBarList = [] as CASCADER_TREE_ITEM[]
}else{
this.menuBarList = this.menuBarList.slice(0,index)
}
// 先更新数据,再切换swiper
this.getNowListSwiper()
// 使用nextTick确保数据更新完成
this.$nextTick(() => {
this.swiperIndex = index
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
this.change(false)
// 延迟重置过渡状态
const _this =this;
this.tid = setTimeout(() => {
_this.isTransitioning = false
}, 200)
})
},
elitext(text : string) : string {
let len = text.length;
return len <= 7 ? text : (text.substring(0, 7) + '..')
},
/**
* 当前是否选中
*/
isSelected(item : CASCADER_TREE_ITEM) : boolean {
return (this.selectedsIds.includes(item.id)) && item.children.length == 0
},
/**
* 本下级选了几个
*/
isSelectedNum(item : CASCADER_TREE_ITEM) : number {
let ps = new Set(this.selectedsIds)
return getTreeSelectedNum(item.children, ps);
},
},
}
</script>
<template>
<view class="xCascaderTree" :style="{width:_width,minHeight:`calc(${_height} + 50px)`}">
<scroll-view direction="horizontal" class="xCascaderTreeBar">
<!--
@slot 顶部头菜单导航插槽,你可以完全写自己的导航样式
@prop {CASCADER_TREE_ITEM[]} menus - 菜单导航,注意,可能为空
-->
<slot name="header" :menus="menuBarList" >
<view @click="menuBarClick(index)" v-if="menuBarList.length>0" class="xCascaderBarTreeItem"
v-for="(item,index) in menuBarList" :key="index">
<text class="xCascaderTreeItemBarText"
:style="{
color:_itemActiveColor,
fontSize:_fontSize,
whiteSpace:'nowrap',
border:`1px solid ${_itemActiveColor}`
}
">
{{elitext(item.title)}}
</text>
<x-icon
v-if="index<menuBarList.length-1||(index==0&&_nextChildren.length>0)||_nextChildren.length>0"
class="xCascaderBarTreeItemRight" :font-size="_fontSize"
name="arrow-right-s-line" :color="_itemActiveColor"></x-icon>
</view>
<view v-if="_nextChildren.length>0||(menuBarList.length==0&&_list.length>0)" @click="menuBarClick(-1)" class="xCascaderBarTreeItem">
<text :style="{color:_itemTextColor,fontSize:_fontSize,whiteSpace:'nowrap'}">
<!-- 请选择 -->
{{i18n!.t('tmui4x.cascader.placeholder')}}
</text>
<x-icon class="xCascaderBarTreeItemRight" font-size="14" name="arrow-right-s-line"
:color="_itemActiveColor"></x-icon>
</view>
</slot>
</scroll-view>
<view :style="{height:'1px',borderBottom:`1px solid ${_borderColor}`}"></view>
<swiper :disable-touch="true" :duration="200" @change="swiperChange" :style="{height:_height}" :current="swiperIndex">
<swiper-item v-for="(children,childrenIndex) in _nowListSwiper" :key="childrenIndex" class="xCascaderTreeSwiperItem"
:style="{height:_height}">
<list-view style="width: 100%;height: 100%;" direction="vertical">
<list-item v-for="(item,index) in children" :key="index" :style="{height:getFontSize('50')}">
<view :hover-start-time="10" :hover-stay-time="100"
:hover-class="item.disabled?'':'xCascaderTreeItemHover'"
@click="nextOnClick(item,childrenIndex,index,false,false)" class="xCascaderTreeItemRight"
:style="{backgroundColor:_sliderContentBgColor,opacity:item.disabled?'0.5':1}">
<view style="flex: 1;">
<text class="xCascaderTreeItemRightText"
:style="{fontSize:_fontSize,color:isSelected(item)?_itemActiveColor : _itemTextColor}">
{{item.title}}
</text>
</view>
<view
style="display: flex;flex-direction: row;justify-content: flex-end;align-items: center;">
<text class="xCascaderTreeItemRightTextBtns" :style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
v-if="item.children.length>0&&_showCurrentBtn" @click.stop="nextOnClick(item,swiperIndex+1,index,false,true)">
<!-- 选择本级 -->
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
</text>
<text :style="{fontSize:_fontSize,color:_itemActiveColor,marginRight:'8px'}"
v-if="item.children.length>0&&isSelectedNum(item)>0">已选({{isSelectedNum(item)}})</text>
<x-icon v-if="isSelected(item)" :color="_itemActiveColor"
name="check-line"></x-icon>
<x-icon v-if="item.children.length>0" :color="_itemTextColor"
name="arrow-right-s-line"></x-icon>
</view>
</view>
</list-item>
</list-view>
</swiper-item>
<swiper-item v-if="_nextChildren.length>0||isAndriod" class="xCascaderTreeSwiperItem" :style="{height:_height}">
<list-view style="width: 100%;height: 100%;" direction="vertical">
<list-item v-for="(item,index) in _nextChildren" :key="index" :style="{height:getFontSize('50')}">
<view :hover-start-time="10" :hover-stay-time="100"
:hover-class="item.disabled?'':'xCascaderTreeItemHover'"
@click="nextOnClick(item,swiperIndex+1,index,true,false)" class="xCascaderTreeItemRight"
:style="{backgroundColor:_sliderContentBgColor,opacity:item.disabled?'0.5':1}">
<view style="flex: 1;">
<text class="xCascaderTreeItemRightText"
:style="{fontSize:_fontSize,color:isSelected(item)?_itemActiveColor : _itemTextColor}">
{{item.title}}
</text>
</view>
<view
style="display: flex;flex-direction: row;justify-content: flex-end;align-items: center;">
<text class="xCascaderTreeItemRightTextBtns" :style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
v-if="item.children.length>0&&_showCurrentBtn" @click.stop="nextOnClick(item,swiperIndex+1,index,true,true)">
<!-- 选择本级 -->
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
</text>
<text :style="{fontSize:_fontSize,color:_itemActiveColor,marginRight:'8px'}"
v-if="item.children.length>0&&isSelectedNum(item)>0">已选({{isSelectedNum(item)}})</text>
<x-icon v-if="isSelected(item)" :color="_itemActiveColor"
name="check-line"></x-icon>
<x-icon v-if="item.children.length>0" :color="_itemTextColor"
name="arrow-right-s-line"></x-icon>
</view>
</view>
</list-item>
</list-view>
</swiper-item>
</swiper>
</view>
</template>
<style scoped>
.xCascaderTreeItemRightTextBtns{
font-size: 12px;
border-radius: 3px;
/* #ifdef APP-ANDROID */
padding: 0px 8px;
line-height:1.5;
/* #endif */
/* #ifndef APP-ANDROID */
padding: 4px 8px;
line-height:1;
/* #endif */
}
.xCascaderTreeItemBarText {
text-overflow: ellipsis;
lines: 1;
border-radius: 3px;
/* #ifdef APP */
padding: 0px 8px;
line-height:1.5;
/* #endif */
/* #ifndef APP */
padding: 4px 8px;
line-height:1;
/* #endif */
/* #ifndef APP */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* 限定为3行 */
overflow: hidden;
text-overflow: ellipsis;
/* #endif */
}
.xCascaderTreeBar {
display: flex;
flex-direction: row;
height: 50px;
}
.xCascaderBarTreeItem {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 100%;
}
.xCascaderBarTreeItemRight {
margin: 0px 0px;
}
.xCascaderTreeSwiperItem {
/* #ifdef WEB */
cursor: default;
/* #endif */
}
.xCascaderTreeItemHover {
background-color: rgba(155, 155, 155, 0.1);
}
.xCascaderTreeItemMoreHeader {
padding: 12px;
flex-direction: row;
align-items: center;
}
.xCascaderTreeItemMore {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
z-index: 10;
}
.xCascaderTreeItem {
width: 100%;
height: 100%;
}
.xCascaderTreeItemRight {
/* padding:0 24rpx; */
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.xCascaderTreeItemRightText {
text-align: left;
lines: 1;
text-overflow: ellipsis;
/* #ifndef APP */
white-space: nowrap;
/* 禁止换行 */
overflow: hidden;
width: 100%;
/* #endif */
}
</style>
@@ -0,0 +1,525 @@
<script lang="ts" setup>
import { PropType, toRaw } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CASCADER_ITEM_INFO, CASCADER_TREE_ITEM } from "../../interface.uts"
import { isParent, getTreeNodesPath, getTreeSelectedNum } from "./util.uts"
type findNodePathType = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentPath : CASCADER_ITEM_INFO[]) => CASCADER_ITEM_INFO[]|null;
type findNodeLayersType = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentLayers : CASCADER_ITEM_INFO[][]) => CASCADER_ITEM_INFO[][]|null
/**
* @name 极联器 xCascader
* @page /pages/index/cascader
* @category 表单组件
* @description 极联选择器,单选模式。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xCascader" })
type menuTypeCascaderType = {
selected : boolean,
item : CASCADER_ITEM_INFO
}
type xCascaderTreeProps = {
/**
* 宽,可以为auto
*/
width : string,
/**
* 高,不可为auto。
*/
height : string,
/**
* 数据结构
*/
list : CASCADER_ITEM_INFO[],
/**
* 当前选中项的id
*/
modelValue : string,
/**
* 项目文字大小id
*/
fontSize : string,
/**
* 选项项目未选中的文字颜色
*/
itemTextColor : string,
/**
* 选项项目未选中的暗黑文字颜色,空值是取白色
*/
darkItemTextColor : string,
/**
* 选项项目选中的文字颜色,空值取全局主题
*/
itemActiveColor : string,
/**
* 内容区域背景颜色
*/
sliderContentBgColor : string,
/**
* 是否在有下级的项目上显示选择本级按钮.
* 当用户选中了本级时就同选择最后一项一样会触发confirm及同步vmodel值
*/
showCurrentBtn : boolean,
}
const props = withDefaults(defineProps<xCascaderTreeProps>(), {
width: 'auto',
height: '150',
list:():CASCADER_ITEM_INFO[] => [] as CASCADER_ITEM_INFO[],
fontSize: "18",
itemTextColor: "#333333",
darkItemTextColor: "",
itemActiveColor: "",
sliderContentBgColor: 'rgba(0,0,0,0)'
})
const emit = defineEmits([
/**
* 选中触发时变化,只要路径变化了就会触发
* @param {String[]} ids - 当前id路径值
*/
'change',
/**
* 点击项目时触发
* @param {CASCADER_TREE_ITEM} item - 项目数据
* @param {number} parentIndex - 父index
* @param {number} childrenIndex - 当前子index
*/
'cellClick',
/**
* 最后一项时触发,或者选择本级时触发
* @param {String} id - 最后一级选中的值
* @param {String[]} ids - 完整的路径id值
*/
'confirm',
/**
* 等同v-model,或者选择本级时触发
* @param {String} id - 当前id
*/
'update:modelValue'
])
const _width = computed(() : string => checkIsCssUnit(props.width, xConfig.unit));
const _height = computed(() : string => checkIsCssUnit(props.height, xConfig.unit));
const _showCurrentBtn = computed(() : boolean => props.showCurrentBtn);
const _fontSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
});
const _itemActiveColor = computed(() : string => {
return props.itemActiveColor != "" ? getDefaultColor(props.itemActiveColor) : getDefaultColor(xConfig.color);
});
const _itemTextColor = computed(() : string => {
let color = props.itemTextColor;
if (xConfig.dark == 'dark') {
color = props.darkItemTextColor != "" ? props.darkItemTextColor : "#ffffff"
}
return getDefaultColor(color);
});
const _sliderContentBgColor = computed(() : string => getDefaultColor(props.sliderContentBgColor));
const _borderColor = computed(() : string => {
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return '#f5f5f5'
});
const nowVal = ref('')
function getNodeArrayPaths(list : CASCADER_ITEM_INFO[], id : string = '') : menuTypeCascaderType[] {
if (list.length == 0) {
return [] as menuTypeCascaderType[]
}
// 如果id为空,从第一级第一个节点开始
if (id == '') {
const firstNode = list[0]
const path : CASCADER_ITEM_INFO[] = [firstNode]
// 如果第一个节点有子级,继续向下找到最后一级
let currentNode = firstNode
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
while (children.length > 0) {
currentNode = children[0]
path.push(currentNode)
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
}
// 转换为menuTypeCascaderType数组,所有节点都设为未选中
return path.map((item, index) : menuTypeCascaderType => ({
selected: false,
item: item
} as menuTypeCascaderType))
}
let findNodePath : findNodePathType| null = null;
// 根据id查找节点路径
findNodePath = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentPath : CASCADER_ITEM_INFO[]) : CASCADER_ITEM_INFO[] | null => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
const newPath : CASCADER_ITEM_INFO[] = [...currentPath, node]
if (node.id == targetId) {
// 找到目标节点,如果有子级则继续向下找到最后一级
let currentNode = node
let finalPath : CASCADER_ITEM_INFO[] = [...newPath]
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
while (children.length > 0) {
currentNode = children[0]
finalPath.push(currentNode)
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
}
return finalPath
}
// 在子节点中继续查找
let children = node?.children ?? ([] as CASCADER_ITEM_INFO[])
if (children.length > 0) {
let fph = findNodePath!;
const result = fph(children, targetId, newPath)
if (result != null) {
return result
}
}
}
return null
}
const result = findNodePath(list, id, [] as CASCADER_ITEM_INFO[])
if (result != null) {
// 转换为menuTypeCascaderType数组,提供的id及之前的节点设为selected,之后的为未选中
let targetIndex = -1
for (let i = 0; i < result.length; i++) {
if (result[i].id == id) {
targetIndex = i
break
}
}
return result.map((item, index) : menuTypeCascaderType => ({
selected: targetIndex >= 0 && index <= targetIndex,
item: item
} as menuTypeCascaderType))
}
return [] as menuTypeCascaderType[]
}
/**
* 同getNodeArrayPaths类似,但不同的是,它是返回 id所在层级的数组,形成一个平铺的列表组供用户选择当前组的某一项。
* 返回格式是CASCADER_ITEM_INFO[][]
*/
function getNodeArraySlier(list : CASCADER_ITEM_INFO[], id : string = '') : CASCADER_ITEM_INFO[][]{
if (list.length == 0) {
return [] as CASCADER_ITEM_INFO[][]
}
// 如果id为空,从第一级开始,继续向下找到最后一级
if (id == '') {
const layers : CASCADER_ITEM_INFO[][] = [list]
let currentNodes = list
while (currentNodes.length > 0 && (currentNodes[0]?.children??([] as CASCADER_ITEM_INFO[])).length > 0) {
currentNodes = (currentNodes[0]?.children??([] as CASCADER_ITEM_INFO[]))
layers.push(currentNodes)
}
return layers
}
let findNodeLayers : findNodeLayersType | null = null;
// 查找id所在的路径
findNodeLayers = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentLayers : CASCADER_ITEM_INFO[][]) : CASCADER_ITEM_INFO[][] | null => {
// 当前层级添加到结果中
const newLayers : CASCADER_ITEM_INFO[][] = [...currentLayers, nodes]
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.id == targetId) {
// 找到目标节点,继续向下找到最后一级
let finalLayers : CASCADER_ITEM_INFO[][] = [...newLayers]
let currentNode = node
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
while (children.length > 0) {
finalLayers.push(children)
currentNode = children[0]
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
}
return finalLayers
}
// 在子节点中继续查找
let children = node?.children ?? ([] as CASCADER_ITEM_INFO[])
if (children.length > 0) {
const result = findNodeLayers!(children, targetId, newLayers)
if (result != null) {
return result
}
}
}
return null
}
const result = findNodeLayers(list, id, [] as CASCADER_ITEM_INFO[][])
return result != null ? result : [list] as CASCADER_ITEM_INFO[][]
}
const currentIndex = ref(0)
const _list = computed(() : CASCADER_ITEM_INFO[][] => getNodeArraySlier(props.list, nowVal.value))
const _menus = computed(() : menuTypeCascaderType[] => getNodeArrayPaths(props.list, nowVal.value))
const _menusList = computed(() : CASCADER_ITEM_INFO[] => {
return _menus.value.map((el : menuTypeCascaderType) : CASCADER_ITEM_INFO => el.item)
})
const getIds = () : string[] => {
let ids : string[] = []
for (let i = 0; i < _menus.value.length; i++) {
let item = _menus.value[i]
if (!item.selected) {
break;
}
ids.push(item.item.id)
}
return ids;
}
const elitext = (text : string) : string => {
let len = text.length;
return len <= 7 ? text : (text.substring(0, 7) + '..')
}
const isSelected = (item : CASCADER_ITEM_INFO) : boolean => {
let ids = _menus.value.findIndex((el : menuTypeCascaderType) : boolean => el.item.id == item.id && el.selected)
return ids > -1
}
const isCurrentNext = computed(() : boolean => {
return _menus.value.some((el : menuTypeCascaderType) : boolean => el.selected == false)
})
const getNowvalIndex = () : number => {
let index = 0
for (let i = 0; i < _menus.value.length; i++) {
let item = _menus.value[i]
if (!item.selected) {
break;
}
index += 1;
}
index = Math.min(_menus.value.length - 1, index)
return index;
}
const menuBarClick = (item : menuTypeCascaderType, index : number) => {
let cindex = index
if (item.selected) {
if (index == 0) {
nowVal.value = '';
} else {
let cureentindex = index - 1;
cureentindex = Math.max(0, Math.min(cureentindex, _menus.value.length - 1))
nowVal.value = _menus.value[cureentindex].item.id
}
} else {
nowVal.value = item.item.id;
}
nextTick(() => {
currentIndex.value = getNowvalIndex();
emit('change', getIds())
emit('update:modelValue', nowVal.value)
})
}
const selectedCurrentChildren = (item : CASCADER_ITEM_INFO, index : number) => {
let disabled = item?.disabled ?? false
if (item.id == nowVal.value || disabled) {
return;
}
nowVal.value = item.id;
// currentIndex.value = index;
emit('change', getIds())
}
const nextCellClick = (item : CASCADER_ITEM_INFO, index : number, childrenIndex : number) => {
let disabled = item?.disabled ?? false
if (disabled) {
return;
}
if (nowVal.value != item.id) {
emit('change', getIds())
}
nowVal.value = item.id;
nextTick(() => {
currentIndex.value = getNowvalIndex();
emit('update:modelValue', nowVal.value)
emit('cellClick', item, index, childrenIndex)
if ((item?.children?.length ?? 0) == 0) {
emit('confirm', nowVal.value, getIds())
}
})
}
watch(() : any => props.modelValue, () => {
nowVal.value = props.modelValue;
nextTick(() => {
currentIndex.value = getNowvalIndex();
})
})
onMounted(() => {
nowVal.value = props.modelValue;
nextTick(() => {
currentIndex.value = getNowvalIndex();
})
})
</script>
<template>
<view class="xCascaderTree" :style="{width:_width,height:_height}">
<!-- 导航 -->
<scroll-view direction="horizontal" class="xCascaderTreeBar">
<!--
@slot 顶部头菜单导航插槽,你可以完全写自己的导航样式
@prop {CASCADER_ITEM_INFO[]} menus - 菜单导航,注意,可能为空
-->
<slot name="header" :menus="_menusList">
<view v-if="_menus.length>0" class="xCascaderBarTreeItem" v-for="(item,index) in _menus" :key="index">
<text @click="menuBarClick(item,index)" v-if="item.selected" class="xCascaderTreeItemBarText"
:style="{
color:_itemActiveColor,
fontSize:_fontSize,
whiteSpace:'nowrap',
border:`1px solid ${_itemActiveColor}`
}
">
{{elitext(item.item.title)}}
</text>
<x-icon v-if="(item.item?.children??[]).length>0&&item.selected" class="xCascaderBarTreeItemRight"
:font-size="_fontSize" name="arrow-right-s-line" :color="_itemActiveColor"></x-icon>
</view>
<view v-if="isCurrentNext" class="xCascaderBarTreeItem">
<text :style="{color:_itemTextColor,fontSize:_fontSize,whiteSpace:'nowrap'}">
<!-- 请选择 -->
{{i18n!.t('tmui4x.cascader.placeholder')}}
</text>
<x-icon class="xCascaderBarTreeItemRight" font-size="14" name="arrow-right-s-line"
:color="_itemActiveColor"></x-icon>
</view>
</slot>
</scroll-view>
<view :style="{height:'1px',borderBottom:`1px solid ${_borderColor}`}"></view>
<!-- 内容区域 -->
<view style="flex: 1;">
<swiper :duration="200" :current="currentIndex" class="xCascaderTreeSwiper" :disable-touch="true">
<swiper-item v-for="(item,index) in _list" :key="index" class="xCascaderTreeSwiperItem">
<list-view class="xCascaderScoll" direction="vertical">
<list-item v-for="(item2,index2) in item" :key="item2.id">
<view @click="nextCellClick(item2,index,index2)"
:class="[(item2?.disabled??false)?'xCascaderItemDisabled':'']" class="xCascaderItem">
<text class="xCascaderItemLeft" :style="{
fontSize:_fontSize,
color:isSelected(item2)?_itemActiveColor : _itemTextColor
}">{{item2.title}}{{item2.id}}</text>
<view class="xCascaderItemRight">
<text class="xCascaderTreeItemRightTextBtns"
:style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
v-if="(item2?.children??[]).length>0&&_showCurrentBtn"
@click.stop="selectedCurrentChildren(item2,index)">
<!-- 选择本级 -->
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
</text>
<x-icon v-if="(item2?.children??[]).length>0" :color="_itemTextColor"
name="arrow-right-s-line"></x-icon>
</view>
</view>
</list-item>
</list-view>
</swiper-item>
</swiper>
</view>
</view>
</template>
<style scoped>
.xCascaderItemDisabled {
opacity: 0.6;
}
.xCascaderTreeBar {
display: flex;
flex-direction: row;
position: relative;
height: 50px;
}
.xCascaderBarTreeItem {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 100%;
}
.xCascaderBarTreeItemRight {
margin: 0px 0px;
}
.xCascaderTreeItemBarText {
text-overflow: ellipsis;
lines: 1;
border-radius: 3px;
/* #ifdef APP */
padding: 0px 8px;
line-height: 1.5;
/* #endif */
/* #ifndef APP */
padding: 4px 8px;
line-height: 1;
/* #endif */
/* #ifndef APP */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/* 限定为3行 */
overflow: hidden;
text-overflow: ellipsis;
/* #endif */
}
.xCascaderItem {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 44px;
}
.xCascaderItemLeft {
lines: 1;
margin-right: 24px;
}
.xCascaderItemRight {
display: flex;
flex-direction: row;
}
.xCascaderTreeItemRightTextBtns {
font-size: 11px;
padding: 1px 3px;
border-radius: 3px;
}
.xCascaderTree {
display: flex;
flex-direction: column;
}
.xCascaderTreeSwiper,
.xCascaderTreeSwiperItem {
height: 100%;
}
.xCascaderScoll {
height: 100%;
}
</style>
@@ -0,0 +1,519 @@
<script lang="ts" setup>
import { computed, watch } from "vue"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit, fillArrayCssValue } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type xCellItemType = {
icon : string,
title : string,
desc : string,
label : string,
bottom : boolean,
link : boolean,
url : string,
iconColor : string,
labelColor : string,
card : boolean
}
/**
* @name 列表 xCell
* @page /pages/index/cell
* @category 展示组件
* @description card为true时,圆角可统一全局配置和动态全局配置,保持所有页面列表样式统一,免于一个一个配置。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xCell" })
defineSlots<{
avatar(props : { icon : string }) : any
default() : any
desc(props : { desc : string }) : any
label(props : { label : string }) : any
right() : any
}>()
const emits = defineEmits([
/**
* 项目点击
*/
'click',
/**
* 等同v-model:show
*/
'update:show'
])
type xCellPropsType = {
/**
* 左图标
*/
icon : string,
/**
* 左侧图标、头像圆角。默认为8
*/
avatarRound : string,
/**
* 背景的主题色
*/
color : string,
/**
* 暗黑背景的主题色,空值时取sheetDarkColor
*/
darkColor : string,
/**
* 图标色,空值时取全局主题值。
*/
iconColor : string,
/**
* 标题
*/
title : string,
/**
* 标题颜色
*/
titleColor : string,
/**
* 暗黑标题颜色,如果不填写取白
*/
darkTitleColor : string,
/**
* 标题大小
*/
titleSize : string,
/**
* 图标大小
*/
iconSize : string,
/**
* 右边文本
*/
label : string,
/**
* 右边文本颜色
*/
labelColor : string,
/**
* 右侧label文字大小
*/
labelSize : string,
/**
* 标题正文的简介文本
*/
desc : string,
/**
* 是否显示下边线
*/
showBottomBorder : boolean,
/**
* 是否让下边线显示居右,不贯穿到左边。
*/
bottomBorderInsert : boolean,
/**
* 下边线的颜色。如果你设定了的话。
* 暗黑的边颜色失效,采用你自定的颜色。
*/
bottomBorderColor : string,
/**
* 是否显示链接状态,有点按效果。包括出现右边跳转指示。
* 关闭的话,事件反应和跳转会更快。
* 如果true右侧箭头图标会显示
*/
link : boolean,
/**
* 右指示图标的颜色
*/
linkColor : string,
/**
* 右指示图标的暗黑颜色
*/
linkDarkColor : string,
/**
* 需要跳转的页面地址。
* 如果填写了右侧箭头图标会显示
* 跳转时如果失败会回退到switchTab跳转。
*/
url : string,
/**
* 是否是卡片模式
*/
card : boolean,
/**
* 卡片模式圆角,不填写采用全局的cardRadius属性值.
*/
round : string,
/**
* 左边图标区域宽和高的大小。
*/
leftSize : string,
/**
* 最小高度,主要是用来统一风格高度不至于让点击范围过小
* 如果你需要紧凑型可以设置为auto
*/
minHeight : string,
/**
* 是否禁用url跳转,当link为true或者url需要跳转时
* 如果禁用,点击时不会触发跳转。
*/
disabled : boolean,
/**
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
* 空数组时取全局值
*/
padding : string[],
/**
* margin 同sheet原理
* [x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
* 空数组时取全局值cellMargin
*/
margin : string[],
/**
* 右侧label宽,插槽时,这个属性不会生效
* 以你自己布局宽为准。
*/
rightWidth : string
}
const props = withDefaults(defineProps<xCellPropsType>(), {
icon: "",
avatarRound: "8",
color: 'white',
darkColor: '',
iconColor: "",
title: "标题",
titleColor: "black",
darkTitleColor: "white",
titleSize: "16",
iconSize: "24",
label: "",
labelColor: "#bfbfbf",
labelSize: "13",
desc: "",
showBottomBorder: true,
bottomBorderInsert: false,
bottomBorderColor: "",
link: true,
linkColor: '#bfbfbf',
linkDarkColor: '#bfbfbf',
url: "",
card: true,
round: "",
leftSize: '32',
minHeight: "55",
disabled: false,
padding: () : string[] => ['12', '0'] as string[],
margin: () : string[] => [] as string[],
rightWidth: "100"
})
// 计算属性
const _padding = computed(() : string => {
if (props.padding.length == 0) {
let par = fillArrayCssValue(xConfig.sheetPadding)
if (par.length == 0) return "0px 0px 0px 0px";
return par.join(" ")
}
let ar : string[] = fillArrayCssValue(props.padding as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _margin = computed(() : string => {
if (props.margin.length == 0) {
let par = fillArrayCssValue(xConfig.cellMargin)
if (par.length == 0) return "0px 0px 0px 0px";
return par.join(" ")
}
let ar : string[] = fillArrayCssValue(props.margin as string[])
if (ar.length == 0) return "0px 0px 0px 0px";
return ar.join(" ")
})
const _disabled = computed(() : boolean => {
return props.disabled;
})
const _color = computed(() : string => {
if (xConfig.dark == 'dark') {
if (props.darkColor != '') return getDefaultColor(props.darkColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(props.color)
})
const _titleColor = computed(() : string => {
if (xConfig.dark == 'dark') {
if (props.darkTitleColor != '') return getDefaultColor(props.darkTitleColor)
return '#ffffff'
}
return getDefaultColor(props.titleColor)
})
const _leftSize = computed(() : string => {
return checkIsCssUnit(props.leftSize, xConfig.unit);
})
const _rightWidth = computed(() : string => {
return checkIsCssUnit(props.rightWidth, xConfig.unit);
})
const _avatarRound = computed(() : string => {
return checkIsCssUnit(props.avatarRound, xConfig.unit);
})
const _minHeight = computed(() : string => {
return checkIsCssUnit(props.minHeight, xConfig.unit);
})
const _bottomBorderColor = computed(() : string => {
if (props.bottomBorderColor != "") return getDefaultColor(props.bottomBorderColor)
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return "#f5f5f5"
})
const _icon = computed(() : string => {
return props.icon
})
const _allAttr = computed(() : xCellItemType => {
let iconColor = props.iconColor;
if (iconColor == '') {
iconColor = xConfig.color;
}
let p = {
icon: props.icon,
title: props.title,
desc: props.desc,
label: props.label,
bottom: props.showBottomBorder,
link: props.link,
url: props.url,
iconColor: getDefaultColor(iconColor),
labelColor: getDefaultColor(props.labelColor),
card: props.card
} as xCellItemType
return p
})
const _cardRadius = computed(() : string => {
if (props.round == "") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
return checkIsCssUnit(xConfig.cellRadius, xConfig.unit)
})
const _titleSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.titleSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _iconSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.iconSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 17
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _rightLableSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.labelSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 13
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _isLinksHover = computed(() : boolean => {
return props.link
})
// 方法
function clickLisent() : void {
/**
* 整个列表被点击
*/
emits("click");
if (props.url != "" && !_disabled.value) {
uni.navigateTo({
url: props.url,
fail() {
uni.switchTab({
url: props.url,
})
}
})
}
}
</script>
<template>
<view @click="clickLisent" <!-- #ifndef APP-HARMONY -->
:hover-start-time="_isLinksHover?50:0"
:hover-stay-time="_isLinksHover?100:0"
:hover-class="_isLinksHover?'cellHover':''"
<!-- #endif -->
class="xCell "
:style="{
backgroundColor:_color,
borderRadius:_allAttr.card==true?_cardRadius:'0px',
minHeight:_minHeight,
padding:_padding,
margin:_allAttr.card?_margin:'0px',
borderBottom:_allAttr.bottom&& !_allAttr.card&&!bottomBorderInsert?`1px solid ${_bottomBorderColor}`:'none'
}">
<view v-if="_icon" class="xCellAvatar" :style="{width:_leftSize,height:_leftSize,borderRadius:_avatarRound}">
<!--
@slot 头像图标
@prop {string} icon - 图标名称
-->
<slot name="avatar" :icon="_icon">
<x-icon :color="_allAttr.iconColor" :font-size="_iconSize" :name="_icon"></x-icon>
</slot>
</view>
<view class="xCellWrap" :style="{
borderBottom:_allAttr.bottom&& !_allAttr.card&&bottomBorderInsert?`1px solid ${_bottomBorderColor}`:'none'
}">
<view class="center">
<!--
@slot 默认标题插槽
-->
<slot>
<text class="title" :style="{color:_titleColor,fontSize:_titleSize}">{{ _allAttr.title}}</text>
</slot>
<!--
@slot 简介
@prop {string} desc - 简介
-->
<slot name="desc" :desc="_allAttr.desc">
<x-text v-if="_allAttr.desc!=''" font-size="12" color='#bfbfbf' dark-color='#bfbfbf'
class="desc">{{_allAttr.desc}}</x-text>
</slot>
</view>
<view class="xcellRight">
<!--
@slot 右边文字
@prop {string} label - 标签内容
-->
<slot name="label" :label="_allAttr.label">
<text v-if="_allAttr.label!=''"
:style="{marginLeft:'16px',color:_allAttr.labelColor,fontSize:_rightLableSize,width:_rightWidth }"
class="rightLabel">{{_allAttr.label}}</text>
</slot>
<!--
@slot 右插槽
-->
<slot name="right"></slot>
<view v-if="_allAttr.url!=''||_allAttr.link" style="margin-left: 5px;">
<x-icon :dark-color="linkDarkColor" :color="linkColor" font-size="20"
name="arrow-right-s-line"></x-icon>
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.cellHover {
opacity: 0.9;
}
.xCell {
padding: 12px 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.cardInset {
// padding: 0 12px;
}
.cellCard {
// padding: 0 12px;
// margin-bottom: 6px;
// margin-left: 12px;
// margin-right: 12px;
}
.xCellWrap {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 100%;
padding: 12px 0px;
}
.title {
lines: 2;
text-overflow: ellipsis;
flex: 1;
flex-shrink: 0;
line-height: 1.7;
}
.desc {
font-size: 12px;
padding-top: 2px;
}
.xCellAvatar {
margin-right: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
overflow: hidden;
}
.center {
flex: 1;
}
.xcellRight {
// padding-left: 16px;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.rightLabel {
lines: 1;
text-overflow: ellipsis;
font-size: 12px;
text-align: right;
// 以下是sdk4.51+不支持
// max-width: 100px;
// width:100px;
// #ifndef APP
white-space: nowrap;
/* 防止文本换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 显示省略号 */
// #endif
}
</style>
@@ -0,0 +1,190 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, provide } from "vue"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHECKBOX_ITEM_INFO } from '../../interface.uts';
type XCHECKBOX_LISTITEM_TYPE = {
id : string,
data : CHECKBOX_ITEM_INFO
}
/**
* @name 多选框组 xCheckboxGroup
* @page /pages/index/checkbox-group
* @category 表单组件
* @description 使用时,从1.1.2开始允许是非直接x-checkbox子节点布局,但考虑到性能建议是直接子节点.
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCheckboxGroup"})
export type xCheckboxGroupPropsType = {
/**
* 当前选中的值。
*/
modelValue: Array<string|number|boolean>,
/**
* 对齐方式
*/
direction: "row" | "column",
/**
* 最大选择数量,-1表示不限制。
*/
max: number
}
const props = withDefaults(defineProps<xCheckboxGroupPropsType>(), {
modelValue: ():Array<string|number|boolean> => [] as Array<string|number|boolean>,
direction: "row",
max: -1
})
const emits = defineEmits([
/**
* 选项变化时触发。
* @param {Array<string|number|boolean>} val - 当前选中的值
*/
'change',
'update:modelValue'
])
// 响应式数据
const oldvalueList = ref<XCHECKBOX_LISTITEM_TYPE[]>([])
const checkvaluelist = ref<Array<string|number|boolean>>([])
const tid = ref(0)
const isDestroy = ref(false)
const id = ref("xCheckboxGroup-" + getUid())
// 计算属性
const oldvalueList_ids = computed((): string[] => {
return oldvalueList.value.map((el : XCHECKBOX_LISTITEM_TYPE) : string => el.id)
})
const _max = computed((): number => {
return props.max;
})
// 方法
//设置当前选中的值
function setOldCheckboxValue() {
oldvalueList.value.forEach((item : XCHECKBOX_LISTITEM_TYPE) => {
if (checkvaluelist.value.includes(item.data.value)) {
item.data.nowvalue = item.data.value
} else {
item.data.nowvalue = item.data.unvalue
}
})
}
//当前选中的值
function getcheckvaluelist() {
let fl = oldvalueList.value.filter((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.data.nowvalue == el.data.value)
let allfill = oldvalueList.value.map((el : XCHECKBOX_LISTITEM_TYPE) : string|number|boolean => el.data.value)
let realValue = fl.map((el : XCHECKBOX_LISTITEM_TYPE) : string|number|boolean => el.data.value);
// 删除已有的,保留差异的。
let psdiff = checkvaluelist.value.filter((el : string|number|boolean) : boolean => !allfill.includes(el))
checkvaluelist.value = realValue.concat(psdiff)
}
function addItem(item : CHECKBOX_ITEM_INFO, ischange : boolean) {
let index = oldvalueList.value.findIndex((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.id == item.id);
let nowitem = item
let fl = oldvalueList.value.filter((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.data.nowvalue == el.data.value)
if (!ischange) {
if (checkvaluelist.value.includes(item.value) && item.nowvalue != item.value) {
nowitem.nowvalue = nowitem.value;
}
}
if (index > -1) {
let oldItem = oldvalueList.value[index]
if (fl.length >= _max.value && _max.value > -1 && ischange && oldItem.data.nowvalue != oldItem.data.value) {
// "已是最大选择数量"
uni.showToast({ title: xConfig.i18n.t("tmui4x.checkbox.tips"), icon: 'none', mask: true })
return;
}
oldvalueList.value.splice(index, 1, {
id: nowitem.id,
data: nowitem
} as XCHECKBOX_LISTITEM_TYPE)
} else {
oldvalueList.value.push({
id: nowitem.id,
data: nowitem
} as XCHECKBOX_LISTITEM_TYPE)
}
getcheckvaluelist()
if (ischange) {
/**
* 等同v-model=""
*/
emits("update:modelValue", checkvaluelist.value)
/**
* 选项变化时触发。
* @param val {string[]} 当前选中的值
*/
emits("change", checkvaluelist.value)
}
}
// 监听器
watch((): Array<string|number|boolean> => props.modelValue, (newValue : Array<string|number|boolean>) => {
let n = newValue.join("")
let v = checkvaluelist.value.join("")
if (n != v) {
checkvaluelist.value = newValue
setOldCheckboxValue();
}
},{deep:true})
// 生命周期
onBeforeUnmount(() => {
isDestroy.value = true;
clearTimeout(tid.value)
})
onMounted(() => {
checkvaluelist.value = props.modelValue;
isDestroy.value = false;
})
// 提供响应式数据给子组件
provide('xCheckboxModelvalue', computed((): Array<string|number|boolean> => checkvaluelist.value))
defineExpose({
/** 添加多选项 **/
addItem,
/** 获取所有选中的值 **/
getAllSelecteds: () => checkvaluelist.value
})
</script>
<template>
<view class="xCheckboxGroup" :style="{'flex-direction':direction}">
<!--
@slot 从1.1.2开始允许是非直接x-checkbox子节点布局,但考虑到性能建议是直接子节点.
-->
<slot></slot>
</view>
</template>
<style>
.xCheckboxGroup {
display: flex;
flex-wrap: wrap;
}
</style>
@@ -0,0 +1,422 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, getCurrentInstance, inject } from "vue"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHECKBOX_ITEM_INFO } from '../../interface.uts';
/**
* @name 多选框 xCheckbox
* @page /pages/index/checkbox
* @category 表单组件
* @description 使用时,x-checkbox能单独使用,如果要与x-checkbox-group配合,只能是它的的直接子节点
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCheckbox"})
defineSlots<{
label(props:{
checked: boolean,
value: string|number|boolean
}):any
}>()
type findeParentCall = (parent:VueComponent|null)=>VueComponent|null
export type xCheckboxPropsType = {
/**
* 当前主题色,空值时取全局
*/
color: string,
/**
* 当前未选中时主题色,空值时取全局
*/
unCheckColor: string,
/**
* 当前未选中时的暗黑主题色
*/
darkUnCheckColor: string,
/**
* 当前选中的值,受控时为v-model="x"
*/
modelValue: string|number|boolean,
/**
* 非受控下默认选中的状态
*/
defaultChecked: boolean,
/**
* 选中的值
*/
value: string|number|boolean,
/**
* 未选中的值
*/
unCheckValue: string|number|boolean,
/**
* 是否禁用
*/
disabled: boolean,
/**
* 选中的图标名称。
*/
icon: string,
/**
* 右侧文字。
*/
label: string,
/**
* 是否隐藏选中框。然后利用默认插槽自定义选中所有样式和状态。
*/
hiddenCheckbox: boolean,
/**
* 半选中
*/
indeterminate: boolean,
/**
* 尺寸
*/
size: string,
/**
* 中间小图标大小
*/
iconSize: string,
/**
* 文字大小
*/
labelFontSize: string,
/**
* label和选中框间的间距
*/
labelSpace: string,
/**
* 圆角
*/
round: string
}
const props = withDefaults(defineProps<xCheckboxPropsType>(), {
color: "",
unCheckColor: "",
darkUnCheckColor: "",
modelValue: '',
defaultChecked: false,
value: '1',
unCheckValue: '',
disabled: false,
icon: "check-line",
label: "",
hiddenCheckbox: false,
indeterminate: false,
size: "24",
iconSize: "20",
labelFontSize: "15px",
labelSpace: '10',
round: '4'
})
const emits = defineEmits([
/**
* 用户交互切换,选中变换时触发。
* @param {Boolean} check - 当前是否选中
* @param {string|number|boolean} value - 当前选中的值
*/
'change',
/**
* 点击事件
*/
'click',
'update:modelValue'
])
const proxy = getCurrentInstance()?.proxy ?? null
const checkboxBoxIconRef = ref<UniElement|null>(null)
// 注入父组件的响应式数据
const groupModelValue = inject('xCheckboxModelvalue', computed(():Array<string|number|boolean>|null => null))
// 响应式数据
const nowValue = ref<string|number|boolean>('')
const boxId = ref("xCheckbox-" + getUid())
const tid = ref(0)
const isDestroy = ref(false)
const undefaultCheck = ref(false)
// 计算属性
const _color = computed((): string => {
if (props.color == "") return getDefaultColor(xConfig.color)
return getDefaultColor(props.color)
})
const _round = computed((): string => {
return checkIsCssUnit(props.round, xConfig.unit);
})
const _unCheckColor = computed((): string => {
if (xConfig.dark == 'dark' && props.darkUnCheckColor != '') {
return getDefaultColor(props.darkUnCheckColor)
}
if (props.unCheckColor == "") return getDefaultColor(xConfig.unRadioAndCheckBoxColor)
return getDefaultColor(props.unCheckColor)
})
const _isCheck = computed((): boolean => {
// 如果在组内,使用组的值判断
if (groupModelValue.value != null) {
return groupModelValue.value!.includes(props.value)
}
// 单独使用时,使用自己的值判断
return nowValue.value == props.value || undefaultCheck.value || props.indeterminate
})
const _disabled = computed((): boolean => {
return props.disabled
})
const _label = computed((): string => {
return props.label
})
const _indeterminate = computed((): boolean => {
return props.indeterminate
})
const _size = computed((): string => {
let size = checkIsCssUnit(props.size, xConfig.unit);
if (xConfig.fontScale == 1) return size;
let sizeNumber = parseInt(size)
if (isNaN(sizeNumber)) {
sizeNumber = 24
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(size)
})
const _labelSpace = computed((): string => {
return checkIsCssUnit(props.labelSpace, xConfig.unit)
})
// 方法
let findParent:findeParentCall|null = null;
findParent = (parent:VueComponent|null):VueComponent|null => {
if(parent == null) return null;
// #ifdef WEB||APP-IOS||MP-WEIXIN
if((parent.$parent?.$options?.name?.indexOf('xCheckboxGroup')??-1)>-1) return parent.$parent;
// #endif
// #ifdef APP-HARMONY
if(parent.$parent?.$options?.name?.indexOf('xCheckboxGroup')>-1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
if(parent.$parent instanceof XCheckboxGroupComponentPublicInstance) return parent.$parent;
// #endif
let findParentCallReal = findParent!
let parents = findParentCallReal(parent.$parent)
// #ifdef WEB||APP-IOS||MP-WEIXIN
if((parents?.$options?.name?.indexOf('xCheckboxGroup')??-1)>-1) return parents;
// #endif
// #ifdef APP-HARMONY
if(parents?.$options?.name?.indexOf('xCheckboxGroup')>-1) return parents;
// #endif
// #ifdef APP-ANDROID
if(parents instanceof XCheckboxGroupComponentPublicInstance) return parents;
// #endif
return null;
}
function setAni() {
if (props.hiddenCheckbox || isDestroy.value) return;
try {
let el = checkboxBoxIconRef.value!;
el.style.setProperty('opacity', _isCheck.value ? 1 : 0)
el.style.setProperty('transform', `scale(${_isCheck.value ? 0.74 : 0})`)
} catch (e) {
//TODO handle the exception
}
}
function pushDataToParent(isChange : boolean) {
let pelement = findParent!(proxy);
if (pelement == null) return;
let parent : XCheckboxGroupComponentPublicInstance = pelement as XCheckboxGroupComponentPublicInstance;
// #ifndef APP-ANDROID
if (typeof parent?.addItem != 'function') return;
// #endif
parent.addItem({
id: boxId.value as string,
nowvalue: nowValue.value,
value: props.value,
unvalue: props.unCheckValue
} as CHECKBOX_ITEM_INFO, isChange)
}
function boxClick() {
/**
* 点击事件
*/
emits("click")
if (_disabled.value) return;
/**
* 要点提示:当本组件属性半选中状态时,应该继续保持选中状态,
* 并触发change。
*/
if ((_isCheck.value && !props.indeterminate) || undefaultCheck.value) {
nowValue.value = props.unCheckValue
} else {
nowValue.value = props.value
}
/**
* 当前选中的值,等同v-model
*/
emits("update:modelValue", nowValue.value)
/**
* 用户交互切换,选中变换时触发。
* @param check {boolean} 当前是否选中
* @param value {string|number|boolean} 当前选中的值
*/
emits("change", _isCheck.value, nowValue.value)
if(undefaultCheck.value){
undefaultCheck.value = false
}else{
pushDataToParent(true);
}
setAni();
}
/**
* 手动切换选中状态,这里不会触发change
*/
function setSelected(val : Array<string|number|boolean>) {
if (!Array.isArray(val)) {
throw new Error("val must be an array");
}
const isChecked = val.includes(props.value);
if (isChecked) {
nowValue.value = props.value;
// emits("update:modelValue", nowValue.value);
} else {
nowValue.value = props.unCheckValue;
// emits("update:modelValue", nowValue.value);
}
setAni()
}
// 监听器
watch((): string|number|boolean => props.modelValue, (newValue : string|number|boolean) => {
if (newValue == nowValue.value) return;
nowValue.value = newValue;
setAni();
// pushDataToParent();
})
watch((): boolean => props.indeterminate, (newValue : boolean) => {
setAni();
})
// 监听组内值变化
if (groupModelValue.value != null) {
watch(groupModelValue, (val:Array<string|number|boolean>|null) => {
if(val!=null){
setSelected(val!);
}
})
}
// 生命周期
onBeforeUnmount(() => {
isDestroy.value = true;
})
onMounted(() => {
isDestroy.value = false;
// 如果在group中,默认选中状态将失效.否则数据异常错乱.
let pelement = findParent!(proxy);
if(pelement!=null){
undefaultCheck.value = false
}else{
undefaultCheck.value = props.defaultChecked
}
nextTick(() => {
nowValue.value = props.modelValue;
setAni();
pushDataToParent(false);
})
})
</script>
<template>
<view :class="[_disabled?'checkboxDisabled':'']" class="checkbox" @click="boxClick">
<view class="checkboxBox" v-if="!hiddenCheckbox" :style="{
backgroundColor:_isCheck?_color:'transparent',
border: `1px solid ${_isCheck?_color:_unCheckColor}`,
borderRadius:_round,
width:_size,
height:_size
}">
<view :id="boxId" ref="checkboxBoxIconRef" class="checkboxBoxIcon">
<x-icon color="white" :name="_indeterminate?'subtract-line':icon" :font-size="iconSize"></x-icon>
</view>
</view>
<view class="checkboxLabelBox" :style="{paddingLeft:!hiddenCheckbox?_labelSpace:'0px'}">
<!--
@slot 默认文本插槽
@prop {boolean} checked - 是否选中
@prop {number} value - 当前选中的值
-->
<slot name="label" :checked="_isCheck" :value="nowValue">
<x-text :font-size="labelFontSize" class="checkboxLabel">{{_label}}</x-text>
</slot>
</view>
</view>
</template>
<style scoped>
.checkbox {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.checkboxLabelBox {
flex: 1;
}
.checkboxDisabled {
opacity: 0.7;
}
.checkboxBoxIcon {
transition-duration: 350ms;
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
transition-property: opacity, transform;
opacity: 0;
transform: scale(0);
}
.checkboxBox {
/* border-radius: 4px; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.checkboxLabelBoxLeftSpace {
padding-left: 10px;
}
.checkboxLabel {
font-size: 14px;
}
</style>
@@ -0,0 +1,394 @@
<script lang="ts">
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit,getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type OPTIONS = {
start : number,
end : number,
duration : number,
run : (current : number) => void,
complete : () => void
}
type ANIMATECALLBACK = {
stop : () => void
}
/**
* @name 圆形进度环 xCircleProgress
* @page /pages/index/circle-progress
* @category 展示组件
* @description 样式灵活多变。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: 'xCircleProgeress-' + getUid(),
nowValue: 0,
tid: 0,
oladValue: 0,
nowback: {
stop: () => { },
} as ANIMATECALLBACK
}
},
props: {
/**
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
*/
size: {
type: String,
default: "60"
},
/**
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
*/
lineWidth: {
type: String,
default: "3"
},
/**
* 圆环背景颜色,暗黑时取inputDarkColor
*/
color: {
type: String,
default: "info"
},
/**
* 当前激活的进度颜色,空值读取全局值。
*/
activeColor: {
type: String,
default: ""
},
/**
* 当前的值,以百分比为值0-100
* 等同v-model=""
* 您直接:model-value="xx"也是一样可以改变值。
*/
modelValue: {
type: Number,
default: 30
},
/**
* 中间文本字号
*/
labelFontSize: {
type: String,
default: "16"
},
/**
* 数字的单位。
*/
labelUnit: {
type: String,
default: "%"
},
/**
* 是否显示中间文本。
*/
showLabel: {
type: Boolean,
default: false
},
/**
* 中间文本的颜色。
*/
labelColor: {
type: String,
default: "#333333"
},
/**
* 中间文本的暗黑颜色。空值是取白色
*/
darkLabelColor: {
type: String,
default: ""
},
/**
* 进度条动画的时间,单位ms。
* 请不要设置的过慢,否则会有停顿感。
*/
duration: {
type: Number,
default: 300
},
},
computed: {
_labelFontSize() : string {
let fontSize = checkIsCssUnit(this.labelFontSize, xConfig.unit);
if(xConfig.fontScale==1) return fontSize;
let sizeNumber = parseInt(fontSize)
if(isNaN(sizeNumber)){
sizeNumber = 14
}
return (sizeNumber*xConfig.fontScale).toString() + getUnit(fontSize)
},
_labelUnit() : string {
return this.labelUnit
},
_labelColor() : string {
if(xConfig.dark=='dark'){
if(this.darkLabelColor!='') return getDefaultColor(this.darkLabelColor)
return "#ffffff"
}
return getDefaultColor(this.labelColor)
},
_showLabel() : boolean {
return this.showLabel
},
_width() : number {
let p = parseInt(this.size);
if (this.size.lastIndexOf('rpx') > -1) {
p = rpx2px(p);
}
return p
},
_lineWidth() : number {
let p = parseInt(this.lineWidth);
if (this.lineWidth.lastIndexOf('rpx') > -1) {
p = rpx2px(p);
}
return Math.floor(p)
},
_color() : string {
if(xConfig.dark=='dark') return getDefaultColor(xConfig.inputDarkColor)
return getDefaultColor(this.color)
},
_activeColor() : string {
if (this.activeColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(this.activeColor)
},
},
watch: {
modelValue(newval : number) {
if (newval != this.nowValue) {
this.oladValue = this.nowValue
this.nowValue = newval
this.play();
}
}
},
mounted() {
this.nowValue = this.modelValue
this.play();
},
beforeUnmount() {
clearTimeout(this.tid)
},
methods: {
play() {
this.nowback.stop()
clearTimeout(this.tid)
this.clear();
let t = this;
this.nowback = this.animate({
start: 0,
end: 100,
duration: t.duration,
complete() {
},
run(cureent : number) {
let angle = Math.PI * 2;
if (t.nowValue >= t.oladValue) {
let startAngle = (t.oladValue / 100) * angle
let endAngle = ((cureent / 100) * (t.nowValue - t.oladValue) / 100) * angle
endAngle = Math.max(endAngle, 0)
t.dreawer(startAngle, endAngle + startAngle)
} else {
let startAngle = (t.nowValue / 100) * angle
let endAngle = ((cureent / 100) * (t.oladValue - t.nowValue) / 100) * angle
let diff = (t.oladValue - t.nowValue) / 100 * angle
t.dreawer(startAngle, diff + startAngle - endAngle)
}
}
} as OPTIONS)
},
animate(options : OPTIONS) : ANIMATECALLBACK {
let tid = this.tid;
const start = options.start; // 初始值,默认为0
const end = options.end; // 目标值,默认为100
const duration = options.duration; // 动画时长,默认为1000毫秒
let current = 0; // 当前值
let startTime = 0; // 动画开始时间
let isRunning = true; // 动画运行状态
function run() {
if (startTime <= 0) {
startTime = Date.now(); // 记录动画开始时间
}
const progress = Math.min((Date.now() - startTime) / duration, 1); // 计算当前进度
current = start + (end - start) * progress; // 根据进度计算当前值
if (isRunning) {
options.run(current); // 执行传入的运行中状态回调函数
}
if (progress < 1 && isRunning) {
tid = setTimeout(() => {
run()
}, 16); // 递归调用自身,实现动画效果
} else {
options.complete(); // 动画结束时执行回调函数
}
}
run(); // 开始执行动画
return {
stop: () => {
// 设置动画运行状态为false
isRunning = false;
}
} as ANIMATECALLBACK;
},
async dreawer(start : number, end : number):Promise<any|null> {
this.clear()
let canvas = uni.getElementById(this.id as string) as UniElement
let ctx = null as DrawableContext | null
let ratio = 1;
// #ifdef APP
ctx = canvas.getDrawableContext()!
// #endif
// #ifdef WEB
let crect = canvas.getBoundingClientRect();
let dom = canvas as HTMLElement;
canvas = dom.querySelector('canvas')
ratio = window.devicePixelRatio;
let w = crect.width
let h = crect.height
canvas.width = w * ratio
canvas.height = h * ratio
ctx = canvas.getContext('2d')!
// #endif
// #ifdef MP-WEIXIN
let ctxnode = await uni.createCanvasContextAsync({
id: this.id,
component: this,
})
const canvasContext = ctxnode.getContext('2d')!;
canvas = canvasContext.canvas;
// 处理高清屏逻辑
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
ctx = canvasContext
// #endif
if (ctx == null) return Promise.resolve(null);
let cx = (this._width) / 2
let cy = cx
let cr = (this._width - this._lineWidth) / 2
ctx!.beginPath()
ctx!.strokeStyle = this._color
ctx!.lineWidth = this._lineWidth
ctx!.lineCap = "round"
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, Math.PI * 2)
ctx!.closePath()
ctx!.stroke()
// 绘制起始的角度。
ctx!.beginPath()
ctx!.strokeStyle = this._activeColor
ctx!.lineWidth = this._lineWidth
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, start)
ctx!.stroke()
// ctx.update()
ctx!.beginPath()
ctx!.strokeStyle = this._activeColor
ctx!.lineWidth = this._lineWidth
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, start, end)
ctx!.stroke()
// #ifdef APP
ctx!.update()
// #endif
return Promise.resolve(null)
},
clear(){
let canvas = uni.getElementById(this.id as string) as UniElement
let ctx = null as DrawableContext | null
let ratio = 1;
// #ifdef APP
ctx = canvas.getDrawableContext()!
// #endif
// #ifdef WEB
let dom = canvas as HTMLElement;
canvas = dom.querySelector('canvas')
ctx = canvas.getContext('2d')!
ratio = window.devicePixelRatio;
// #endif
if (canvas == null || ctx == null) return
// #ifndef APP
ctx!.reset()
// #endif
ctx!.fillStyle = 'rgba(0,0,0,0)'
ctx!.fillRect(0, 0, this._width * ratio, this._width * ratio)
ctx!.fill()
// #ifdef APP
ctx!.update()
// #endif
}
}
}
</script>
<template>
<view :style="{width:_width+'px',height:_width+'px',position: 'relative'}">
<!-- #ifdef APP -->
<view class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></view>
<!-- #endif -->
<!-- #ifdef WEB || MP -->
<canvas class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></canvas>
<!-- #endif -->
<view v-if="_showLabel" class="xCircleProgressLabel">
<!--
@slot 默认文本插槽
@prop {number} current - 当前的进度值。
-->
<slot current="nowValue">
<text :style="{
fontSize:_labelFontSize,
color:_labelColor
}">{{nowValue}}{{_labelUnit}}</text>
</slot>
</view>
</view>
</template>
<style scoped>
.xCircleProgress {
transform: rotate(-90deg);
}
.xCircleProgressLabel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,385 @@
<script lang="ts" setup>
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue"
import { PropType } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import xSkeleton from "../x-skeleton/x-skeleton.uvue"
/**
* @name 验证码输入框 xCodeInput
* @description 验证码输入框,截止4.22安卓会自动拉起系统键盘,ios无法使用系统键盘。目前仅配合我的组件键盘可以全局兼容。已向官方返回Input的bug
* @page /pages/index/code-input
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCodeInput"})
// emits
const emits = defineEmits([
/**
* 输入框点击时触发
*/
"click",
/**
* 自带键盘上确认或者达到指定长度位数时触发,可能会多次触发
* @param {String} value - 值
*/
"confirm",
/**
* 输入时触发
* @param {String} value - 值
*/
"change",
/**
* 等同vmodel,可与我的keyborad键盘配合使用。
*/
"update:modelValue"
])
// props
type xCodeInputPropsType = {
/**
* 进入时自动获取焦点,并弹出系统自带的键盘(需要useSysKeyborad=true)。
*/
autoFocus: boolean,
/**
* 是否使用系统自带的键盘。,如果为false你需要自行配置输入键盘
* 比如使用我的keyborad键盘组件。
*/
useSysKeyborad: boolean,
/**
* 当前输入的值
*/
modelValue: string,
/**
* 最大长度
*/
maxlength: number,
/**
* 间距
*/
gutter: string,
/**
* 验证码框的宽
*/
width: string,
/**
* 验证码框的高
*/
height: string,
/**
* 当前输入项激活时的文字颜色同时也是高亮时的背景色。
* 默认取全局主题
*/
fontColor: string,
/**
* 暗黑时的主题色,不填写等同fontColor
*/
darkFontColor: string,
/**
* 文字大小
*/
fontSize: string,
/**
* 圆角
*/
round: string,
/**
* skin = fill时的背景
*/
bgColor: string,
/**
* skin = fill时的暗黑背景
*/
darkBgColor: string,
/**
* skin = outline时的边线颜色
*/
borderColor: string,
/**
* skin = outline时的暗黑边线颜色
*/
darkBorderColor: string,
/**
* skin = outline时的边线颜色[非激活时]
*/
unBorderColor: string,
/**
* skin = outline时的暗黑边线颜色[非激活时]
*/
unDarkBorderColor: string,
skin: 'fill' | 'outline',
/**
* 待输入时的占位形状
* line线型
* round圆形
* 空值表示不需要占位符号
*/
placeShape: string,
}
const props = withDefaults(defineProps<xCodeInputPropsType>(), {
autoFocus: false,
useSysKeyborad: true,
modelValue: "",
maxlength: 4,
gutter: "8",
width: "50",
height: "50",
fontColor: "",
darkFontColor: "",
fontSize: "21",
round: "8",
bgColor: "#f0f0f0",
darkBgColor: "#272727",
borderColor: "",
darkBorderColor: "",
unBorderColor: "#e3e3e3",
unDarkBorderColor: "#2c2b2c",
skin: "outline",
placeShape: "round",
})
// state
const _autoFocus = ref<boolean>(false)
const inputvalue = ref<string>("")
const tid = ref<number>(0)
// computed
const _fontColor = computed(() : string => {
let fontcolor = props.fontColor == "" ? xConfig.color : props.fontColor;
let darkFontcolor = props.darkFontColor == "" ? fontcolor : props.darkFontColor;
if (xConfig.dark == 'dark') {
return getDefaultColor(darkFontcolor)
}
return getDefaultColor(fontcolor)
})
const _borderColor = computed(() : string => {
let outLineColor = props.borderColor == "" ? xConfig.color : props.borderColor;
let darkOutlineColor = props.darkBorderColor == "" ? _fontColor.value : props.darkBorderColor;
if (xConfig.dark == 'dark') {
return getDefaultColor(darkOutlineColor)
}
return getDefaultColor(outLineColor)
})
const _unborderColor = computed(() : string => {
let unBorderColor = props.unBorderColor == "" ? _fontColor.value : props.unBorderColor;
let unDarkBorderColor = props.unDarkBorderColor == "" ? _fontColor.value : props.unDarkBorderColor;
if (xConfig.dark == 'dark') {
return getDefaultColor(unDarkBorderColor)
}
return getDefaultColor(unBorderColor)
})
const _bgcolor = computed(() : string => {
if (xConfig.dark == 'dark') {
return getDefaultColor(props.darkBgColor)
}
return getDefaultColor(props.bgColor)
})
const _fontSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 21
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _maxLength = computed(() : number => {
return props.maxlength
})
const _round = computed(() : string => {
return checkIsCssUnit(props.round, xConfig.unit)
})
const _gutter = computed(() : string => {
return checkIsCssUnit(props.gutter, xConfig.unit)
})
const _width = computed(() : string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _height = computed(() : string => {
return checkIsCssUnit(props.height, xConfig.unit)
})
// methods first (so they can be referenced below)
function getValue(index : number) : string {
if (index > inputvalue.value.length - 1) return ""
return inputvalue.value.split("")[index]
}
function onconfirm() {
emits('update:modelValue',inputvalue.value)
nextTick(()=>{
/**
* 输入长度等于指定长度时触发
*/
emits('confirm', inputvalue.value)
})
_autoFocus.value = false;
}
function borderColorAc(index : number) : string {
let isActive = inputvalue.value.split("").length >= (index)
if (!isActive) {
if (props.skin == 'fill') return "transparent"
}
return isActive ? _borderColor.value : _unborderColor.value
}
function blur() {
_autoFocus.value = false;
}
function onFocus(){
let len = inputvalue.value.split("").length;
if (len > _maxLength.value){
inputvalue.value = inputvalue.value.substring(0,_maxLength.value)
}
}
function onClick() {
/**
* 点击框时触发
*/
emits('click', inputvalue.value)
_autoFocus.value = true;
}
function inputEvent(evt : UniInputEvent):string {
let value = evt.detail.value;
let len = value.split("").length;
if (len > _maxLength.value){
inputvalue.value =value
clearTimeout(tid.value)
tid.value = setTimeout(function() {
inputvalue.value = value.substring(0,_maxLength.value)
}, 100)
return inputvalue.value;
}
inputvalue.value = value;
emits('update:modelValue',inputvalue.value)
if (len == _maxLength.value) {
nextTick(()=>{
/**
* 输入长度等于指定长度时触发
*/
emits('confirm', inputvalue.value)
_autoFocus.value = false;
})
}
/**
* 变动时触发
*/
emits('change', inputvalue.value)
return inputvalue.value;
}
onMounted(() => {
_autoFocus.value = props.autoFocus;
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
watch(():string => props.modelValue, (newval : string) => {
if (newval == inputvalue.value) return;
inputvalue.value = newval;
let len = newval.split("").length;
if (len == _maxLength.value) {
/**
* 输入长度等于指定长度时触发
*/
emits('confirm', inputvalue.value)
}
})
</script>
<template>
<view @click="onClick" class="xCodeInput">
<input
@focus="onFocus"
v-if="useSysKeyborad"
:focus="_autoFocus"
:adjust-position="false"
:style="{
width:'100%',
height:_height,
}" @confirm="onconfirm" @input="inputEvent" v-model="inputvalue" @blur="blur"
:auto-focus="_autoFocus"
class="xCodeInputInput" type="number" />
<view class="xCodeInputItem" :style="{
borderRadius:_round,
border:`2px solid ${borderColorAc(index)}`,
backgroundColor:skin=='fill'?_bgcolor:'transparent',
width:_width,
height:_height,
marginRight:(index==_maxLength-1)?'0px':_gutter}" v-for="(_,index) in _maxLength" :key="index">
<text
:class="[
(index<=inputvalue.length)?'xCodeInputItemTextOn':'xCodeInputItemTextOff'
]"
class="xCodeInputItemText" :style="{
fontWeight:'bold',
color:_fontColor,
fontSize:_fontSize
}">
{{getValue(index)}}
</text>
<x-skeleton v-if="(index<=inputvalue.length)&&getValue(index)==''&&placeShape=='round'" height="5" width="5" :color="_borderColor" :dark-color="_borderColor" ></x-skeleton>
<x-skeleton v-if="(index<=inputvalue.length)&&getValue(index)==''&&placeShape=='line'" height="2" width="33%" :color="_borderColor" :dark-color="_borderColor"></x-skeleton>
</view>
</view>
</template>
<style scoped>
.xCodeInputItemText{
transition-property: transform,opacity;
transition-duration: 250ms;
transition-timing-function: linear;
transition-delay: 50ms;
}
.xCodeInputItemTextOn{
transform: scale(1);
opacity: 1;
}
.xCodeInputItemTextOff{
transform: scale(0);
opacity: 0;
}
.xCodeInputItem {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition-property: background-color,border-color;
transition-duration: 250ms;
transition-timing-function: linear;
}
.xCodeInput {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.xCodeInputInput {
/* pointer-events: none;*/
opacity: 0;
/* transform: translateX(-1000%); */
position: absolute;
left: -1000px;
top: -1000px;
z-index: -1;
}
</style>
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
* @name 布局子组件 xCol
* @description 只能放置中x-row中布局
* @page /pages/index/row
* @category 常用组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xCol" })
export type xColPropsType = {
/**
* 列宽,它是根据row定义的总列宽计算具体的宽度值。
*/
span:number,
/**
* 可以输入%,数字或者带单位的偏移量
*/
offset:string,
/**
* 自定义内部标签的style
* 请使用_style
*/
_style:string,
/**
* 自定义内部标签的class,
* 请使用_class
*/
_class:string
}
const props = withDefaults(defineProps<xColPropsType>(), {
span:3,
offset:'0',
_style:"",
_class:""
})
const emit = defineEmits<{
/**
* 单元格被点击的事件
*/
click: []
}>()
const xRowCol = inject('xRowCol', computed(():number=>12))
const _width = computed(():string=>{
return `${(props.span / xRowCol.value * 100)}%`
})
const __style = computed(():string=>props._style)
const ___class = computed(():string=>props._class)
const _offset = computed(():string=>{
return checkIsCssUnit(props.offset, xConfig.unit)
})
const onClick = () => {
emit("click")
}
</script>
<template>
<view @click="onClick" :style="{width:_width,transform:`translateX(${_offset})`}">
<view :style="[__style]" :class="___class">
<!--
@slot 默认插槽
-->
<slot></slot>
</view>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,422 @@
<script lang="ts">
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit,getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHIDREN_ITEM } from "../x-collapse/interface.uts"
/**
* @name 折叠面板子组件 xCollapseItem
* @description 可单,可多开,只可放置在x-collapse直接子节点组件,为了避免重复计算和性能x-collapse-item不能通过响应式修改内容。如果确实需要请通过刷新key解决
* @page /pages/index/collapse-item
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
itemHeight: 0,
opened: false,
id: 'xCollapseItem-' + getUid(),
list: [] as string[],
resizeObserver: null as UniResizeObserver | null,
}
},
emits:[
/**
* 点击组件标题时触发
* @param {string} name 当前标识
* @param {boolean} opened 当前项目打开状态
*/
'click'
],
inject: {
xCollapseDefaultName: { type: Array, default: [] as string[] },
},
props: {
/**
* 唯一标识
*/
name: {
type: String,
default: "",
required: true
},
/**
* 是否显示底部边线
*/
showBottomLine: {
type: Boolean,
default: true
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
},
/**
* 标题大小
*/
titleFontSize: {
type: String,
default: '16px'
},
/**
* 标题颜色
*/
titleColor: {
type: String,
default: '#333333'
},
/**
* 拒绝礼佛标题颜色,如果不填写取白
*/
darkTitleColor: {
type: String,
default: ''
},
/**
* 激活时的颜色,空值读取全局值。
*/
activeColor: {
type: String,
default: ''
},
/**
* 背景
*/
color: {
type: String,
default: 'white'
},
/**
* 暗黑时的背景,如果不填写默认取sheetDarkColor
*/
darkColor: {
type: String,
default: ''
},
/**
* 左边图标
*/
leftIcon: {
type: String,
default: ''
},
/**
* 标题
*/
title: {
type: String,
default: ''
},
/**
* 标题高度
*/
titleHeight: {
type: String,
default: '55'
},
/**
* 标题最多显示几行出现省略号
*/
titleLines: {
type: Number,
default: 1
}
},
computed: {
_disabled() : boolean {
return this.disabled;
},
_titleFontSize() : string {
let fontSize = checkIsCssUnit(this.titleFontSize, xConfig.unit);
if(xConfig.fontScale==1) return fontSize;
let sizeNumber = parseInt(fontSize)
if(isNaN(sizeNumber)){
sizeNumber = 16
}
return (sizeNumber*xConfig.fontScale).toString() + getUnit(fontSize)
},
_titleHeight() : string {
return checkIsCssUnit(this.titleHeight, xConfig.unit);
},
_titleColor() : string {
if(xConfig.dark=='dark'){
if(this.darkTitleColor!='') return getDefaultColor(this.darkTitleColor)
return '#ffffff'
}
return getDefaultColor(this.titleColor);
},
_activeColor() : string {
if (this.activeColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(this.activeColor);
},
_color() : string {
if(xConfig.dark=='dark'){
if(this.darkColor!='') return getDefaultColor(this.darkColor)
return xConfig.sheetDarkColor
}
return getDefaultColor(this.color);
},
_leftIcon() : string {
return this.leftIcon;
},
_title() : string {
return this.title;
},
_isActive() : boolean {
return this.list.includes(this.name);
},
_textMap() : Map<string, string> {
let styleMap = new Map<string, string>()
styleMap.set("fontSize", this._titleFontSize)
styleMap.set("color", this._isActive ? this._activeColor : this._titleColor)
// #ifdef APP
styleMap.set("lines", this.titleLines.toString())
// #endif
// #ifndef APP
styleMap.set("-webkit-line-clamp", this.titleLines.toString())
// #endif
return styleMap;
}
},
mounted() {
// @ts-ignore
this.list = this.xCollapseDefaultName as string[];
// @ts-ignore
const parent = this.getParent() as XCollapseComponentPublicInstance|null
if(parent!=null){
parent!.addItem({ id: this.name, ele: this } as CHIDREN_ITEM)
}
if (this._isActive) {
this.getNodes()
}
let t = this;
// #ifdef APP || WEB
let ele = this.$refs['xCollapseItemContent'] as UniElement
if(ele==null) return;
if (this.resizeObserver == null) {
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
entries.forEach(entry => {
if (entry.target == ele) {
if (t._isActive) {
t.getNodes()
}
}
})
})
}
this.resizeObserver!.observe(ele!)
// #endif
// #ifdef MP
setTimeout(function() {
t.getNodes()
}, 300);
// #endif
},
updated() {
// #ifdef MP-WEIXIN
this.getNodes()
// #endif
},
beforeUnmount() {
// @ts-ignore
const parent = this.getParent() as XCollapseComponentPublicInstance|null
if(parent!=null){
parent!.delItem(this.name)
}
this.resizeObserver?.disconnect()
},
methods: {
// @ts-ignore
getParent():any|null{
// @ts-ignore
let parent : XCollapseComponentPublicInstance | null = null;
try {
// @ts-ignore
parent = this.$parent as XCollapseComponentPublicInstance
} catch (e) {}
return parent
},
setList(items : string[]) {
this.list = items
this.getNodes()
},
itemClick() {
/**
* 项目被点击时触发。
* @param name {string} 当前name值
*/
this.$emit('click', this.name,!this.opened);
if (!this._disabled) {
// @ts-ignore
let parent : XCollapseComponentPublicInstance | null = null;
try {
// @ts-ignore
parent = this.$parent as XCollapseComponentPublicInstance
} catch (e) {
}
if (parent != null) {
parent.addChange(this.name)
}
}
},
getNodes() {
// #ifndef MP
let _this = this;
let ele = this.$refs['xCollapseItemContent'] as UniElement|null;
if(ele==null) return;
ele.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
_this.itemHeight = rect.height;
if (_this._isActive) {
_this.open()
} else {
_this.close()
}
})
// #endif
// #ifdef MP
uni.createSelectorQuery().in(this)
.select(".xCollapseItemContent")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
this.itemHeight = nodeinfo.height!;
if (this._isActive) {
this.open()
} else {
this.close()
}
})
// #endif
},
open() {
this.opened = true;
},
close() {
this.opened = false;
}
},
}
</script>
<template>
<view class="xCollapseItemBox" :style="{background:_color}">
<view @click="itemClick" class="xCollapseItem" :style="{opacity:_disabled?0.5:1}">
<view class="xCollapseItemBoxLeft">
<x-icon :font-size="_titleFontSize" v-if="_leftIcon" :name="_leftIcon"
:color="_isActive?_activeColor:_titleColor" style="margin-right: 12px;"></x-icon>
<!--
@slot 左边插槽
@prop {boolean} status - 当前展开状态
-->
<slot name="left" :status="opened"></slot>
<view class="xCollapseItemBoxTextBox" :style="{height:_titleHeight}">
<view style="flex:1">
<!--
@slot 标题插槽,如果你要完全自定标题样式请在此插槽内布局
@prop {boolean} status - 当前展开状态
-->
<slot name="title" :status="opened">
<text class="xCollapseItemBoxText" :style="_textMap">
{{_title}}
</text>
</slot>
</view>
</view>
</view>
<view class="xCollapseItemBoxRight">
<!--
@slot 右边插槽
@prop {boolean} status - 当前展开状态
-->
<slot name="right" :status="opened"></slot>
<x-icon :color="_isActive?_activeColor:'#bfbfbf'" style="margin-left: 12px;"
:name="opened?'arrow-down-s-line':'arrow-right-s-line'"></x-icon>
</view>
</view>
<view class="xCollapseItemWrap" :style="{height:opened?(itemHeight+'px'):'0rpx'}">
<view class="xCollapseItemContent" ref="xCollapseItemContent">
<!--
@slot 默认内容插槽。
-->
<slot></slot>
</view>
</view>
<x-divider v-if="showBottomLine"></x-divider>
</view>
</template>
<style scoped>
.xCollapseItemBoxTextBox {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xCollapseItemBoxText {
text-overflow: ellipsis;
/* #ifndef APP */
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
/* #endif */
}
.xCollapseItemContent {
padding: 12px 0rpx;
}
.xCollapseItemBoxLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
flex: 1;
}
.xCollapseItemBoxRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.xCollapseItemBox {
/* background-color: white; */
padding: 0px 12px;
}
.xCollapseItem {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.xCollapseItemWrap {
display: flex;
flex-direction: column;
transition-property: height;
transition-duration: 350ms;
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
overflow: hidden;
}
</style>
@@ -0,0 +1,4 @@
export type CHIDREN_ITEM = {
id:string,
ele:XCollapseItemComponentPublicInstance
}
@@ -0,0 +1,142 @@
<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHIDREN_ITEM } from "../x-collapse/interface.uts"
/**
*
* @name 折叠面板 xCollapse
* @description 可单,可多开,内部只可放置x-collapse-item直接子节点组件,为了避免重复计算和性能x-collapse-item不能通过响应式修改内容。如果确实需要请通过刷新key解决
* @page /pages/index/collapse
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
list: [] as CHIDREN_ITEM[],
activeName: [] as string[],
}
},
emits: [
/**
* 变换时触发
* @param {String[]} value - 当前打开的值
*/
'change',
'update:modelValue'],
props: {
/**
* 当前打开的组。可v-model
*/
modelValue: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
/**
* 是否允许打开多个。
*/
multiple: {
type: Boolean,
default: true
}
},
beforeMount() {
this.activeName = this.modelValue;
},
provide() {
return {
xCollapseDefaultName: this.activeName
}
},
watch: {
modelValue(newValue : string[]) {
let newstr = newValue.join("")
if (newstr == this.activeName.join("")) return;
if (this.multiple) {
this.activeName = newValue;
} else {
if (newValue.length >= 1) {
this.activeName = [newValue[0]]
}else{
this.activeName = []
}
}
this.pushChildren();
}
},
methods: {
addItem(item : CHIDREN_ITEM) {
let index = this.list.findIndex((el : CHIDREN_ITEM) : boolean => el.id == item.id)
if (index > -1) {
this.list.splice(index, 1, item)
} else {
this.list.push(item)
}
if (this.activeName.length > 0) {
this.pushChildren()
}
},
delItem(id : string) {
let index = this.list.findIndex((el : CHIDREN_ITEM) : boolean => el.id == id)
if (index > -1) {
this.list.splice(index, 1)
}
this.pushChildren()
},
pushChildren() {
this.list.forEach((el : CHIDREN_ITEM) => {
el.ele.setList(this.activeName)
})
},
addChange(id : string) {
if (this.multiple) {
let index = this.activeName.findIndex((el : string) : boolean => el == id)
if (index > -1) {
this.activeName.splice(index, 1)
} else {
this.activeName.push(id)
}
} else {
if (this.activeName.includes(id)) {
this.activeName = []
} else {
this.activeName = [id]
}
}
this.pushChildren();
/**
* 变换时触发
* @param value {string[]} 当前打开的值
*/
this.$emit('change', this.activeName)
/**
* 等同v-model=""
*/
this.$emit('update:modelValue', this.activeName)
}
}
}
</script>
<template>
<view>
<!--
@slot 默认插槽,仅可放置x-collapse-item子节点
-->
<slot></slot>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,278 @@
<template>
<view>
<view class="colorViewBoxAlphaBox">
<x-text _style="opacity:0.5" font-size="13">
<!-- 不透明度 -->
{{i18n.t('tmui4x.colorView.alpha')}}
</x-text>
<view class="boxRight">
<view :class="[isDark?'dark':'light']" class="barBox " :style="{borderRadius:(barsize+4)+'px'}">
<view class="barBoxRealWrap" @touchstart.stop="mStart" @touchmove.stop="mMove" @touchend="mEnd"
@touchcancel="mEnd" <!-- #ifdef WEB -->
@mousedown="mmStart(($event as UniMouseEvent),index)"
<!-- #endif -->
>
<view ref="bar" class="bar"
:style="{width:barsize+'px',height:barsize+'px',borderRadius:barsize+'px'}"></view>
<view ref="barWrap" class="barWrap" :style="{height:barsize+'px'}"></view>
<view class="barPlace" :style="{width:barsize+'px',height:barsize+'px'}"></view>
</view>
</view>
<x-input font-size="14" input-padding="0px" type="number" @input="inputChange" :model-value="rgbvalue"
width="70" height="30" align="center" placeholder="">
<template v-slot:inputRight>
<x-text _style="margin-right:5px">%</x-text>
</template>
</x-input>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, getCurrentInstance, nextTick } from "vue"
import { hexToRgb, hslToRgb, rgbToHexNoAlpha, hslaToRgbCss, rgbToHex, rgbToHsl } from "../../core/util/xCoreColorUtil.uts";
import { xConfig } from "../../config/xConfig.uts";
const i18n = xConfig.i18n;
const props = defineProps({
hexValue: {
type: Number,
default: 1
}
})
const emits = defineEmits<{
/**
* 值变化时触发。
* @param {number} alpha - 当前的颜色值
*/
(e : 'change', alpha : number) : void,
}>()
const proxy = getCurrentInstance()?.proxy
const rgbvalue = ref<string>((Math.max(0, Math.min(props.hexValue * 100, 100))).toString())
const valuestr = computed(() : number => {
let val = (parseInt(rgbvalue.value) / 100) + ''
val = val.substring(0, 4)
return Math.max(0, Math.min(1, parseFloat(val)))
})
let tid = 0;
let _x = 0
let _startLeft = 0
const barsize = ref(28)
const isMoving = ref(false)
const barWrapNodes = ref<NodeInfo|null>(null);
let xColorviewEventId = ''
const toChangeEvents = () => {
emits('change', valuestr.value)
}
const isDark = computed(() : boolean => xConfig.dark == 'dark')
const inputChange = (val : string) => {
if (val == '') return;
let realvalue = parseInt(val)
realvalue = isNaN(realvalue) ? 0 : realvalue;
let realvalueNew = Math.max(0, Math.min(realvalue, 100));
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let nodes = result[0] as NodeInfo
let realwidth = nodes.width!
let left = Math.floor(realvalueNew / 100 * realwidth)
let nowdom = proxy!.$refs['bar'] as UniElement
nowdom.style.setProperty('left', left.toString() + 'px')
rgbvalue.value = realvalue.toString()
nextTick(() => {
rgbvalue.value = realvalueNew.toString()
toChangeEvents()
})
})
}
const setPostionTranslaterStart = (x : number, y : number) => {
let dom = proxy!.$refs['bar'] as UniElement
let barWrapdom = proxy!.$refs['barWrap'] as UniElement
let domleft = dom.style.getPropertyValue('left') as null | string
if (isNaN(parseInt(domleft == null ? '0' : domleft!))) {
dom.style.setProperty('left', '0px')
}
let barwRound = barWrapNodes.value!
_x = x
_startLeft = (_x - barwRound.left!) - barsize.value / 2
_startLeft = Math.ceil(Math.max(0, Math.min(_startLeft, barwRound.width!)))
dom.style.setProperty('left', _startLeft + 'px')
let val = Math.floor(_startLeft / barwRound.width! * 100).toString()
rgbvalue.value = val
toChangeEvents()
}
const setPostionTranslaterMove = (mx : number, my : number) => {
let dom = proxy!.$refs['bar'] as UniElement
let barWrapdom = proxy!.$refs['barWrap'] as UniElement
let barwRound = barWrapNodes.value!
let x = mx - _x + _startLeft - barsize.value / 2
// #ifdef MP||WEB
x = mx - _x + _startLeft
// #endif
x = Math.ceil(Math.max(0, Math.min(x, barwRound.width!)))
dom.style.setProperty('left', x + 'px')
let val = Math.floor(x / barwRound.width! * 100).toString()
if (val == rgbvalue.value) return;
rgbvalue.value = val
toChangeEvents()
}
const mStart = (evt : UniTouchEvent) => {
evt.preventDefault()
isMoving.value = true;
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let nodes = result[0] as NodeInfo
barWrapNodes.value = nodes
setPostionTranslaterStart(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
})
}
const mMove = (evt : UniTouchEvent) => {
if (!isMoving.value) return
setPostionTranslaterMove(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
}
const mEnd = (evt : UniTouchEvent) => {
if (!isMoving.value) return
isMoving.value = false;
}
// #ifdef WEB
const mmStart = (evt : UniMouseEvent, index) => {
isMoving.value = true;
xColorviewEventId = Math.random().toString(16).substring(2, 16)
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY)
}
const mmMove = (evt : UniMouseEvent) => {
if (!isMoving.value) return
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY)
}
const mmEnd = (evt : UniMouseEvent) => {
if (!isMoving.value) return
isMoving.value = false;
xColorviewEventId = ''
}
document.body.addEventListener('mousemove', mmMove)
document.body.addEventListener('mouseup', mmEnd)
document.body.addEventListener('mouseleave', mmEnd)
// #endif
const setViewPosition = () => {
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let nodes = result[0] as NodeInfo
barWrapNodes.value = nodes
let realwidth = nodes.width!
let doms = proxy!.$refs['bar'] as UniElement
let left = Math.floor(valuestr.value * realwidth)
doms.style.setProperty('left', left.toString() + 'px')
})
}
watch(() : number => props.hexValue, (newval : number) => {
if (valuestr.value == newval) return;
rgbvalue.value = (Math.max(0, Math.min(newval * 100, 100))).toString()
setViewPosition()
})
onBeforeUnmount(() => {
// #ifdef WEB
document.body.removeEventListener('mousemove', mmMove)
document.body.removeEventListener('mouseup', mmEnd)
document.body.removeEventListener('mouseleave', mmEnd)
// #endif
})
onMounted(() => {
clearTimeout(tid)
tid = setTimeout(function () {
setViewPosition()
}, 100);
})
</script>
<style scoped>
.colorViewBoxAlphaBox {
margin-top: 12px;
}
.bar {
border: 3px solid white;
background-color: transparent;
position: absolute;
top: 0px;
pointer-events: none;
/* #ifdef MP-WEIXIN||WEB */
box-sizing: border-box;
/* #endif */
}
.barPlace {
pointer-events: none;
}
.box0,
.box1 {
margin-bottom: 15px;
}
.boxRight {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.barBox {
flex: 1;
margin-right: 12px;
}
.barBox.light {
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));
}
.barBox.dark {
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(255, 255, 255, 1));
}
.barBoxRealWrap {
margin: 2px 2px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
position: relative;
/* #ifdef WEB */
cursor: cell;
/* #endif */
}
.barWrap {
flex: 1;
pointer-events: none;
}
</style>
@@ -0,0 +1,229 @@
<template>
<view class="colorViewGridBox" ref="colorViewGridBoxRef" :style="{minHeight:boxHeight+'px'}">
<view class="colorViewGridBoxGrid"
:style="{width:gridWidth+'px',height:gridHeight+'px',top:barTop+'px',left:barLeft+'px'}"></view>
<canvas :id="canvasUid" @touchstart.stop="mStart" @touchmove.stop="mMove" @touchend="mEnd" @touchcancel="mEnd"
v-if="isInitDom" ref="canvas" :style="{width:boxWidth+'px',height:boxHeight+'px'}" <!-- #ifdef WEB -->
@mousedown="mmStart"
<!-- #endif -->
></canvas>
</view>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance } from "vue"
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHexNoAlpha } from "../../core/util/xCoreColorUtil.uts";
const emits = defineEmits<{
/**
* 值变化时触发。
* @param {string} hexstr - 当前的颜色值
*/
(e : 'change', hexstr : string) : void,
}>()
const proxy = getCurrentInstance()?.proxy
const canvas = ref<UniCanvasElement | null>(null)
const colorViewGridBoxRef = ref<UniCanvasElement | null>(null)
let tid = 0;
let gridWidth = ref(0)
let gridHeight = ref(0)
let boxWidth = ref(0)
let boxHeight = ref(250)
let isInitDom = ref(false)
let colosXYData = [] as string[][]
let barLeft = ref(0)
let barTop = ref(0)
let windTop = uni.getWindowInfo().windowTop
const isMoving = ref(false)
let xColorviewEventId = ''
const canvasUid = 'xColorView_' + Math.random().toString(16).substring(4, 8)
const drawBlackWhite = (ctx : CanvasRenderingContext2D) => {
let colors = [] as string[]
for (let i = 0; i < 12; i++) {
let blv = (11 - i) / 11 * 100
let l = Math.floor(blv)
let rgba = hslToRgb({ h: 0, s: 0, l: l, a: 1 } as UTSJSONObject)
let color = rgbToHexNoAlpha(rgba)
ctx.fillStyle = color
ctx.fillRect(i * gridWidth.value, 0, gridWidth.value, gridHeight.value)
colors.push(color)
}
colosXYData.push(colors)
}
const drawHsla = (ctx : CanvasRenderingContext2D) => {
let colorsx = new Array<string[]>()
for (let i = 0; i < 9; i++) {
let colorsB = [] as string[]
for (let j = 0; j < 12; j++) {
colorsB.push('')
}
colorsx.push(colorsB)
}
for (let i = 0; i < 12; i++) {
for (let j = 1; j < 10; j++) {
let blv = (j + 0.2) / 10 * 100
let l = Math.floor(blv)
let rgba = hslToRgb({ h: i * 25 + 180, s: 100 - blv * 0.65, l: l, a: 1 } as UTSJSONObject)
let color = rgbToHexNoAlpha(rgba)
ctx.fillStyle = color
ctx.fillRect(i * gridWidth.value, j * gridHeight.value, gridWidth.value, gridHeight.value)
colorsx[j - 1][i] = color
}
}
colosXYData.push(...colorsx)
}
const getInitDraw = () => {
uni.createCanvasContextAsync({
id: canvasUid,
component: proxy,
success(context) {
let ctx = context.getContext('2d')!;
let canvas = ctx.canvas
let dpr = uni.getWindowInfo().pixelRatio
canvas.width = canvas.offsetWidth * dpr
canvas.height = canvas.offsetHeight * dpr
ctx.scale(dpr, dpr)
gridWidth.value = canvas.offsetWidth / 12
gridHeight.value = canvas.offsetHeight / 10
drawBlackWhite(ctx)
drawHsla(ctx)
},
fail(err) {
console.error(err)
uni.showToast({ title: '错误', icon: 'none' })
}
})
}
let nowselectedStr = ref('')
const colorViewGridBox = ref<NodeInfo | null>(null)
const setPostion = (x : number, y : number) => {
let node = colorViewGridBox.value!;
// #ifdef MP
if(!node?.left){
node.left = 0
}
if(!node?.top){
node.top = 0
}
// #endif
let _x = x - node.left!;
let _y = y - node.top! - windTop;
let pos_x = Math.ceil(_x / gridWidth.value)
pos_x = Math.max(1, Math.min(12, pos_x))
let pos_y = Math.ceil(_y / gridHeight.value)
pos_y = Math.max(1, Math.min(10, pos_y))
let xystr = colosXYData[pos_y - 1][pos_x - 1]
barLeft.value = (pos_x - 1) * gridWidth.value
barTop.value = (pos_y - 1) * gridHeight.value
if (nowselectedStr.value == xystr) return;
nowselectedStr.value = xystr
emits('change', xystr)
}
const mStart = (evt : UniTouchEvent) => {
evt.preventDefault()
isMoving.value = true
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
let node = nodes[0] as NodeInfo;
colorViewGridBox.value = node
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
})
}
const mMove = (evt : UniTouchEvent) => {
if (!isMoving.value) return;
// #ifdef MP-WEIXIN||WEB
evt.stopPropagation()
evt.preventDefault()
// #endif
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
}
const mEnd = (evt : UniTouchEvent) => {
if (!isMoving.value) return;
isMoving.value = false
}
// #ifdef WEB
const mmStart = (evt : UniMouseEvent) => {
isMoving.value = true;
xColorviewEventId = Math.random().toString(16).substring(2, 16)
setPostion(evt.clientX, evt.clientY)
}
const mmMove = (evt : UniMouseEvent) => {
if (!isMoving.value) return
setPostion(evt.clientX, evt.clientY)
}
const mmEnd = (evt : UniMouseEvent) => {
if (!isMoving.value) return
isMoving.value = false;
xColorviewEventId = ''
}
document.body.addEventListener('mousemove', mmMove)
document.body.addEventListener('mouseup', mmEnd)
document.body.addEventListener('mouseleave', mmEnd)
// #endif
onBeforeUnmount(() => {
clearTimeout(tid)
// #ifdef WEB
document.body.removeEventListener('mousemove', mmMove)
document.body.removeEventListener('mouseup', mmEnd)
document.body.removeEventListener('mouseleave', mmEnd)
// #endif
})
onMounted(() => {
clearTimeout(tid)
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
let node = nodes[0] as NodeInfo;
colorViewGridBox.value = node
boxHeight.value = Math.min(Math.max(250, node.width!), 250)
boxWidth.value = Math.max(Math.max(node.width!, 250), 250)
isInitDom.value = true;
tid = setTimeout(function () {
getInitDraw();
}, 80);
})
})
</script>
<style scoped>
.colorViewGridBox {
position: relative;
border-radius: 6px;
overflow: hidden;
margin-top: 8px;
/* #ifdef WEB */
cursor: cell;
/* #endif */
}
.colorViewGridBoxGrid {
position: absolute;
border: 2px solid rgba(255, 255, 255, 0.6);
z-index: 3;
pointer-events: none;
/* #ifdef MP||WEB */
box-sizing: border-box;
/* #endif */
}
</style>
@@ -0,0 +1,355 @@
<template>
<view class="colorViewGridBox"
ref="gridBoxref"
@touchstart.stop="mStart"
@touchmove.stop="mMove" @touchend="mEnd" @touchcancel="mEnd"
:style="{minHeight:boxHeight+'px'}"
<!-- #ifdef WEB -->
@mousedown="mmStart"
<!-- #endif -->
>
<!-- #ifndef APP -->
<view class="colorViewGridHueBox"
:style="{width:boxWidth+'px',height:boxHeight+'px',margin:`${(btnSize/2)}px`,borderRadius:'10px'}">
<view class="colorViewGridHueBoxMask">
</view>
</view>
<!-- #endif -->
<!-- #ifdef APP -->
<canvas v-if="isInitDom" ref="canvas"
:style="{width:boxWidth+'px',height:boxHeight+'px',margin:`${(btnSize/2)}px`,borderRadius:'10px'}"></canvas>
<!-- #endif -->
<view class="colorViewGridBoxGridBar"
:style="{width:btnSize+'px',height:btnSize+'px',top:barTop+'px',left:barLeft+'px',backgroundColor:valuestr,opacity:isShowBar?'1':'0'}">
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, getCurrentInstance, watch, onMounted, onBeforeMount, onBeforeUnmount } from "vue"
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHsl, rgbToHexNoAlpha } from "../../core/util/xCoreColorUtil.uts";
const props = defineProps({
hexValue: {
type: String,
default: ""
}
})
const emits = defineEmits<{
/**
* 值变化时触发。
* @param {string} hexstr - 当前的颜色值
*/
(e : 'change', hexstr : string) : void,
}>()
const proxy = getCurrentInstance()?.proxy
let boxWidth = ref(0)
let boxHeight = ref(300)
let tid = 0;
const btnSize = ref(24)
let barLeft = ref(0)
let barTop = ref(0)
const isMoving = ref(false)
let nowselectedStr = ref('')
let windTop = uni.getWindowInfo().windowTop
const canvas = ref<UniCanvasElement | null>(null)
let isInitDom = ref(false)
const valuestr = ref(`rgba(255,255,255,1)`)
const isShowBar = ref(false)
let xColorviewEventId =''
const gridBox = ref<NodeInfo|null>(null);
const gridBoxref = ref<UniElement | null>(null)
const setPostion = (x : number, y : number) => {
let node = gridBox.value! as NodeInfo;
let bsize = btnSize.value
// 计算相对于容器的坐标
let _x = x - node.left!;
let _y = y - node.top! - windTop;
// 容器的margin
let margin = bsize / 2
// 计算按钮中心点相对于颜色区域的位置
let center_x = _x - margin
let center_y = _y - margin
// 限制按钮中心点在颜色区域内(确保按钮不会超出容器)
// 按钮中心点的有效范围:从 0 到 boxWidth/boxHeight(因为颜色区域就是boxWidth x boxHeight
center_x = Math.max(0, Math.min(boxWidth.value, center_x))
center_y = Math.max(0, Math.min(boxHeight.value, center_y))
// 设置按钮位置(按钮左上角坐标,相对于整个容器)
barLeft.value = center_x
barTop.value = center_y
// 计算颜色采样位置(按钮中心点在颜色区域内的位置)
let color_x = center_x
let color_y = center_y
// 计算百分比位置(0-100
let xBlv = Math.max(0, Math.min(100, (color_x / boxWidth.value) * 100))
let yBlv = Math.max(0, Math.min(100, (color_y / boxHeight.value) * 100))
// 计算饱和度s:0-100
let s = 0
if (xBlv > 0 && xBlv <= 50) {
s = xBlv / 50 * 100
} else if (xBlv > 50) {
s = 100
}
// 计算亮度
let l = 100 - xBlv
// 计算色相
let h = yBlv / 100 * 360
let rgba = hslToRgb({ h: h, s: s, l: l, a: 1 } as UTSJSONObject)
let color = rgbToHexNoAlpha(rgba)
if (valuestr.value == color) return;
valuestr.value = color
emits('change', color)
}
const mStart = (evt : UniTouchEvent) => {
if(!isInitDom.value) return;
evt.preventDefault()
// #ifdef MP-WEIXIN||WEB
evt.stopPropagation()
// #endif
isMoving.value = true
isShowBar.value = true;
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
gridBox.value = nodes[0] as NodeInfo;
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
})
}
const mMove = (evt : UniTouchEvent) => {
if (!isMoving.value) return;
evt.preventDefault()
// #ifdef MP-WEIXIN||WEB
evt.stopPropagation()
// #endif
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
}
const mEnd = (evt : UniTouchEvent) => {
if (!isMoving.value) return;
isMoving.value = false
}
// #ifdef WEB
const mmStart = (evt:UniMouseEvent)=>{
isMoving.value = true;
xColorviewEventId = Math.random().toString(16).substring(2,16)
isShowBar.value = true;
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
gridBox.value = nodes[0] as NodeInfo;
setPostion(evt.clientX, evt.clientY)
})
}
const mmMove = (evt:UniMouseEvent)=>{
if (!isMoving.value) return
// #ifdef MP-WEIXIN||WEB
evt.stopPropagation()
evt.preventDefault()
// #endif
setPostion(evt.clientX, evt.clientY)
}
const mmEnd = (evt:UniMouseEvent)=>{
if (!isMoving.value) return
isMoving.value = false;
xColorviewEventId=''
}
document.body.addEventListener('mousemove',mmMove)
document.body.addEventListener('mouseup',mmEnd)
document.body.addEventListener('mouseleave',mmEnd)
// #endif
const drawBgHsla = (ctx : CanvasRenderingContext2D) => {
// 绘制渐变
let liner = ctx.createLinearGradient(boxWidth.value / 2, 0, boxWidth.value / 2, boxHeight.value / 2)
liner.addColorStop(0, '#ff0000')
liner.addColorStop(0.3333, '#ffff00')
liner.addColorStop(0.6666, '#00ff00')
liner.addColorStop(1, '#00ffff')
ctx.fillStyle = liner
ctx.fillRect(0, 0, boxWidth.value, boxHeight.value)
let liner2 = ctx.createLinearGradient(boxWidth.value / 2, boxHeight.value / 2, boxWidth.value / 2, boxHeight.value)
liner2.addColorStop(0, '#00ffff')
liner2.addColorStop(0.3333, '#0000ff')
liner2.addColorStop(0.6666, '#ff00ff')
liner2.addColorStop(1, '#ff0000')
ctx.fillStyle = liner2
ctx.fillRect(0, boxHeight.value / 2, boxWidth.value, boxHeight.value / 2)
// 绘制遮罩.
let liner3 = ctx.createLinearGradient(0, boxHeight.value / 2, boxWidth.value, boxHeight.value / 2)
liner3.addColorStop(0, '#ffffff')
liner3.addColorStop(0.5, 'rgba(0,0,0,0)')
liner3.addColorStop(1, '#000000')
ctx.fillStyle = liner3
ctx.fillRect(0, 0, boxWidth.value, boxHeight.value)
}
const getInitDraw = () => {
if (canvas.value == null) return;
let dom = canvas.value as UniCanvasElement;
let dpr = uni.getWindowInfo().pixelRatio
dom.width = dom.offsetWidth * dpr;
dom.height = dom.offsetHeight * dpr;
let ctx = dom.getContext('2d')!
ctx.scale(dpr, dpr)
drawBgHsla(ctx)
}
const valueStrToPosition = () => {
if (valuestr.value == '') return;
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
let node = nodes[0] as NodeInfo;
let hsl = rgbToHsl(hexToRgb(valuestr.value))
let h = hsl.getNumber('h')!
let s = hsl.getNumber('s')!
let l = hsl.getNumber('l')!
let bsize = btnSize.value
let xBlv = 0
let yBlv = 0
// 如果饱和度为0,隐藏按钮
if (s == 0) {
isShowBar.value = false;
return;
}
// 计算X轴位置(饱和度)
if (s >= l) {
xBlv = 100 - l
} else {
xBlv = s / 100 * 50
}
// 计算Y轴位置(色相)
yBlv = h / 360 * 100
// 计算按钮中心点在颜色区域内的位置
let center_x = (xBlv / 100) * boxWidth.value
let center_y = (yBlv / 100) * boxHeight.value
// 计算按钮位置(考虑margin
let margin = bsize / 2
barLeft.value = center_x + margin - bsize / 2
barTop.value = center_y + margin - bsize / 2
isShowBar.value = true;
})
}
watch(() : string => props.hexValue, (newval : string) => {
if (newval == '' || valuestr.value == newval) return;
valuestr.value = newval
valueStrToPosition()
})
onBeforeUnmount(() => {
clearTimeout(tid)
// #ifdef WEB
document.body.removeEventListener('mousemove',mmMove)
document.body.removeEventListener('mouseup',mmEnd)
document.body.removeEventListener('mouseleave',mmEnd)
// #endif
})
onBeforeMount(() => {
valuestr.value = props.hexValue
})
onMounted(() => {
clearTimeout(tid)
uni.createSelectorQuery()
.in(proxy)
.select('.colorViewGridBox')
.boundingClientRect()
.exec(nodes => {
let node = nodes[0] as NodeInfo;
boxHeight.value = Math.max(Math.min(270, node.width!), 270)
boxWidth.value = Math.max(node.width!, 270) - btnSize.value
isInitDom.value = true
gridBox.value = node
tid = setTimeout(function () {
valueStrToPosition()
getInitDraw()
}, 80);
})
})
</script>
<style scoped>
.colorViewGridHueBox {
background-image: linear-gradient(to bottom, red 0, #ff0 16.66666%, #0f0 33.333333%, #0ff 50%, #00f 66.66666%, #f0f 83.333333%, red 100%);
position: relative;
pointer-events: none;
/* border-radius: 6px;
overflow: hidden; */
/* #ifdef MP-WEIXIN||WEB */
box-sizing: border-box;
/* #endif */
}
.colorViewGridBox {
/* #ifdef MP-WEIXIN||WEB */
position: relative;
box-sizing: border-box;
cursor: cell;
/* #endif */
}
.colorViewGridHueBoxMask {
background-image: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0), #000000 100%);
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
top: 0px;
left: 0px;
}
.colorViewGridBoxGridBar {
position: absolute;
width: 24px;
height: 24px;
border: 3px solid white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 24px;
background-color: rgba(0, 0, 0, 0.1);
pointer-events: none;
/* #ifdef MP-WEIXIN||WEB */
box-sizing: border-box;
/* #endif */
}
</style>
@@ -0,0 +1,314 @@
<template>
<view>
<view :style="{height:'8px'}"></view>
<view class="box" v-for="(item,index) in rgbvalue" :key="index" :class="[`box${index}`]">
<x-text _style="opacity:0.5" font-size="13">{{rgbvalueText[index]}}</x-text>
<view class="boxRight">
<view :class="[
index==0?'barR':'',
index==1?'barG':'',
index==2?'barB':''
]" class="barBox " :style="{
borderRadius:(barsize+4)+'px',
}">
<view class="barBoxRealWrap" @touchstart.stop="mStart(($event as UniTouchEvent),index)"
@touchmove.stop="mMove(($event as UniTouchEvent),index)"
@touchend.stop="mEnd(($event as UniTouchEvent),index)"
@touchcancel="mEnd(($event as UniTouchEvent),index)" <!-- #ifdef WEB -->
@mousedown="mmStart(($event as UniMouseEvent),index)"
<!-- #endif -->
>
<view ref="bar" class="bar"
:style="{width:barsize+'px',height:barsize+'px',borderRadius:barsize+'px'}"></view>
<view ref="barWrap" class="barWrap" :style="{height:barsize+'px'}"></view>
<view class="barPlace" :style="{width:barsize+'px',height:barsize+'px'}"></view>
</view>
</view>
<x-input font-size="14" input-padding="0px" type="number" @input="inputChange(($event as string),index)"
:model-value="realRgb[index]" width="70" height="30" align="center" placeholder=""></x-input>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { xConfig } from "../../config/xConfig.uts";
import { ref, computed, getCurrentInstance, nextTick } from "vue"
import { hexToRgb, hslToRgb, rgbToHexNoAlpha, hslaToRgbCss, rgbToHex, rgbToHsl } from "../../core/util/xCoreColorUtil.uts";
const i18n = xConfig.i18n;
const props = defineProps({
hexValue: {
type: String,
default: ""
}
})
const emits = defineEmits<{
/**
* 值变化时触发。
* @param {string} hexstr - 当前的颜色值
*/
(e : 'change', hexstr : string) : void,
}>()
const proxy = getCurrentInstance()?.proxy
const rgbvalue = ref<number[]>([0, 0, 0])
const rgbvalueText = ref<string[]>([i18n.t('tmui4x.colorView.r'), i18n.t('tmui4x.colorView.g'), i18n.t('tmui4x.colorView.b')])
const isMoving = ref(false)
const realRgb = computed(() : string[] => {
let r = rgbvalue.value[0].toString()
let g = rgbvalue.value[1].toString()
let b = rgbvalue.value[2].toString()
return [r, g, b] as string[]
})
let tid = 0;
let _x = 0
let _startLeft = 0
const barsize = ref(28)
const valuestr = ref('#ffffff')
let nowIndex = 0
let xColorviewEventId = ''
const barWrapNodes = ref<NodeInfo | null>(null);
const toChangeEvents = () => {
let str = rgbToHexNoAlpha({ r: rgbvalue.value[0], g: rgbvalue.value[1], b: rgbvalue.value[2] } as UTSJSONObject)
if (str == valuestr.value) return;
valuestr.value = str;
emits('change', str)
}
const setPostionTranslaterStart = (x : number, y : number, index : number) => {
let doms = proxy!.$refs['bar'] as UniElement[]
let dom = doms[index] as UniElement
let bardoms = proxy!.$refs['barWrap'] as UniElement[]
let barWrapdom = bardoms[0] as UniElement
let domleft = dom.style.getPropertyValue('left') as null | string
if (isNaN(parseInt(domleft == null ? '0' : domleft!))) {
dom.style.setProperty('left', '0px')
}
let barwRound = barWrapNodes.value!;
_x = x
_startLeft = (_x - barwRound.left!) - barsize.value / 2
_startLeft = Math.ceil(Math.max(0, Math.min(_startLeft, barwRound.width!)))
dom.style.setProperty('left', _startLeft + 'px')
let val = Math.floor(_startLeft / barwRound.width! * 255)
rgbvalue.value[index] = val
toChangeEvents()
}
const setPostionTranslaterMove = (mx : number, my : number, index : number) => {
let doms = proxy!.$refs['bar'] as UniElement[]
let dom = doms[index] as UniElement
let bardoms = proxy!.$refs['barWrap'] as UniElement[]
let barWrapdom = bardoms[0] as UniElement
let barwRound = barWrapNodes.value!;
let x = mx - _x + _startLeft - barsize.value / 2
// #ifdef MP||WEB
x = mx - _x + _startLeft
// #endif
x = Math.ceil(Math.max(0, Math.min(x, barwRound.width!)))
dom.style.setProperty('left', x + 'px')
let val = Math.floor(x / barwRound.width! * 255)
rgbvalue.value[index] = val
toChangeEvents()
}
const inputChange = (val : string, index : number) => {
if (val == '') return;
let realvalue = parseInt(val)
realvalue = isNaN(realvalue) ? 0 : realvalue;
let realvalueNew = Math.max(0, Math.min(realvalue, 255));
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let nodes = result[0] as NodeInfo
let realwidth = nodes.width!
let left = Math.floor(realvalueNew / 255 * realwidth)
let doms = proxy!.$refs['bar'] as UniElement[]
let nowdom = doms[index]
nowdom.style.setProperty('left', left.toString() + 'px')
rgbvalue.value[index] = realvalue
nextTick(() => {
rgbvalue.value[index] = realvalueNew
toChangeEvents()
})
})
}
const mStart = (evt : UniTouchEvent, index : number) => {
evt.preventDefault()
isMoving.value = true;
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let nodes = result[0] as NodeInfo
barWrapNodes.value = nodes
setPostionTranslaterStart(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY, index)
})
}
const mMove = (evt : UniTouchEvent, index : number) => {
if (!isMoving.value) return
setPostionTranslaterMove(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY, index)
}
const mEnd = (evt : UniTouchEvent, index : number) => {
if (!isMoving.value) return
isMoving.value = false;
}
// #ifdef WEB
const mmStart = (evt : UniMouseEvent, index) => {
isMoving.value = true;
xColorviewEventId = Math.random().toString(16).substring(2, 16)
nowIndex = index
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY, index)
}
const mmMove = (evt : UniMouseEvent) => {
if (!isMoving.value) return
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY, nowIndex)
}
const mmEnd = (evt : UniMouseEvent) => {
if (!isMoving.value) return
isMoving.value = false;
xColorviewEventId = ''
}
document.body.addEventListener('mousemove', mmMove)
document.body.addEventListener('mouseup', mmEnd)
document.body.addEventListener('mouseleave', mmEnd)
// #endif
const setViewPosition = () => {
let colors = hexToRgb(valuestr.value)
uni.createSelectorQuery()
.in(proxy)
.select(".barWrap")
.boundingClientRect()
.exec(result => {
let r = colors.getNumber('r')!
let g = colors.getNumber('g')!
let b = colors.getNumber('b')!
let rgb = [r, g, b] as number[]
let nodes = result[0] as NodeInfo
barWrapNodes.value = nodes
let realwidth = nodes.width!
let doms = proxy!.$refs['bar'] as UniElement[]
for (let i = 0; i < rgb.length; i++) {
let leftvalue = rgb[i];
let left = Math.floor(leftvalue / 255 * realwidth)
let nowdom = doms[i]
nowdom.style.setProperty('left', left.toString() + 'px')
}
rgbvalue.value = rgb.splice(0)
})
}
watch(() : string => props.hexValue, (newval : string) => {
if (newval == '' || valuestr.value == newval) return;
valuestr.value = newval
setViewPosition()
})
onBeforeMount(() => {
valuestr.value = props.hexValue
})
onBeforeUnmount(() => {
// #ifdef WEB
document.body.removeEventListener('mousemove', mmMove)
document.body.removeEventListener('mouseup', mmEnd)
document.body.removeEventListener('mouseleave', mmEnd)
// #endif
})
onMounted(() => {
clearTimeout(tid)
tid = setTimeout(function () {
setViewPosition()
}, 60);
})
</script>
<style scoped>
.bar {
border: 3px solid white;
background-color: transparent;
position: absolute;
top: 0px;
pointer-events: none;
/* #ifdef WEB||MP */
box-sizing: border-box;
/* #endif */
}
.barPlace {
pointer-events: none;
}
.box0,
.box1 {
margin-bottom: 15px;
}
.boxRight {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.barBox {
flex: 1;
margin-right: 12px;
/* #ifdef WEB||MP */
box-sizing: border-box;
/* #endif */
}
.barBoxRealWrap {
margin: 2px 2px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
position: relative;
/* #ifdef WEB||MP */
cursor: cell;
box-sizing: border-box;
/* #endif */
}
.barWrap {
flex: 1;
pointer-events: none;
/* #ifdef WEB||MP */
box-sizing: border-box;
/* #endif */
}
.barR {
background-image: linear-gradient(to right, #000000, #ff0000);
}
.barG {
background-image: linear-gradient(to right, #000000, #00ff00);
}
.barB {
background-image: linear-gradient(to right, #000000, #0000ff);
}
</style>
@@ -0,0 +1,307 @@
<template>
<view>
<x-radio-button @change="panelChange" height="32" fontSize="14" v-model="menubarId" :list="menubar"></x-radio-button>
<color-rgb-uvue :hexValue="colorStr" @change="colorChange" v-if="menubarId=='rgb'"></color-rgb-uvue>
<color-grid-uvue @change="colorChange" v-if="menubarId=='grid'&&isReadyShow"></color-grid-uvue>
<color-hue-uvue :hexValue="colorStr" @change="colorChange" v-if="menubarId=='hue'&&isReadyShow"></color-hue-uvue>
<color-alpha-uvue v-if="_showAlpha" :hexValue="aplpha" @change="colorChangeAlpha"></color-alpha-uvue>
<view v-if="menubarId=='rgb'||menubarId=='grid'" style="height:16px"></view>
<view class="xColorViewFooter">
<!--
@slot 默认插槽,用于自行布局底部,比如当你需要改造尾部时用.你也可以通过插槽来隐藏底部
@prop {string} rgba - 当前的rgbacss颜色值
-->
<slot name="default" :rgba="realColorRgbaCss">
<view class="xColorViewLeftSHapers">
<view class="xColorViewLeftSHapersLeft" :style="{backgroundColor:realColorHex}"></view>
<view class="xColorViewLeftSHapersRight" :style="{backgroundColor:realColorHex,opacity:aplpha}">
</view>
</view>
<view class="xColorViewFooterRight">
<view @click="clickItemColor(item)" class="xColorViewFooterRightItem"
v-for="(item,index) in _colorList" :key="index" :style="{backgroundColor:item}"></view>
</view>
</slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { xConfig } from "../../config/xConfig.uts"
import colorRgbUvue from './color-rgb.uvue';
import colorGridUvue from './color-grid.uvue';
import colorHueUvue from './color-hue.uvue';
import { RADIO_BUTTON } from '../../interface';
import colorAlphaUvue from './color-alpha.uvue';
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHsl, rgbToHexNoAlpha, isValidColor, getDefaultColor } from "../../core/util/xCoreColorUtil.uts";
const i18n = xConfig.i18n;
/**
* @name 颜色选择 xColorView
* @description 精致且方便用户操作移动颜色选择容器,为你的APP增彩,兼容PC端操作,如果要把组件嵌套在弹层内时,那么在展示的时候需要延迟显示本组件。
* @page /pages/index/color-view
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xColorView" })
const props = defineProps({
/**
* 当前显示的颜色值,可以是合法的值,可以v-model双向绑定
* 如:颜色名称,hex,rgb,rgba,hsl,hsla等值
*/
modelValue: {
type: String,
default: ""
},
/**
* 输出格式:hex,rgba,hsla
* 如果输出hex,那么透明度将丢失
*/
format: {
type: String,
default: 'rgba'
},
/**
* 默认显示的面板
* hue:光谱,rgb:rgb拖动条,grid:网格面板
*/
panel: {
type: String,
default: 'hue'
},
/**
* 是否展示透明度设置
*/
showAlpha:{
type:Boolean,
default:true
},
/**
* 默认的底部颜色快速选择.
*/
colorList: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
}
})
const emits = defineEmits<{
/**
* 值变化时触发。
* @param {string} hexstr - 当前的颜色值
*/
(e : 'change', hexstr : string) : void,
/**
* 当用户切换面板时触发
* @param {string} type - 当前面板类型
*/
(e : 'panelChange', type : string) : void,
/**
* 值变化时触发。可以v-model绑定.
* @param {string} hexstr - 当前的颜色值
*/
(e : 'update:modelValue', hexstr : string) : void,
}>()
const slots = defineSlots<{
default : {
rgba : string
}
}>()
const menubarId = ref(props.panel as string)
const menubar = ref<RADIO_BUTTON[]>([
{ id: 'rgb', title: i18n.t('tmui4x.colorView.rgb') } as RADIO_BUTTON,
{ id: 'hue', title: i18n.t('tmui4x.colorView.hub') } as RADIO_BUTTON,
{ id: 'grid', title: i18n.t('tmui4x.colorView.grid') } as RADIO_BUTTON
] as RADIO_BUTTON[])
const colorStr = ref('')
const realColorArrayRgb = ref<string[]>(['0', '0', '0'])
const realColorHex = ref<string>('#000000')
const aplpha = ref<number>(1)
const isReadyShow = ref(false)
let tid = 56
const _colorList = computed(() : string[] => {
let defaults = ['#0579FF', '#002FA7', '#FF0000', '#FF4F00', '#1034A6', '#6C3082', '#1256A7', '#009B3A', '#004225', '#E3A857', '#FFDF00'] as string[]
if (props.colorList.length == 0) return defaults;
return props.colorList
})
// @ts-ignore
const _showAlpha = computed(():boolean=>props.showAlpha)
const changeFormat = (val : string) : string => {
if (val == '') {
// @ts-ignore
if (props.format == 'hex') return '#ffffff'
// @ts-ignore
if (props.format == 'rgba') return 'rgba(255,255,255,1)'
return 'hsla(0,0%,100%,1)'
}
let rgba = hexToRgb(val);
let r = rgba.getNumber('r')!
let g = rgba.getNumber('g')!
let b = rgba.getNumber('b')!
let a = rgba.getNumber('a')!
// @ts-ignore
if (props.format == 'hex') return rgbToHexNoAlpha(rgba)
// @ts-ignore
if (props.format == 'rgba') return `rgba(${r},${g},${b},${a})`
let hsl = rgbToHsl(rgba)
let h = hsl.getNumber('h')!
let s = hsl.getNumber('s')!
let l = hsl.getNumber('l')!
return `hsla(${h},${s}%,${l}%)`
}
const realColorRgbaCss = computed(() : string => {
return `rgba(${realColorArrayRgb.value[0]},${realColorArrayRgb.value[1]},${realColorArrayRgb.value[2]},${aplpha.value})`
})
const changeFormatToArr = (val : string) => {
if (val == '' || !isValidColor(val)) {
return
}
let color = hexToRgb(val);
let r = color.getNumber('r')!
let g = color.getNumber('g')!
let b = color.getNumber('b')!
let a = color.getNumber('a')!
aplpha.value = a
realColorArrayRgb.value = [r.toString(), g.toString(), b.toString()] as string[]
realColorHex.value = rgbToHexNoAlpha(color)
colorStr.value = realColorHex.value
}
const toChanges = (hexstr : string, alphas : number) => {
let rgba = hexToRgb(hexstr)
let r = rgba.getNumber('r')!
let g = rgba.getNumber('g')!
let b = rgba.getNumber('b')!
let a = alphas
colorStr.value = hexstr;
let rgbastr = `rgba(${r},${g},${b},${a})`
let tostring = changeFormat(rgbastr)
emits('change', tostring)
emits('update:modelValue', tostring)
}
const colorChange = (hexstr : string) => {
if (colorStr.value == hexstr) return;
toChanges(hexstr, aplpha.value)
}
const panelChange = (panelId:string,panelIndex:number)=>{
emits("panelChange",panelId)
}
const colorChangeAlpha = (hexAlpha : number) => {
if (hexAlpha == aplpha.value) return;
aplpha.value = hexAlpha;
toChanges(colorStr.value, hexAlpha)
}
const clickItemColor = (selectedColor : string) => {
let hexstr = getDefaultColor(selectedColor)
changeFormatToArr(hexstr)
toChanges(realColorHex.value, aplpha.value)
}
onBeforeMount(() => {
// @ts-ignore
colorStr.value = getDefaultColor(props.modelValue)
changeFormatToArr(colorStr.value)
})
// @ts-ignore
watch(() : string => props.modelValue, (newval : string) => {
if (colorStr.value == newval) return;
changeFormatToArr(getDefaultColor(newval))
})
// @ts-ignore
watch(() : string => props.panel, (newval : string) => {
if (menubarId.value == newval) return;
menubarId.value = newval;
})
onMounted(()=>{
// #ifdef APP
clearTimeout(tid)
tid = setTimeout(function() {
isReadyShow.value = true;
}, 150);
// #endif
// #ifndef APP
isReadyShow.value = true;
// #endif
})
onBeforeUnmount(()=>{
// #ifdef APP
clearTimeout(tid)
// #endif
})
defineExpose({
/**
* 获取当前颜色的透明度
*/
getAlpha():number{
return aplpha.value
},
/**
* 获取当前颜色,含alpha通道
*/
getColor():string{
return colorStr.value
},
/**
* 获取当前颜色,不含alpha通道
*/
getColorNoAlpha():string{
return rgbToHexNoAlpha(hexToRgb(colorStr.value))
}
})
</script>
<style scoped>
.xColorViewFooterRight {
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: wrap;
transform: translateY(-8px);
}
.xColorViewFooter {
margin-top: 8px;
display: flex;
flex-direction: row;
}
.xColorViewLeftSHapers {
width: 64px;
height: 64px;
display: flex;
flex-direction: row;
/* margin-right: 20px; */
border-radius: 32px;
overflow: hidden;
border: 1px solid #E1E4EE;
}
.xColorViewLeftSHapersLeft {
width: 31px;
height: 62px;
border-radius:32px 0 0 32px;
}
.xColorViewLeftSHapersRight {
width: 31px;
height: 62px;
border-radius:0 32px 32px 0;
}
.xColorViewFooterRightItem {
border-radius: 32px;
margin-left: 16px;
margin-top: 8px;
width: 25px;
height: 25px;
border: 1px solid #E1E4EE;
}
</style>
@@ -0,0 +1,298 @@
<script lang="ts" setup>
import { type PropType, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type TIME_OBJ = {
ms : string,
ss : string,
mm : string,
hh : string,
dd : string,
format : string
}
/**
*
* @name 倒计时 xCountdown
* @description 倒计时,可以精确到秒,毫秒,记住
* @page /pages/index/countdown
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xCountdown"})
defineSlots<{
default(props: {
status: "initial" | "running" | "paused" | "finished",
time: number,
label: string,
ms: string,
ss: string,
mm: string,
hh: string,
dd: string,
}): any
}>()
const emits = defineEmits([
/**
* 时间变化时触发
* @param {number} time - 当前剩余的时间
*/
"change",
/**
* 暂停时触发
*/
'pause',
/**
* 开始时触发
*/
'start',
/**
* 结束时触发
*/
'complete'
])
type xCountdownPropsType = {
/** */
time: number,
/**
* 指令,可以通过变动此值来达到暂停,开始,结束的功能,当然也可以通过ref方法控制。"pause" | "play" | "reset" | ""
*/
actions: "pause" | "play" | "reset" | "",
/**
* 显示格式
* DD天,HH时,MM分,SS秒,MS毫秒
*/
format: string,
autoStart: boolean,
/**
* 以秒还是毫秒为单位到计时。
* ss|ms
*/
unit: "ss" | "ms",
/** 文本大小 */
fontSize: string,
/** 文本颜色 */
color: string,
/**
* 是否使用验证码模式
* 统一倒计时实例
*/
captcha: boolean
}
const props = withDefaults(defineProps<xCountdownPropsType>(), {
time: 0,
actions: "",
format: "DD天HH时MM分SS秒",
autoStart: false,
unit: "ss",
fontSize: "16",
color: "#333333",
captcha: false
})
// 状态
const intervalId = ref<number>(0)
const status = ref<"initial" | "running" | "paused" | "finished">("initial")
const totalTime = ref<number>(0)
function displayTime(): TIME_OBJ {
let milliseconds = totalTime.value;
let seconds = Math.floor(milliseconds / 1000);
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
milliseconds %= 1000;
seconds %= 60;
minutes %= 60;
hours %= 24;
let day_str = days < 10 ? "0" + days.toString() : days.toString()
let hours_str = hours < 10 ? "0" + hours.toString() : hours.toString()
let minutes_str = minutes < 10 ? "0" + minutes.toString() : minutes.toString()
let seconds_str = seconds < 10 ? "0" + seconds.toString() : seconds.toString()
let milliseconds_str = milliseconds < 10 ? "0" + milliseconds.toString() : milliseconds.toString()
let formattedTime = props.format.replace("DD", day_str)
formattedTime = formattedTime.replace("HH", hours_str)
formattedTime = formattedTime.replace("MM", minutes_str)
formattedTime = formattedTime.replace("SS", seconds_str)
formattedTime = formattedTime.replace("MS", milliseconds_str)
return {
ms: milliseconds_str,
ss: seconds_str,
mm: minutes_str,
hh: hours_str,
dd: day_str,
format: formattedTime
} as TIME_OBJ;
}
// 计算属性
const _label = computed((): TIME_OBJ => {
return displayTime();
})
const _time = computed((): number => props.time)
const _color = computed((): string => getDefaultColor(props.color))
const _fontSize = computed((): string => checkIsCssUnit(props.fontSize, xConfig.unit))
// 工具函数与业务函数(被调用者在前)
function setCactcha(): void {
if (!props.captcha) return;
uni.setStorageSync("timeidLasttime", totalTime.value)
}
/**
* 开始
*/
function start(): void {
let intms = props.unit == "ss" ? 1000 : 16
if (status.value === "initial" || status.value === "paused") {
status.value = "running";
/** 开始时触发 */
emits("start")
setCactcha()
intervalId.value = setInterval(() => {
totalTime.value -= intms;
if (props.captcha) {
uni.setStorageSync("timeidLasttime", totalTime.value)
}
/**
* 时间变化时触发
* @param time {number} 当前剩余的时间
*/
emits("change", totalTime.value)
if (totalTime.value < 0) {
totalTime.value = 0
if (props.captcha) {
uni.setStorageSync("timeidLasttime", totalTime.value)
}
clearInterval(intervalId.value);
status.value = "finished"; // 倒计时结束
/** 结束时触发 */
emits("complete")
return;
}
}, intms);
}
}
/**
* 暂停
*/
function pause(): void {
if (status.value === "running") {
clearInterval(intervalId.value);
status.value = "paused"; // 暂停倒计时
setCactcha()
/** 暂停时触发 */
emits("pause")
}
}
/**
* 重置
*/
function reset(): void {
clearInterval(intervalId.value);
status.value = "initial"; // 重置倒计时为未开始状态
totalTime.value = _time.value
if (props.captcha) {
uni.setStorageSync("timeidLasttime", 0)
}
}
/**
* 获取当前运行状态
*/
function getStatus(): "initial" | "running" | "paused" | "finished" {
return status.value
}
// 生命周期
onMounted(() => {
totalTime.value = _time.value;
if (props.captcha) {
const oldTime = uni.getStorageSync("timeidLasttime")
if (oldTime != null && typeof oldTime == 'number') {
if (oldTime > 0) {
totalTime.value = oldTime;
start();
return;
}
}
}
if (props.autoStart) {
start();
}
})
onBeforeUnmount(() => {
clearInterval(intervalId.value);
if (status.value == 'running' || status.value == 'paused') {
setCactcha()
}
})
watch((): number => props.time, (): void => {
reset();
})
watch((): any => props.actions, (act:"pause" | "play" | "reset" | ""): void => {
if (act == 'reset') {
reset();
} else if (act == 'play') {
start();
} else if (act == 'pause') {
pause();
}
})
// 暴露可选方法
defineExpose({
/**
* 开始
*/
start,
/**
* 暂停
*/
pause,
/**
* 重置
*/
reset,
/**
* 获取当前运行状态,返回值是:"initial" | "running" | "paused" | "finished"
*/
getStatus
})
</script>
<template>
<view>
<!--
@slot 插槽
@prop {string} status - 状态值可能为:"initial" | "running" | "paused" | "finished"
@prop {number} time - 当前剩余的时间:单位为毫秒
@prop {string} label - 当前被属性format格式化后的文本
@prop {string} ms - 剩余的毫秒数
@prop {string} ss - 剩余的秒数
@prop {string} mm - 剩余的分钟
@prop {string} hh - 剩余的小时
@prop {string} dd - 剩余的天数
-->
<slot :status="status" :time="totalTime" :label="_label.format" :ms="_label.ms" :ss="_label.ss" :mm="_label.mm"
:hh="_label.hh" :dd="_label.dd">
<text :style="{color:_color,fontSize:_fontSize}">{{_label.format}}</text>
</slot>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,507 @@
<script lang="ts">
import { type PropType } from "vue"
import { getUid, setPagePullRefresh, getPagePullRefresh } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xDate, xDateTypeTime, createDate } from "../../core/util/xDate.uts"
import { xConfig } from "../../config/xConfig.uts"
import { PICKER_ITEM_INFO } from "../../interface.uts"
type coverValue = {
value : string[][],
str : string
}
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
/**
* @name 嵌入式日期选择器 xDateView
* @description 内嵌日期选择,可以控制显示精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
* 使用时,建议不要显示过多年份以防卡太多数据。
* @page /pages/index/picker-date
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
let startValue = new xDate();
let endValue = new xDate();
startValue.subtraction(1, 'y')
return {
nowValue: [] as string[][],
nowValueStr: '',
startDate: startValue,
endDate: endValue,
dateList: [] as PICKER_ITEM_INFO[][],
changeIndex: 0,
nowPull: false
}
},
emits: [
/**
* 滑动变换时触发
* @param {string} date - 当前选中时间
*/
'change',
/**
* 经格式化后的值。等同v-model:model-str
*/
'update:modelStr',
'update:modelValue'
],
props: {
/**
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
* 否则报错,无法运行。可以提供以下合法格式:
* YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
*/
modelValue: {
type: String,
default: ""
},
/**
* 当前时间经过format格式化后输出的值。
* 此值不会处理输入,只输出显示。
*/
modelStr: {
type: String,
default: ""
},
/**
* 顶部标题
*/
title: {
type: String,
default: "请选择时间"
},
/**
* 开始时间,请提供正确的时间格式
*/
start: {
type: String,
default: ""
},
/**
* 结束时间,请提供正确的时间格式
*/
end: {
type: String,
default: ""
},
/**
* 精确到的级别
* year:年
* month:年月
* day:年月日
* hour:年月日小时
* minute:年月日小时分钟
* second:年月日小时分钟秒
*/
type: {
type: String as PropType<ModelType>,
default: "day"
},
/**
* 输出时间格式,只对v-model:modelStr有效
* 如果桢同步对vmodel:modelValue有效需要设置formatSyncValue为true
* 有效格式:
* YYYY年
* MM月
* DD日
* hh小时
* mm分钟
* ss秒
*/
format: {
type: String,
default: "YYYY-MM-DD"
},
/**
* 是否将format格式化的v-model:modelStr同步到v-model:modelValue
* 默认false,注意:如果开启了同步,你要确保format的值是正常的时间值
* 正常兼容以下时间格式:
* YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
*/
formatSyncValue:{
type:Boolean,
default:false
},
/**
* 上方的单位名称,'年', '月', '日', '时', '分', '秒'
*/
cellUnits: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
},
computed: {
_cellUnits():string[]{
if(this.cellUnits.length==0){
return [
this!.i18n.t("tmui4x.pickerDate.year"),
this!.i18n.t("tmui4x.pickerDate.month"),
this!.i18n.t("tmui4x.pickerDate.day"),
this!.i18n.t("tmui4x.pickerDate.hour"),
this!.i18n.t("tmui4x.pickerDate.minute"),
this!.i18n.t("tmui4x.pickerDate.second"),
]
}
return this.cellUnits;
},
_start_date() : xDate {
if (this.start == "") return this.startDate
return new xDate(this.start)
},
_end_date() : xDate {
if (this.end == "") return this.endDate
return new xDate(this.end)
},
_getDateType():xDateTypeTime{
let isType = 's' as xDateTypeTime
if (this.type == 'year') isType = 'y'
if (this.type == 'month') isType = 'm'
if (this.type == 'day') isType = 'd'
if (this.type == 'hour') isType = 'h'
if (this.type == 'minute') isType = 'M'
return isType;
}
},
watch: {
modelValue(newvalue : string) {
if (newvalue == '') return;
let isType = this._getDateType
if (new xDate(newvalue).isBetweenOf(new xDate(this.nowValueStr), '=', isType)) return;
this.defaultModelvalue(newvalue,true)
}
},
mounted() {
this.nowPull = getPagePullRefresh()
let nowValue = new xDate(this.modelValue)
this.defaultModelvalue(nowValue.format('YYYY-MM-DD'), this.modelValue != '')
},
methods: {
defaultModelvalue(newvalue:string, showStr : boolean){
let isType = this._getDateType
let nowValue = new xDate(newvalue)
if(nowValue.isBetweenOf(this._start_date, '<=', isType)){
nowValue = this._start_date;
}
if(nowValue.isBetweenOf(this._end_date, '>=', isType)){
nowValue = this._end_date;
}
let stp = this.getRangByDateTime(nowValue)
this.nowValue = stp.value;
this.nowValueStr = stp.str;
this.dateList = this.getTimeTreeByStartAndEnd(this._start_date, this._end_date)
/**
* 经格式化后的值。等同v-model:model-str
*/
this.$emit('update:modelStr', this.formatTimeDate());
},
getRangByDateTime(d : xDate) : coverValue {
let nowRange = [
[d.getYear().toString()],
[(d.getMonth()).toString()],
[d.getDate().toString()],
[d.getHours().toString()],
[d.getMinutes().toString()],
[d.getSeconds().toString()],
] as string[][]
let nowRangeStr = d.getYear().toString() + "-" + (d.getMonth() + 1).toString()
+ "-" + d.getDate().toString()
+ " " + d.getHours().toString() + ":"
+ d.getMinutes().toString() + ":"
+ d.getSeconds().toString()
return {
value: nowRange as string[][],
str: nowRangeStr
} as coverValue
},
getNowTypeLenIndex() : number {
let index = 6
if (this.type == 'year') {
index = 1;
} else if (this.type == 'month') {
index = 2;
} else if (this.type == 'day') {
index = 3;
} else if (this.type == 'hour') {
index = 4;
} else if (this.type == 'minute') {
index = 5;
}
return index;
},
indexToHex(i : number) : string {
let n = i.toString()
if (n.length == 1) return '0' + n;
return n;
},
getTimeTreeByStartAndEnd(start : xDate, end : xDate) : PICKER_ITEM_INFO[][] {
let startCopy = start.getClone();
let nowDate = new xDate(this.nowValueStr)
let endCopy = end.getClone();
let years = [] as PICKER_ITEM_INFO[];
let months = [] as PICKER_ITEM_INFO[];
let days = [] as PICKER_ITEM_INFO[];
let hours = [] as PICKER_ITEM_INFO[];
let minutes = [] as PICKER_ITEM_INFO[];
let seconds = [] as PICKER_ITEM_INFO[];
for (let i = startCopy.getYear(); i <= endCopy.getYear(); i++) {
years.push({
id: i.toString(),
title: this.indexToHex(i)
} as PICKER_ITEM_INFO)
}
let _this = this;
function getD(type : string, s : number, n : number) {
if (type == 'm') {
for (let i = s; i <= n; i++) {
months.push({
id: i.toString(),
title: _this.indexToHex(i + 1)
} as PICKER_ITEM_INFO)
}
} else if (type == 'd') {
for (let i = s; i <= n; i++) {
days.push({
id: i.toString(),
title: _this.indexToHex(i)
} as PICKER_ITEM_INFO)
}
} else if (type == 'h') {
for (let i = s; i <= n; i++) {
hours.push({
id: i.toString(),
title: _this.indexToHex(i)
} as PICKER_ITEM_INFO)
}
} else if (type == 'M') {
for (let i = s; i <= n; i++) {
minutes.push({
id: i.toString(),
title: _this.indexToHex(i)
} as PICKER_ITEM_INFO)
}
} else if (type == 's') {
for (let i = s; i <= n; i++) {
seconds.push({
id: i.toString(),
title: _this.indexToHex(i)
} as PICKER_ITEM_INFO)
}
}
}
function getDnumber(type : xDateTypeTime, target : xDateTypeTime) : number[] {
let st = 0;
let et = 0
// 不包含起始, 开始和结束内。
if (nowDate.isBetween(startCopy, endCopy, type, '()')) {
if (target == 'm') {
st = 0
et = 11
} else if (target == 'd') {
st = 1
et = nowDate.getMonthCountDay()
} else if (target == 'h') {
st = 0
et = 23
} else if (target == 'M' || target == 's') {
st = 0
et = 59
}
//
} else {
// 开始和结束相等
if (startCopy.isBetweenOf(endCopy, '=', type)) {
if (target == 'm') {
st = startCopy.getMonth()
et = endCopy.getMonth()
} else if (target == 'd') {
st = startCopy.getDate()
et = endCopy.getDate()
} else if (target == 'h') {
st = startCopy.getHours()
et = endCopy.getHours()
} else if (target == 'M') {
st = startCopy.getMinutes()
et = endCopy.getMinutes()
} else if (target == 's') {
st = startCopy.getSeconds()
et = endCopy.getSeconds()
}
} else if (nowDate.isBetweenOf(startCopy, '<=', type)) {
if (target == 'm') {
st = startCopy.getMonth()
et = 11
} else if (target == 'd') {
st = startCopy.getDate()
et = startCopy.getMonthCountDay()
} else if (target == 'h') {
st = startCopy.getHours()
et = 23
} else if (target == 'M') {
st = startCopy.getMinutes()
et = 59
} else if (target == 's') {
st = startCopy.getSeconds()
et = 59
}
} else if (nowDate.isBetweenOf(endCopy, '>=', type)) {
if (target == 'm') {
st = 0
et = endCopy.getMonth()
} else if (target == 'd') {
st = 1
et = endCopy.getDate()
} else if (target == 'h') {
st = 0
et = endCopy.getHours()
} else if (target == 'M') {
st = 0
et = endCopy.getMinutes()
} else if (target == 's') {
st = 0
et = endCopy.getSeconds()
}
}
}
return [st, et] as number[]
}
let maxlen = this.getNowTypeLenIndex();
if (maxlen > 1) {
let sdate = getDnumber('y', 'm')
getD('m', sdate[0]!, sdate[1]!)
}
if (maxlen > 2) {
let sdate = getDnumber('m', 'd')
getD('d', sdate[0]!, sdate[1]!)
}
if (maxlen > 3) {
let sdate = getDnumber('d', 'h')
getD('h', sdate[0]!, sdate[1]!)
}
if (maxlen > 4) {
let sdate = getDnumber('h', 'M')
getD('M', sdate[0]!, sdate[1]!)
}
if (maxlen > 5) {
let sdate = getDnumber('m', 's')
getD('s', sdate[0]!, sdate[1]!)
}
return [years, months, days, hours, minutes, seconds].slice(0, this.getNowTypeLenIndex()) as PICKER_ITEM_INFO[][];
},
getRangNumber(start : number, end : number) : string[] {
let iar = [] as string[];
for (let i = start; i <= end; i++) {
iar.push(i.toString());
}
return iar;
},
stringArValuCoverToString() : string {
if (this.nowValue.length != 6) return "";
let newsday = new xDate(this.nowValue[0][0] + "-" + (parseInt(this.nowValue[1][0]) + 1).toString() + "-1")
let days = parseInt(this.nowValue[2][0])
days = days >= newsday.getMonthCountDay() ? newsday.getMonthCountDay() : days
this.nowValue.splice(2, 1, [days.toString()])
return this.fillNumber(this.nowValue[0][0]) + "-" + this.fillNumber((parseInt(this.nowValue[1][0]) + 1).toString())
+ "-" + this.fillNumber(this.nowValue[2][0])
+ " " + this.fillNumber(this.nowValue[3][0]) + ":"
+ this.fillNumber(this.nowValue[4][0]) + ":"
+ this.fillNumber(this.nowValue[5][0])
},
fillNumber(n : string) : string {
if (parseInt(n) > 9) return n;
return "0" + n
},
formatTimeDate() : string {
if (this.nowValue.length != 6) return "";
let sp = this.format;
sp = sp.replace(/YYYY/g, this.fillNumber(this.nowValue[0][0]))
sp = sp.replace(/MM/g, this.fillNumber((parseInt(this.nowValue[1][0]) + 1).toString()))
sp = sp.replace(/DD/g, this.fillNumber(this.nowValue[2][0]))
sp = sp.replace(/hh/g, this.fillNumber(this.nowValue[3][0]))
sp = sp.replace(/mm/g, this.fillNumber(this.nowValue[4][0]))
sp = sp.replace(/ss/g, this.fillNumber(this.nowValue[5][0]))
return sp;
},
mchange(ids : string[], index : number) {
this.nowValue.splice(index, 1, ids)
this.nowValueStr = this.stringArValuCoverToString()
/**
* 滑动变换时触发
* @param {string} date 当前选中时间
*/
this.$emit('change', this.nowValueStr)
this.dateList = this.getTimeTreeByStartAndEnd(this._start_date, this._end_date)
this.$forceUpdate()
this.onConfirm()
},
onTouchstart() {
setPagePullRefresh(false)
},
onTouchend() {
setPagePullRefresh(this.nowPull)
},
onConfirm() {
const syncValue:string = this.formatSyncValue?this.formatTimeDate():(toRaw(this.nowValueStr) as string)
/**
* 点击确认时同步。等同v-model
*/
this.$emit('update:modelValue', syncValue);
/**
* 经格式化后的值。等同v-model:model-str
*/
this.$emit('update:modelStr', this.formatTimeDate());
}
},
}
</script>
<template>
<view class="xPickerDateWrap" @touchstart="onTouchstart" @touchend="onTouchend" @touchcancel="onTouchend">
<x-picker-view :cellUnits="[_cellUnits[index]]" @change="mchange($event as string[],index)"
:model-value="nowValue[index]" v-for="(item,index) in dateList" :key="index" style="flex: 1;"
:list="item"></x-picker-view>
</view>
</template>
<style scoped>
.xPickerDateWrap {
display: flex;
flex-direction: row;
}
</style>
@@ -0,0 +1,105 @@
<script lang="ts">
import { type PropType } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit,getUid } from "../../core/util/xCoreUtil.uts"
import { xRequestCall } from "../../core/util/config.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xTableColumns,xRequestHistoryType } from "../..//interface.uts"
/**
* @name 开发组件 xDevtool
* @page /pages/index/xrequest
* @category 展示组件
* @description 需要配合xRequest库,并打开dev模式,才可记录和使用。开发者可以将此组件预埋在某个页面,通过后台请求是否打开 和显示开发模式。这样在app发布后,通过远程调试也可打开请求记录等一些实质的数据。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
columns: [
{
title: '接口',
key: 'api',
width: "30%"
} as xTableColumns,
{
title: '状态',
key: 'status',
width: "20%"
} as xTableColumns,
{
title: '时间',
key: 'time',
width: "30%",
desc: true
} as xTableColumns,
{
title: '结果',
key: 'result',
width: '100%'
} as xTableColumns
] as xTableColumns[],
}
},
computed: {
hostUrl() : string {
return xRequestCall.hostUrl
},
list():UTSJSONObject[]{
let ls = xRequestCall.history.map((el:xRequestHistoryType):UTSJSONObject=>{
return {
api:el.api,
status:el.status,
time:el.time,
result:el.result
} as UTSJSONObject
})
return ls
},
_headBgColor():string{
return xConfig.dark=='dark'?'#333':'#eee'
}
}
}
</script>
<template>
<x-float-drawer size="20%" :only-header="true" :disabledScroll="true">
<template v-slot:default="{show,height}">
<view class="xDevHeader" :style="{backgroundColor:_headBgColor}">
<x-text font-size="12" class="xDevHeaderLeft">请求域名</x-text>
<view class="xDevHeaderRightWrap">
<x-text font-size="12" class="xDevHeaderRight">{{hostUrl}}</x-text>
</view>
</view>
<x-divider></x-divider>
<view style="flex:1">
<x-table :columns="columns" cell-height="100rpx" :list="list" height="100%" max-height="800rpx"></x-table>
</view>
</template>
</x-float-drawer>
</template>
<style scoped>
.xDevHeader {
padding: 12px 12px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.xDevHeaderRightWrap{
flex:1;
margin-left: 16px;
}
.xDevHeaderLeft {
font-size: 12px;
}
.xDevHeaderRight {
font-size: 12px;
}
</style>
@@ -0,0 +1,136 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
*
* @name 分割线 xDivider
* @description 横和竖向,内容左,中,右。
* @page /pages/index/divider
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xDivider"})
type alignType = "left" | "right" | "center"
type modelType = "solid" | "dotted"
type xDividerPropsType = {
/**
* 对齐方式
*/
align: alignType,
/**
* 文本
*/
label: string,
/**
* 线的颜色
*/
color: string,
/**
* 线的暗黑颜色,如果不提供取全局的borderDarkColor
*/
darkColor: string,
/**
* 线粗细度。
*/
lineWidth: string,
/**
* 竖向时的高度
*/
height: string,
/**
* 文本颜色
*/
labelColor: string,
/**
* 线条样式
*/
model: modelType,
/**
* 字体大小
*/
fontSize: string,
/**
* 是否是竖向
*/
vertical: boolean
}
const props = withDefaults(defineProps<xDividerPropsType>(), {
align: "center",
label: "",
color: "#e5e5e5",
darkColor: "",
lineWidth: "1",
height: "10",
labelColor: "#a2a2a2",
model: "solid",
fontSize: "11",
vertical: false
})
// 响应式数据
const id = ref("xDivider" + Date.now())
// 计算属性
const _label = computed((): string => props.label)
const _lineWidth = computed((): string => checkIsCssUnit(props.lineWidth, xConfig.unit))
const _fontSize = computed((): string => checkIsCssUnit(props.fontSize, xConfig.unit))
const _height = computed((): string => checkIsCssUnit(props.height, xConfig.unit))
const _color = computed((): string => {
if(xConfig.dark == 'dark'){
if(props.darkColor != '') return getDefaultColor(props.darkColor)
return xConfig.borderDarkColor
}
return getDefaultColor(props.color)
})
const _model = computed((): string => props.model)
const _labelColor = computed((): string => getDefaultColor(props.labelColor))
const _vertical = computed((): boolean => props.vertical)
</script>
<template>
<view class="xDivider"
:style="{height:_vertical?_height:'auto','border-left':_vertical?`${_lineWidth} ${_model} ${_color}`:'none'}">
<view v-if="!_vertical" class="xDividerLeft"
:style="{flex:align=='left'?1:6,'border-bottom':`${_lineWidth} ${_model} ${_color}`}"></view>
<!--
@slot 默认文本插槽。建议通过属性label填写,如果你有特殊要求可以
在插槽中自定义样式和布局。
-->
<slot>
<text v-if="_label!=''&&!_vertical" class="xDividerText"
:style="{color:_labelColor,fontSize:_fontSize}">{{_label}}</text>
</slot>
<view v-if="!_vertical" class="xDividerRight"
:style="{flex:align=='right'?1:6,'border-bottom':`${_lineWidth} ${_model} ${_color}`}"></view>
</view>
</template>
<style>
.xDivider {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.xDividerText {
padding: 0 24rpx;
}
.xDividerLeft {
flex: 6;
}
.xDividerRight {
flex: 6;
}
</style>
@@ -0,0 +1,249 @@
<script lang="ts">
import { type PropType, Ref } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHILDREN_INFO,CHILDREN_SIZE } from "../x-drag/interface.uts"
/**
* @name 拖拽排序子组件 xDragItem
* @description 仅可放置在父容器x-drag中,如果在组件上写style时,不可写left,top,width,height等属性来影响组件的高和宽。
* 也不可直接写padding,margin来影响组件的位置。你可以在组件中自己写view后,再自由的布局写间隙等。
* @page /pages/index/drag
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: ('xDragItem-' + getUid()) as string,
cellHeight: 0,
cellWidth: 0,
nowActiveIndex: -1,
targetActiveIndex: -1,
orderIndex: -1,
nowId: ''
}
},
props: {
/**
* 索引,vfor时提供index,必填
* 而且一定是正确的索引列表顺序,不可随便填写。
*/
order: {
type: Number,
default: 0,
required: true
},
/**
* 是禁用本项目被拖动,禁用时本项目顺序会被固定。不会被打乱。
*/
disabled:{
type:Boolean,
default:false
}
},
emits: [
/**
* 点击时触发
*/
"click"
],
mounted() {
this.orderIndex = this.order
let _this = this;
uni.$on("onResize",this.getNodes)
// #ifdef APP-ANDROID||MP||WEB
this.getNodes()
// #endif
// #ifdef APP-IOS
setTimeout(function() {
_this.getNodes()
}, 20);
// #endif
// #ifdef APP-HARMONY
setTimeout(function() {
_this.getNodes()
}, 60);
// #endif
},
inject: {
XDRAGE_HEIGHT: {
type: String,
default: '0px'
},
XDRAGE_COL: {
type: Number,
default: 0
},
XDRAGE_MAX_LEN: {
type: Number,
default: 0
},
},
computed: {
_height() : string {
return this.XDRAGE_HEIGHT
},
_width() : string {
return (100 / this.XDRAGE_COL).toString() + '%'
},
_defaultTop() : number {
let rowIndex = Math.floor(this.orderIndex / this.XDRAGE_COL);
return this.cellHeight * rowIndex
},
_defaultLeft() : number {
let colindex = this.orderIndex % this.XDRAGE_COL;
return this.cellWidth * colindex
},
_disabled() : boolean {
return this.disabled
}
},
watch:{
disabled(){
this.getNodes()
}
},
beforeUnmount() {
// @ts-ignore
let parent : XDragComponentPublicInstance | null = null;
try {
// @ts-ignore
parent = this.$parent as XDragComponentPublicInstance
} catch (e) {
}
if (parent != null) {
parent!.delItem(this.id)
}
uni.$off("onResize",this.getNodes)
},
methods: {
onClick() {
this.$emit("click")
console.log(8)
},
getNodes() {
let ele = this.$refs['xDragItem'] as UniElement|null;
if(ele == null) return;
ele.getBoundingClientRectAsync()
?.then(rect=>{
this.cellHeight = rect.height!
this.cellWidth = rect.width!
this.pushChildren(rect)
this.setStylSetProperty('height',this._height)
this.setStylSetProperty('width',this._width)
this.setStylSetProperty('top',this._defaultTop+'px')
this.setStylSetProperty('left',this._defaultLeft+'px')
this.setStylSetProperty('z-index','1')
this.setStylSetProperty('transition-duration','0s')
})
.catch(()=>{})
},
pushChildren(node : DOMRect) {
// @ts-ignore
let parent : XDragComponentPublicInstance | null = null;
try {
// @ts-ignore
parent = this.$parent as XDragComponentPublicInstance
} catch (e) {
}
if (parent != null) {
parent!.addItem({
id: this.id,
index: this.order,
oldindex: this.order,
ele: this,
disabled:this._disabled,
node: node
} as CHILDREN_INFO)
}
},
updateForce() {
this.$forceUpdate()
},
setOrderIndex(index : number) {
this.orderIndex = index;
},
setActivdId(id : string) {
this.nowId = id;
},
setStylSetProperty(name:string,value:any|null){
let ele = this.$refs['xDragItem'] as UniElement
ele.style.setProperty(name,value)
},
getStylSetProperty(name:string): any | null{
let ele = this.$refs['xDragItem'] as UniElement
return ele.style.getPropertyValue(name);
},
updatePos(){
this.setStylSetProperty('height',this._height)
this.setStylSetProperty('width',this._width)
this.setStylSetProperty('top',this._defaultTop+'px')
this.setStylSetProperty('left',this._defaultLeft+'px')
this.setStylSetProperty('transition-duration','0.4s')
this.setStylSetProperty('z-index','1')
}
},
}
</script>
<template>
<view
<!-- #ifdef APP||MP -->
@click="onClick"
<!-- #endif -->
<!-- #ifdef WEB -->
@mouseup="onClick"
<!-- #endif -->
ref="xDragItem"
class="xDragItem"
:style="{
height:_height,
width:_width,
top:_defaultTop+'px',
left:_defaultLeft+'px',
zIndex:nowId==id&&nowId!=''?'5':'1',
transitionDuration:nowId!=id&&nowId!=''?'0.4s':'0s',
}">
<!--
@slot 默认插槽
-->
<slot :order="orderIndex"></slot>
</view>
</template>
<style scoped>
.xDragItem {
position: absolute;
left: 0px;
top: 0px;
transition-timing-function: ease;
transition-property: top, left;
/* #ifdef WEB */
cursor: grab;
/* #endif */
z-index: 1;
}
/* #ifdef WEB */
.xDragItem:active{
cursor: grabbing;
}
/* #endif */
</style>
@@ -0,0 +1,12 @@
export type CHILDREN_INFO = {
id:string,
index:number,
oldindex:number,
node:DOMRect,
disabled:boolean,
ele:XDragItemComponentPublicInstance
}
export type CHILDREN_SIZE = {
width:number,
height:number,
}
@@ -0,0 +1,665 @@
<script lang="ts">
import { type PropType, ref, computed } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { CHILDREN_INFO, CHILDREN_SIZE } from "../x-drag/interface.uts"
import { vibrator } from "@/uni_modules/x-vibrate-s"
type POSITION = {
col : number,
row : number,
index : number
}
type POSITION_XY = {
x : number,
y : number
}
type XDRAG_DOMRECT = {
width : number,
height : number,
left : number,
top : number,
right : number,
bottom : number,
}
/**
* @name 拖拽排序 xDrag
* @description 自由布局拖拽排序组件,列或者宫格都支持。使用时,需要将需要拖拽排序的子元素设置为x-drag-item。
* 并且子元素和父元素不允许通过style来动态设置宽高,否则拖拽排序会失效。并且list及子元素中的order不允许动态设置,否则拖拽排序会失效。
* 想要动态修改数据可以通过vif切换重新渲染下。web支持响应式屏幕。本插件:引用了原生插件x-vibrate-s
* @page /pages/index/drag
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
domlist: [] as CHILDREN_INFO[],
backckList: [] as string[],
oldList: [] as UTSJSONObject[],
activeIndex: -1,
targetIndex: -1,
isApplongStartMove:false,
cellHeight: 0,
cellWidth: 0,
isMoveing: false,
_x: 0,
_y: 0,
tid: 0,
oragie_x: 0,
oragie_y: 0,
scrollDiffTopJuli: 0,
tid2: 12,
oldStartXy: { col: 0, row: 0, index: 0 } as POSITION,
xdragRect: {
width: 0,
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
} as XDRAG_DOMRECT,
// 拖动检测相关
hasMoved: false,
moveThreshold: 1, // 移动阈值,超过这个距离才认为是拖动
startTouchX: 0,
startTouchY: 0
}
},
emits: [
/**
* 排序变动时触发
* @param {UTSJSONObject[]} list - 当前变动后的数据列表
*/
"change",
/**
* 拖动的时候触发,可以根据参数进行对你超出页面屏幕高时进行一个滚动.
* @param {number} diff - 滚动的距离向上是-diff,向下是正
*/
"move",
"end",
"start",
],
props: {
/**
* 项目的高度,不要动态更改。
*/
itemHeight: {
type: String,
default: "50"
},
/**
* 列数,默认1即列表布局,不要动态更改。
* 如果是1以上就是宫格布局了。
*/
col: {
type: Number,
default: 1
},
/**
* 你的排序数据list,变动后,请通过change事件来取得
* 主要是用来骗编译器用的。
*/
list: {
type: Array as PropType<UTSJSONObject[]>,
default: () : UTSJSONObject[] => [] as UTSJSONObject[],
required: true
},
/**
* 当组件放置在scroll页面中,如果拖动时项目需要滚动时自动滚动的进步值
* 比如组件在屏幕被底部或者顶部遮挡一部分,当拖动靠近底部时,会向下滚动的值.
* 这个值是你外部自己通过设置滚动页面的scrollTop来达成的,具体见demo,已经为你写了示例.
*/
scrollDiff: {
type: Number,
default: 25
}
},
computed: {
_rows() : number {
let row = Math.ceil(this.domlist.length / this.col)
return row
},
_cols() : number {
return this.col
},
_totalHeight() : number {
return this._rows * this.cellHeight
}
},
provide() {
return {
XDRAGE_HEIGHT: checkIsCssUnit(this.itemHeight, xConfig.unit),
XDRAGE_COL: this.col,
XDRAGE_MAX_LEN: this.list.length,
}
},
mounted() {
this.oldList = this.list.slice(0)
this.getXdrageDomRect();
},
updated() {
this.getXdrageDomRect()
},
methods: {
updataResize(resize : CHILDREN_SIZE) {
this.cellHeight = resize.height
this.cellWidth = resize.width
},
addItem(item : CHILDREN_INFO) {
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == item.id)
if (index > -1) {
this.domlist.splice(index, 1, item)
} else {
this.domlist.push(item)
}
this.cellHeight = item.node.height!;
this.cellWidth = item.node.width!;
},
delItem(id : string) {
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == id)
if (index > -1) {
this.domlist.splice(index, 1)
}
},
indexTransformer(originalIndex : number) : number {
let detail = this.targetIndex - this.activeIndex;
if (detail > 0) {
return originalIndex - 1
}
return originalIndex + 1
},
getLastMaxCol() : number {
// 计算总行数,虽然这里主要关注最后一行,但计算总行数是理解过程的一部分
const totalRows = Math.ceil(this.domlist.length / this.col);
// 计算最后一行的列数,通过求余数得到
const lastRowColumns = this.domlist.length % this.col;
// 如果最后一行是满列的(即余数为0),直接返回列数;否则返回实际的列数(余数)
return lastRowColumns === 0 ? this.col : lastRowColumns;
},
coverXy(x : number, y : number) : POSITION {
let maxRow = Math.floor((this.domlist.length - 1) / this.col);
let lastMacCol = this.getLastMaxCol();
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
// #ifdef APP||WEB
bounce = this.getByAppDomRect(el)
// #endif
let row = Math.floor((y - bounce.top!) / this.cellHeight)
let col = Math.floor((x - bounce.left!) / this.cellWidth)
row = Math.min(Math.max(0, row), maxRow)
col = Math.min(Math.max(0, col), this.col - 1)
let index = (row + 1) * this.col - (this.col - col);
col = maxRow == row ? Math.min(lastMacCol - 1, col) : col;
index = Math.min(Math.max(0, index), this.domlist.length - 1)
return {
row,
col,
index
} as POSITION
},
eventTransformer_start(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0) return;
this.backckList = this.domlist.map((el) : string => el.id)
let x = evt.x;
let y = evt.y;
const startPos = this.coverXy(x, y);
this.activeIndex = startPos.index
this.targetIndex = this.activeIndex;
this.oldStartXy = startPos
let childrenIndex = this.domlist.findIndex((el) : boolean => el.id == this.backckList[this.activeIndex])
if (childrenIndex == -1) return;
let children = this.domlist[childrenIndex]!;
let childrenEle = children!.ele
let childrenTop = parseFloat(childrenEle!.getStylSetProperty('top') as string);
let childrenLeft = parseFloat(childrenEle!.getStylSetProperty('left') as string);
childrenEle!.setStylSetProperty('transition-duration', '0s')
childrenEle!.setStylSetProperty('z-index', '5')
this._x = x - childrenLeft;
this._y = y - childrenTop;
for (let i = 0; i < t.backckList.length; i++) {
let temdom = t.backckList[i]
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
let el = t.domlist[elIndex];
el.ele!.setActivdId(children.id)
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
},
eventTransformer_move(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0 || t.backckList.length == 0) return;
let x = evt.x;
let y = evt.y;
let el = this.$refs["xDrag"] as UniElement;
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
if (childrenIndex == -1) return
let children = t.domlist[childrenIndex]!;
let childrenEle = children!.ele
let offsetY = y - t._y
let offsetX = x - t._x
childrenEle!.setStylSetProperty('top', offsetY.toString() + 'px')
childrenEle!.setStylSetProperty('left', offsetX.toString() + 'px')
childrenEle!.setStylSetProperty('transition-duration', '0s')
childrenEle!.setStylSetProperty('z-index', '5')
let target = t.coverXy(x, y)
let targetIndex = target.index
let targetChildren = t.domlist[targetIndex]!;
// 如果目标位置是禁用项目,则不进行任何位置交换操作,直接返回
if (targetChildren.disabled) {
return;
}
// 如果目标位置与当前位置相同,也不需要进行交换
if (targetIndex == t.activeIndex) {
return;
}
this.oldStartXy = target
let backChildrent = t.backckList.slice(0)[t.activeIndex];
let backTargChildren = t.backckList.slice(0)[targetIndex];
t.targetIndex = targetIndex;
let backChildrent_model = t.oldList.slice(0)[t.activeIndex];
let backTargChildren_model = t.oldList.slice(0)[t.targetIndex];
t.backckList.splice(t.activeIndex, 1, backTargChildren)
t.backckList.splice(t.targetIndex, 1, backChildrent)
t.oldList.splice(t.activeIndex, 1, backTargChildren_model)
t.oldList.splice(t.targetIndex, 1, backChildrent_model)
for (let i = 0; i < t.backckList.length; i++) {
let temdom = t.backckList[i]
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
let el = t.domlist[elIndex];
el.oldindex = el.index;
el.index = i;
el.ele!.setOrderIndex(i)
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
t.activeIndex = t.targetIndex
},
eventTransformer_end(evt : POSITION_XY) {
let t = this;
if (t.domlist.length == 0 || t.backckList.length == 0) return;
let x = evt.x;
let y = evt.y;
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
if (childrenIndex == -1) return
let children = t.domlist[childrenIndex]!;
let childrenEle = children!.ele
let result = t.coverXy(x, y);
let targetChildren = t.domlist[result.index]!;
if (targetChildren.disabled) {
result = this.oldStartXy
}
let col = result.col;
let row = result.row;
childrenEle!.setStylSetProperty('top', (this.cellHeight * row) + 'px')
childrenEle!.setStylSetProperty('left', (this.cellWidth * col) + 'px')
t.domlist.sort((ela, elb) : number => ela.index - elb.index)
for (let i = 0; i < t.domlist.length; i++) {
let temdom = t.domlist[i]
temdom.oldindex = temdom.index
temdom.index = i
temdom.ele!.setOrderIndex(i)
temdom.ele!.setActivdId("")
temdom.ele!.updateForce()
}
// #ifdef MP
t.domlist.forEach((el, index) : boolean => {
if (index != childrenIndex) {
el!.ele.updatePos()
}
})
// #endif
// this.activeIndex = -1;
// this.targetIndex = -1;
this.$emit('change', JSON.parseArray<UTSJSONObject>(JSON.stringify(this.oldList)!)!)
},
mLongStart(evt : UniTouchEvent) {
this.isApplongStartMove = true;
this.mStart(evt)
},
mStart(evt : UniTouchEvent) {
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
// 记录初始触摸位置和重置拖动状态
this.startTouchX = x;
this.startTouchY = y;
this.hasMoved = false;
this.oragie_x = x
this.oragie_y = y
let tempActiveIndex = this.coverXy(x, y).index
if (tempActiveIndex == -1) return;
let children = this.domlist[tempActiveIndex]!;
if (children.disabled) return;
this.isMoveing = true;
this.getXdrageDomRect()
this.scrollDiffTopJuli = 0
clearTimeout(this.tid2)
vibrator(100)
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
this.$emit('start')
// 暂时不阻止事件传播,等检测到真正拖动时再阻止
},
getXdrageDomRect() {
// #ifdef MP-WEIXIN
let t = this;
let el = this.$refs["xDrag"] as UniElement;
el.getBoundingClientRectAsync()
.then(res => {
t.xdragRect = {
width: res.width,
height: res.height,
left: res.left,
right: res.right,
top: res.top,
bottom: res.bottom,
} as XDRAG_DOMRECT
})
// #endif
},
getByAppDomRect(ele : UniElement) : XDRAG_DOMRECT {
let rect = {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
bottom: 0,
} as XDRAG_DOMRECT
// #ifdef APP || WEB
let res = ele.getBoundingClientRect();
rect = {
width: res.width,
height: res.height,
left: res.left,
right: res.right,
top: res.top,
bottom: res.bottom,
} as XDRAG_DOMRECT
// #endif
return rect
},
mMove(evt : UniTouchEvent) {
if (!this.isMoveing || this.activeIndex == -1) return;
let x = evt.changedTouches[0].clientX + this.scrollDiffTopJuli;
let y = evt.changedTouches[0].clientY;
// 检测是否真正发生了拖动
if (!this.hasMoved) {
let deltaX = Math.abs(x - this.startTouchX);
let deltaY = Math.abs(y - this.startTouchY);
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > this.moveThreshold) {
this.hasMoved = true;
// 一旦检测到真正拖动,就开始阻止事件传播
evt.preventDefault();
evt.stopPropagation();
} else {
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
return;
}
} else if(this.isApplongStartMove||this.hasMoved) {
// 已经确认是拖动状态,继续阻止事件传播
evt.preventDefault();
evt.stopPropagation();
}
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
// #ifdef APP||WEB
bounce = this.getByAppDomRect(el)
// #endif
let maxheight = uni.getWindowInfo().windowHeight
let diffBottom = Math.abs(bounce.bottom - maxheight)
let _this = this;
// #ifdef APP||WEB
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(() => {
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
// #endif
// #ifdef MP-WEIXIN
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(async () => {
bounce = await el.getBoundingClientRectAsync();
diffBottom = Math.abs(bounce.bottom - maxheight)
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
// #endif
},
mEnd(evt : UniTouchEvent) {
this.isApplongStartMove = false;
this.scrollDiffTopJuli = 0
clearTimeout(this.tid2)
this.$emit('end')
if (!this.isMoveing) return;
this.isMoveing = false;
// 重置拖动状态
this.hasMoved = false;
this.startTouchX = 0;
this.startTouchY = 0;
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
this.eventTransformer_end({ x: x, y: y } as POSITION_XY)
},
// #ifdef WEB
mmStart(evt : UniMouseEvent) {
let x = evt.clientX;
let y = evt.clientY;
// 记录初始鼠标位置和重置拖动状态
this.startTouchX = x;
this.startTouchY = y;
this.hasMoved = false;
let tempActiveIndex = this.coverXy(x, y).index
if (tempActiveIndex == -1) return;
let children = this.domlist[tempActiveIndex]!;
if (children.disabled) return;
this.isMoveing = true;
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
this.$emit('start')
},
mmMove(evt : UniMouseEvent) {
if (!this.isMoveing || this.activeIndex == -1) return;
let x = evt.clientX;
let y = evt.clientY;
// 检测是否真正发生了拖动
if (!this.hasMoved) {
let deltaX = Math.abs(x - this.startTouchX);
let deltaY = Math.abs(y - this.startTouchY);
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > this.moveThreshold) {
this.hasMoved = true;
// 一旦检测到真正拖动,就开始阻止事件传播
evt.preventDefault();
evt.stopPropagation();
} else {
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
return;
}
} else {
// 已经确认是拖动状态,继续阻止事件传播
evt.preventDefault();
evt.stopPropagation();
console.log(777)
}
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
let el = this.$refs["xDrag"] as UniElement;
let bounce = this.xdragRect;
bounce = this.getByAppDomRect(el)
let maxheight = uni.getWindowInfo().windowHeight
let diffBottom = Math.abs(bounce.bottom - maxheight)
let _this = this;
clearTimeout(_this.tid2)
_this.tid2 = setTimeout(() => {
// 可见区域尾部在下面,向下滚动
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
this.$emit('move', _this.scrollDiff)
_this.scrollDiffTopJuli += _this.scrollDiff
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
this.$emit('move', _this.scrollDiff * -1)
_this.scrollDiffTopJuli -= _this.scrollDiff
}
}, 200)
},
mmEnd(evt : UniMouseEvent) {
this.$emit('end')
if (!this.isMoveing) return;
this.isMoveing = false;
// 重置拖动状态
this.hasMoved = false;
this.startTouchX = 0;
this.startTouchY = 0;
this.eventTransformer_end({ x: evt.clientX, y: evt.clientY } as POSITION_XY)
}
// #endif
}
}
</script>
<template>
<!-- @longpress="mStart" @longpress="mLongStart @touchstart="mStart"" -->
<view
class="xDrag"
ref="xDrag"
<!-- #ifdef APP || MP -->
@longpress="mLongStart"
<!-- #endif -->
<!-- #ifdef H5 -->
@touchstart="mStart"
<!-- #endif -->
@touchmove="mMove"
@touchend="mEnd"
@touchcancel="mEnd"
<!-- #ifdef WEB -->
@mousedown.parent="mmStart"
@mousemove.stop="mmMove"
@mouseup.parent="mmEnd"
@mouseleave="mmEnd"
<!-- #endif -->
:style="{height:_totalHeight+'px'}">
<!--
@slot 只可放置子组件x-drag-item
-->
<slot></slot>
</view>
</template>
<style scoped>
.xDrag {
position: relative;
}
</style>
@@ -0,0 +1,985 @@
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig,xProvitae } from "../../config/xConfig.uts"
type callbackType = ()=>Promise<boolean>;
type positionType = "top" | "bottom" | "left" | "right"
/**
* @name 抽屉 xDrawer
* @description 提供四个方向的弹出。
* @page /pages/index/drawer
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xDrawer"})
const i18n = xConfig.i18n
const proxy = getCurrentInstance()?.proxy??null;
defineSlots<{
trigger(props: { show: boolean }): any
}>()
const emits = defineEmits([
/**
* 点击遮罩事件
*/
'click',
/**
* 关闭是触发
*/
'close',
/**
* 打开时触发
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 等同v-model:show
*/
'update:show',
/**
* 取消时触发
*/
'cancel',
/**
* 确认时触发
*/
'confirm'
])
type xDrawerPropsType = {
/**
* 自定义遮罩样式
*/
customStyle: string,
/**
* 自定义容器背景层样式
*/
customWrapStyle: string,
/**
* 标题
*/
title: string,
/**
* 显示底部操作栏
*/
showFooter: boolean,
/**
* 是否显示标题
*/
showTitle: boolean,
/**
* 是否显示底部关闭按钮
*/
showClose: boolean,
/**
* 遮罩是否允许点击被关闭
*/
overlayClick: boolean,
/**
* 显示可v-model:show双向绑定
*/
show: boolean,
/**
* 显示取消按钮
*/
showCancel: boolean,
/**
* 取消按钮的文本
*/
cancelText: string,
/**
* 确认按钮的文本
*/
confirmText: string,
/**
* 动画时间
*/
duration: number,
/**
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
* 此值仅在ios下生效。
*/
watiDuration: number,
/**
* 打开方向。
*/
position: positionType,
/**
* 打开方向为上和下时的圆角
* 空值时,取全局配置的圆角。
*/
round: string,
/**
* 左右时为内容宽,
* 上下时为内容高
* 百分比,数字字符或者带单位,或者为auto(根据内容自动高度或者宽高)
*/
size: string,
/**
* 弹层最大的高度值,默认为屏幕的可视高
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: string,
/**
* 背景颜色
*/
bgColor: string,
/**
* 暗黑背景颜色,如果不提供默认读取全局的sheet配置
*/
darkBgColor: string,
/**
* 遮罩的背景色
*/
overflayBgColor: string,
/**
* 是否禁用内部的scroll标签
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
* 但如果没有指定高,内容自动的话,高是自动的。
*/
disabledScroll: boolean,
/**
* 内容区域左右和下的边距。
*/
contentMargin: string,
/**
* 宽屏时是否让内容剧中显示
* 并限制其宽为屏幕宽,只展示中间内容以适应宽屏。
* 注意只有top,bottom才会生效。
*/
widthCoverCenter: boolean,
/**
* 滑动左右或者上下关闭弹出层
* 注意如果设置为0就表示关闭该功能。
* 默认drawer嵌套了scroll-view,再你滚动到顶或者底时,如果继续滑动的距离大于此值关闭层。
* 但如果你是禁用了内部scroll-view,而是采用自己的scorll-view,此时该功能会与你的滚动手势冲突,请自行考虑。
* 建议要打开时设置为80-100比较合理
*/
swiperLenClose: number,
/**
* 距离顶部的偏移量,如果你布局顶会遮罩弹层可以考虑使用此值
*/
offsetTop: string,
/**
* 距离底部的偏移量,如果你布局底会遮罩弹层可以考虑使用此值
*/
offsetBottom: string,
/**
* 弹层的层级
*/
zIndex: number,
/**
* 懒加载
* 为了解决业务布局节点超多时,你可能需要内容延迟加载以免阻塞动画流畅度.
* 如果你启用了lazy,每次打开时,动画执行后才会显示内容.这样动画就流畅,不会因为节点过多造成的卡.
* 开启了此属性后ios端前面的watiDuration属性可以不用再设置了.
*/
lazy: boolean,
/**
* 是否禁用确认按钮
*/
disabledConfirm: boolean,
/**
* 底部按钮操作的主题色,空取全局
*/
btnColor: string,
/**
* 关闭前异步执行的函数,如果返回false阻止关闭,返回true允许关闭
* 必须返回的是Promise异步函数,且类型返回值必须是Promise<boolean>,不然会报错。
*/
beforeClose: callbackType,
/**
* 关闭图标的颜色
*/
closeColor: string,
/**
* 关闭图标的暗黑颜色
*/
closeDarkColor: string
}
const props = withDefaults(defineProps<xDrawerPropsType>(), {
customStyle: "",
customWrapStyle: "",
title: "",
showFooter: false,
showTitle: true,
showClose: false,
overlayClick: true,
show: false,
showCancel: true,
cancelText: "",
confirmText: "",
duration: 300,
watiDuration: 120,
position: "bottom",
round: "",
size: "50%",
maxHeight: "",
bgColor: 'white',
darkBgColor: '',
overflayBgColor: 'rgba(0, 0, 0, 0.4)',
disabledScroll: false,
contentMargin: '16',
widthCoverCenter: false,
swiperLenClose: 0,
offsetTop: '0',
offsetBottom: '0',
zIndex: 1100,
lazy: false,
disabledConfirm: false,
btnColor: "",
beforeClose: () : Promise<boolean> => {
return Promise.resolve(true)
},
closeColor: "#e6e6e6",
closeDarkColor: "#545454"
})
// 响应式数据
const _width = ref(0)
const _height = ref(0)
const showOverflay = ref(false)
const actioning = ref(false)
const status = ref("")
const id = ref("xDrawer" + getUid())
const wrapId = ref("xDrawerWrap" + getUid())
const first = ref(true)
const tid = ref(0)
const windtop = ref(0)
const windowBottom = ref(0)
const start_move_x = ref(0)
const start_move_y = ref(0)
const move_x = ref(0)
const move_y = ref(0)
const move_end_x = ref(0)
const move_end_y = ref(0)
const scrollTop = ref(-1)
const isTopOrBottomByScroll = ref(false)
const xDrawerContentHeight = ref(0)
const safeFooterHeight = ref(0)
const lezyShowModal = ref(true)
const isOpenedDefault = ref(false)
const isLoading = ref(false)
const anitid = ref(23)
// #ifdef H5
const teleportElH5 = ref("uni-app")
const teleportTarget = ref<string | null>(null)
const getTeleportTarget = () => {
try {
if(status.value == ''||status.value=='close') return 'uni-app'
// 优先尝试 uni-page
if (document.querySelector('uni-page')) {
return 'uni-page'
}
// 优先尝试 uni-app
if (document.querySelector('uni-app')) {
return 'uni-app'
}
// 备用方案:尝试 app
if (document.querySelector('#app')) {
return '#app'
}
// 最后备用:body
return 'body'
} catch (error) {
// console.warn('Failed to get teleport target:', error)
return 'body'
}
}
// 检查teleport目标是否可用
const isTeleportTargetValid = (target: string) => {
try {
return !!document.querySelector(target)
} catch {
return false
}
}
// #endif
// 计算属性
const _disabledConfirm = computed((): boolean => props.disabledConfirm)
const _lazy = computed((): boolean => props.lazy)
const _customStyle = computed((): string => props.customStyle)
const _customWrapStyle = computed((): string => props.customWrapStyle)
const _show = computed((): boolean => props.show)
const _widthCoverCenter = computed((): boolean => props.widthCoverCenter)
const _showClose = computed((): boolean => props.showClose)
const _duration = computed((): number => props.duration)
const _position = computed((): string => props.position)
const _showTitle = computed((): boolean => props.showTitle)
const _round = computed((): string => {
let round = props.round;
if (round == "") {
round = xConfig.drawerRadius
}
let radius = checkIsCssUnit(round, xConfig.unit);
let _r = "none"
if (props.position == 'top') {
_r = `0px 0px ${radius} ${radius}`
}
if (props.position == 'bottom') {
_r = `${radius} ${radius} 0px 0px`
}
return _r
})
const _offset = computed((): number => {
let offset = checkIsCssUnit(props.offsetTop, xConfig.unit);
let x = parseFloat(offset)
let unit = getUnit(offset)
if (unit == 'rpx') {
x = uni.rpx2px(x)
}
return x;
})
const _offsetBottom = computed((): number => {
let offset = checkIsCssUnit(props.offsetBottom, xConfig.unit);
let x = parseFloat(offset)
let unit = getUnit(offset)
if (unit == 'rpx') {
x = uni.rpx2px(x)
}
return x;
})
const _size = computed((): string => checkIsCssUnit(props.size, xConfig.unit))
const _contentMargin = computed((): string => checkIsCssUnit(props.contentMargin, xConfig.unit))
const _showFooter = computed((): boolean => props.showFooter)
const _maxHeight = computed((): string => {
if (props.maxHeight == "") return ""
if (props.position == 'left' || props.position == 'right') return ""
return checkIsCssUnit(props.maxHeight, xConfig.unit);
})
const _showCancel = computed((): boolean => props.showCancel)
const _title = computed((): string => {
if(props.title==''){
return i18n.t("tmui4x.modal.title")
}
return props.title
})
const _cancelText = computed((): string => {
if(props.cancelText==''){
return i18n.t("tmui4x.cancel")
}
return props.cancelText
})
const _confirmText = computed((): string => {
if(props.confirmText==''){
return i18n.t("tmui4x.confirm")
}
return props.confirmText
})
const _animationFun = computed((): string => xConfig.animationFun)
const _bgColor = computed((): string => {
let bgcolor = props.bgColor;
if (xConfig.dark == 'dark') {
if (props.darkBgColor != '') {
}
bgcolor = props.darkBgColor != '' ? props.darkBgColor : xConfig.sheetDarkColor
}
return getDefaultColor(bgcolor)
})
const _btnColor = computed((): string => {
if(props.btnColor == '') return getDefaultColor(xConfig.color)
return getDefaultColor(props.btnColor)
})
const __height = computed((): string => {
let h = '100%';
// #ifdef WEB
h = `calc(100% - ${windtop.value}px - ${_offsetBottom.value}px)`
// #endif
// #ifdef APP || MP-WEIXIN
if (_offset.value > 0 || _offsetBottom.value > 0) {
h = (_height.value - _offsetBottom.value) + 'px'
}
// #endif
return h;
})
const _titleFontSize = computed((): string => (xConfig.fontScale * 16).toString() + 'px')
const _isDark = computed((): boolean => xConfig.dark == 'dark')
const _closeIcon = computed((): string => xConfig.closeIcon)
function onEnd() {
actioning.value = false;
if (status.value == 'close') {
showOverflay.value = false;
/**
* 关闭时执行
*/
emits('close')
/**
* 等同v-model:show
*/
emits('update:show', false)
if (_lazy.value) {
lezyShowModal.value = false
}
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
} else {
/**
* 打开执行的事件
*/
emits('open')
if (_lazy.value) {
lezyShowModal.value = true
}
}
}
function setStyleAni() {
try {
let sys = uni.getWindowInfo()
_height.value = sys.windowHeight - _offset.value
let watiDuration = 60;
// #ifdef APP-IOS
watiDuration = props.watiDuration
// #endif
if (status.value == 'open') {
showOverflay.value = true;
clearTimeout(tid.value)
tid.value = setTimeout(function () {
let element = proxy!.$refs['xDrawerWrap'] as UniElement | null
let elementWrap = proxy!.$refs['xDrawerWrapContent'] as UniElement | null
if (element== null || elementWrap == null) return;
element!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
elementWrap!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
element!.style.setProperty('opacity', 1)
elementWrap!.style.setProperty('transform', `translate(0%,0%)`)
}, watiDuration);
} else if (status.value == 'close') {
let element = proxy!.$refs['xDrawerWrap'] as UniElement | null
let elementWrap = proxy!.$refs['xDrawerWrapContent'] as UniElement | null
if (element== null || elementWrap == null) return;
element.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
elementWrap.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
element.style.setProperty('opacity', 0)
if (_position.value == 'bottom') {
elementWrap.style.setProperty('transform', `translate(0%,100%)`)
} else if (_position.value == 'top') {
elementWrap.style.setProperty('transform', `translate(0%,-100%)`)
} else if (_position.value == 'left') {
elementWrap.style.setProperty('transform', `translate(-100%,0%)`)
} else if (_position.value == 'right') {
elementWrap.style.setProperty('transform', `translate(100%,0%)`)
}
}
} catch (e) {
//TODO handle the exception
}
}
function overflayMoveTouch(evt : TouchEvent) {
evt.preventDefault();
}
function closeAlert() {
if (actioning.value) return;
if (status.value == 'close') return;
actioning.value = true;
status.value = 'close'
/**
* 关闭前执行
*/
emits('beforeClose')
setStyleAni();
anitid.value = setTimeout(function() {
onEnd()
}, _duration.value);
}
function showAlert() {
if (actioning.value) return;
if (status.value == 'open') return;
showOverflay.value = true;
actioning.value = true;
status.value = 'open'
// #ifdef WEB
teleportTarget.value = getTeleportTarget()
// #endif
/**
* 打开前执行
*/
emits('beforeOpen')
setStyleAni();
anitid.value = setTimeout(function() {
onEnd()
}, _duration.value+60);
}
function onClickOverflowy(evt : Event) {
evt.stopPropagation()
/**
* 点击遮罩事件
*/
emits("click")
if (!props.overlayClick||isLoading.value) return;
closeAlert();
}
// 方法
function cancelEvt() {
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
// #ifdef APP-IOS
actioning.value = false;
// #endif
/**
* 取消时触发
*/
emits('cancel')
closeAlert()
}
async function confirmEvt(): Promise<any> {
isLoading.value = true;
let isCanClose = await props.beforeClose()
isLoading.value = false;
if(!isCanClose){
return Promise.resolve(true)
}
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
// #ifdef APP-IOS
actioning.value = false;
// #endif
/**
* 确认时触发
*/
emits('confirm')
closeAlert()
return Promise.resolve(false)
}
function openDrawer() {
showAlert();
}
function swiperClose() {
let offsetX = move_end_x.value - start_move_x.value
let offsetY = move_end_y.value - start_move_y.value
if (props.swiperLenClose == 0 || (actioning.value && status.value == 'close')) return;
if (props.position == 'left' && offsetX < props.swiperLenClose * -1 && Math.abs(offsetX) >= Math.abs(offsetY)) {
closeAlert()
}
if (props.position == 'right' && offsetX > props.swiperLenClose && Math.abs(offsetX) >= Math.abs(offsetY)) {
closeAlert()
}
if (props.position == 'top' && offsetY < props.swiperLenClose * -1 && Math.abs(offsetY) >= Math.abs(offsetX)) {
closeAlert()
}
if (props.position == 'bottom' && offsetY > props.swiperLenClose && Math.abs(offsetY) >= Math.abs(offsetX)) {
closeAlert()
}
}
function maskerMove(evt : UniTouchEvent) {
// #ifdef WEB
evt.preventDefault()
// #endif
}
function mStart(evt : UniTouchEvent) {
if (props.swiperLenClose == 0) return;
start_move_x.value = evt.changedTouches[0].clientX
start_move_y.value = evt.changedTouches[0].clientY
}
function mMove(evt : UniTouchEvent) {
if (props.swiperLenClose == 0) return;
// #ifdef WEB
evt.preventDefault()
// #endif
if (evt.changedTouches.length == 0) return;
move_x.value = evt.changedTouches[0].clientX
move_y.value = evt.changedTouches[0].clientY
}
function mEnd(evt : UniTouchEvent) {
if (props.swiperLenClose == 0) return;
if (evt.changedTouches.length == 0) return;
let x = evt.changedTouches[0].clientX
let y = evt.changedTouches[0].clientY
move_end_x.value = x
move_end_y.value = y;
swiperClose();
}
function onScroll(evt : UniScrollEvent) {
if (props.position == 'bottom') {
if (evt.detail.scrollTop > 0) {
start_move_x.value = move_x.value
start_move_y.value = move_y.value
}
}
if (props.position == 'top') {
let ele = proxy!.$refs['xDrawerContent'] as UniElement | null;
if (ele == null) return;
let height = ele.getBoundingClientRect().height;
let maxheight = evt.detail.scrollHeight - evt.detail.scrollTop
if (evt.detail.scrollTop < maxheight - 1) {
start_move_x.value = move_x.value
start_move_y.value = move_y.value
}
}
}
function onScrollTop(evt : UniScrollToUpperEvent) {
start_move_x.value = move_x.value
start_move_y.value = move_y.value
}
function onScrollBottom(evt : UniScrollToLowerEvent) {
start_move_x.value = move_x.value
start_move_y.value = move_y.value
}
// 监听器
watch(():boolean => props.show, (newval : boolean) => {
if (newval) {
showAlert()
} else {
closeAlert()
}
})
// 生命周期
onMounted(() => {
lezyShowModal.value = _lazy.value ? false : true;
// #ifdef H5
nextTick(() => {
teleportTarget.value = getTeleportTarget()
})
// #endif
function oninitready(){
isOpenedDefault.value = true;
let sys = uni.getWindowInfo()
// #ifdef WEB
_width.value = sys.windowWidth
_height.value = sys.windowHeight;
windtop.value = sys.windowTop + _offset.value;
// #endif
// #ifdef APP|| MP-WEIXIN
_width.value = sys.windowWidth
_height.value = sys.windowHeight + 44;
windtop.value = _offset.value;
// #endif
safeFooterHeight.value = sys.safeAreaInsets.bottom == 0 ? 16 : sys.safeAreaInsets.bottom
if (_show.value) {
showAlert();
}
}
oninitready()
})
// 监听页面变化,重新获取teleport目标
onUpdated(() => {
// #ifdef H5
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
teleportTarget.value = getTeleportTarget()
}
// #endif
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
defineExpose({
/** 打开 **/
open:()=>showAlert(),
/** 关闭 **/
close:()=>closeAlert()
})
</script>
<template>
<view>
<view @click="openDrawer">
<!--
@slot 标签触发显示遮罩,免于使用变量控制
@prop {Boolean} show - 当前是否已显示
-->
<slot name="trigger" :show="show"></slot>
</view>
<!-- #ifdef H5 -->
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<root-portal >
<!-- #endif -->
<view @click="onClickOverflowy" @touchmove="maskerMove" v-if="showOverflay" :id="id" ref="xDrawerWrap"
class="xDrawerWrap" :class="[
(_position=='top'||_position=='bottom')&&_widthCoverCenter?'xDrawerWrapContentMinwidthWrapDir':'',
'xDrawerWrap_'+_position
]" :style="[{backgroundColor:overflayBgColor,width:'100%',
top:windtop+'px',
height:__height,
zIndex:zIndex,
'transition-timing-function':_animationFun},_customStyle]">
<view v-if="showOverflay&&_position == 'bottom'&&!actioning">
<!--
@slot 内容顶部的额外插槽,仅为下向上弹出(position=bottom)下才会显示。
-->
<slot name="contentTop"></slot>
</view>
<!-- @transitionend="onEnd" -->
<view @click.stop="" ref="xDrawerWrapContent" class="xDrawerWrapContent"
@touchstart="mStart" @touchmove="mMove" @touchend="mEnd" :class="[
(_position=='top'||_position=='bottom')&&_widthCoverCenter?'xDrawerWrapContentMinwidth':'',
'xDrawerWrapContent_'+_position
]" :id="wrapId"
:style="[
{
width:_position=='left'||_position=='right'?_size:'100%',
height:_position=='left'||_position=='right'?'100%':_size,
borderRadius:_round,
maxHeight:_maxHeight!=''?_maxHeight:'100%',
'transition-timing-function':_animationFun,
backgroundColor:_bgColor,
},
customWrapStyle
]">
<view v-if="_showClose" class="xDrawerXclose">
<x-icon @click="cancelEvt" :color="closeColor" :dark-color="closeDarkColor" font-size="24px"
:name="_closeIcon"></x-icon>
</view>
<view v-if="_showTitle">
<!--
@slot 标题插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="title" :show="show">
<view class="xDrawerTitleBox">
<text :style="{fontSize:_titleFontSize,color:_isDark?'white':'black',opacity:'0.64'}"
class="xDrawertitleBox">{{_title}}</text>
</view>
</slot>
</view>
<x-loading v-if="!lezyShowModal"><text></text></x-loading>
<view ref="xDrawerContent" class="xDrawerContent"
:style="{flex:'1',margin:`0px 0px ${_contentMargin} 0px`}">
<scroll-view v-if="!disabledScroll&&lezyShowModal" @scroll="onScroll"
@scrolltoupper="onScrollTop" @scrolltolower="onScrollBottom"
<!-- #ifdef APP||WEB -->
:style="{flex:'1'}"
<!-- #endif -->
<!-- #ifdef MP -->
:style="{position:'absolute',width:'100%',height:'100%'}"
<!-- #endif -->
:scroll-y="true" :rebound="false">
<view :style="{padding:`0px ${_contentMargin} 0px ${_contentMargin}`}">
<!--
@slot 默认插槽
-->
<slot name="default"></slot>
</view>
</scroll-view>
<view v-if="disabledScroll&&lezyShowModal"
:style="{flex:'1',padding:`0px ${_contentMargin} 0px ${_contentMargin}`}">
<!--
默认插槽
-->
<slot name="default"></slot>
</view>
</view>
<view v-if="showFooter&&lezyShowModal" class="xDrawerFooter" :style="{backgroundColor:_bgColor}">
<!--
@slot 底部操作栏
-->
<slot name="footer">
<view style="flex-direction: row;align-items: center;justify-content: center;display: flex;">
<x-button :disabled="isLoading" :color="_btnColor" @click="cancelEvt" v-if="_showCancel" skin="thin" width="0px" :block="true"
style="margin-right: 16rpx;flex:1">{{_cancelText}}</x-button>
<x-button :loading="isLoading" :color="_btnColor" @click="confirmEvt" width="0px" :disabled="_disabledConfirm" :block="true"
style="flex:1">{{_confirmText}}</x-button>
</view>
</slot>
<view :style="{height:safeFooterHeight+'px'}"></view>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
</root-portal>
<!-- #endif -->
<!-- #ifdef H5 -->
</teleport>
<!-- #endif -->
</view>
</template>
<style>
.xDrawerContent{
position: relative;
}
.xDrawerFooter {
width: 100%;
/* background-color: white; */
padding: 0 16px 0px 16px;
/* #ifdef MP-WEIXIN||WEB */
box-sizing: border-box;
/* #endif */
}
.xDrawerXclose {
position: absolute;
right: 16px;
top: 9px;
z-index: 100;
}
.xDrawerXcloseOutter {
padding-right: 16px;
}
.xDrawerTitleBox {
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.xDrawertitleBox {
max-width: 175px;
overflow: hidden;
lines: 1;
text-overflow: ellipsis;
}
.xDrawerWrap_bottom {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.xDrawerWrap_top {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.xDrawerWrap_left {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.xDrawerWrap_right {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.xDrawerWrapContent {
transition-duration: 350ms;
transition-property: transform;
/* background-color: white; */
display: flex;
flex-direction: column;
position: relative;
}
.xDrawerWrapContentMinwidth {
max-width: 500px;
}
.xDrawerWrapContentMinwidthWrapDir {
align-items: center;
}
.xDrawerWrapContent_bottom {
transform: translate(0%, 100%);
}
.xDrawerWrapContent_top {
transform: translate(0%, -100%);
}
.xDrawerWrapContent_left {
transform: translate(-100%, 0%);
}
.xDrawerWrapContent_right {
transform: translate(100%, 0%);
}
.xDrawerWrap {
/* background: rgba(0, 0, 0, 0.4); */
opacity: 0;
position: fixed;
left: 0;
top: 0px;
/* #ifdef APP-HARMONY */
transition-duration: 0ms;
/* #endif */
/* #ifndef APP-HARMONY */
transition-duration: 350ms;
/* #endif */
transition-property: opacity;
}
</style>
@@ -0,0 +1,270 @@
<script lang="ts">
import { checkIsCssUnit, getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { XDROPDOWN_LISTITEM_INFO_TYPE } from '../../interface.uts';
/**
*
* @name 下拉菜单子组件 xDropdownItem
* @description 注意只能放置在父组件x-dropdown-menu中
* @page /pages/index/dropdown-menu
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: ("xDropMenuItem" + getUid()) as string,
show: false,
tid: 0
}
},
props: {
/**
* 菜单标题
*/
title: {
type: String,
default: "标题"
},
/**
* 标识,变换或者点击时,会通过事件传回。
*/
keyName: {
type: String,
default: ""
},
/**
* 未选中时的图标
*/
icon: {
type: String,
default: "arrow-down-s-fill"
},
/**
* 激活时的图标
*/
activeIcon: {
type: String,
default: "arrow-up-s-fill"
},
/**
* 默认的文字及图标颜色
*/
fontColor: {
type: String,
default: "#333333"
},
/**
* 暗黑时的默认的文字及图标颜色,
* 空取时白色
*/
darkFontColor: {
type: String,
default: ""
},
/**
* 文字及图标大小
*/
fontSize: {
type: String,
default: "16"
},
/**
* 激活的文字及图标颜色
* 空值时取全局统一的主题色。
*/
activeFontColor: {
type: String,
default: ""
},
/**
* 是否是按钮选项。
*/
isBtn: {
type: Boolean,
default: false
},
/**
* 内容背景颜色
*/
color: {
type: String,
default: "white"
},
/**
* 暗黑时的内容背景颜色,空值取sheetDarkColor
*/
darkColor: {
type: String,
default: ""
},
/**
* 渲染,如果设置为true,内部使用vif切换渲染
* 注意它不会影响其它菜单只影响本菜单.vif切换会导致内容重绘,但
* 可以解决sdk的一些异常组件嵌套的问题.
* 当您嵌套的组件在此内出现异常,闪退,报错时请设置为true,否则不用理会本属性
* 本属性存在就是为了绕开sdk bug.
*/
render:{
type:Boolean,
default:false
}
},
beforeMount() {
this.pushDataToParent();
},
mounted() {
},
beforeUnmount() {
this.removeSelf();
clearTimeout(this.tid)
},
watch: {
title() { this.pushDataToParent() },
keyName() { this.pushDataToParent() },
icon() { this.pushDataToParent() },
activeIcon() { this.pushDataToParent() },
fontColor() { this.pushDataToParent() },
activeFontColor() { this.pushDataToParent() },
fontSize() { this.pushDataToParent() }
},
computed: {
_color() : string {
if (xConfig.dark == 'dark') {
if (this.darkColor != '') return getDefaultColor(this.darkColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(this.color)
},
_activeFontColor() : string {
if (this.activeFontColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(this.activeFontColor)
},
_fontColor() : string {
if (xConfig.dark == 'dark') {
if (this.darkFontColor != '') return getDefaultColor(this.darkFontColor)
return "#ffffff"
}
return getDefaultColor(this.fontColor)
},
_fontSize() : string {
return checkIsCssUnit(this.fontSize, xConfig.unit)
},
_showrender():boolean{
if(this.render){
return this.show
}
return true;
}
},
methods: {
pushDataToParent() {
let t = this;
let parent : XDropdownMenuComponentPublicInstance | null = null;
try {
parent = this.$parent as XDropdownMenuComponentPublicInstance | null
} catch (_e) {
console.error("x-dropdown-item:本组件必须放置在x-dropdown-menu中的直接子节点,不可单独或者嵌套使用。")
}
if (parent == null) return;
// #ifdef WEB
if (typeof parent?.addMenu != 'function') return;
// #endif
clearTimeout(this.tid)
this.tid = setTimeout(function () {
parent!.addMenu(t as XDropdownItemComponentPublicInstance, {
id: t.id as string,
title: t.title as string,
keyName: t.keyName as string,
icon: t.icon as string,
activeIcon: t.activeIcon as string,
fontColor: t._fontColor as string,
activeFontColor: t._activeFontColor as string,
fontSize: t._fontSize as string,
isBtn: t.isBtn as boolean
} as XDROPDOWN_LISTITEM_INFO_TYPE)
}, 5);
},
removeSelf() {
let parent : XDropdownMenuComponentPublicInstance | null = null;
try {
parent = this.$parent as XDropdownMenuComponentPublicInstance | null
} catch (_e) {
console.error("x-dropdown-item:本组件必须放置在x-dropdown-menu中的直接子节点,不可单独或者嵌套使用。")
}
if (parent != null) {
// #ifdef WEB
if (typeof parent?.delMenu != 'function') return;
// #endif
parent!.delMenu(this.id as string)
}
},
open() {
this.show = true;
},
close() {
this.show = false;
},
},
}
</script>
<template>
<!-- :class="show?'x-dropdown-itemOn':'x-dropdown-itemOff'" -->
<!-- :class="[
show&&!render?'xDrodownItemOn':'',
!show&&!render?'xDrodownItemOff':'',
]" -->
<view v-if="_showrender" @click.stop="" :ref="id" :id="id" class="x-dropdown-item" :class="show?'x-dropdown-itemOn':'x-dropdown-itemOff'">
<view class="x-dropdown-itemWrap"
:style="{backgroundColor:_color}"
>
<!--
@slot 默认插槽,弹层内容。
-->
<slot></slot>
</view>
</view>
</template>
<style scoped>
.x-dropdown-item {
transition-duration: 250ms;
transition-property: transform , opacity;
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
transform: translateY(-100%);
position: absolute;
top:0px;
left:0;
width:100%;
}
.x-dropdown-itemOn{
transform: translateY(0%);
}
.x-dropdown-itemOff{
transition-duration: 50ms;
transform: translateY(-100%);
}
.x-dropdown-itemWrap{
transition-duration: 250ms;
transition-property: height;
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
pointer-events: auto;
padding: 12px;
border-radius: 0px 0px 12px 12px;
}
.xDrodownItemOn {
display: flex;
}
.xDrodownItemOff {
display: none;
}
</style>
@@ -0,0 +1,524 @@
<script lang="ts">
import { PropType } from 'vue'
import { XDROPDOWN_LISTITEM_INFO_TYPE, NODE_INFO } from '../../interface.uts';
import { checkIsCssUnit, getUid, rpx2px, getUnit } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type XDROPDOWN_LISTITEM_TYPE = {
ele : XDropdownItemComponentPublicInstance,
id : string,
data : XDROPDOWN_LISTITEM_INFO_TYPE
}
/**
* @name 下拉菜单 xDropdownMenu
* @description 下拉菜单,标签内只能放置子项目x-dropdown-item
* @page /pages/index/dropdown-menu
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
name: "x-dropdown-menu",
data() {
return {
id: ("xDropMenu" + getUid()) as string,
_width: 0,
_height: 0,
menubarNodeino: null as null | NODE_INFO,
nowIndex: -1,
opended: false,
cacheListItem: [] as XDROPDOWN_LISTITEM_TYPE[],
windtop: 0,
tid: 0,
maskMoveX:0,
maskMoveY:0,
maskTouchTime:0
}
},
emits: [
/**
* 切换菜单时触发
* @param index {number} - 当前被切换的索引值
* @param keyName {string} - 菜单keyName标识
* @param status {boolean} - 当前切换的状态:false表示关闭,true表示打开。
*/
'change',
'update:modelValue'],
props: {
/**
* 位置,
* static | fixed
* 静态 | 展开后悬浮在顶部。
*/
position: {
type: String as PropType<"static" | 'fixed'>,
default: "fixed"
},
/**
* 顶部的偏移量,针对你们自定标题导航时可能需要让出顶部位置。
* 数字字符,prx,px等单位
* 如果position=static此属性失效。
*/
offsetTop: {
type: String,
default: "0"
},
/**
* 当前激活的索引
* -1表示关闭。提供值时请大于-1,
* 如果不想要变量控制,些值 可不提供。交由内部自行处理。
* 当你想要用变量控制开关时可v-model="索引"来控制关闭和打开。
*/
modelValue: {
type: Number,
default: -1
},
/**
* 菜单栏的高度
*/
height: {
type: String,
default: "44"
},
/**
* 宽度
*/
width: {
type: String,
default: "auto"
},
/**
* 背景颜色
*/
color: {
type: String,
default: "white"
},
/**
* 暗黑时的背景颜色
*/
darkColor: {
type: String,
default: ""
},
/**
* 层级
*/
zIndex: {
type: Number,
default: 88
},
/**
* 对于要把此组件嵌套在fiexd中时,并且position为static时
* 请一定设置为true,否则在web端会出现层级混乱(这是css dom规则所定)
* 为了全平台对齐请嵌套组件时一定注意使用事项.
*/
hidnMask:{
type:Boolean,
default:false
}
},
computed: {
_cutomhHeight() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_cutomWidth() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_color() : string {
if (xConfig.dark == 'dark') {
if (this.darkColor != '') return getDefaultColor(this.darkColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(this.color)
},
_offsetTop() : number {
if (this.position == 'static') return this.windtop
let height = checkIsCssUnit(this.offsetTop, xConfig.unit)
let unit = getUnit(height)
let realheight = parseInt(height)
if (unit == 'rpx') {
realheight = rpx2px(realheight)
}
return realheight + this.windtop;
},
menuLeft() : string {
if (this.menubarNodeino == null) return ""
if (this.position == 'fixed') {
return "0px";
}
return (this.menubarNodeino!.left).toString() + 'px';
},
menuWidth() : string {
if (this.menubarNodeino == null) return ""
if (this.position == 'fixed') {
return "100%"
}
return (this.menubarNodeino!.width).toString() + 'px';
},
menuTop() : string {
if (this.menubarNodeino == null) return ""
if (this.position == 'fixed') {
return this.windtop+"px";
}
// + this.menubarNodeino!.height
// let top = this.menubarNodeino!.top
let parentTop = 0
// #ifdef WEB
if(this.$parent?.$el?.getBoundingClientRect){
parentTop = this.$parent?.$el?.getBoundingClientRect()?.top??0
}
parentTop = this.menubarNodeino!.top - parentTop + this.menubarNodeino!.height;
// #endif
// #ifdef APP||MP-WEIXIN
parentTop = this.menubarNodeino!.top
// #endif
let top = parentTop
return top.toString() + 'px';
},
nowItemIsBtn() : boolean {
if (this.nowIndex <= -1 || this.nowIndex >= this.cacheListItem.length || this.cacheListItem.length == 0) return false;
let item = this.cacheListItem[this.nowIndex];
return item.data.isBtn
},
__height() : string {
let h = '100%';
// #ifdef WEB
h = `calc(100% - ${this.windtop}px)`
// #endif
return h;
},
},
watch: {
modelValue(newValue : number) {
if (this.nowIndex == newValue) return;
if (newValue == -1) {
this.nowIndex = -1;
this.opended = false;
this.closeMenuContent();
return;
}
this.nowIndex = newValue;
this.openMenuContent();
}
},
mounted() {
let t = this;
let sys = uni.getWindowInfo()
// #ifdef WEB
this._width = sys.windowWidth
this._height = sys.windowHeight;
this.windtop = sys.windowTop;
// #endif
// #ifdef APP || MP-WEIXIN
this._width = sys.windowWidth
this._height = sys.windowHeight + 44;
// #endif
this.nowIndex = this.modelValue;
this.getNodes();
uni.$on("onResize", this.getNodes)
},
beforeUnmount() {
uni.$off("onResize", this.getNodes)
clearTimeout(this.tid)
},
methods: {
addMenu(insCom : XDropdownItemComponentPublicInstance, obj : XDROPDOWN_LISTITEM_INFO_TYPE) {
let index = this.cacheListItem.findIndex((el : XDROPDOWN_LISTITEM_TYPE) : boolean => el.id == obj.id)
if (index > -1) {
this.cacheListItem[index] = {
ele: insCom,
id: obj.id,
data: obj
} as XDROPDOWN_LISTITEM_TYPE
} else {
this.cacheListItem.push({
ele: insCom,
id: obj.id,
data: obj
} as XDROPDOWN_LISTITEM_TYPE);
}
this.$nextTick(() => {
this.openMenuContent();
})
},
delMenu(id : string) {
if (this.cacheListItem.length == 0) return;
let index : number = this.cacheListItem.findIndex((el : XDROPDOWN_LISTITEM_TYPE) : boolean => el.id == id);
if (index > -1) {
if (index == this.nowIndex) {
this.nowIndex = -1;
}
this.cacheListItem.splice(index, 1)
}
},
openMenuContent() {
this.cacheListItem.forEach((item : XDROPDOWN_LISTITEM_TYPE, index : number) => {
if (index != this.nowIndex) {
item.ele.close();
} else {
item.ele.open();
this.opended = true;
}
})
},
closeMenuContent() {
this.cacheListItem.forEach((item : XDROPDOWN_LISTITEM_TYPE) => {
item.ele.close();
})
},
maskerMove(evt : UniTouchEvent) {
// evt.preventDefault();
},
closeMenu() {
let nkey = ""
try {
nkey = this.cacheListItem[this.nowIndex].data.keyName
} catch (e) {
//TODO handle the exception
}
/**
* 切换菜单时触发
* @param index {number} 当前被切换的索引值
* @param keyName {string} 菜单keyName标识
* @param status {boolean} 当前切换的状态:false表示关闭,true表示打开。
*/
this.$emit("change", this.nowIndex, nkey, false)
this.nowIndex = -1;
this.opended = false;
/**
* 等同v-model=""
*/
this.$emit("update:modelValue", this.nowIndex)
this.closeMenuContent();
// #ifdef WEB
document.body.style.removeProperty("overflow")
// #endif
},
openMenu(index : number) {
this.getNodes();
this.nowIndex = index;
this.opended = false;
/**
* 等同v-model=""
*/
this.$emit("update:modelValue", index)
this.openMenuContent();
let nkey = ""
try {
nkey = this.cacheListItem[index].data.keyName
} catch (e) {
//TODO handle the exception
}
this.$emit("change", index, nkey, true)
// #ifdef WEB
document.body.style.setProperty("overflow","hidden")
// #endif
},
menuClick(index : number) {
if (index == this.nowIndex) {
this.closeMenu();
} else {
this.openMenu(index);
}
console.log(this.nowIndex)
},
maskmStart(evt:UniTouchEvent){
evt.preventDefault()
this.maskMoveX = evt.changedTouches[0].clientX
this.maskMoveY = evt.changedTouches[0].clientY
this.maskTouchTime = Date.now()
},
maskmMove(evt:UniTouchEvent){
// #ifdef WEB||MP-WEIXIN
evt.preventDefault()
evt.stopPropagation()
// #endif
},
maskmEnd(evt:UniTouchEvent){
let diffx = evt.changedTouches[0].clientX - this.maskMoveX
let diffy = evt.changedTouches[0].clientY - this.maskMoveY
let difftime = Date.now() - this.maskTouchTime;
if(diffx==diffy&&difftime>50&&difftime<250){
this.closeMenu();
}
},
getNodes() {
uni.createSelectorQuery().in(this)
.select('.xDropMenu')
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
this.menubarNodeino = {
left: nodeinfo.left!,
width: nodeinfo.width!,
height: nodeinfo.height!,
bottom: nodeinfo.bottom!,
right: nodeinfo.right!,
top: nodeinfo.top!,
} as NODE_INFO
})
},
},
}
</script>
<template>
<view :id="id" class="xDropMenu" :style="{
width:_cutomWidth,
height:_cutomhHeight,
backgroundColor:_color
}">
<view class="xDropMenuBarStatic"
:style="{height:_cutomhHeight,visibility:!opended||nowItemIsBtn?'visible':'hidden'}">
<view @click.stop="menuClick(index)" class="xDropMenuBaritem" v-for="(item,index) in cacheListItem"
:key="index">
<text :style="{
color:nowIndex==index? item.data.activeFontColor:item.data.fontColor,
fontSize:item.data.fontSize
}">{{item.data.title}}</text>
<x-icon style="margin-left:5px"
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
v-if="nowIndex==index&&item.data.activeIcon!=''" :name="item.data.activeIcon"></x-icon>
<x-icon style="margin-left:5px"
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
v-if="nowIndex!=index&&item.data.icon!=''" :name="item.data.icon"></x-icon>
</view>
</view>
<!-- #ifdef WEB -->
<teleport to="uni-app">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<root-portal>
<!-- #endif -->
<view v-if="!hidnMask"
@touchstart="maskmStart"
@touchmove="maskmMove"
@touchend="maskmEnd"
class="xDropMenuWrap" :style="{
width:'100%',
height:__height,
display:opended&&!nowItemIsBtn?'flex':'none',
top:_offsetTop+'px',
zIndex:zIndex.toString(),
position:'fixed'
}
">
</view>
<!-- #ifdef MP-WEIXIN -->
</root-portal>
<!-- #endif -->
<!-- #ifdef WEB -->
</teleport>
<!-- #endif -->
<view @click.stop="closeMenu" class="xDropMenuWrapContent" :style="{
width:menuWidth,
height:'100%',
left:menuLeft,
top:menuTop,
zIndex:(zIndex+1).toString(),
display:opended&&!nowItemIsBtn?'flex':'none',
position: 'fixed'
}">
<view class="xDropMenuBarStatic xDropMenuBarAbs" :style="{
width:_cutomWidth,
height:_cutomhHeight,
backgroundColor:_color,
}">
<view v-if="opended" @click.stop="menuClick(index)" class="xDropMenuBaritem"
v-for="(item,index) in cacheListItem" :key="index">
<text :style="{
color:nowIndex==index? item.data.activeFontColor:item.data.fontColor,
fontSize:item.data.fontSize
}">{{item.data.title}}</text>
<x-icon style="margin-left:5px"
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
v-if="nowIndex==index&&item.data.activeIcon!=''" :name="item.data.activeIcon"></x-icon>
<x-icon style="margin-left:5px"
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
v-if="nowIndex!=index&&item.data.icon!=''" :name="item.data.icon"></x-icon>
</view>
</view>
<view class="xDropMenuBgColor" :style="{'background-color': opended?`rgba(0, 0, 0, 0.4)`:`rgba(0, 0, 0, 0)`}">
<!--
@slot 默认插槽,只能放置子项目x-dropdown-item
-->
<slot></slot>
</view>
</view>
</view>
</template>
<style scoped>
.xDropMenuBgColor{
transition-duration: 300ms;
transition-property: background-color;
transition-timing-function: linear;
overflow: hidden;position: relative;
background-color:rgba(0,0,0,0);
transition-delay: 50ms;
flex:1;
}
.xDropMenu {
display: flex;
flex-direction: column;
}
.xDropMenuBaritem {
flex: 1;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.xDropMenuBarStatic {
display: flex;
flex-direction: row;
pointer-events: auto;
}
.xDropMenuWrap {
position: fixed;
/* z-index: 88; */
background-color: transparent;
left: 0px;
top: 0px;
}
.xDropMenuWrapContent {
/* position: fixed; */
/* z-index: 89; */
/* background-color: rgba(0, 0, 0, 0.4); */
left: 0px;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,110 @@
// @ts-nocheck
export default class WxCanvas {
constructor(ctx, canvasId, isNew, canvasNode) {
this.ctx = ctx;
this.canvasId = canvasId;
this.chart = null;
this.isNew = isNew
if (isNew) {
this.canvasNode = canvasNode;
}
else {
this._initStyle(ctx);
}
// this._initCanvas(zrender, ctx);
this._initEvent();
}
getContext(contextType) {
if (contextType === '2d') {
return this.ctx;
}
}
// canvasToTempFilePath(opt) {
// if (!opt.canvasId) {
// opt.canvasId = this.canvasId;
// }
// return wx.canvasToTempFilePath(opt, this);
// }
setChart(chart) {
this.chart = chart;
}
attachEvent() {
// noop
}
detachEvent() {
// noop
}
_initCanvas(zrender, ctx) {
zrender.util.getContext = function () {
return ctx;
};
zrender.util.$override('measureText', function (text, font) {
ctx.font = font || '12px sans-serif';
return ctx.measureText(text);
});
}
_initStyle(ctx) {
// @ts-ignore
ctx.createRadialGradient = () => {
return ctx.createCircularGradient(arguments);
};
}
_initEvent() {
this.event = {};
const eventNames = [{
wxName: 'touchStart',
ecName: 'mousedown'
}, {
wxName: 'touchMove',
ecName: 'mousemove'
}, {
wxName: 'touchEnd',
ecName: 'mouseup'
}, {
wxName: 'touchEnd',
ecName: 'click'
}];
eventNames.forEach(name => {
this.event[name.wxName] = e => {
console.log(e)
const touch = e.touches[0];
this.chart.getZr().handler.dispatch(name.ecName, {
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
zrY: name.wxName === 'tap' ? touch.clientY : touch.y
});
};
});
}
addEventListener(el, name, handler, opt) {
// el?.addEventListener(name, handler, opt);
}
set width(w) {
if (this.canvasNode) this.canvasNode.width = w
}
set height(h) {
if (this.canvasNode) this.canvasNode.height = h
}
get width() {
if (this.canvasNode)
return this.canvasNode.width
return 0
}
get height() {
if (this.canvasNode)
return this.canvasNode.height
return 0
}
}
@@ -0,0 +1,518 @@
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
// #ifdef MP-WEIXIN
import WxCanvas from './canvasinit.uts';
let echarts : any = null
// #endif
type eventsType = (data : any) => void;
/**
* @name 图表 xEchart
* @description 是百度图表6.0.0,全量版本
* 传递正常的百度对象数据且需要将数据JSON.stringify化
* 图表文档:https://echarts.apache.org/zh/index.html
* 编译微信版本:https://echarts.apache.org/zh/builder.html
* 微信版本请使用1.1.18下dmeo qita/echarts.esm.min.js文件。或者自己下载[Echart下载](https://github.com/apache/echarts/tree/6.0.0/dist)
* 注意的是:如果你的配置中函数函数,需要自己转换为字符串【如果是微信端建议直接传对象,不要转为字符串这样兼容性更好。】
* 比如:函数对象请参考我demo页面的示例规则分平台写否则无法实现函数对象。
* @page /pages/index/echart
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xEchart" })
const proxy = getCurrentInstance()?.proxy ?? null
type xEchartPropsType = {
/**
* 容器宽
*/
width : string,
/**
* 容器高
*/
height : string,
/**
* hbx sdk4.76+后建议不要使用此属性,请改用ref方法setOptions
*/
opts : string
}
const props = withDefaults(defineProps<xEchartPropsType>(), {
width: 'auto',
height: '250px',
opts: ''
})
const emits = defineEmits<{
/**
* 当图表初始化完成后触发,此时可以使用ref或者参数chart来来设置图表数据了。
*/
(e : 'init', chart : any | null) : void
}>()
const id = ref<string>("xEchart-" + getUid())
const webviewContext = ref<WebviewContext | null>(null)
const isLoaded = ref<boolean>(false)
const boxWidth = ref<number>(10)
const boxHeight = ref<number>(10)
const tid = ref<number>(0)
const tid2 = ref<number>(0)
const realLoaded = ref<boolean>(false)
const dipcatchEvents = ref<Map<string, eventsType>>(new Map())
// #ifdef MP-WEIXIN
const chart = ref<any | null>(null)
const echartCanvasObj = ref<any>({})
// #endif
const _width = computed(() : string => checkIsCssUnit(props.width, xConfig.unit))
const _height = computed(() : string => checkIsCssUnit(props.height, xConfig.unit))
const _options = computed(() : string => props.opts)
function cahrtActions(fun : string, opts : string, evt : eventsType | null) {
let filterevents = ["click"]
let eventsid = ('x-' + getUid()) as string;
if (evt != null && filterevents.includes(fun)) {
dipcatchEvents.value.set(eventsid, evt!)
}
// #ifdef WEB
var iframe = document.getElementById(id.value) as any;
if (!iframe) return;
iframe.contentWindow['chart_call'](fun, opts, eventsid);
// #endif
// #ifdef APP
let wb = webviewContext.value!;
wb.evalJS(`chart_call('${fun}','${opts}','${eventsid}')`)
// #endif
}
function eventJsCall(callfun : string, str : string) {
// #ifdef WEB
var iframe = document.getElementById(id.value) as any;
if (!iframe) return;
iframe.contentWindow[callfun](str);
// #endif
// #ifdef APP
let wb = webviewContext.value!;
wb.evalJS(`${callfun}(${str})`)
// #endif
// #ifdef MP-WEIXIN
(chart.value as any)[callfun](str)
// #endif
}
function drawer() {
if (!realLoaded.value) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
// #ifdef WEB
eventJsCall('chart_setOption', `${_options.value}`)
// #endif
// #ifdef APP
eventJsCall('chart_setOption', `'${_options.value}'`)
// #endif
// #ifdef MP-WEIXIN
setoptsByWx(_options.value)
// #endif
}
function onResizeChart() {
if (realLoaded.value) {
cahrtActions('resize', '', null)
}
}
function getNodeInfo() {
uni.createSelectorQuery().in(proxy as any)
.select(".xEchart")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
boxWidth.value = nodeinfo.width!
boxHeight.value = nodeinfo.height!
if (webviewContext.value != null) return;
isLoaded.value = true;
tid.value = setTimeout(function () {
// #ifdef APP
webviewContext.value = uni.createWebviewContext(id.value, proxy)
// #endif
// #ifdef WEB
webviewContext.value = document.getElementById(id.value) as HTMLElement
// #endif
// #ifdef MP-WEIXIN
uni.createCanvasContextAsync({
id: id.value,
component: proxy as any,
success(canvascontext) {
const pixelRatio = uni.getWindowInfo().pixelRatio
const context = canvascontext.getContext('2d')
const canvas = context?.canvas;
canvas.width = canvas?.offsetWidth * pixelRatio
canvas.height = canvas?.offsetHeight * pixelRatio
context.scale(pixelRatio, pixelRatio)
let echartCanvas = {}
echartCanvas = new WxCanvas(context, id.value, true, canvas)
echarts.setPlatformAPI({
createCanvas() {
return canvas;
}
});
chart.value = echarts.init(echartCanvas, null, {
width: boxWidth.value,
height: boxHeight.value,
devicePixelRatio: pixelRatio
});
echartCanvas.setChart(chart.value);
chart.value.on('mousedown', (e) => {
dipcatchEvents.value.forEach(fun => {
const datas = e;
delete (datas as any).event;
delete (datas as any).encode;
delete (datas as any).encode;
fun(datas)
})
})
echartCanvasObj.value = echartCanvas
realLoaded.value = true;
drawer()
emits("init", chart.value)
}
})
// #endif
}, 50);
})
}
function onAddlisentMesage() {
// #ifdef WEB
window.addEventListener('message', function (event) {
if (event.data.iframeId == id.value && event.data.action == 'onJSBridgeReady') {
clearTimeout(tid2.value)
tid2.value = setTimeout(function () {
realLoaded.value = true;
drawer()
emits("init", null)
}, 50);
}
if (event.data.iframeId == id.value && event.data.action == 'click') {
let eventId = event.data.eventId
let filterEvents = dipcatchEvents.value.get(eventId)
if (filterEvents != null) {
let evt = filterEvents!;
evt(JSON.parse(event.data.data))
}
}
});
// #endif
}
function onMessage(event : WebViewMessageEvent) {
let msgdatas = event.detail.data
// #ifdef APP-ANDROID
if (msgdatas.length > 0) {
let msg = msgdatas[0]! as UTSJSONObject
let ac = msg!.getString("action") as string;
if (ac == 'img') {
let imgbase64 = msg!.getString("url") as string;
console.log(imgbase64)
} else if (ac == "onJSBridgeReady") {
} else if (ac == 'click') {
let eventId = msg!.getString("eventId") as string
let filterEvents = dipcatchEvents.value.get(eventId)
if (filterEvents != null) {
let handler = filterEvents! as eventsType;
let datas = msg!.getString("data")
if (datas == null || datas == '') {
handler({} as UTSJSONObject)
} else {
let eventData = JSON.parse(datas!)! as UTSJSONObject
handler(eventData)
}
}
}
}
// #endif
// #ifdef APP-IOS || APP-HARMONY
if (msgdatas.length > 0) {
let msg = msgdatas[0]
let ac = msg["action"] as string;
if (ac == 'img') {
let imgbase64 = msg['url'] as string;
console.log(imgbase64)
} else if (ac == "onJSBridgeReady") {
} else if (ac == 'click') {
let eventId = msg['eventId'] as string
let filterEvents = dipcatchEvents.value.get(eventId)
if (filterEvents != null) {
let handler = filterEvents! as eventsType;
let datas = msg['data']
if (datas == null || datas == '') {
handler({} as UTSJSONObject)
} else {
let eventData = JSON.parse(datas!)! as UTSJSONObject
handler(eventData)
}
}
}
}
// #endif
}
// #ifdef MP-WEIXIN
function wrapTouch(event) {
for (let i = 0; i < event.touches.length; ++i) {
const touch = event.touches[i];
touch.offsetX = touch.x;
touch.offsetY = touch.y;
}
return event;
}
function touchStart(e) {
if (chart.value && e.touches.length > 0) {
var touch = e.touches[0];
var handler = chart.value.getZr().handler;
handler.dispatch('mousedown', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => { },
stopImmediatePropagation: () => { },
stopPropagation: () => { }
});
handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => { },
stopImmediatePropagation: () => { },
stopPropagation: () => { }
});
handler.processGesture(wrapTouch(e), 'start');
}
}
function touchMove(e) {
if (chart.value && e.touches.length > 0) {
var touch = e.touches[0];
var handler = chart.value.getZr().handler;
handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y,
preventDefault: () => { },
stopImmediatePropagation: () => { },
stopPropagation: () => { }
});
handler.processGesture(wrapTouch(e), 'change');
}
}
function touchEnd(e) {
if (chart.value) {
const touch = e.changedTouches ? e.changedTouches[0] : {};
var handler = chart.value.getZr().handler;
handler.dispatch('mouseup', {
zrX: (touch as any).x,
zrY: (touch as any).y,
preventDefault: () => { },
stopImmediatePropagation: () => { },
stopPropagation: () => { }
});
handler.dispatch('click', {
zrX: (touch as any).x,
zrY: (touch as any).y,
preventDefault: () => { },
stopImmediatePropagation: () => { },
stopPropagation: () => { }
});
handler.processGesture(wrapTouch(e), 'end');
}
}
const touchmove = touchMove
const touchend = touchEnd
function setEcharts(e : any) {
echarts = e;
echarts.registerPreprocessor((option : any) => {
if (option && option.series) {
if (option.series.length > 0) {
option.series.forEach((series : any) => {
series.progressive = 0;
});
}
else if (typeof option.series === 'object') {
(option.series as any).progressive = 0;
}
}
});
getNodeInfo()
}
function parseJsonWithFunction(jsonString) {
if (typeof jsonString !== 'string') {
return jsonString;
}
const obj = JSON.parse(jsonString, function (k, v) {
var isFunctionStr =
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*\}\s*$/.test(v) ||
/^\s*\([^)]*\)\s*=>\s*\{[\s\S]*\}\s*$/.test(v) ||
/^\s*\([^)]*\)\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
/^\s*[a-zA-Z0-9_$]+\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
/^\s*[a-zA-Z0-9_$]+\s*=>\s*\{[\s\S]*\}\s*$/.test(v);
if (typeof v === 'string' && isFunctionStr) {
const funcMatch = v.match(/function\s*\(([^)]*)\)\s*\{([\s\S]*)\}\s*$/);
if (funcMatch) {
const params = funcMatch[1].split(',').map(p => p.trim());
const body = funcMatch[2].trim();
v = new Function(...params, body);
}
}
return v;
});
return obj;
}
function setoptsByWx(opts : any) {
if (!opts) return;
if (typeof opts == 'string') {
const dataopts = parseJsonWithFunction(opts);
if (dataopts && dataopts != null && typeof dataopts == 'object') {
chart.value.setOption(dataopts)
}
} else {
chart.value.setOption(opts)
}
}
// #endif
function setOptions(opts : any) {
if (!realLoaded.value) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
// #ifdef WEB
eventJsCall('chart_setOption', `${opts}`)
// #endif
// #ifdef APP
eventJsCall('chart_setOption', `'${opts as string}'`)
// #endif
// #ifdef MP-WEIXIN
setoptsByWx(opts)
// #endif
}
function getImg() {
eventJsCall('EchartImg', '')
}
function appWebViewLoaded() {
// #ifdef APP-ANDROID||APP-IOS
realLoaded.value = true;
drawer()
emits("init", null)
// #endif
// #ifdef APP-HARMONY
setTimeout(function () {
realLoaded.value = true;
drawer()
emits("init", null)
}, 150);
// #endif
}
watch(() : string => props.opts, () => {
drawer()
})
onMounted(() => {
// #ifdef WEB||APP-ANDROID||APP-IOS
getNodeInfo()
onAddlisentMesage()
uni.$on('onResize', onResizeChart)
// #endif
// #ifdef APP-HARMONY
setTimeout(function () {
getNodeInfo()
onAddlisentMesage()
uni.$on('onResize', onResizeChart)
}, 100)
// #endif
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
uni.$off('onResize', onResizeChart)
})
defineExpose({
/**
* 设置图表数据,小程序可以直接传图表对象数据。非小程序,需要序列化为字符串再赋值。
* @param {string|object} data 小程序是object,非小程序是json序列化的字符串。
*/
setOptions,
/**
* 暂未开放
*/
getImg,
// #ifdef MP-WEIXIN
/**
* 设置图表实例
* @param {Echart} ins 微信专用函数。Echart实例
*/
setEcharts,
// #endif
eventJsCall,
/**
* chart对象函数操作
* @param {string} funName 第一个参数是方法名如:resize
* @param {string} args 方法参数
* @param {null} arg 固定为null
*/
cahrtActions
})
</script>
<template>
<view class="xEchart" :style="{ width: _width, height: _height }">
<view v-if="!isLoaded"
style="width:100%;height:100%;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<x-icon color="primary" :spin="true" name="loader-4-line"></x-icon>
</view>
<!-- #ifdef APP||WEB -->
<web-view @load="appWebViewLoaded" v-else :id="id" :src="`/hybrid/html/local.html?id=${id}`"
:style="{ width: '100%', height: '100%', opacity: isLoaded ? 1 : 0 }" @message="onMessage"></web-view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<canvas v-else :id="id" @touchstart="touchStart" @touchmove="touchmove" @touchend="touchend"
:style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"></canvas>
<!-- #endif -->
</view>
</template>
<style scoped></style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,785 @@
<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
// #ifdef MP
import { Marked } from "./marked.uts"
// #endif
type DATATYP = {
action : string
}
type xEditeListType = "ordered" | "bullet" | "unchecked" | "checked" | ""
type xEditeAlign = "center" | "left" | "right" | ""
type xEditeOptsType = {
img : string,
link : string,
b : boolean,
i : boolean,
s : boolean,
u : boolean,
align : xEditeAlign,
list : xEditeListType,
indent : number,
header : number,
color : string,
background : string,
size : string
}
type xEditeListItemType = {
name : xEditeListType,
icon : string
}
/**
* @name 富文本编辑器 xEditor
* @description 传递正常markdown或者html内容即可,传递markdown时会自动转换为html,如果直接传递html不会转换直接赋值.
* 值得注意的是:在微信小程序端它是没有样式高亮显示的.其它平台有样式指示,这是因为受限于微信官方本身就不支持.
* 另外我测试发现HBX4.53 sdk ios端的输入框焦点有问题,导致无法设置样式,待官方修复。
* @page /pages/biaodan/editor
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: ("xEdte-" + getUid()) as string,
webviewContext: null as WebviewContext | null,
isLoaded: false,
boxWidth: 10,
boxHeight: 0,
tid: 0,
tid2: 0,
realLoaded: false,
isMp: false,
optsStatus: {
img: '',
link: '',
b: false,
i: false,
s: false,
u: false,
align: '',
list: '',
indent: 0,
header: 0,
color: '',
background: '',
size: ''
} as xEditeOptsType,
listDataItems: [
{ icon: 'list-ordered', name: 'ordered' },
{ icon: 'list-unordered', name: 'bullet' },
{ icon: 'list-check-2', name: 'unchecked' },
{ icon: 'list-check-3', name: 'checked' },
] as xEditeListItemType[],
// #ifdef MP
markdownObj: new Marked(),
htmlMpContent: "",
editorCtx: null
// #endif
}
},
emits: [
/**
* 特定的a,img标签被点击触发,小程序不支持,其它平台支持.
* @return {Object<{text,tag,attr}>}
*/
'tagClick',
/**
* 是否初始化成功
*/
'init',
/**
* 需要通过ref函数调用getHtml才会触发此函数
*/
'getValue'
],
props: {
/**
* 窗口宽
*/
width: {
type: String,
default: 'auto'
},
/**
* 窗口高,可以传递所支持的任意单位高.
*/
height: {
type: String,
default: '350'
},
/**
* 需要渲染的markdow或者html内容。
*/
value: {
type: String,
default: ""
},
/**
* 是否启用纯html渲染。如果你的内容含有特殊字符比如:%,^&%这种不要出现在里面
* 此时你启用isHtml会经过数据处理直接跳过插件,直接赋值内容到html.就不要启用Markdown了.
*/
isHtml: {
type: Boolean,
default: false
},
/**
* 富文本的style样式,不可以动态更改.
* 为了对齐所有端,默认已经把所有平台的样式删除.因此你可以自己设置默认样式来对齐所有平台.
*/
nodeStyle: {
type: String,
default: "line-height:1.6;color:#000"
},
/**
* 同上,暗黑时的样式.
*/
nodeDarkStyle: {
type: String,
default: "line-height:1.6;color:#fff"
},
/**
* 默认的按钮背景色
*/
color: {
type: String,
default: "#f5f5f5"
},
/**
* 激活时的选中背景色
* 空值取全局
*/
activeColor: {
type: String,
default: ""
},
/**
* 自定义默认的文字/背景颜色
*/
customColos: {
type: Array as PropType<string[]>,
default: () : string[] => ['#ff0000', '#ff00ff', '#00ff00', '#00ffff', '#ffff00', '#ffffff', '#000000'] as string[]
}
},
computed: {
_customColos() : string[] {
return this.customColos
},
_color() : string {
if (this._isDark) return "#222222"
return getDefaultColor(this.color)
},
_activeColor() : string {
return getDefaultColor(this.activeColor == '' ? 'primary' : this.activeColor)
},
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_value() : string {
return this.value
},
_nodeStyle() : string {
return xConfig.dark == 'dark' ? this.nodeDarkStyle : this.nodeStyle
},
_isDark() : boolean {
return xConfig.dark == 'dark'
}
},
watch: {
value() {
// #ifdef APP||WEB
this.drawer(this._value, this.isHtml)
// #endif
// #ifdef MP
this.setContent(this.value);
// #endif
}
},
mounted() {
let t = this;
// #ifdef MP
t.isMp = true;
// #endif
t.isLoaded = true;
this.tid = setTimeout(function () {
// #ifdef APP
t.webviewContext = uni.createWebviewContext(t.id, t);
// #endif
// #ifdef WEB
t.webviewContext = document.getElementById(t.id) as HTMLElement;
// #endif
// #ifdef MP
t.mpOnInit()
// #endif
}, 50);
this.onAddlisentMesage();
},
beforeMount() {
clearTimeout(this.tid)
},
beforeUnmount() {
},
methods: {
// #ifdef MP
mpOnInit() {
let _this = this;
uni.createSelectorQuery()
.in(this)
.select('#editor').context((res) => {
console.log(res)
_this.editorCtx = res.context
_this.realLoaded = true;
_this.setContent(_this.value);
/**
* 图表加载初始化完成后触发此事件。
*/
this.$emit("init")
}).exec()
},
setContent(str) {
if (!this.realLoaded) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
if (this.isHtml) {
this.htmlMpContent = str
} else {
const htmlcontent = this.markdownObj.parse(str)
this.htmlMpContent = htmlcontent
}
console.log(this.htmlMpContent)
this.editorCtx.setContents({ html: this.htmlMpContent })
},
onItemClick(event) {
console.log(event)
},
onfocus(event) {
// console.log(this.editorCtx)
},
onstatuschange(event) {
const detail = event.detail || {}
let key = ''
let value = ''
if (Object(detail).hasOwnProperty('bold')) {
this.optsStatus.b = value as boolean;
} else if (Object(detail).hasOwnProperty('underline')) {
this.optsStatus.u = value as boolean;
} else if (Object(detail).hasOwnProperty('italic')) {
this.optsStatus.italic = value as boolean;
} else if (Object(detail).hasOwnProperty('header')) {
this.optsStatus.header = value as number;
console.log(detail)
} else if (Object(detail).hasOwnProperty('align')) {
this.optsStatus.align = value;
} else if (Object(detail).hasOwnProperty('list')) {
this.optsStatus.list = value as xEditeListType;
} else if (Object(detail).hasOwnProperty('background')) {
this.optsStatus.list = value as string;
} else if (Object(detail).hasOwnProperty('size')) {
this.optsStatus.size = value as string;
}
},
setFormart(key, value) {
const eobj = {
'b': 'bold', //true,false
'i': 'italic', //true,false
's': 'strike', //true,false
'u': 'underline', //true,false
'link': 'link',
'img': 'image',
'video': 'video',
'align': 'align', //left,right,center
/**
* ordered:有序列表(编号列表)
* bullet:无序列表(项目符号列表)
* unchecked:未选中的复选框列表项
* checked:选中的复选框列表项
*/
'list': 'list',
'blockquote': 'blockquote', //true,false
'indent': 'indent', //缩进值0-8
'color': 'color',
'background': 'backgroundColor',
'size': 'fontSize',
'script': 'script', //super,sub
'header': 'header', //1-6
}
console.log(eobj[key], value)
this.editorCtx?.format(eobj[key], value)
},
// #endif
// h5端
onAddlisentMesage() {
// #ifdef WEB
let t = this;
window.addEventListener('message', function (event) {
if (event.data.iframeId == t.id && event.data.action == 'onJSBridgeReady') {
clearTimeout(t.tid2)
t.tid2 = setTimeout(function () {
t.realLoaded = true;
t.drawer(t._value, t.isHtml)
t.$emit("init")
t.eventJsCall('setBodyStyle', t._nodeStyle)
}, 50);
}
if (event.data.iframeId == t.id && event.data.action == 'offsetHeight') {
t.boxHeight = event.data.data + 20
} else if (event.data.iframeId == t.id && event.data.action == 'toValue') {
t.$emit('getValue', event.data.data)
} else if (event.data.iframeId == t.id && event.data.action == 'click') {
let dataStr = JSON.stringify(event.data.data);
let dataJson = JSON.parseObject(dataStr)!
t.$emit('tagClick', dataJson)
} else if (event.data.iframeId == t.id && event.data.action == 'fontStyleOpts') {
let localOptsNow = event.data.data as xEditeOptsType;
t.optsStatus = localOptsNow;
}
});
// #endif
},
onMessage(event : WebViewMessageEvent) {
let t = this;
let msgdatas = event.detail.data
if (msgdatas.length == 0) return;
// #ifdef APP-ANDROID || APP-HARMONY
if (msgdatas.length > 0) {
let dataStr = JSON.stringify(event.detail);
let dataJson = JSON.parseObject(dataStr)!
let msgeAr = dataJson.getArray<UTSJSONObject>('data')!
let msg = msgeAr[0]!
let ac = msg["action"] as string;
if (ac == 'offsetHeight') {
// const h = msg["data"]! as number;
// t.boxHeight = h + 25
} else if (ac == 'toValue') {
t.$emit('getValue', msg["data"]! as string)
} else if (ac == 'click') {
t.$emit('tagClick', msg['data']! as UTSJSONObject)
} else if (ac == 'fontStyleOpts') {
let datamsg = JSON.stringify(msg['data']!)!;
let localOptsNow = JSON.parseObject<xEditeOptsType>(datamsg! as string)! as xEditeOptsType;
t.optsStatus = localOptsNow;
}
}
// #endif
// #ifdef APP-IOS
if (msgdatas.length > 0) {
let msg = msgdatas[0]
let ac = msg['action'] as string;
if (ac == 'offsetHeight') {
// t.boxHeight = (msg['data']! as Number) + 25
} else if (ac == 'toValue') {
t.$emit('getValue', msg['data'])
} else if (ac == 'click') {
let dataStr = JSON.stringify(msg['data']);
let dataJson = JSON.parseObject(dataStr)!
t.$emit('tagClick', dataJson)
} else if (ac == 'fontStyleOpts') {
let localOptsNow = msg['data']! as xEditeOptsType;
t.optsStatus = localOptsNow;
}
}
// #endif
},
drawer(value : string, isHtml : boolean) {
if (!this.realLoaded) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
// #ifdef WEB
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(value), render: true }))
// #endif
// #ifdef APP-IOS
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(value), render: true }))
// #endif
// #ifdef APP-ANDROID || APP-HARMONY
this.eventJsCall('markdown', JSON.stringify({ value: isHtml ? btoa(encodeURIComponent(value)!) : value, render: isHtml ? 'android' : '' }))
// #endif
},
eventJsCall(callfun : string, str : string) {
// #ifdef WEB
var iframe = document.getElementById(this.id);
if (!iframe) return;
if ((iframe.contentWindow[callfun] || null)) {
iframe.contentWindow[callfun](str, this.isHtml);
}
// #endif
// #ifdef APP
this.webviewContext?.evalJS(`${callfun}(${str},${this.isHtml})`)
// #endif
},
setEventContent(callanem : string, value : string, ishtml : boolean) {
if (!this.realLoaded) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
const _this = this;
function evenjscall(callfun : string, str : string, isHtml : boolean) {
// #ifdef WEB
var iframe = document.getElementById(_this.id);
if (!iframe) return;
if ((iframe.contentWindow[callfun] || null)) {
iframe.contentWindow[callfun](str, isHtml);
}
// #endif
// #ifdef APP
_this.webviewContext?.evalJS(`${callfun}(${str},${isHtml})`)
// #endif
}
// #ifdef WEB
evenjscall(callanem, JSON.stringify({ value: encodeURIComponent(value), render: true }), ishtml)
// #endif
// #ifdef APP-IOS || APP-HARMONY
evenjscall(callanem, JSON.stringify({ value: encodeURIComponent(value), render: true }), ishtml)
// #endif
// #ifdef APP-ANDROID
let rendervalue = (ishtml ? 'android' : '') as string
let cvalue = (ishtml ? btoa(encodeURIComponent(value)!) : value) as any
let resultValue = JSON.stringify({ value: cvalue, render: rendervalue })! as string
evenjscall(callanem, resultValue, ishtml)
// #endif
},
/**
* 获取html内容。注意本函数不会返回内容,你要通过事件getValue得到html内容.
* @public
*/
getHtml() {
// #ifdef WEB
var iframe = document.getElementById(this.id);
if (!iframe) return;
iframe.contentWindow['getHtml']()
// #endif
// #ifdef APP
this.webviewContext?.evalJS(`getHtml()`)
// #endif
// #ifdef MP
this.$emit('getValue', this.htmlMpContent)
// #endif
},
appWebViewLoaded() {
this.realLoaded = true;
this.drawer(this._value, this.isHtml)
/**
* 图表加载初始化完成后触发此事件。
*/
this.$emit("init")
// setBodyStyle
this.eventJsCall('setBodyStyle', this._nodeStyle)
},
/**
* 获取选区
*/
getSelected() {
},
setFontStyle(key : string, value : boolean | number | string) {
// #ifndef MP
this.setEventContent('setSelectedStyle', JSON.stringify({ key: key, value: value }), true)
this.$nextTick(() => {
if (key == 'b') {
this.optsStatus.b = value as boolean;
} else if (key == 'u') {
this.optsStatus.u = value as boolean;
} else if (key == 'i') {
this.optsStatus.i = value as boolean;
} else if (key == 's') {
this.optsStatus.s = value as boolean;
} else if (key == 'header') {
this.optsStatus.header = value as number;
} else if (key == 'align') {
this.optsStatus.align = value as xEditeAlign;
} else if (key == 'list') {
this.optsStatus.list = value as xEditeListType;
} else if (key == 'background') {
this.optsStatus.list = value as string;
} else if (key == 'size') {
this.optsStatus.size = value as string;
}
})
// #endif
// #ifdef MP
this.setFormart(key, value)
console.log(key)
// #endif
},
getBgColor(isActive : boolean) : string {
if (isActive) return this._activeColor;
return this._color
},
getFontColor(isActive : boolean) : string {
if (isActive || this._isDark) return "#ffffff";
return "#333333"
}
}
}
</script>
<template>
<view class="xEdite" :style="{width:_width,height:_height}">
<view v-if="!isLoaded"
style="width:100%;height:100%;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<x-icon color="primary" :spin="true" name="loader-4-line"></x-icon>
</view>
<view class="FontTabsGroup">
<view class="fontTabs" style="flex:1;margin-right: 2px;">
<view @click="setFontStyle('b',!optsStatus.b)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.b)}">
<x-icon :color="getFontColor(optsStatus.b)" name="bold"></x-icon>
</view>
<view @click="setFontStyle('i',!optsStatus.i)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.i)}">
<x-icon :color="getFontColor(optsStatus.i)" name="italic"></x-icon>
</view>
<view @click="setFontStyle('u',!optsStatus.u)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.u)}">
<x-icon :color="getFontColor(optsStatus.u)" name="underline"></x-icon>
</view>
<view @click="setFontStyle('s',!optsStatus.s)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.s)}">
<x-icon :color="getFontColor(optsStatus.s)" name="strikethrough"></x-icon>
</view>
</view>
<view class="fontTabs" style="width: 36px;align-self: stretch;margin-right: 2px;">
<x-popover position="bc" style="flex:1;align-self: stretch;">
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.size!='')}">
<x-icon :color="getFontColor(optsStatus.size!='')" name="font-size"></x-icon>
</view>
<template #menu>
<view style="width:110px">
<view v-for="item in ['12px', '14px', '16px', '18px', '24px']" :key="item"
@click="setFontStyle('size',optsStatus.size==item?'':item)" class="fontTabsCirlItem"
:style="{backgroundColor:getBgColor(optsStatus.size==item)}">
<x-text :color="getFontColor(optsStatus.size==item)">{{item}}</x-text>
</view>
</view>
</template>
</x-popover>
</view>
<view class="fontTabs" style="width: 120px;align-self: stretch;">
<x-popover position="bc" style="flex:1;align-self: stretch;">
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.background!='')}">
<view class="fontTabsCirl"
:style="{backgroundColor:optsStatus.background==''?'transparent':optsStatus.background}">
</view>
<x-icon :color="getFontColor(optsStatus.background!='')" name="drop-fill"></x-icon>
</view>
<template #menu>
<view style="width:110px">
<view @click="setFontStyle('background',item)" v-for="(item,index) in _customColos"
:key="index" class="fontTabsCirlItem"
:style="{backgroundColor:_isDark?'#222222':'#ffffff'}">
<view class="fontTabsCirl2" :style="{backgroundColor:item}"></view>
</view>
</view>
</template>
</x-popover>
<x-popover position="br" style="flex:1;align-self: stretch;">
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.color!='')}">
<view class="fontTabsCirl"
:style="{backgroundColor:optsStatus.color==''?'transparent':optsStatus.color}"></view>
<x-icon :color="getFontColor(optsStatus.color!='')" name="font-color"></x-icon>
</view>
<template #menu>
<view style="width:110px">
<view @click="setFontStyle('color',item)" v-for="(item,index) in _customColos" :key="index"
class="fontTabsCirlItem" :style="{backgroundColor:_isDark?'#222222':'#ffffff'}">
<view class="fontTabsCirl2" :style="{backgroundColor:item}"></view>
</view>
</view>
</template>
</x-popover>
</view>
</view>
<view class="FontTabsGroup">
<view class="fontTabs" style="width: 36px;align-self: stretch;margin-right: 2px;">
<x-popover position="bl" style="flex:1;align-self: stretch;">
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.header!=0)}">
<x-icon :color="getFontColor(optsStatus.header!=0)" name="heading"></x-icon>
</view>
<template #menu>
<view style="width:110px">
<view v-for="item in 6" :key="item"
@click="setFontStyle('header',optsStatus.header==item?0:item)" class="fontTabsCirlItem"
:style="{backgroundColor:getBgColor(optsStatus.header==item)}">
<x-icon :color="getFontColor(optsStatus.s)" :name="`h-${item}`"></x-icon>
</view>
</view>
</template>
</x-popover>
</view>
<view class="fontTabs" style="flex:1;margin-right: 2px;">
<view v-for="(item,index) in ['left','center','right']" :key="index"
@click="setFontStyle('align',optsStatus.align==item?'':item)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.align==item)}">
<x-icon :color="getFontColor(optsStatus.align==item)" :name="`align-${item}`"></x-icon>
</view>
</view>
<view class="fontTabs" style="align-self: stretch;flex:1">
<view v-for="(item,index) in listDataItems" :key="index"
@click="setFontStyle('list',optsStatus.list==item.name?'':item.name)" class="xEditorBtns"
:style="{backgroundColor:getBgColor(optsStatus.list==item.name)}">
<x-icon :color="getFontColor(optsStatus.list==item.name)" :name="item.icon"></x-icon>
</view>
</view>
</view>
<view style="height: 8px;"></view>
<view style="flex:1;">
<!-- #ifdef APP||WEB -->
<web-view v-if="isLoaded" :horizontalScrollBarAccess="true" :verticalScrollBarAccess="false"
class="xMarkdownNoevents" @load="appWebViewLoaded" :id="id" src="/hybrid/html/edite.html"
:style="{width:'100%',height:'100%',opacity:isLoaded?1:0}" @message="onMessage"></web-view>
<!-- #endif -->
<!-- #ifdef MP -->
<editor class="xMarkdownNoevents" id="editor" @ready="mpOnInit" @itemclick="onItemClick" @focus="onfocus"
@statuschange="onstatuschange" :selectable="true" placeholder="请输入"
:style="[{width:'100%',height:'100%'},_nodeStyle]"></editor>
<!-- #endif -->
<!-- 兼容安卓。webview到4.19+页面无法滚动 -->
<!-- <view v-if="!_edite" class="xMarkdownAndrod"></view> -->
</view>
</view>
</template>
<style scoped>
.xEdite{
display: flex;
flex-direction: column;
}
.xEditorBtns {
height: 40px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.fontTabsCirl {
width: 12px;
height: 12px;
border-radius: 10px;
border: 1px solid #b2b2b2;
margin-right: 5px;
}
.fontTabsCirl2 {
width: 20px;
height: 20px;
border-radius: 20px;
border: 1px solid #b2b2b2;
}
.fontTabsCirlItem {
height: 36px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.fontTabs {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
border-radius: 4px;
overflow: hidden;
}
.FontTabsGroup {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.xMarkdownAndrod {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.xMarkdownNoevents {
/* pointer-events: none; */
width: 100%;
/* height: 100%; */
}
</style>
@@ -0,0 +1,174 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, getOutlineColorObj } from "../../core/util/xCoreColorUtil.uts"
import { toFillMarginAr, checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
const i18n = xConfig.i18n;
/**
* @name 空状态 xEmpty
* @description 主要用于列表加载页面或者空状态页面时使用。
* @page /pages/index/action-menu
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xEmpty"})
const emits = defineEmits([
/**
* 刷新按钮被点击时触发
*/
'click'
])
type xEmptyPropsType = {
/**
* 加载状态
*/
loading: boolean,
/**
* 是否为空
*/
empty: boolean,
/**
* 错误状态
*/
error: boolean,
/**
* 是否有更多数据状态
*/
more: boolean,
/**
* 没有数据时的提示,用于加载更多数据时
* ,没有更多数据啦
*/
moreLabel: string,
/**
* 列表加载出错时,出错啦~
*/
errorLabel: string,
/**
* 按钮文本,点击重试
*/
btnLabel: string,
/**
* 按钮颜色,默认取全局值
*/
btnColor: string,
/**
* 按钮文本颜色,默认自动
*/
btnTextColor: string,
/**
* 空或者加载出错时的标语,当前没有数据
*/
title: string,
/**
* 图片路径
*/
src: string,
/**
* 是否显示重试按钮
*/
showBtn: boolean,
}
const props = withDefaults(defineProps<xEmptyPropsType>(), {
loading: true,
empty: false,
error: false,
more: false,
moreLabel: "",
errorLabel: "",
btnLabel: "",
btnColor: "",
btnTextColor: "",
title: "",
src: "/static/tmui4xLibs/static/empty.png",
showBtn: true,
})
const _showBtn = computed(() : boolean => {
return props.showBtn
})
const _loading = computed(() : boolean => {
return props.loading
})
const _error = computed(() : boolean => {
return props.error
})
const _more = computed(() : boolean => {
return props.more
})
const _empty = computed(() : boolean => {
return props.empty
})
const _moreLabel = computed(() : string => {
if(props.moreLabel == '') return i18n.t("tmui4x.empty.moreLabel")
return props.moreLabel
})
const _errorLabel = computed(() : string => {
if(props.errorLabel == '') return i18n.t("tmui4x.empty.errorLabel")
return props.errorLabel
})
const _btnLabel = computed(() : string => {
if(props.btnLabel == '') return i18n.t("tmui4x.empty.btnLabel")
return props.btnLabel
})
const _btnColor = computed(() : string => {
if (props.btnColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(props.btnColor)
})
const _btnTextColor = computed(() : string => {
return getDefaultColor(props.btnTextColor)
})
const _title = computed(() : string => {
if(props.title == '') return i18n.t("tmui4x.empty.title")
return props.title
})
function onclick() {
/**
* 刷新按钮被点击时触发
*/
emits('click')
}
</script>
<template>
<view class="xEmpty">
<x-loading v-if="_loading"></x-loading>
<view class="xEmptyWrap" v-if="!_loading&&!_more&&(_empty||_error)">
<image style="width: 200px;height:200px" :src="src" />
<x-text font-size="16" v-if="_empty" style="opacity: 0.5;">{{_title}}</x-text>
<x-text font-size="16" v-if="_error" style="opacity: 0.5;">{{_errorLabel}}</x-text>
<!--
@slot 按钮位置的插槽
-->
<slot>
<view v-if="_showBtn" style="padding-top: 21px;">
<x-button @click="onclick" width="150px" :color="_btnColor"
:font-color="_btnTextColor">{{_btnLabel}}</x-button>
</view>
</slot>
</view>
<x-text font-size="16" v-if="_more&&!_loading&&!_error&&!_empty"
style="padding: 16px;opacity: 0.5;">{{_moreLabel}}</x-text>
</view>
</template>
<style scoped>
.xEmpty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 16px 0;
}
.xEmptyWrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
@@ -0,0 +1,899 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
import { getUid } from '../../core/util/xCoreUtil.uts';
type CHECKPOINT_XY = { x : number | null, y : number | null }
type DRect = {
left:number,
right:number,
top:number,
bottom:number,
width:number,
height:number,
}
/**
* @name 手势库 xFinger
* @description 多方向的手势库,包括旋转,捏合,轻扫,双击等手势
* @page /pages/index/finger
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xFinger"})
type Props = {
/**
* 滑动多长距离,识别并触发滑动的方向
*/
swiperDiff: number
/**
* 定义双击的时间间隔触发时机
*/
dbClickDiff: number
/**
* 定义单击click的时间间隔触发时机
*/
clickDiff: number
/**
* 定义长按的时间间隔触发时机
*/
longDiff: number
/**
* 是否禁用
*/
disabled: boolean
/**
* 移动事件的节流间隔(毫秒),0表示不节流
*/
throttleDelay: number
/**
* 是否启用防抖,防止快速连续触发
*/
debounce: boolean
/**
* 最小移动距离,小于此距离不触发移动事件
*/
minMoveDistance: number
/**
* 连续性坐标初始位置x
*/
accOffsetX:number,
/**
* 连续性坐标初始位置y
*/
accOffsetY:number,
/**
* 是否启用无障碍访问支持
*/
accessibility: boolean
/**
* 无障碍标签
*/
ariaLabel: string
}
defineSlots<{
/**
* 默认插槽default
*/
default(props:{
/**
* 当前x坐标
*/
x:number,
/**
* 当前y坐标
*/
y:number,
/**
* 累积x坐标(连续定位)
*/
accX:number,
/**
* 累积y坐标(连续定位)
*/
accY:number,
/**
* 当前事件类型click,start,end,dbclick,longPress,swiper,pinch,rotate
*/
type:string
}):any
}>()
const props = withDefaults(defineProps<Props>(), {
swiperDiff: 50,
dbClickDiff: 300,
clickDiff: 50,
longDiff: 800,
disabled: false,
throttleDelay: 16, // 约60fps
debounce: false,
minMoveDistance: 1,
accessibility: true,
accOffsetX:0,
accOffsetY:0,
ariaLabel: '手势识别区域'
})
// Emits
const emit = defineEmits<{
/**
* 触摸开始
* @param evt - {x,y,type,width,height}
*/
start: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 触摸移动时触发
* @param evt - {x,y,type,width,height}
*/
move: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 触摸结束
* @param evt - {x,y,type,width,height}
*/
end: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 触摸中断
* @param evt - {x,y,type,width,height}
*/
cancel: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* doubleClick
* @param evt - {x,y,type,width,height}
*/
doubleClick: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 长按事件
* @param evt - {x,y,type,width,height}
*/
longPress: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 滑动时触发
* @param evt - {x,y,type,width,height,direction:方向UP|DOWN|LEFT|RIGHT}
*/
swiper: [evt: {x: number, y: number, diffX: number, diffY: number, direction: string, type: string, width: number, height: number}]
/**
* 单击
* @param evt - {x,y,type,width,height}
*/
click: [evt: {x: number, y: number, type: string, width: number, height: number}]
/**
* 缩放事件
* @description len为两点间的距离,scale为当前的缩放比例(最小为0.1)
* @param evt - {x,y,x1,y1,len,scale,type,width,height}
*/
pinch: [evt: {x: number, y: number, x1: number, y2: number, type: string, width: number, height: number, len: number, scale: number}]
/**
* 旋转事件
* @description len为两点间的距离,angle为当前当前旋转的角度
* @param evt - {x,y,x1,y1,len,angle,type,width,height}
*/
rotate: [evt: {x: number, y: number, x1: number, y2: number, type: string, width: number, height: number, len: number, angle: number}]
}>()
// Reactive data
const isMouseDown = ref(false)
const wheelScale = ref(1)
const wheelDelta = ref(0.1) // 滚轮缩放系数
const dubleTime = ref(0)
const tid = ref(56)
const _x = ref(0)
const _y = ref(0)
const _start_x = ref(0)
const _start_y = ref(0)
const eventName = ref('')
const mX = ref(0)
const mY = ref(0)
const swipeDirection = ref("")
// 累积定位坐标,保持连续性
const accumulatedX = ref(props.accOffsetX)
const accumulatedY = ref(props.accOffsetY)
// 本次手势开始时的累积坐标
const startAccumulatedX = ref(0)
const startAccumulatedY = ref(0)
const xFingerRef = ref<UniElement | null>(null)
const left = ref(0)
const top = ref(0)
const id = ref("xFinGer" + getUid())
const zoomFactor = ref(0.55) // 缩放速率
const zoomFactorAb = ref(0.03) // 旋转速率
const pinchStartLen = ref(0)
const scale = ref(0)
const angle = ref(0)
const pinth_x = ref(0)
const pinth_y = ref(0)
const preV = ref<CHECKPOINT_XY>({ x: null, y: null })
const parentRect = ref<DRect>({
left:0,
right:0,
top:0,
bottom:0,
width:0,
height:0,
})
// 性能优化相关
const lastMoveTime = ref(0)
const throttleTimer = ref<number | null>(null)
const debounceTimer = ref<number | null>(null)
// Computed
const _disabled = computed(() => props.disabled)
// Get current instance
const instance = getCurrentInstance()?.proxy;
// 工具函数
const throttle = (func: Function, delay: number) => {
return (...args: any[]) => {
const now = Date.now()
if (now - lastMoveTime.value >= delay) {
lastMoveTime.value = now
func.apply(null, args)
}
}
}
const debounce = (func: Function, delay: number) => {
return (...args: any[]) => {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}
debounceTimer.value = setTimeout(() => {
func.apply(null, args)
}, delay)
}
}
const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
const isGestureValid = (deltaX: number, deltaY: number): boolean => {
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
return distance >= props.minMoveDistance
}
// #ifdef WEB
// 处理鼠标滚轮事件,模拟缩放功能
const handleWheel = (evt: WheelEvent) => {
if (_disabled.value) return
const delta = evt.deltaY > 0 ? -wheelDelta.value : wheelDelta.value;
wheelScale.value = Math.max(0.1, wheelScale.value + delta);
emit("pinch", {
x: evt.clientX,
y: evt.clientY,
type: 'pinch',
width: parentRect.value.width,
height: parentRect.value.height,
len: 0,
scale: wheelScale.value
});
}
// 鼠标事件处理函数
const mouseStart = (evt: MouseEvent) => {
if (_disabled.value) return
isMouseDown.value = true;
let rectBox = parentRect.value;
_x.value = evt.clientX - rectBox.left;
_y.value = evt.clientY - rectBox.top;
// 重置移动距离,开始新的手势
mX.value = 0
mY.value = 0
// 记录本次手势开始时的累积坐标
startAccumulatedX.value = accumulatedX.value
startAccumulatedY.value = accumulatedY.value
_start_x.value = evt.clientX - rectBox.left;
_start_y.value = evt.clientY - rectBox.top;
swipeDirection.value = "";
emit("start", {
x: _start_x.value,
y: _start_y.value,
type: 'start',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'start';
// 处理双击
let difftime = new Date().getTime() - dubleTime.value;
if (difftime > 0 && difftime <= props.dbClickDiff) {
emit("doubleClick", {
x: _start_x.value,
y: _start_y.value,
type: 'doubleClick',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'doubleClick';
}
dubleTime.value = new Date().getTime();
// 处理长按
clearTimeout(tid.value);
tid.value = setTimeout(() => {
if (isMouseDown.value) {
emit("longPress", {
x: _start_x.value,
y: _start_y.value,
type: 'longPress',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'longPress';
}
}, props.longDiff);
}
const mouseMove = (evt: MouseEvent) => {
if (_disabled.value || !isMouseDown.value) return
let rectBox = parentRect.value;
let x = evt.clientX - rectBox.left;
let y = evt.clientY - rectBox.top;
let deltaX = Math.abs(x);
let deltaY = Math.abs(y);
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
// 在移动过程中实时更新累积坐标(用于插槽输出)
accumulatedX.value = startAccumulatedX.value + mX.value
accumulatedY.value = startAccumulatedY.value + mY.value
if (deltaX > deltaY && deltaX > props.swiperDiff) {
swipeDirection.value = (_x.value > x) ? "left" : "right";
} else if (deltaY > deltaX && deltaY > props.swiperDiff) {
swipeDirection.value = (_y.value < y) ? "down" : "up";
}
if (swipeDirection.value != "") {
emit("swiper", {
x: mX.value,
y: mY.value,
diffX: deltaX,
diffY: deltaY,
direction: swipeDirection.value,
type: 'swiper',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'swiper';
}
emit("move", {
x: mX.value,
y: mY.value,
type: 'move',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'move';
clearTimeout(tid.value);
}
const mouseEnd = (evt: MouseEvent) => {
if (_disabled.value) return
let rectBox = parentRect.value;
let x = evt.clientX - rectBox.left;
let y = evt.clientY - rectBox.top;
let deltaX = Math.abs(x);
let deltaY = Math.abs(y);
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
// 更新累积坐标(手势结束时累加本次移动的偏移量)
accumulatedX.value = startAccumulatedX.value + mX.value
accumulatedY.value = startAccumulatedY.value + mY.value
emit("end", {
x: mX.value,
y: mY.value,
type: 'end',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'end';
if (new Date().getTime() - dubleTime.value > props.clickDiff) {
emit("click", {
x: mX.value,
y: mY.value,
type: 'click',
width: rectBox.width,
height: rectBox.height
});
eventName.value = 'click';
}
isMouseDown.value = false;
// 重置所有状态,为下次触摸做准备
swipeDirection.value = ""
scale.value = 0
angle.value = 0
}
const mouseCancel = (evt: MouseEvent) => {
if (_disabled.value) return
let rectBox = parentRect.value;
let x = evt.clientX - rectBox.left;
let y = evt.clientY - rectBox.top;
let deltaX = Math.abs(x);
let deltaY = Math.abs(y);
// mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
// mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
// emit("cancel", {
// x: mX.value,
// y: mY.value,
// type: 'cancel',
// width: rectBox.width,
// height: rectBox.height
// });
eventName.value = 'cancel';
isMouseDown.value = false;
// 重置所有状态,为下次触摸做准备
swipeDirection.value = ""
scale.value = 0
angle.value = 0
}
// #endif
const getLen = (v : CHECKPOINT_XY) : number => {
if (v.x == null || v.y == null) return 0
return Math.hypot(v.x!, v.y!)
}
const dot = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
if (v1.x == null || v1.y == null || v2.x == null || v2.y == null) return 0
return v1.x! * v2.x! + v1.y! * v2.y!;
}
const getRectBox = (call:(rect:DRect)=>void) => {
try {
if (xFingerRef.value == null) {
console.warn('x-finger: Element not found')
return
}
let rectBox = {
left:0,
right:0,
top:0,
bottom:0,
width:0,
height:0,
} as DRect
xFingerRef.value?.getBoundingClientRectAsync()?.then((res:DOMRect)=>{
rectBox = {
left:res.left,
right:res.right,
top:res.top,
bottom:res.bottom,
width:res.width,
height:res.height,
} as DRect
call(rectBox)
}).catch(err => {
console.error('x-finger: getBoundingClientRectAsync error', err)
})
} catch (error) {
console.error('x-finger: getRectBox error', error)
}
}
const getAngle = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
let mr = getLen(v1) * getLen(v2);
if (mr == 0) return 0;
let r = dot(v1, v2) / mr;
if (r > 1) r = 1;
let jd = Math.acos(r);
jd = jd * (180 / Math.PI);
return (jd + 360) % 360;;
}
const cross = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
if (v1.x == null || v1.y == null || v2.x == null || v2.y == null) return 0
return v1.x! * v2.y! - v2.x! * v1.y!;
}
const getRotateAngle = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
let angle = getAngle(v1, v2);
if (cross(v1, v2) > 0) {
angle *= -1;
}
return angle * 180 / Math.PI;
}
const mStart = (evt : UniTouchEvent) => {
if (_disabled.value) return
let event = evt.changedTouches[0]
clearTimeout(tid.value)
let rectBox = parentRect.value
_x.value = event.clientX - rectBox.left;
_y.value = event.clientY - rectBox.top;
// 重置移动距离,开始新的手势
mX.value = 0
mY.value = 0
// 记录本次手势开始时的累积坐标
startAccumulatedX.value = accumulatedX.value
startAccumulatedY.value = accumulatedY.value
_start_x.value = event.clientX - rectBox.left
_start_y.value = event.clientY - rectBox.top
swipeDirection.value = ""
/**
* 触摸开始
* @param evt {object} {x,y,type,width,height}
*/
emit("start", { x: _start_x.value, y: _start_y.value, type: 'start', width: rectBox.width, height: rectBox.height })
eventName.value = 'start'
let difftime = new Date().getTime() - dubleTime.value;
if (difftime > 0 && difftime <= props.dbClickDiff) {
/**
* 双击事件
* @param evt {object} {x,y,type,width,height}
*/
emit("doubleClick", { x: _start_x.value, y: _start_y.value, type: 'doubleClick', width: rectBox.width, height: rectBox.height })
eventName.value = 'doubleClick'
}
dubleTime.value = new Date().getTime();
if (evt.changedTouches.length >= 2) {
pinth_x.value = evt.touches[0].pageX;
pinth_y.value = evt.touches[0].pageY;
let otx = evt.touches[1].pageX;
let oty = evt.touches[1].pageY;
let preVs = { x: otx - pinth_x.value, y: oty - pinth_y.value } as CHECKPOINT_XY;
preV.value = preVs
pinchStartLen.value = getLen(preVs);
}
tid.value = setTimeout(function () {
/**
* 长按事件
* @param evt {object} {x,y,type,width,height}
*/
emit("longPress", { x: _start_x.value, y: _start_y.value, type: 'longPress', width: rectBox.width, height: rectBox.height })
eventName.value = 'longPress'
}, props.longDiff);
}
// 键盘事件处理(无障碍访问)
// #ifdef WEB
const handleKeyDown = (evt: KeyboardEvent) => {
if (!props.accessibility || _disabled.value) return
const rectBox = parentRect.value
const centerX = rectBox.width / 2
const centerY = rectBox.height / 2
switch (evt.key) {
case 'Enter':
case ' ':
evt.preventDefault()
emit("click", {
x: centerX,
y: centerY,
type: 'click',
width: rectBox.width,
height: rectBox.height
})
break
case 'ArrowUp':
evt.preventDefault()
emit("swiper", {
x: centerX,
y: centerY,
diffX: 0,
diffY: props.swiperDiff,
direction: 'up',
type: 'swiper',
width: rectBox.width,
height: rectBox.height
})
break
case 'ArrowDown':
evt.preventDefault()
emit("swiper", {
x: centerX,
y: centerY,
diffX: 0,
diffY: props.swiperDiff,
direction: 'down',
type: 'swiper',
width: rectBox.width,
height: rectBox.height
})
break
case 'ArrowLeft':
evt.preventDefault()
emit("swiper", {
x: centerX,
y: centerY,
diffX: props.swiperDiff,
diffY: 0,
direction: 'left',
type: 'swiper',
width: rectBox.width,
height: rectBox.height
})
break
case 'ArrowRight':
evt.preventDefault()
emit("swiper", {
x: centerX,
y: centerY,
diffX: props.swiperDiff,
diffY: 0,
direction: 'right',
type: 'swiper',
width: rectBox.width,
height: rectBox.height
})
break
}
}
// #endif
const mMove = (evt : UniTouchEvent) => {
if (_disabled.value) return
let event = evt.changedTouches[0]
let rectBox = parentRect.value
let x = event.clientX - rectBox.left;
let y = event.clientY - rectBox.top;
let deltaX = Math.abs(x - _x.value);
let deltaY = Math.abs(y - _y.value);
// 检查最小移动距离
if (!isGestureValid(deltaX, deltaY)) {
return
}
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
// 在移动过程中实时更新累积坐标(用于插槽输出)
accumulatedX.value = startAccumulatedX.value + mX.value
accumulatedY.value = startAccumulatedY.value + mY.value
if (deltaX > deltaY && deltaX > props.swiperDiff) {
swipeDirection.value = (_x.value > x) ? "left" : "right";
} else if (deltaY > deltaX && deltaY > props.swiperDiff) {
swipeDirection.value = (_y.value < y) ? "down" : "up";
}
if (swipeDirection.value != "") {
/**
* 滑动时触发
* @param evt {object} {x,y,type,width,height,direction:方向UP|DOWN|LEFT|RIGHT}
*/
emit("swiper", { x: mX.value, y: mY.value, diffX: deltaX, diffY: deltaY, direction: swipeDirection.value, type: 'swiper', width: rectBox.width, height: rectBox.height })
eventName.value = 'swiper'
}
// _x.value = x;
// _y.value = y;
/**
* 触摸移动时触发
* @param evt {object} {x,y,type,width,height}
*/
emit("move", { x: mX.value, y: mY.value, type: 'move', width: rectBox.width, height: rectBox.height })
eventName.value = 'move'
clearTimeout(tid.value)
if (evt.changedTouches.length >= 2) {
let currentX = evt.touches[0].pageX;
let currentY = evt.touches[0].pageY;
let otx = evt.touches[1].pageX;
let oty = evt.touches[1].pageY;
let v = { x: otx - currentX, y: oty - currentY } as CHECKPOINT_XY;
if (preV.value.x !== null) {
let nowLenPitch = getLen(v);
if (pinchStartLen.value > 0) {
let temsc = (nowLenPitch / pinchStartLen.value);
// 计算缩放比例
const deltaScale = (temsc - 1) * zoomFactor.value + scale.value;
// 最小值为0.1
scale.value = Math.max(deltaScale, 0.1)
/**
* 缩放事件
* @param evt {object} {x,y,x1,y1,len,scale,type,width,height}
* @description len为两点间的距离,scale为当前的缩放比例(最小为0.1)
*/
emit("pinch", {
x: currentX, y: currentY,
x1: otx, y2: oty,
type: 'pinch',
width: rectBox.width, height: rectBox.height,
len: nowLenPitch,
scale: scale.value
})
}
let testjd = getRotateAngle(v, preV.value);
angle.value = Math.floor((testjd - 1) * zoomFactorAb.value) + angle.value;
pinchStartLen.value = nowLenPitch;
/**
* 旋转事件
* @param evt {object} {x,y,x1,y1,len,angle,type,width,height}
* @description len为两点间的距离,angle为当前当前旋转的角度
*/
emit("rotate", {
x: currentX, y: currentY,
x1: otx, y2: oty,
type: 'rotate',
width: rectBox.width, height: rectBox.height,
len: nowLenPitch,
angle: angle.value
})
}
preV.value = v;
}
}
const mEnd = (evt : UniTouchEvent) => {
clearTimeout(tid.value)
getRectBox((rect:DRect)=>{
parentRect.value = rect
})
if (_disabled.value) return
let event = evt.changedTouches[0]
let rectBox = parentRect.value
let x = event.clientX - rectBox.left;
let y = event.clientY - rectBox.top;
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
/**
* 触摸结束
* @param evt {object} {x,y,type,width,height}
*/
emit("end", { x: mX.value, y: mY.value, type: 'end', width: rectBox.width, height: rectBox.height })
eventName.value = 'end'
if (new Date().getTime() - dubleTime.value > props.clickDiff) {
/**
* 单击
* @param evt {object} {x,y,type,width,height}
*/
emit("click", { x: mX.value, y: mY.value, type: 'click', width: rectBox.width, height: rectBox.height })
eventName.value = 'click'
}
// 重置所有状态,为下次触摸做准备
preV.value = { x: 0, y: 0 } as CHECKPOINT_XY;
pinchStartLen.value = 0
swipeDirection.value = ""
scale.value = 0
angle.value = 0
}
const mCancel = (evt : UniTouchEvent) => {
clearTimeout(tid.value)
getRectBox((rect:DRect)=>{
parentRect.value = rect
})
if (_disabled.value) return
let event = evt.changedTouches[0]
let rectBox = parentRect.value
let x = event.clientX - rectBox.left;
let y = event.clientY - rectBox.top;
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
// 更新累积坐标(手势取消时也要累加本次移动的偏移量)
accumulatedX.value = startAccumulatedX.value + mX.value
accumulatedY.value = startAccumulatedY.value + mY.value
/**
* 触摸结束
* @param evt {object} {x,y,type,width,height}
*/
emit("cancel", { x: mX.value, y: mY.value, type: 'end', width: rectBox.width, height: rectBox.height })
eventName.value = 'cancel'
// 重置所有状态,为下次触摸做准备
preV.value = { x: 0, y: 0 } as CHECKPOINT_XY;
pinchStartLen.value = 0
swipeDirection.value = ""
scale.value = 0
angle.value = 0
}
// Methods
const initFunc = () => {
getRectBox((rect:DRect)=>{
parentRect.value = rect;
top.value = rect.top;
left.value = rect.left;
})
}
// Lifecycle
onMounted(() => {
// #ifdef APP-HARMONY
setTimeout(function() {
initFunc()
}, 120);
// #endif
// #ifndef APP-HARMONY
initFunc()
// #endif
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
if (throttleTimer.value!=null) {
clearTimeout(throttleTimer.value!)
}
if (debounceTimer.value!=null) {
clearTimeout(debounceTimer.value!)
}
})
</script>
<template>
<view :id="id" ref="xFingerRef" class="finger"
@touchstart="mStart" @touchmove="mMove" @touchend="mEnd"
@touchcancel="mCancel"
:aria-label="props.ariaLabel"
:role="props.accessibility ? 'button' : null"
:tabindex="props.accessibility ? 0 : null"
<!-- #ifdef WEB -->
@mousedown="mouseStart"
@mousemove="mouseMove"
@mouseup="mouseEnd"
@mouseleave="mouseCancel"
@wheel="handleWheel"
@keydown="handleKeyDown"
<!-- #endif -->
>
<!--
@slot 默认插槽
@prop {number} x - 触摸的位置x
@prop {number} y - 触摸的位置y
@prop {number} accX - 累积x坐标(连续定位)
@prop {number} accY - 累积y坐标(连续定位)
@prop {string} type - 事件类型click,start,end,dbclick,longPress,swiper,pinch,rotate
-->
<slot :x="mX" :y="mY" :accX="accumulatedX" :accY="accumulatedY" :type="eventName"></slot>
</view>
</template>
<style lang="scss">
</style>
@@ -0,0 +1,489 @@
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue"
import { type PropType } from "vue"
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type POSITION_TYPE_XY = {
x : number,
y : number
}
/**
* @name 浮球 xFloatButton
* @description 可以左右四个角定位放置,自动靠边吸咐,也可自由拖动放置。
* @page /pages/index/float-button
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xFloatButton"})
const emits = defineEmits([
/**
* 点击组件时触发
*/
'click',
/**
* 长按组件时触发大于500ms
*/
'longpress',
/**
* 坐标改变时触发
* @param {number[]} x,y坐标信息
*/
'change',
/**
* 动态修改当前的位置
* 等同v-model:offset具体见offset属性那。
*/
'update:offset'
])
type xFloatButtonPropsType = {
duration: number,
/**
* 松开后,如果是吸附adsorption为true的话,吸附在两边的位置时的距离边界的距离。
* 单位为px,左右的安全距离.
*/
threshold: number,
/**
* 单位为px,顶部的安全距离
*/
thresholdTop: number,
/**
* 单位为px,底部的安全距离
*/
thresholdBottom: number,
/**
* 圆角,空值时取全局的drawr圆角。
*/
round: string,
/**
* 自己定义位置:以可视范围内的左上角算起。如何自己定义位置时
* 需要计算屏幕坐标时请使用uni.getWindowInfo()
* 来获取可视屏幕的宽和高定位你自己需要的自由位置
* 可以v-model:offset="[x,y]"来动态更改其位置。
* 我预置了以下几种常见模式:
* [-1,-1]会在右下角。会让出threshold边界距离
* [-2,-2]会在左下角。会让出threshold边界距离
* [-3,-3]会在左上角。会让出threshold边界距离
* [-4,-4]会在右上角。会让出threshold边界距离
* [-5,-5]会在底部居中。会让出threshold边界距离
*/
offset: number[],
/**
* 背景,支持渐变值如:linear-gradient(to left, #FFED46, #FF7EC7)
* 默认空值,取全局主题值。
*/
bgColor: string,
/**
* 宽
*/
width: string,
/**
* 高
*/
height: string,
/**
* 是否开启吸附在两边。
* 如果设置为false,可以自由拖动在屏幕上。
*/
adsorption: boolean,
/**
* 是否禁止拖动。
*/
disabled: boolean,
/**
* 层级
*/
zIndex: number,
}
const props = withDefaults(defineProps<xFloatButtonPropsType>(), {
duration: 650,
threshold: 12,
thresholdTop: 0,
thresholdBottom: 12,
round: "64",
offset: (): number[] => [-1, -1] as number[],
bgColor: "",
width: '50px',
height: '50px',
adsorption: true,
disabled: false,
zIndex: 87,
})
// refs (data)
const id = ref<string>(("xFloatButtonId-" + getUid()) as string)
const _x = ref(0)
const _y = ref(0)
const winHeight = ref(0)
const winWidth = ref(0)
const nowXy = ref<number[]>([0, 0])
const windtop = ref(0)
const isMoveing = ref(false)
const dateTime = ref(0)
const diffX = ref(0)
const _real_X = ref(0)
const _real_Y = ref(0)
const lastX = ref(0)
const lastY = ref(0)
const first = ref(true)
const longtimeid = ref<number>(22)
const isReady = ref(false)
const xFloatButton = ref<UniElement | null>(null)
const proxy = getCurrentInstance()?.proxy;
// computed
const _diffLen = computed(():number=>{
let p = parseInt(props.width);
if (props.width.lastIndexOf('rpx') > -1) {
p = rpx2px(p);
}
return Math.floor(p)
})
const _round = computed(() : string => {
return checkIsCssUnit(props.round, xConfig.unit)
})
const _width = computed(() : number => {
let p = parseInt(props.width);
if (props.width.lastIndexOf('rpx') > -1) {
p = rpx2px(p);
}
return Math.floor(p)
})
const _height = computed(() : number => {
let p = parseInt(props.height);
if (props.height.lastIndexOf('rpx') > -1) {
p = rpx2px(p);
}
return Math.floor(p)
})
const _bgColor = computed(() : object => {
if (props.bgColor.indexOf('linear-gradient') > -1) {
return {
backgroundImage: props.bgColor
}
}
let color = props.bgColor == "" ? getDefaultColor(xConfig.color) : getDefaultColor(props.bgColor)
return {
backgroundColor: color
};
})
const _disabled = computed(() : boolean => {
return props.disabled
})
function onClick() {
emits('click')
}
function setProperty(x : number, y : number) {
let node = xFloatButton.value as UniElement
node.style.setProperty("transition-duration", first.value ? '0' : props.duration.toString() + 'ms')
if (x == -1 || x == -4) {
x = winWidth.value - _width.value - props.threshold;
} else if (x == -2 || x == -3) {
x = props.threshold;
} else if (x == -5) {
x = (winWidth.value - _width.value) / 2;
}
if (y == -1 || y == -2 || y == -5) {
y = winHeight.value - _height.value - props.thresholdBottom;
} else if (y == -3 || y == -4) {
y = props.thresholdTop;
}
x = Math.max(props.threshold, Math.min(winWidth.value - _width.value - props.threshold, x))
if(y>winHeight.value/2){
y = Math.max(props.thresholdBottom, Math.min(winHeight.value - _height.value - props.thresholdBottom, y))
}else{
y = Math.max(props.thresholdTop, Math.min(winHeight.value - _height.value - props.thresholdTop, y))
}
node.style.setProperty("left", `${x}px`)
node.style.setProperty("top", `${y + windtop.value}px`)
nowXy.value = [x, y];
lastX.value = x;
lastY.value = y;
/**
* 当前的位置,等同v-model:offset
* @param postion {number[]} [x,y]位置
*/
emits('change', nowXy.value)
first.value = false
}
function eventTrasform_start(evt : POSITION_TYPE_XY) {
isMoveing.value = true;
diffX.value = 0
dateTime.value = new Date().getTime()
let node = xFloatButton.value as Element
let leftpos = parseInt(node.style.getPropertyValue("left")! as string)
let toppos = parseInt(node.style.getPropertyValue("top")! as string)
_x.value = evt.x - leftpos
_y.value = evt.y - toppos
_real_X.value = evt.x
_real_Y.value = evt.y
node.style.setProperty("transition-duration", '0ms')
let realx = Math.floor(evt.x - _real_X.value)
let realy = Math.floor(evt.y - _real_Y.value)
clearTimeout(longtimeid.value)
longtimeid.value = setTimeout(function() {
emits('longpress')
}, 500);
}
function eventTrasform_move(evt : POSITION_TYPE_XY) {
clearTimeout(longtimeid.value)
let x = evt.x - _x.value
let y = evt.y - _y.value
let diff_x = evt.x - _real_X.value
let diff_y = evt.y - _real_Y.value
diffX.value = Math.max(Math.abs(diff_x), Math.abs(diff_y))
let node = xFloatButton.value as Element
let maxX = winWidth.value - _width.value;
let maxY = winHeight.value - _height.value + windtop.value;
x = Math.max(Math.min(maxX, x), 0)
y = Math.max(Math.min(maxY, y), 0)
node.style.setProperty("left", `${x}px`)
node.style.setProperty("top", `${y}px`)
nowXy.value = [x, y];
/**
* 当前的位置,等同v-model:offset
* @param postion {number[]} [x,y]位置
*/
emits('change', [x, y])
}
function eventTrasform_end(evt : POSITION_TYPE_XY) {
isMoveing.value = false;
let node = xFloatButton.value as Element
let x = evt.x - _x.value
let y = evt.y - _y.value - windtop.value
let maxX = winWidth.value - _width.value;
let maxY = winHeight.value - _height.value;
x = Math.max(Math.min(maxX, x), 0)
y = Math.max(Math.min(maxY, y), 0)
if (props.adsorption) {
y = Math.max(Math.min(maxY - props.threshold, y), props.threshold)
if (x >= (winWidth.value - _width.value) / 2) {
x = winWidth.value - _width.value - props.threshold;
} else {
x = props.threshold;
}
nowXy.value = [x, y];
setProperty(x, y)
}
let diffTiff = new Date().getTime() - dateTime.value
// sdk的click和touch会同时触发,呃。目前只能以触发时间自行判断是单击还是滑动。
let realx = Math.floor(evt.x - _real_X.value)
let realy = Math.floor(evt.y - _real_Y.value)
if(realx==0&&realy==0&&realx==realy){
if (diffTiff > 50 && diffTiff<250){
onClick()
}
}
}
function mStart(evt : TouchEvent) {
// evt.preventDefault()
if (_disabled.value) return
// #ifdef WEB||MP
evt.preventDefault();
evt.stopPropagation();
// #endif
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
_real_X.value = x;
_real_Y.value = x;
eventTrasform_start({ x, y } as POSITION_TYPE_XY)
}
function mMove(evt : TouchEvent) {
// #ifdef WEB||MP
evt.preventDefault();
evt.stopPropagation();
// #endif
if (_disabled.value) return
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
eventTrasform_move({ x, y } as POSITION_TYPE_XY)
}
function mEnd(evt : TouchEvent) {
if (_disabled.value) return
let x = evt.changedTouches[0].clientX;
let y = evt.changedTouches[0].clientY;
eventTrasform_end({ x, y } as POSITION_TYPE_XY)
}
function getNodes() : Promise<boolean> {
return new Promise((res, rej) => {
uni.createSelectorQuery()
.in(proxy)
.select(".xFloatButtonBox")
.boundingClientRect()
.exec((nodes) => {
let node = nodes[0] as NodeInfo;
winWidth.value = node.width!;
winHeight.value = node.height! - windtop.value;
res(true)
})
})
}
function onresizeOffsetXy() {
getNodes().then(() => {
setProperty(props.offset[0], props.offset[1])
})
}
watch(():Array<number> => props.offset, (newValue : Array<number>) => {
if (newValue.length == 2 && nowXy.value.join("") != newValue.join("")) {
setProperty(newValue[0], newValue[1])
}
})
onMounted(() => {
isReady.value = false;
let sys = uni.getWindowInfo()
// #ifndef APP
winWidth.value = sys.windowWidth
winHeight.value = sys.windowHeight;
windtop.value = sys.windowTop;
// #endif
// #ifdef APP
winWidth.value = sys.windowWidth
winHeight.value = sys.windowHeight + 44;
// #endif
// 兼容电脑端
// #ifdef WEB
window.addEventListener('mouseup', mmEnd);
window.addEventListener('mousemove', mmMove);
// #endif
let t = this;
getNodes().then(() => {
setProperty(props.offset[0], props.offset[1])
isReady.value = true;
})
uni.$on("onResize", onresizeOffsetXy)
})
onBeforeUnmount(() => {
// 兼容电脑端
// #ifdef WEB
window.removeEventListener('mouseup', mmEnd);
window.removeEventListener('mousemove', mmMove);
// #endif
uni.$off("onResize", onresizeOffsetXy)
})
// #ifdef WEB
function mmStart(evt : UniMouseEvent) {
;(window as any).xFloatButtonId = id.value;
if (_disabled.value) return
evt.preventDefault();
let x = evt.clientX;
let y = evt.clientY;
_real_X.value = x;
_real_Y.value = x;
eventTrasform_start({ x, y } as POSITION_TYPE_XY)
}
function mmMove(evt : UniMouseEvent) {
evt.preventDefault();
// evt.stopPropagation();
if (_disabled.value || !isMoveing.value || (window as any).xFloatButtonId != id.value) return
let x = evt.clientX;
let y = evt.clientY;
eventTrasform_move({ x, y } as POSITION_TYPE_XY)
}
function mmEnd(evt : UniMouseEvent) {
if (_disabled.value || !isMoveing.value || (window as any).xFloatButtonId != id.value) return
let x = evt.clientX;
let y = evt.clientY;
eventTrasform_end({ x, y } as POSITION_TYPE_XY)
}
// #endif
</script>
<template>
<view>
<view
:id="id"
@touchstart="mStart"
@touchmove.stop="mMove"
@touchend="mEnd"
ref="xFloatButton"
<!--#ifdef WEB -->
@mousedown="mmStart"
<!-- #endif -->
:style="[{
width:_width+'px',
height:_height+'px',
borderRadius:_round,
zIndex:(zIndex+1),
opacity:isReady?'1':'0'
},_bgColor]" class="xFloatButton"
>
<!--
@slot 请在插槽内自由布局你的样式及功能块。
-->
<slot></slot>
</view>
<view class="xFloatButtonBox" :style="{zIndex:zIndex}"></view>
</view>
</template>
<style scoped>
.xFloatButtonBox {
pointer-events: none;
/* z-index: 87; */
width: 100%;
height: 100%;
background-color: transparent;
opacity: 0;
position: fixed;
left: 0px;
top: 0px;
transform: translate(-100%, -100%);
}
.xFloatButton {
/* z-index: 88; */
transition-duration: 0ms;
transition-property: left, right, top, bottom;
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
top: 0px;
left: 0px;
position: fixed;
/* #ifndef APP-HARMONY */
/* box-shadow: 0 5px 24px rgba(0, 0, 0, 0.06); */
/* #endif */
/* #ifdef WEB */
cursor: grab;
/* #endif */
}
/* #ifdef WEB */
.xFloatButton:active {
cursor: grabbing;
}
/* #endif */
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,919 @@
<!-- #ifdef MP-WEIXIN -->
<!-- @vue-docgen-ignore-next-line -->
<script module="xfs" lang="wxs" src="./xfs.wxs"></script>
<!-- #endif -->
<script lang="ts" setup>
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
import { checkIsCssUnit, rpx2px, getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type POSITION_TYPE = "bottom" | "top" | "left" | "right"
type POSITION_EVENT = {
x : number,
y : number,
classList : Array<string>
}
/**
* @name 浮动面板 FloatDrawer
* @description 提供流畅的拖拉阻尼效果,回弹丝滑。右滑关闭逻辑已经实现,但在app体验不好,主要是scoll与事件冲突需要官方优化.
* @page /pages/index/float-drawer
* @category 反馈组件
* @constant 平台兼容
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
*/
defineOptions({name:"xFloatDrawer"})
const proxy = getCurrentInstance()?.proxy??null;
defineSlots<{
default(props: { show: boolean, height: number }): any
}>()
const emits = defineEmits([
/**
* 关闭时执行
*/
'close',
/**
* 打开执行的事件
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 高度位置变化时触发这个差值.返回参数evt是个百分比,0%是最低下,100%代表是在最顶部.
*/
'heightChange',
/**
* 开始拖动
*/
'movestart',
/**
* 结束拖动
*/
'moveend',
/**
* 等同v-model:show
*/
'update:show'
])
export type xFloatDrawerPropsType = {
/**
* 显示可v-model:show双向绑定
* 默认是打开还是放置在底部。
*/
show: boolean,
/**
* 是否仅允许通过标题栏拖动。
*/
onlyHeader: boolean,
/**
* 动画时间
*/
duration: number,
/**
* 向上的圆角
* 空值时,取全局配置的圆角。
*/
round: string,
/**
* 百分比,数字字符或者带单位,
* 默认露出的内容高度
*/
size: string,
/**
* 弹层最大的高度值,默认为屏幕的可视高
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: string,
/**
* 当拖动时,触发打开和关闭时的临界值,单位是px
* 如果没有达到此临界值时,将会回弹至原始位置。
*/
triggerDy: number,
/**
* 当拖动时,如果已经达到了关闭和打开时的临界值时
* 可以继续拖拉时缓动阻尼值
*/
threshold: number,
/**
* 内容层的背景色
*/
bgColor: string,
/**
* 暗黑的背景色,空时,取全局的sheetDarkColor
*/
darkBgColor: string,
/**
* 拖动标题栏的横线背景色
*/
actionColor: string,
/**
* 禁用内部的容器并采用view容器
*/
disabledScroll: boolean,
/**
* 没有禁用disabledScroll生效
* 容器内部使用的类型
* scroll :scroll-view
* list : list-view
*/
containerType: string,
/**
* 是否禁用用户滚动等来触发关闭或者打开。
*/
disabled: boolean,
/**
* 层级
*/
zIndex: number,
/**
* 控制内容的边跑,有时需要自定布局时非常有用.
* 请直接使用style css规则写margin,
*/
contentMargin: string
}
const props = withDefaults(defineProps<xFloatDrawerPropsType>(), {
show: false,
onlyHeader: false,
duration: 350,
round: "",
size: "15%",
maxHeight: "80%",
triggerDy: 180,
threshold: 0.045,
bgColor: "white",
darkBgColor: "",
actionColor: "#888888",
disabledScroll: false,
containerType: 'scroll',
disabled: false,
zIndex: 100,
contentMargin: "0 16px 16px 16px"
})
// 响应式数据
const _width = ref(0)
const _height = ref(0)
const elementWrap = ref<UniElement | null>(null)
//是否动画中
const actioning = ref(false)
const status = ref("")
const wrapId = ref("xFloatDrawerWrap" + getUid())
const _y = ref(0)
const _realY = ref(0)
const _realYDiff = ref(0)
const isHover = ref(false)
const disabledMove = ref(true)
const netId = ref('xFloatDrawerScrollIds-' + getUid())
const tid = ref(0)
const startMoveWEB = ref(false)
const first = ref(true)
const _test_x = ref(0)
const _test_y = ref(0)
const _scrollDetail_y = ref(0)
const moveDy = ref(0)
const endtid = ref(12)
const isClicking = ref(false)
const _heade_x = ref(0)
const _heade_y = ref(0)
const _scrollDetail_y_start = ref(0)
const disabledScolly = ref(false)
const isMoving = ref(false)
const touchmoevIsOver = ref(false)
const tid2 = ref(11)
const nowTouchType = ref('body')
const moveDir = ref("none")
const _reeal_x = ref(0)
const _reeal_y = ref(0)
const scrollTopDiff = ref(0)
// 计算属性
const _contentMargin = computed((): string => props.contentMargin)
const _show = computed((): boolean => props.show)
const _disabled = computed((): boolean => props.disabled)
const _onlyHeader = computed((): boolean => props.onlyHeader)
const _duration = computed((): number => props.duration)
const _round = computed((): string => {
let round = props.round;
if (round == "") {
round = xConfig.drawerRadius
}
let radius = checkIsCssUnit(round, 'rpx');
let _r = `${radius} ${radius} 0px 0px`
return _r
})
const _size = computed((): number => {
if (props.size == "") {
return _height.value * 0.8
}
if (props.size.lastIndexOf('rpx') > -1) {
let sz = rpx2px(parseFloat(props.size))
return _height.value - sz
}
if (props.size.lastIndexOf('px') > -1) {
let sz = parseFloat(props.size)
return _height.value - sz
}
if (props.size.lastIndexOf('%') > -1) {
let sz = parseFloat(props.size) / 100 * _height.value
return _height.value - sz
}
let sz = uni.rpx2px(parseFloat(props.size))
return _height.value - sz
})
const _maxHeight = computed((): number => {
if (props.size == "") {
return _height.value * 0.2
}
if (props.maxHeight.lastIndexOf('rpx') > -1) {
let sz = rpx2px(parseFloat(props.maxHeight))
return _height.value - sz
}
if (props.maxHeight.lastIndexOf('px') > -1) {
let sz = parseFloat(props.maxHeight)
return _height.value - sz
}
if (props.maxHeight.lastIndexOf('%') > -1) {
let sz = parseFloat(props.maxHeight) / 100 * _height.value
return _height.value - sz
}
let sz = rpx2px(parseFloat(props.maxHeight))
return _height.value - sz
})
const _bgColor = computed((): string => {
if (xConfig.dark == 'dark') {
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(props.bgColor)
})
const _actionColor = computed((): string => getDefaultColor(props.actionColor))
const _animationFun = computed((): string => xConfig.animationFun)
// 方法
function onEnd() {
if (status.value == 'close') {
/**
* 关闭时执行
*/
emits('close')
/**
* 等同v-model:show
*/
emits('update:show', false)
emits('heightChange', 0)
} else {
/**
* 打开执行的事件
*/
emits('open')
emits('update:show', true)
emits('heightChange', 100)
}
actioning.value = false;
_realYDiff.value = 0
}
function setStyleAni() {
try {
let duration = _duration.value;
if (first.value == true) {
duration = 0
}
if (status.value == 'open') {
clearTimeout(tid.value)
tid.value = setTimeout(function () {
elementWrap.value!.style.setProperty("transition-duration", duration.toString() + 'ms')
elementWrap.value!.style.setProperty('transform', `translate(0%,${_maxHeight.value}px)`)
}, 50);
} else if (status.value == 'close') {
clearTimeout(tid.value)
tid.value = setTimeout(function () {
elementWrap.value!.style.setProperty("transition-duration", duration.toString() + 'ms')
elementWrap.value!.style.setProperty('transform', `translate(0%,${_size.value}px)`)
}, 50);
}
} catch (e) {
//TODO handle the exception
}
first.value = false;
}
function closeAlert() {
if (actioning.value || status.value == 'close') return
actioning.value = true;
status.value = 'close'
/**
* 关闭前执行
*/
emits('beforeClose')
emits('heightChange', 0)
// #ifndef MP-WEIXIN
setStyleAni();
// #endif
}
function showAlert() {
if (actioning.value) return;
if (status.value == 'open') return;
actioning.value = true;
status.value = 'open'
/**
* 打开前执行
*/
emits('beforeOpen')
emits('heightChange', 100)
// #ifndef MP-WEIXIN
setStyleAni();
// #endif
}
function openDrawer() {
showAlert();
}
function headerClickClose() {
if (_disabled.value) return;
if (moveDy.value != 0) return;
actioning.value = false;
if (status.value == 'open') {
closeAlert();
} else {
showAlert();
}
onEnd()
}
// #ifdef MP-WEIXIN
function setOpts(opts: any) {
status.value = opts.status;
}
function callEmits(args: any) {
if(!args.name) return;
if(args.args!=null&&args.args!=undefined){
emits(args.name as any, args.args)
}else{
emits(args.name as any)
}
}
function setDisabledScolly(opts: any) {
disabledScolly.value = opts.disabledScolly
isMoving.value = opts.isMoving
}
// #endif
function scrollTopChange() {
// 空实现
}
function scollChange(evt: UniScrollEvent) {
const stop = Math.max(0,Math.floor(evt.detail.scrollTop));
scrollTopDiff.value = stop
// #ifdef APP-ANDROID||WEB||APP-HARMONY
if(!isMoving.value){
_scrollDetail_y.value = stop
}
disabledScolly.value = _scrollDetail_y.value<=0;
// #endif
// #ifdef APP-IOS
_scrollDetail_y.value = stop
//当手指已经离开了屏幕,再置顶不需要再禁用滚动了。如果手指一直在划动则需要禁用。
if(!touchmoevIsOver.value){
disabledScolly.value = _scrollDetail_y.value<=0;
}else{
disabledScolly.value = false
}
// #endif
}
function eventTransform_start(evt: POSITION_EVENT) {
try {
let ele = elementWrap.value!;
if (status.value == 'open') {
_y.value = evt.y - (_maxHeight.value)
} else if (status.value == 'close') {
_y.value = evt.y - (_size.value)
}
_realY.value = evt.y
ele.style.setProperty("transition-duration", '0ms')
} catch (e) {
//TODO handle the exception
}
}
function eventTransform_move(evt: POSITION_EVENT) {
try {
if (!disabledMove.value) return;
let ele = elementWrap.value! as UniElement
if(moveDir.value == 'v'||status.value=='close'){
let movey = evt.y - _y.value - (nowTouchType.value == 'header'?0:_scrollDetail_y_start.value )
if (movey <= _maxHeight.value) {
movey = _maxHeight.value - (_maxHeight.value - movey) * props.threshold
} else if (movey >= _size.value) {
movey = _size.value + (movey - _size.value) * props.threshold
}
ele.style.setProperty("transform", `translate(0%, ${movey.toString()}px)`)
_realYDiff.value = evt.y - _realY.value
let ratioValue = (movey - _size.value) / (_maxHeight.value - _size.value) * 100;
emits('heightChange',ratioValue )
}else if(moveDir.value == 'h'&&status.value=='open'){
disabledScolly.value = true;
let traslateX = evt.x - _test_x.value
traslateX = Math.max(0,Math.min(traslateX,_width.value))
ele.style.setProperty("transform", `translate(${traslateX.toString()}px,${_maxHeight.value}px)`)
let ratioValue = (1-traslateX / _width.value) * 100
emits('heightChange',ratioValue )
}
} catch (e) {
console.error("floatDrawer出现意外错误:",e,"不用担心,这是兼容性错误,不会影响使用!!仅供参考.")
}
}
function eventTransform_end(evt: POSITION_EVENT) {
const oldStatus = status.value
try {
if (!disabledMove.value) return;
isHover.value = false;
let ele = elementWrap.value! as UniElement
ele.style.setProperty("transition-duration", _duration.value.toString() + 'ms');
if(moveDir.value == 'v'||status.value=='close'){
if (_realYDiff.value >= props.triggerDy) {
ele.style.setProperty("transform", `translate(0%, ${_size.value.toString()}px)`);
status.value = 'close';
emits('beforeClose');
emits('heightChange', 0);
} else if (_realYDiff.value < (props.triggerDy * -1)) {
ele.style.setProperty("transform", `translate(0%, ${_maxHeight.value.toString()}px)`);
status.value = 'open';
emits('heightChange', 100);
} else {
if (status.value === 'open') {
ele.style.setProperty("transform", `translate(0%, ${_maxHeight.value.toString()}px)`);
emits('heightChange', 100);
} else {
ele.style.setProperty("transform", `translate(0%, ${_size.value.toString()}px)`);
emits('heightChange', 0);
}
}
}else if(moveDir.value == 'h'&&status.value=='open'){
let traslateX = evt.x - _test_x.value
traslateX = Math.max(0,Math.min(traslateX,_width.value))
if(traslateX>=50){
status.value = 'close';
emits('heightChange',0 )
ele.style.setProperty("transform", `translate(0px,${_size.value}px)`)
}else{
status.value = 'open';
emits('heightChange',100 )
ele.style.setProperty("transform", `translate(0px,${_maxHeight.value}px)`)
disabledScolly.value = true;
}
}
} catch (e) {
// 处理异常
console.error(e)
}
if(oldStatus != status.value){
onEnd()
}
}
function mStart(evt: UniTouchEvent, type: string) {
if (type == 'header'){
isHover.value = true;
scrollTopDiff.value = -1
// 这里的意思,是让这个值引用界面的变化,这样可以避免可能的界面不渲染。
nextTick((()=>{
scrollTopDiff.value = 0
}))
}
nextTick(()=>{
emits('movestart');
nowTouchType.value = type
touchmoevIsOver.value = true;
if (_disabled.value) return;
if (_onlyHeader.value && type == 'body') return;
moveDy.value = 0;
_test_x.value = evt.changedTouches[0].clientX;
_reeal_x.value = evt.changedTouches[0].clientX
_reeal_y.value = evt.changedTouches[0].clientY
_scrollDetail_y_start.value = _scrollDetail_y.value; // 记录内部滚动的起始位置
disabledScolly.value = false
isMoving.value = false
// 按头拖时,不要记录起始的位置,否则body与头会出现heady的位移。
if (type == 'header'){
disabledScolly.value = true
}else{
_test_y.value = evt.changedTouches[0].clientY;
}
eventTransform_start({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY,
classList: [] as string[],
} as POSITION_EVENT);
})
}
function mMove(evt: UniTouchEvent, type: string) {
if (_disabled.value) return;
if (_onlyHeader.value && nowTouchType.value == 'body') return;
const currentY = evt.changedTouches[0].clientY;
const currentX = evt.changedTouches[0].clientX;
const dy = currentY - _test_y.value;
const dx_real = currentX - _reeal_x.value
const dy_real = currentY - _reeal_y.value
moveDir.value = 'v'
if (_scrollDetail_y.value>0&& nowTouchType.value == 'body'&&moveDir.value=='v') return;
// #ifdef WEB
evt.preventDefault();
// #endif
if(nowTouchType.value == 'body'){
if(dy<0){
disabledScolly.value = false;
isMoving.value = false;
}else{
_scrollDetail_y.value = 0
}
}
let scrollDetail_y_start = nowTouchType.value == 'header'?0:_scrollDetail_y_start.value
if (scrollDetail_y_start > 0 && dy < 0) {
// 如果内部可以向上滚动,并且拖动是向上的
// 计算需要滚动的内部距离
const internalScrollDelta = -dy;
if (_scrollDetail_y.value - internalScrollDelta >= 0) {
// 内部可以滚动,更新内部滚动位置
_scrollDetail_y.value -= internalScrollDelta;
} else {
// 内部已经滚动到顶部,开始拖动整个弹层
const excess = internalScrollDelta - scrollDetail_y_start;
isMoving.value = true;
eventTransform_move({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY - excess,
classList: [] as string[],
} as POSITION_EVENT);
}
} else {
isMoving.value = true;
// 其他情况,允许拖动整个弹层
eventTransform_move({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY,
classList: [] as string[],
} as POSITION_EVENT);
}
}
function mEnd(evt: UniTouchEvent, type: string) {
emits('moveend');
isMoving.value = false;
touchmoevIsOver.value = false;
if (_disabled.value){
return;
}
if (nowTouchType.value == 'header'||_scrollDetail_y.value<0){
disabledScolly.value = false
}
let dy = evt.changedTouches[0].clientY - _test_y.value;
let dx = evt.changedTouches[0].clientX - _test_x.value;
if (dy == dx) return;
moveDy.value = dy;
eventTransform_end({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY,
classList: [] as string[],
});
disabledScolly.value = false;
moveDir.value = 'none'
}
// #ifdef WEB
function mmStart(evt: UniMouseEvent, type: string) {
// @ts-ignore
window.FloatDrawerId = wrapId.value
emits('movestart')
_heade_x.value = evt.clientX
_heade_y.value = evt.clientY
_test_y.value = evt.clientY;
_test_x.value = evt.clientX;
if (_disabled.value) return;
if (type == 'header'){
isHover.value = true;
}
if (_onlyHeader.value && type == 'body') return
startMoveWEB.value = true;
eventTransform_start({
x: evt.clientX,
y: evt.clientY,
classList: [] as string[],
} as POSITION_EVENT)
}
function mmMove(evt: UniMouseEvent, type: string) {
// @ts-ignore
if(window.FloatDrawerId!=wrapId.value) return
if (_onlyHeader.value && type == 'body') return
if (!startMoveWEB.value) return;
const currentY = evt.clientY;
const currentX = evt.clientX;
const dy = currentY - _test_y.value;
const dx = currentX - _test_x.value
if(moveDir.value=='none'&&(Math.abs(dx) - Math.abs(dy))>0){
moveDir.value = 'h'
}else if(moveDir.value=='none'&&(Math.abs(dx) - Math.abs(dy))<=0){
moveDir.value = 'v'
}
eventTransform_move({
x: evt.clientX,
y: evt.clientY,
classList: [] as string[],
} as POSITION_EVENT)
}
function mmEnd(evt: UniMouseEvent, type: string) {
emits('moveend')
let diffx = evt.clientX - _heade_x.value
let diffy = evt.clientY - _heade_y.value
// @ts-ignore
window.FloatDrawerId = ''
if (diffx == diffy) return;
if (_disabled.value) return;
startMoveWEB.value = false;
eventTransform_end({
x: evt.clientX,
y: evt.clientY,
classList: [] as string[],
})
moveDir.value = 'none'
}
// #endif
// 监听器
watch((): boolean => props.show, (newval: boolean) => {
if (newval && status.value == 'close') {
actioning.value = false;
showAlert()
onEnd()
} else if(!newval && status.value == 'open') {
actioning.value = false;
closeAlert()
onEnd()
}
})
// 生命周期
onMounted(() => {
let yachi = 0
// #ifdef APP
yachi = 250
// #endif
tid.value = setTimeout(function () {
let sys = uni.getWindowInfo()
// #ifndef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight;
// #endif
// #ifdef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight + 44;
// #endif
if (_show.value) {
showAlert();
actioning.value = false;
status.value = 'open'
} else {
closeAlert()
actioning.value = false;
status.value = 'close'
}
}, yachi);
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
clearTimeout(tid2.value)
})
defineExpose({
/** 打开 **/
open: () => showAlert(),
/** 关闭 **/
close: () => closeAlert()
})
</script>
<template>
<!-- @transitionend="onEnd" -->
<view @click.stop=""
ref="elementWrap"
class="xFloatDrawerWrapContent" :id="wrapId" :style="{
width:'100%',
height:_height+'px',
borderRadius:_round,
backgroundColor:_bgColor,
'transition-timing-function':_animationFun,
zIndex:props.zIndex
}"
<!-- #ifdef MP-WEIXIN -->
@touchstart="xfs.mStart"
@touchend="xfs.mEnd"
@touchmove="xfs.mMove"
:change:prop="xfs.propObserver"
:prop="status"
:data-opts="{
maxHeight:_maxHeight,
size:_size,height:_height,
status:status,
threshold:props.threshold,
duration:_duration,
triggerDy:props.triggerDy,
_scrollDetail_y:_scrollDetail_y
}"
<!-- #endif -->
>
<view
<!-- #ifdef APP||H5 -->
@touchstart="mStart($event as TouchEvent,'header')"
@touchend="mEnd($event as TouchEvent,'header')"
@touchmove="mMove($event as TouchEvent,'header')"
<!-- #endif -->
<!-- #ifdef WEB -->
@mousedown="mmStart($event as MouseEvent,'header')"
@mousemove="mmMove($event as MouseEvent,'header')"
@mouseup="mmEnd($event as MouseEvent,'header')"
@mouseleave="mmEnd"
<!-- #endif -->
class="xFloatDrawerBar"
>
<view @click="headerClickClose" style="height:100%" class="xFloatDrawerBarSop">
<view class="xFloatDrawerBarLine" :style="{'background-color':_actionColor,opacity: isHover?1:0.5}">
</view>
</view>
</view>
<view class="xFLoatViewWrap"
style="flex:1"
<!-- #ifdef APP||H5 -->
@touchstart.stop="mStart($event as TouchEvent,'body')"
@touchend.stop="mEnd($event as TouchEvent,'body')"
@touchmove.stop="mMove($event as TouchEvent,'body')"
<!-- #endif -->
<!-- #ifdef WEB -->
@mousedown="mmStart($event as MouseEvent,'body')"
@mousemove="mmMove($event as MouseEvent,'body')"
@mouseup="mmEnd($event as MouseEvent,'body')"
@mouseleave="mmEnd"
@mousewheel="disabledScolly = false"
<!-- #endif -->
>
<scroll-view
:scroll-top="scrollTopDiff"
v-if="!props.disabledScroll&&props.containerType=='scroll'"
:direction="status=='close'||disabledScolly?'none':'vertical'"
:scroll-y="status=='close'||disabledScolly?false:true"
@scroll="scollChange"
@scrolltoupper="scrollTopChange" :style="{flex:'1',margin:_contentMargin,height:'100px'}"
:bounces="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
@prop {Number} height - 容器的高度
-->
<slot name="default" :show="_show" :height="_maxHeight"></slot>
<view :style="{height:_maxHeight+'px'}"></view>
</scroll-view>
<list-view v-else-if="!props.disabledScroll&&props.containerType=='list'"
:scroll-top="scrollTopDiff"
:scroll-y="status=='close'||disabledScolly?'none':'vertical'"
:direction="status=='close'||disabledScolly?'none':'vertical'" @scroll="scollChange"
@scrolltoupper="scrollTopChange" :style="{flex:'1',margin:_contentMargin}" :bounces="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
@prop {Number} height - 容器的高度
-->
<slot name="default" :show="_show" :height="_maxHeight"></slot>
<list-item>
<view :style="{height:_maxHeight+'px'}"></view>
</list-item>
</list-view>
<view v-else :style="{flex:'1',margin:_contentMargin}">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
@prop {Number} height - 容器的高度
-->
<slot name="default" :show="_show" :height="_maxHeight"></slot>
</view>
</view>
</view>
</template>
<style>
.xFloatDrawerBarSop {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xFloatDrawerBar {
height: 44px;
/* #ifdef WEB */
cursor: grab;
/* #endif */
}
/* #ifdef WEB */
.xFloatDrawerBar:active {
cursor: grabbing;
}
/* #endif */
.xFloatDrawerBarLine {
width: 60px;
height: 4px;
border-radius: 8px;
}
.xFloatDrawerWrapContent {
display: flex;
flex-direction: column;
/* #ifndef APP-HARMONY */
transition-duration: 250ms;
/* #endif */
/* #ifdef APP-HARMONY */
transition-duration: 0ms;
/* #endif */
/* transition-timing-function: cubic-bezier(.18,.89,.32,1); */
transition-property: transform;
display: flex;
flex-direction: column;
transform: translate(0%, 100%);
position: fixed;
/* z-index: 100; */
left: 0px;
bottom: 0px;
}
</style>
@@ -0,0 +1,837 @@
<!-- #ifdef MP-WEIXIN -->
<!-- @vue-docgen-ignore-next-line -->
<script module="xfs" lang="wxs" src="./xfs.wxs"></script>
<!-- #endif -->
<script lang="ts">
import { checkIsCssUnit, rpx2px, getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type POSITION_TYPE = "bottom" | "top" | "left" | "right"
type POSITION_EVENT = {
x : number,
y : number,
classList : Array<string>
}
/**
* @name 浮动面板 FloatDrawer
* @description 提供流畅的拖拉阻尼效果,回弹丝滑。右滑关闭逻辑已经实现,但在app体验不好,主要是scoll与事件冲突需要官方优化.
* @page /pages/index/float-drawer
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
_width: 0,
_height: 0,
status:'close' as 'close'|'open',
disabledScrolling:false,
actioning:false,
diffY:0,
first:true,
tid:5,
_lastTouchY:0,
_isTouching:false,
_scrollTop:0,
_lastScrollTop:0,
_topEpsilon:2,
_translateY:0,
_currentTranslateY:0
}
},
emits: [
/**
* 关闭时执行
*/
'close',
/**
* 打开执行的事件
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 高度位置变化时触发这个差值.返回参数evt是个百分比,0%是最低下,100%代表是在最顶部.
*/
'heightChange',
/**
* 开始拖动
*/
'movestart',
/**
* 结束拖动
*/
'moveend',
/**
* 等同v-model:show
*/
'update:show'],
props: {
/**
* 显示可v-model:show双向绑定
* 默认是打开还是放置在底部。
*/
show: {
type: Boolean,
default: false
},
/**
* 是否仅允许通过标题栏拖动。
*/
onlyHeader: {
type: Boolean,
default: false
},
/**
* 动画时间
*/
duration: {
type: Number,
default: 350
},
/**
* 向上的圆角
* 空值时,取全局配置的圆角。
*/
round: {
type: String,
default: ""
},
/**
* 百分比,数字字符或者带单位,
* 默认露出的内容高度
*/
size: {
type: String,
default: "15%"
},
/**
* 弹层最大的高度值,默认为屏幕的可视高
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: {
type: String,
default: "80%"
},
/**
* 当拖动时,触发打开和关闭时的临界值,单位是px
* 如果没有达到此临界值时,将会回弹至原始位置。
*/
triggerDy: {
type: Number,
default: 100
},
/**
* 当拖动时,如果已经达到了关闭和打开时的临界值时
* 可以继续拖拉时缓动阻尼值
*/
threshold: {
type: Number,
default: 0.045
},
/**
* 内容层的背景色
*/
bgColor: {
type: String,
default: "white"
},
/**
* 暗黑的背景色,空时,取全局的sheetDarkColor
*/
darkBgColor: {
type: String,
default: ""
},
/**
* 拖动标题栏的横线背景色
*/
actionColor: {
type: String,
default: "#888888"
},
/**
* 禁用内部的容器并采用view容器
*/
disabledScroll: {
type: Boolean,
default: false
},
/**
* 没有禁用disabledScroll生效
* 容器内部使用的类型
* scroll :scroll-view
* list : list-view
*/
containerType: {
type: String,
default: 'scroll'
},
/**
* 是否禁用用户滚动等来触发关闭或者打开。
*/
disabled: {
type: Boolean,
default: false
},
/**
* 层级
*/
zIndex: {
type: Number,
default: 100
},
/**
* 控制内容的边跑,有时需要自定布局时非常有用.
* 请直接使用style css规则写margin,
*/
contentMargin: {
type: String,
default: "0 16px 16px 16px"
}
},
watch: {
show(_newval : boolean) {
if (_newval&&this.status == 'close') {
this.actioning = false;
this.open()
this.onEnd()
} else if(!_newval&&this.status == 'open') {
this.actioning = false;
this.close()
this.onEnd()
}
},
},
computed: {
_contentMargin() : string {
return this.contentMargin
},
_show() : boolean {
return this.show
},
_disabled() : boolean {
return this.disabled
},
_onlyHeader() : boolean {
return this.onlyHeader
},
_duration() : number {
return this.duration
},
_round() : string {
let round = this.round;
if (round == "") {
round = xConfig.drawerRadius
}
let radius = checkIsCssUnit(round, 'rpx');
let _r = `${radius} ${radius} 0px 0px`
return _r
},
_size() : number {
if (this.size == "") {
return this._height * 0.8
}
if (this.size.lastIndexOf('rpx') > -1) {
let sz = rpx2px(parseFloat(this.size))
return this._height - sz
}
if (this.size.lastIndexOf('px') > -1) {
let sz = parseFloat(this.size)
return this._height - sz
}
if (this.size.lastIndexOf('%') > -1) {
let sz = parseFloat(this.size) / 100 * this._height
return this._height - sz
}
let sz = uni.rpx2px(parseFloat(this.size))
return this._height - sz
},
_maxHeight() : number {
if (this.size == "") {
return this._height * 0.2
}
if (this.maxHeight.lastIndexOf('rpx') > -1) {
let sz = rpx2px(parseFloat(this.maxHeight))
return this._height - sz
}
if (this.maxHeight.lastIndexOf('px') > -1) {
let sz = parseFloat(this.maxHeight)
return this._height - sz
}
if (this.maxHeight.lastIndexOf('%') > -1) {
let sz = parseFloat(this.maxHeight) / 100 * this._height
return this._height - sz
}
let sz = rpx2px(parseFloat(this.maxHeight))
return this._height - sz
},
_bgColor() : string {
if (xConfig.dark == 'dark') {
if (this.darkBgColor != '') return getDefaultColor(this.darkBgColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(this.bgColor)
},
_actionColor() : string {
return getDefaultColor(this.actionColor)
},
_animationFun() : string {
return xConfig.animationFun
}
},
mounted() {
let t = this;
let yachi = 0
// #ifdef APP
yachi = 250
// #endif
this.tid = setTimeout(function () {
let sys = uni.getWindowInfo()
t._width = sys.windowWidth
t._height = sys.windowHeight;
if (t._show) {
t.status = 'close'
t.open();
} else {
t.status = 'open'
t.close()
}
}, yachi);
},
beforeUnmount() {
},
methods: {
// #ifdef MP-WEIXIN
setOpts(opts){
this.status = opts.status;
},
callEmits(args){
if(!args.name) return;
if(args.name!='open'&&args.name!='close'){
if(args.args!=null&&args.args!=undefined){
this.$emit(args.name,args.args)
}else{
this.$emit(args.name)
}
}else{
if(args.name == 'open'){
this.status = 'open';
}
if(args.name == 'close'){
this.status = 'close';
}
this.onEnd()
}
},
setDisabledScolly(opts){
this.disabledScrolling = opts.disabledScolly
this._isTouching = opts.isMoving
},
// #endif
getCurrentHeightPercentByTranslate(translateY:number) : number {
const minY = this._maxHeight
const maxY = this._size
const total = maxY - minY
if (total <= 0) return 0
let clamped = translateY
if (clamped < minY) clamped = minY
if (clamped > maxY) clamped = maxY
const percent = (maxY - clamped) / total * 100
return percent
},
setStyleAni() {
let t = this;
try {
const xFLoatDrawerRef = this.$refs['xFLoatDrawerRef'] as UniElement|null;
if(xFLoatDrawerRef == null) return;
let duration = t._duration;
if (t.first == true) {
duration = 0
}
if (this.status == 'open') {
clearTimeout(this.tid)
this.tid = setTimeout(function () {
xFLoatDrawerRef.style.setProperty("transition-duration", duration.toString() + 'ms')
xFLoatDrawerRef.style.setProperty('transform', `translate(0%,${t._maxHeight}px)`)
}, 50);
} else if (this.status == 'close') {
clearTimeout(this.tid)
this.tid = setTimeout(function () {
xFLoatDrawerRef.style.setProperty("transition-duration", duration.toString() + 'ms')
xFLoatDrawerRef.style.setProperty('transform', `translate(0%,${t._size}px)`)
}, 50);
}
} catch (e) {
//TODO handle the exception
}
t.first = false;
},
onEnd(){
let _this = this;
clearTimeout(this._duration)
_this.tid = setTimeout(function() {
if (_this.status == 'close') {
/**
* 关闭时执行
*/
_this.$emit('close')
/**
* 等同v-model:show
*/
_this.$emit('update:show', false)
_this.$emit('heightChange', 0)
} else {
/**
* 打开执行的事件
*/
_this.$emit('open')
_this.$emit('update:show', true)
_this.$emit('heightChange', 100)
}
_this.actioning = false;
_this.diffY = 0
}, _this._duration);
},
open(){
if (this.actioning) return;
if (this.status == 'open') return;
this.actioning = true;
this.status = 'open'
/**
* 打开前执行
*/
this.$emit('beforeOpen')
this.$emit('heightChange', 100)
this.setStyleAni();
},
close(){
if (this.actioning || this.status == 'close') return
this.actioning = true;
this.status = 'close'
/**
* 关闭前执行
*/
this.$emit('beforeClose')
this.$emit('heightChange', 0)
this.setStyleAni();
},
scrollTouchStart(){
if (this.status == 'open') {
this.disabledScrolling = false
}
},
headerClickClose(){
if(this._isTouching) return;
if(this.status == 'open'){
this.actioning = false;
this.close()
}else if(this.status == 'close'){
this.actioning = false;
this.open()
}
},
diffStart(eventx:number,eventy:number){
this._isTouching = true
this._lastTouchY = eventy
this.diffY = 0
let ele = this.$refs['xFLoatDrawerRef'] as UniElement
if (this.status == 'open') {
this._translateY = eventy - (this._maxHeight)
this._currentTranslateY = this._maxHeight
} else if (this.status == 'close') {
this._translateY = eventy - (this._size)
this._currentTranslateY = this._size
}
ele.style.setProperty("transition-duration", '0ms')
this.$emit('heightChange', this.getCurrentHeightPercentByTranslate(this._currentTranslateY))
},
diffMove(eventx:number,eventy:number){
if(!this._isTouching) return
const dy = eventy - this._lastTouchY
this.diffY = dy
// 手指向上滑动(内容希望上滚)
if (dy < 0) {
// 允许滚动
if (this.status == 'open') this.disabledScrolling = false
} else if (dy > 0) {
// 手指向下滑(内容希望下滚)。若已在顶部(含回弹阈值),禁用滚动交由外层处理
const atTop = this._scrollTop <= this._topEpsilon
if (atTop) {
if (!this.disabledScrolling) {
this.disabledScrolling = true
// 切换为外层拖拽时,重置拖拽基线,避免位置跳变
const base = this.status == 'open' ? this._maxHeight : this._size
this._translateY = eventy - base
this._lastTouchY = eventy
this._currentTranslateY = base
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
ele.style.setProperty("transition-duration", '0ms')
}
} else {
if (this.disabledScrolling) this.disabledScrolling = false
}
}
// 拖拽位移与阻尼:
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
const base = this.status == 'open' ? this._maxHeight : this._size
const rawTranslate = eventy - this._translateY
let newTranslate = base
const minY = this._maxHeight
const maxY = this._size
if (this.status == 'open') {
const atTop = this._scrollTop <= this._topEpsilon
if (atTop) {
if (dy > 0) {
// 向下:在 [minY, maxY] 线性;超过 maxY 才强阻尼
if (rawTranslate <= maxY) {
newTranslate = Math.max(minY, Math.min(rawTranslate, maxY))
} else {
const over = rawTranslate - maxY
const damp = over * this.threshold
newTranslate = maxY + Math.min(this.triggerDy, damp)
}
} else if (dy < 0) {
// 向上:仅越过顶部时强阻尼
if (rawTranslate >= minY) {
newTranslate = minY
} else {
const overUp = minY - rawTranslate
const dampUp = overUp * this.threshold
newTranslate = minY - Math.min(this.triggerDy, dampUp)
}
}
}
} else {
if (dy < 0) {
// 向上:在 [minY, maxY] 线性;超过 minY 才强阻尼
if (rawTranslate >= minY) {
newTranslate = Math.max(minY, Math.min(rawTranslate, maxY))
} else {
const overUp = minY - rawTranslate
const dampUp = overUp * this.threshold
newTranslate = minY - Math.min(this.triggerDy, dampUp)
}
} else if (dy > 0) {
// 向下:仅越过底部时强阻尼
if (rawTranslate <= maxY) {
newTranslate = maxY
} else {
const overDown = rawTranslate - maxY
const dampDown = overDown * this.threshold
newTranslate = maxY + Math.min(this.triggerDy, dampDown)
}
}
}
this._currentTranslateY = newTranslate
ele.style.setProperty('transform', `translate(0%,${newTranslate}px)`)
this.$emit('heightChange', this.getCurrentHeightPercentByTranslate(newTranslate))
},
diffEnd(eventx:number,eventy:number){
this._isTouching = false
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
const base = this.status == 'open' ? this._maxHeight : this._size
const moved = this._currentTranslateY - base
const absMoved = Math.abs(moved)
if (absMoved >= this.triggerDy) {
if (this.status == 'open' && moved > 0) {
this.status = 'close'
this.onEnd()
}
if (this.status == 'close' && moved < 0) {
this.status = 'open'
this.onEnd()
}
}
// 复位
const fuweiBase = this.status == 'open' ? this._maxHeight : this._size
ele.style.setProperty("transition-duration", this._duration.toString() + 'ms')
ele.style.setProperty('transform', `translate(0%,${fuweiBase}px)`)
if (this.status == 'open') {
this.$emit('heightChange', 100)
} else if (this.status == 'close') {
this.$emit('heightChange', 0)
}
this.diffY = 0
},
scrolltoTop(){
this.disabledScrolling = true;
// #ifdef APP
let el = this.$refs['xFloatScrollRef']! as UniElement;
// el.scrollTop = 0
// this._scrollTop = 0
// this._lastTouchY = 0
// #endif
console.log('top....')
},
onscroll(evt:UniScrollEvent){
const scrollTop = evt.detail.scrollTop
this._lastScrollTop = this._scrollTop
this._scrollTop = scrollTop
// 在滚动过程中,如果向上滚动,则始终允许滚动
if (this._isTouching) {
const delta = this._scrollTop - this._lastScrollTop
if (delta > 0) {
// 内容向上滚动
if (this.disabledScrolling) this.disabledScrolling = false
} else if (delta < 0) {
// 内容向下滚动
const atTop = this._scrollTop <= this._topEpsilon
if (atTop) {
if (!this.disabledScrolling) this.disabledScrolling = true
} else {
if (this.disabledScrolling) this.disabledScrolling = false
}
}
}
},
mStart(evt:UniTouchEvent){
const x = evt.changedTouches[0].clientX
const y = evt.changedTouches[0].clientY
this.diffStart(x,y)
},
mMove(evt:UniTouchEvent){
const x = evt.changedTouches[0].clientX
const y = evt.changedTouches[0].clientY
this.diffMove(x,y)
},
mEnd(evt:UniTouchEvent){
const x = evt.changedTouches[0].clientX
const y = evt.changedTouches[0].clientY
this.diffEnd(x,y)
},
// #ifdef WEB
mmStart(evt:UniMouseEvent){
const x = evt.clientX
const y = evt.clientY
this.diffStart(x,y)
},
mmMove(evt:UniMouseEvent){
const x = evt.clientX
const y = evt.clientY
this.diffMove(x,y)
},
mmEnd(evt:UniMouseEvent){
const x = evt.clientX
const y = evt.clientY
this.diffEnd(x,y)
},
// #endif
},
}
</script>
<template>
<!-- @transitionend="onEnd" -->
<view
ref="xFLoatDrawerRef"
class="xFloatDrawerWrapContent"
:style="{
width:'100%',
height:_height+'px',
borderRadius:_round,
backgroundColor:_bgColor,
'transition-timing-function':_animationFun,
zIndex:zIndex
}"
<!-- #ifdef MP-WEIXIN -->
@touchstart="xfs.mStart"
@touchend="xfs.mEnd"
@touchcancel="xfs.mEnd"
@touchmove="xfs.mMove"
:change:prop="xfs.propObserver"
:prop="status"
:data-opts="{
maxHeight:_maxHeight,size:_size,height:_height,
status:status,
threshold:threshold,
duration:_duration,
triggerDy:triggerDy,
_scrollDetail_y:_lastScrollTop
}"
<!-- #endif -->
<!-- #ifdef APP||H5 -->
@touchstart="mStart"
@touchmove="mMove"
@touchend="mEnd"
<!-- #endif -->
<!-- #ifdef WEB -->
@mousedown="mmStart"
@mousemove="mmMove"
@mouseup="mmEnd"
<!-- #endif -->
>
<view class="xFloatDrawerBar">
<view @click.stop="headerClickClose" style="height:100%" class="xFloatDrawerBarSop">
<view class="xFloatDrawerBarLine"
:disable="_disabled"
:style="{'background-color':_actionColor,opacity: _disabled?0.5:1}">
</view>
</view>
</view>
<view class="xFLoatViewWrap" :style="{flex:'1',padding:_contentMargin}">
<scroll-view
ref="xFloatScrollRef"
@scrolltoupper="scrolltoTop"
@scroll="onscroll"
@touchstart="scrollTouchStart"
v-if="!disabledScroll&&containerType=='scroll'"
<!-- #ifndef MP-WEIXIN -->
:scroll-y="!(status=='close'||disabledScrolling)"
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
:scroll-y="!(status=='close'||disabledScrolling)"
<!-- #endif -->
:style="{flex:'1'}"
:bounces="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="default" :show="show"></slot>
<view :style="{height:_maxHeight+'px'}"></view>
</scroll-view>
<list-view
ref="xFloatScrollRef"
@scroll="onscroll"
@touchstart="scrollTouchStart"
@scrolltoupper="scrolltoTop"
v-else-if="!disabledScroll&&containerType=='list'"
<!-- #ifndef MP-WEIXIN -->
:direction="status=='close'||disabledScrolling?'none':'vertical'"
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
:scroll-y="!(status=='close'||disabledScrolling)"
<!-- #endif -->
:style="{flex:'1',margin:_contentMargin}" :bounces="false">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
-->
<slot name="default" :show="show"></slot>
<list-item>
<view :style="{height:_maxHeight+'px'}"></view>
</list-item>
</list-view>
<view v-else :style="{flex:'1',margin:_contentMargin}">
<!--
@slot 默认插槽
@prop {Boolean} show - 当前是否已显示
@prop {Number} height - 容器的高度
-->
<slot name="default" :show="show" :height="_maxHeight"></slot>
</view>
</view>
</view>
</template>
<style lang="scss">
.xFloatDrawerBarSop {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xFloatDrawerBar {
height: 44px;
/* #ifdef WEB */
cursor: grab;
/* #endif */
}
/* #ifdef WEB */
.xFloatDrawerBar:active {
cursor: grabbing;
}
/* #endif */
.xFloatDrawerBarLine {
width: 60px;
height: 4px;
border-radius: 8px;
/* #ifdef WEB */
&:not[disable=true]:hover{
opacity: 0.5;
}
/* #endif */
}
.xFloatDrawerWrapContent {
display: flex;
flex-direction: column;
/* #ifndef APP-HARMONY */
transition-duration: 250ms;
/* #endif */
/* #ifdef APP-HARMONY */
transition-duration: 0ms;
/* #endif */
/* transition-timing-function: cubic-bezier(.18,.89,.32,1); */
transition-property: transform;
display: flex;
flex-direction: column;
transform: translate(0%, 100%);
position: fixed;
/* z-index: 100; */
left: 0px;
bottom: 0px;
}
</style>
@@ -0,0 +1,233 @@
var nowTouchType = 'head'
var touchmoevIsOver = true;
var isHover = true;
var isMoving = false;
var _test_x = 0
var _test_y = 0
var _x = 0
var _y = 0
var _realY = 0
var _realYDiff = 0
var _scrollDetail_y_start = 0
var disabledScolly = false;
var defaultTranformYValue = 'matrix(1, 0, 0, 1, 0, 0) '
var opts = {
maxHeight: 650,
size: 480,
height: 0,
threshold: 0,
duration: 300,
status: 'close',
triggerDy: 180,
_scrollDetail_y:0
}
function eventTransform_start(evt, ins) {
isHover = true;
_scrollDetail_y_start = opts._scrollDetail_y
disabledScolly = false
isMoving = false
if (opts.status == 'open') {
_y = evt.y - opts.maxHeight
} else if (opts.status == 'close') {
_y = evt.y - opts.size
}
_realY = evt.y
_test_y = evt.y;
ins.selectComponent('.xFloatDrawerWrapContent').setStyle({
'transition-duration': '0ms',
'transform': defaultTranformYValue
})
ins.callMethod('callEmits', {
disabledScolly: disabledScolly,
isMoving: isMoving
})
ins.callMethod('setDisabledScolly', {
disabledScolly: disabledScolly,
isMoving: isMoving
})
}
function eventTransform_move(evt, ins) {
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
var dy = evt.y - _test_y;
if(dy<0){
disabledScolly = false;
isMoving = false;
opts._scrollDetail_y = 0
}else{
opts._scrollDetail_y = 0
}
ins.callMethod('callEmits', {
disabledScolly: disabledScolly,
isMoving: isMoving
})
ins.callMethod('setDisabledScolly', {
disabledScolly: disabledScolly,
isMoving: isMoving
})
if(disabledScolly) return true;
var movey = evt.y - _y
if (movey <= opts.maxHeight) {
movey = opts.maxHeight - (opts.maxHeight - movey) * opts.threshold
} else if (movey >= opts.size) {
movey = opts.size + (movey - opts.size) * opts.threshold
}
ele.setStyle({
'transition-duration': '0ms',
'transform': 'matrix(1, 0, 0, 1, 0, ' + movey + ')'
})
_realYDiff = evt.y - _realY
var ratioValue = (movey - opts.size) / (opts.maxHeight - opts.size) * 100;
ins.callMethod('callEmits', {
name: 'heightChange',
args: ratioValue
})
}
function eventTransform_end(evt, ins) {
isHover = false;
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
var duration = opts.duration
var offset = 0
if (_realY - evt.y == 0) return;
if (_realYDiff >= opts.triggerDy) {
offset = opts.size
opts.status = 'close';
ins.callMethod('callEmits', 'beforeClose')
ins.callMethod('callEmits', {
name: 'heightChange',
args: 0
})
ins.callMethod('callEmits', {name:'close'})
} else if (_realYDiff < (opts.triggerDy * -1)) {
offset = opts.maxHeight
opts.status = 'open';
ins.callMethod('callEmits', {
name: 'heightChange',
args: 100
})
ins.callMethod('callEmits', {name:'open'})
} else {
if (opts.status === 'open') {
offset = opts.maxHeight
ins.callMethod('callEmits', {
name: 'heightChange',
args: 100
})
} else {
offset = opts.size
ins.callMethod('callEmits', {
name: 'heightChange',
args: 0
})
}
}
ins.callMethod('setOpts', opts)
ele.setStyle({
'transition-duration': duration + 'ms',
'transform': 'matrix(1, 0, 0, 1, 0, ' + offset + ')'
})
}
function setTatus(status,instance){
var ele = instance.selectComponent('.xFloatDrawerWrapContent')
var offset = 0
var duration = opts.duration
if (status === 'open') {
offset = opts.maxHeight
} else {
offset = opts.size
}
ele.setStyle({
'transition-duration': duration + 'ms',
'transform': 'matrix(1, 0, 0, 1, 0, ' + offset + ')'
})
}
function mStartHead(evt, ins) {
}
function mMoveHead(evt, ins) {
}
function mEndHead(evt, ins) {
}
function mStart(evt, ins) {
_test_x = evt.changedTouches[0].clientX;
_test_y = evt.changedTouches[0].clientY;
// var otps = ins.callMethod('getNowOpts')
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
opts = ele.getDataset().opts;
defaultTranformYValue = ele.getComputedStyle(['transform']).transform
isMoving = false
ins.callMethod('callEmits', {
name: 'movestart',
args: null
})
eventTransform_start({
x: _test_x,
y: _test_y
}, ins);
}
function mMove(evt, ins) {
isMoving = true
eventTransform_move({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY
}, ins);
return false
}
function mEnd(evt, ins) {
ins.callMethod('callEmits', {
name: 'moveend',
args: null
})
eventTransform_end({
x: evt.changedTouches[0].clientX,
y: evt.changedTouches[0].clientY
}, ins)
}
module.exports = {
mStart: mStart,
mMove: mMove,
mEnd: mEnd,
mStartHead: mStartHead,
mMoveHead: mMoveHead,
mEndHead: mEndHead,
propObserver: function(newValue, oldValue, ownerInstance, instance) {
var ele = ownerInstance.selectComponent('.xFloatDrawerWrapContent')
opts = ele.getDataset().opts;
if(newValue!=undefined&&oldValue!=undefined&&newValue!=oldValue){
setTatus(newValue,ownerInstance)
}
}
}
@@ -0,0 +1,509 @@
<script lang="ts">
import { PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { FORM_RULE, FORM_SUBMIT_OBJECT} from "../../interface.uts"
import { FORM_ITEM } from "../x-form/interface.uts"
import { formVaild} from "../x-form/util.uts"
/**
* @name 表单子组件 xFormItem
* @description 从1.1.2开始允许非xform直接子节点了,也就是在xform可以嵌套view进行form-item布局了,但建议不要嵌套太深,影响性能.
* 1.1.17开始支持嵌套对象字段,key必须/分割如'user/id'sdk不支持以.分割符。
* @page /pages/index/form
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
name:"xFormItem",
data() {
return {
id: ("xFormItem-" + getUid()) as string,
errorText: "请正确填写",
isError: false,
first: true,
tid: 0
}
},
props: {
/**
* 表单名称
*/
label: {
type: String,
default: ""
},
/**
* 是否显示标题
*/
showLabel: {
type: [Boolean, null] as PropType<boolean | null>,
default: null
},
/**
* 校验的字段名称
* 字段名称一定要存在于form表单数据中。否则报错。
*/
field: {
type: String,
default: ""
},
/**
* 是否是必填项.
* 设置了此值,才会执行校验。
*/
required: {
type: Boolean,
default: false
},
/**
* 是否显示校验必填项前面的红*符号
*/
showRequired: {
type: Boolean,
default: true
},
/**
* 校验规则对象,uniappx暂不支持对象中嵌套泛型函数
* 从1.1.10开始rule对象中增加了trigger:change,blur校验时机,blur主要是用在input组件上的
* 如果你内部没有input而使用blur,会造成不校验的问题,请认真阅读文档.
*/
rule: {
type: Array as PropType<FORM_RULE[]>,
default: () : FORM_RULE[] => [] as FORM_RULE[]
},
/**
* 标签宽(横向表单时有效)
* 默认空值取父form统一的设置。
*/
labelWidth: {
type: String,
default: ""
},
/**
* 默认空值取父form统一的设置。
* vertical|horizontal
*/
labelDirection: {
type: String,
default: ""
},
/**
* 默认空值取父form统一的设置。
* 标签的文本颜色
*/
labelFontColor: {
type: String,
default: ""
},
/**
* 默认空值取父form统一的设置。
* 标签的文本颜色
*/
labelFontSize: {
type: String,
default: ""
},
/**
* 标签标题对齐方式
* left,right,center
*/
labelAlign:{
type:String,
default:"left"
},
/**
* 是否显示底部边框
*/
showBottomBorder: {
type: Boolean,
default: true
},
/**
* 排版布局上和下的间隙。
* 数组必须是2长度
* 第一个是上间隙,第二个是下间隙,
* 如果showBottomBorder为false时下间隙第二个参数会被取消。
* 如果label为horizontal横向时,第一个参数的上间隙生效,如果是上下排列不生效。
*/
cellPadding: {
type: Array as PropType<string[]>,
default: () : string[] => ['10', '10'] as string[]
},
/**
* 排版布局上和下的间隙。
* 数组必须是2长度
* 第一个是上间隙,第二个是下间隙,
* 如果label为horizontal横向时,些参数失效,只有竖向时才生效。
*/
labelPadding: {
type: Array as PropType<string[]>,
default: () : string[] => ['12', '12'] as string[]
},
/**
* 校验时,是否显示下边的出错信息
*/
showError:{
type:Boolean,
default:true
},
/**
* 默认区域内的自定样式
*/
contentStyle:{
type:String,
default:""
}
},
inject: {
XFORMITEM_TOP: { type: Number, default: 0 },
XFORMITEM_LABEL_WIDTH: { type: String, default: '100' },
XFORMITEM_LABEL_Direction: { type: String, default: 'horizontal' },
XFORMITEM_LABEL_FontColor: { type: String, default: '#333333' },
XFORMITEM_LABEL_SCROLL: { type: Boolean, default: true },
XFORMITEM_LABEL_FontSize: { type: String, default: '16' },
XFORMITEM_SHOWLABEL: { type: Boolean, default: true },
XFORMITEM_ERROR_ALIGN: { type: String, default: 'right' },
},
mounted() {
this.pushDataToParent();
},
beforeUnmount() {
this.removeSelf();
clearTimeout(this.tid);
},
computed: {
_contentStyle():string{
return this.contentStyle
},
_showError():boolean{
return this.showError
},
_cellPadding() : string[] {
let mb = checkIsCssUnit(this.cellPadding[0], xConfig.unit)
let pb = checkIsCssUnit(this.cellPadding[1], xConfig.unit)
return [mb, pb]
},
_labelPadding() : string[] {
let mb = checkIsCssUnit(this.labelPadding[0], xConfig.unit)
let pb = checkIsCssUnit(this.labelPadding[1], xConfig.unit)
return [mb, pb]
},
_parentTop() : number {
return this.XFORMITEM_TOP
},
_labelWidth() : string {
if (this.labelWidth != "") return checkIsCssUnit(this.labelWidth, xConfig.unit)
return checkIsCssUnit(this.XFORMITEM_LABEL_WIDTH, xConfig.unit)
},
_labelFontSize() : string {
let labelsize = checkIsCssUnit(this.XFORMITEM_LABEL_FontSize, xConfig.unit)
if (this.labelFontSize != "") {
labelsize = checkIsCssUnit(this.labelFontSize, xConfig.unit)
}
return labelsize
},
_labelDirection() : string {
if (this.labelDirection != "") return this.labelDirection
return this.XFORMITEM_LABEL_Direction
},
_labelFontColor() : string {
if (this.labelFontColor != "") return getDefaultColor(this.labelFontColor)
return getDefaultColor(this.XFORMITEM_LABEL_FontColor)
},
_label() : string {
return this.label
},
_showLabel() : boolean {
let show = this.XFORMITEM_SHOWLABEL;
if (this.showLabel != null) {
show = this.showLabel! as boolean;
}
return show
},
_showRequired() : boolean {
return this.showRequired
},
_rule() : FORM_RULE[] {
return this.rule;
},
_showBottomBoder() : boolean {
return this.showBottomBorder
},
_required() : boolean {
return this.required
},
_errorFontsize() : string {
return (xConfig.fontScale * 14).toString() + 'px'
},
_bordrBottomSolid() : string {
if (!this.showBottomBorder) return "border-bottom:none";
let lightSolid = "border-bottom: 1px solid #f5f5f5";
let darkSolid = `border-bottom: 1px solid ${xConfig.borderDarkColor}`
return xConfig.dark == 'dark' ? darkSolid : lightSolid
},
_labelAlign():string{
let dq = 'flex-start'
if(this.labelAlign=='right'){
dq = 'flex-end'
}else if(this.labelAlign=='center'){
dq = 'center'
}
return dq;
}
},
watch: {
field() {
this.pushDataToParent();
},
},
methods: {
pushDataToParent() {
let pelement = this.findParent(this);
if (pelement == null) return;
// @ts-ignore
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
// 预先传递,怕子组件内的内容复杂导致查询延误,导致顺序错乱。
parent.pushAdd({
id: this.id,
ele: this,
top: 0,
name: this.field,
} as FORM_ITEM)
let t = this;
uni.createSelectorQuery().in(t)
.select(".xFormItem")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
parent.pushAdd({
id: t.id,
ele: t,
top: nodeinfo.top!,
name: t.field,
} as FORM_ITEM)
})
},
removeSelf() {
let pelement = this.findParent(this);
if (pelement == null) return;
// @ts-ignore
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
parent.delItem(this.id)
},
getParentRules():FORM_RULE[] {
let pelement = this.findParent(this);
if (pelement == null) return [] as FORM_RULE[];
// @ts-ignore
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
return parent.getRules(this.field) as FORM_RULE[]
},
vaildCompele(val : any|null,isSkipBlur:boolean|null = true) : FORM_SUBMIT_OBJECT {
this.first = false;
if (!this._required) {
return {
valid: true,
key: this.field,
value: val,
errorMessage: ""
} as FORM_SUBMIT_OBJECT;
}
const rulesList = [...this._rule,...this.getParentRules()] as FORM_RULE[]
if (rulesList.length == 0 && this._required) {
let isSuccess = formVaild(val,{} as FORM_RULE)
this.isError = !isSuccess
return {
valid: isSuccess,
key: this.field,
value: val,
errorMessage: isSuccess ? "" : '请正确填写/选择'
} as FORM_SUBMIT_OBJECT;
}
let isSuccess = true;
for (let i = 0; i < rulesList.length; i++) {
let item = rulesList[i]
if(item.trigger == 'blur'&&isSkipBlur==true) continue;
isSuccess = formVaild(val,item)
this.isError = !isSuccess
if (!isSuccess) {
if (item.errorMessage != "" && typeof item.errorMessage == 'string') {
this.errorText = item.errorMessage! as string
} else {
this.errorText = '请正确填写/选择'
}
break;
}
}
return {
valid: isSuccess,
key: this.field,
value: val,
errorMessage: isSuccess ? "" : this.errorText
} as FORM_SUBMIT_OBJECT;
},
clearValid(){
this.isError=false;
this.first = true;
},
/**
* 实时校验结果。form中要开启,如果没有这种需求可以不开启,会损耗性能。
*/
getVaildStatus(val : any|null) : FORM_SUBMIT_OBJECT {
if (!this._required) {
return {
valid: true,
key: this.field,
value: val,
errorMessage: ""
} as FORM_SUBMIT_OBJECT;
}
const rulesList = [...this._rule,...this.getParentRules()] as FORM_RULE[]
if (rulesList.length == 0 && this._required) {
let isSuccess = formVaild(val,{} as FORM_RULE)
return {
valid: isSuccess,
key: this.field,
value: val,
errorMessage: isSuccess ? "" : '请正确填写/选择'
} as FORM_SUBMIT_OBJECT;
}
let isSuccess = true;
let errotips = ''
for (let i = 0; i < rulesList.length; i++) {
let item = rulesList[i]
isSuccess = formVaild(val,item)
if (!isSuccess) {
if (item.errorMessage != "" && typeof item.errorMessage == 'string') {
errotips = item.errorMessage! as string
} else {
errotips = '请正确填写/选择'
}
break;
}
}
return {
valid: isSuccess,
key: this.field,
value: val,
errorMessage: isSuccess ? "" : errotips
} as FORM_SUBMIT_OBJECT;
},
/**
* 为Input定制校验函数.
*/
validByblur(value:any){
this.vaildCompele(value,false)
},
findParent(parent:VueComponent|null):VueComponent|null{
if(parent == null) return null;
// #ifdef WEB||APP-IOS|| MP-WEIXIN
if(parent.$parent?.id?.indexOf('xForm')>-1) return parent.$parent;
// #endif
// #ifdef APP-HARMONY
if(parent.$parent?.$options?.name?.indexOf('xForm')>-1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
if(parent.$parent instanceof XFormComponentPublicInstance) return parent.$parent;
// #endif
let parents = this.findParent(parent.$parent)
// #ifdef WEB||APP-IOS || MP-WEIXIN
if(parents?.id?.indexOf('xForm')>-1) return parents;
// #endif
// #ifdef APP-HARMONY
if(parents?.$options?.name?.indexOf('xForm')>-1) return parents;
// #endif
// #ifdef APP-ANDROID
if(parents instanceof XFormComponentPublicInstance) return parents;
// #endif
return null;
}
}
}
</script>
<template>
<view class="xFormItem" ref="xFormItem" :style="[
_bordrBottomSolid,
{
paddingTop: _cellPadding[0],
paddingBottom: _cellPadding[1]
}
]">
<view class="xFormIitemWrap" :style="{flexDirection:_labelDirection=='horizontal'?'row':'column'}">
<view v-if="_showLabel" class="xFormIteLabel" :style="{
width:_labelDirection=='horizontal'?_labelWidth:'auto',
paddingBottom:_labelDirection=='horizontal'?'0':_labelPadding[1],
paddingTop:_labelDirection=='horizontal'?'0':_labelPadding[0],
'justify-content': _labelAlign
}">
<!--
@slot 标题
-->
<slot name="label">
<text v-if="_showRequired&&_required"
:style="{fontSize:_labelFontSize,color:'red',paddingRight:'8rpx'}">*</text>
<x-text v-if="_label" :font-size="_labelFontSize" :color="_labelFontColor">
{{_label}}
</x-text>
</slot>
</view>
<view class="xFormIteContent" :style="[{flex:_labelDirection=='horizontal'?'1':'auto'},_contentStyle]">
<!--
@slot 默认内容区域
-->
<slot></slot>
</view>
</view>
<view v-if="isError&&!first&&_required&&_showError" class="xFormItemError">
<!--
@slot 出错提示的插槽
-->
<slot name="error">
<text class="xFormItemErrorText" :style="{fontSize:_errorFontsize,textAlign:XFORMITEM_ERROR_ALIGN}">{{errorText}}</text>
</slot>
</view>
</view>
</template>
<style scoped>
.xFormIteLabel {
display: flex;
flex-direction: row;
align-items: center;
}
.xFormItem {
display: flex;
flex-direction: column;
}
.xFormItemErrorText {
color: red;
padding-top: 5px;
display: flex;
}
.xFormIitemWrap{
display: flex;
}
</style>
@@ -0,0 +1,35 @@
import { FORM_RULE_TYPE } from "../../interface.uts"
export type FORM_ITEM = {
id : string,
ele : XFormItemComponentPublicInstance,
top : number,
name : string
}
export type FORM_VAILD_FUN = (val : any | null) => boolean
export type FORM_RULE_SELF = {
/**
* 字段类型,空值为自动判断 ,不填写默认为空,自动判断.
*/
type : FORM_RULE_TYPE,
/**
* 校验函数。默认根据泛型判断字段值,如果不填写默认为自带的校验函数校验.
*/
valid : FORM_VAILD_FUN|null,
/**
* 错误信息,默认为自动根据类型编写提示
*/
errorMessage : string,
/** 最大值,如果为空值或者为-1表示不限制大小,(数组和字符串是长度,数字是值,utsjson是key字段数量) */
max : number,
/** 最小值,如果为空默认为1,(数组,字符,utsjson为1,长不能小于1) */
min : number,
/**
* 触发校验时机,只针对input有效.
*/
trigger : string
}
export type FORM_SUBMIT_SELF_OBJECT = {
errorMessage : string,
valid : boolean
}
@@ -0,0 +1,184 @@
import { FORM_RULE_SELF, FORM_VAILD_FUN } from "./interface.uts";
import { FORM_RULE, FORM_RULE_TYPE } from "../../interface.uts";
function getDefaultRule(rule : FORM_RULE) : FORM_RULE_SELF {
let type : FORM_RULE_TYPE = rule?.type ?? ''
let errorMessage : string = rule?.errorMessage ?? '请正确填写/选择'
let max : number = rule?.max ?? -1
let min : number = rule?.min ?? 1
let selfvaild:FORM_VAILD_FUN|null = rule.valid
let validfaun : FORM_VAILD_FUN | null = selfvaild
let trigger:string = rule?.trigger??'change'
return {
type, errorMessage, max, min, valid: validfaun,trigger
} as FORM_RULE_SELF
}
function vaild(val : any | null, rule : FORM_RULE_SELF) : boolean {
if (rule.type != '') return vaildBytType(val, rule)
if (val == null) return false;
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
if (val == undefined) return false;
// #endif
if (typeof val === 'string') {
let vallen = (val as string).trim().split('').length
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
if (Array.isArray(val)) {
let vallen = (val as any[]).length
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
if (typeof val === 'boolean') {
return val as boolean;
}
if (typeof val === 'number') {
if (isNaN(val as number)) {
return false;
}
let vallen = val as number
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
// #ifdef APP-ANDROID
if (typeof val == 'Int') {
let vallen = val as Int
if (rule.max == -1) return vallen >= rule.min.toInt()
return vallen >= rule.min.toInt() && vallen <= rule.max.toInt();
}
if (typeof val == 'Float') {
let vallen = val as Float
if (rule.max == -1) return vallen >= rule.min.toFloat()
return vallen >= rule.min.toFloat() && vallen <= rule.max.toFloat();
}
if (typeof val == 'Double') {
let vallen = val as Double
if (rule.max == -1) return vallen >= rule.min.toDouble()
return vallen >= rule.min.toDouble() && vallen <= rule.max.toDouble();
}
if (typeof val == 'Long') {
let vallen = val as Long
if (rule.max == -1) return vallen >= rule.min.toLong()
return vallen >= rule.min.toLong() && vallen <= rule.max.toLong();
}
// #endif
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
if (val == undefined) {
return false;
}
// #endif
return false;
}
export function formVaild(val : any | null, rule : FORM_RULE) : boolean {
let rulereal = getDefaultRule(rule)
let selfVaildFun = rulereal?.valid??null;
let isPass = true;
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
isPass = (selfVaildFun == null||selfVaildFun == undefined) ? vaild(val, rulereal) : selfVaildFun(val)
// #endif
// #ifdef APP-ANDROID
isPass = selfVaildFun == null ? vaild(val, rulereal) : selfVaildFun(val)
// #endif
return isPass
}
/** 指定了type的情况下. */
export function vaildBytType(val : any | null, rule : FORM_RULE_SELF) : boolean {
if (val == null) return false;
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
if (val == undefined) return false;
// #endif
if (rule.type == 'string') {
let vallen = (val as string).trim().split('').length
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
if (rule.type == 'array') {
let vallen = (val as any[]).length
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
if (rule.type == 'boolean') {
return val as boolean;
}
if (rule.type == 'number') {
let v = 0;
if(typeof val != 'number'&&typeof val != 'string') return false;
// #ifdef APP
if (typeof val == 'Int') {
let vallen = val as Int
v = vallen + 0
}
if (typeof val == 'Float') {
let vallen = val as Float
v = vallen + 0
}
if (typeof val == 'Double') {
let vallen = val as Double
v = vallen + 0
}
if (typeof val == 'Long') {
let vallen = val as Long
v = vallen + 0
}
// #endif
if(typeof val == 'number'){
v = val as number;
}else if(typeof val == 'string'){
let pv = val as string;
let vazhi = parseFloat(pv)
if (isNaN(vazhi)) {
return false;
}
v = vazhi;
}
let vallen =v
if (rule.max == -1) return vallen >= rule.min
return vallen >= rule.min && vallen <= rule.max;
}
if(rule.type == 'phone'){
let vallen = ''
if(typeof val == 'string'){
vallen = val as string;
}else if(typeof val == 'number'){
vallen = (val as number).toString()
}
let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
return reg.test(vallen)
}
if(rule.type == 'email'){
let vallen = val as string;
let reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return reg.test(vallen)
}
if(rule.type == 'UTSJSON'){
let vallen = val as UTSJSONObject;
let mapkeys = vallen.toMap()
let len = mapkeys.size
if (rule.max == -1) return len >= rule.min
return len >= rule.min && len <= rule.max;
}
if(rule.type == 'date'&& (typeof val == 'string')){
let vallen = val as string;
let date = new Date(vallen)
let times = date.getTime()
if(isNaN(times)) return false;
let len = times
if (rule.max == -1) return len >= rule.min
return len >= rule.min && len <= rule.max;
}
return false;
}
@@ -0,0 +1,365 @@
<script lang="ts" setup>
import { getCurrentInstance } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { FORM_ITEM } from "../x-form/interface.uts"
import { FORM_RULE, FORM_SUBMIT_OBJECT, FORM_SUBMIT_RESULT } from "../../interface.uts"
type FORMITEM_R = {
key : string,
value : any | null,
}
type FORMITEM_J = {
value : any | null,
item : FORM_ITEM
}
type labelDirType = 'vertical' | 'horizontal'
type errorAlignType = 'left' | 'center'| 'right'
type flatFunCall = (obj: UTSJSONObject, prefix: string) => UTSJSONObject
/**
* @name 表单 xForm
* @description 从1.1.2开始允许xform可以嵌套view进行form-item布局了,但建议不要嵌套太深,影响性能.
* @page /pages/index/form
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xForm"})
const proxy = getCurrentInstance()?.proxy??null;
const emits = defineEmits([
/**
* 按钮提交表单时触发。
* @param {FORM_SUBMIT_RESULT} result 表单验证结果
*/
'submit',
/**
* 校验事件,这个不是submit是用户在输入内容或者在校验阶段向外发出的事件
* 里面包含了实时的校验信息,用于绑定外部的按钮提供实时的可提交状态。
* 它对外输出,不能够双向绑定对内更改校验状态。
*/
'update:modelValid'
])
export type xFormPropsType = {
/**
* 标签宽(横向表单时有效)
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
*/
labelWidth:string,
/**
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
* vertical,horizontal
*/
labelDirection:labelDirType,
/**
* 标签的文本颜色,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
*/
labelFontColor:string,
/**
* 出错时,是否滚动到表单的位置
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
*/
errorAutoPage:boolean,
/**
* 等同v-model
*/
modelValue:any,
/**
* 标签文本大小
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
*/
labelFontSize:string,
/**
* 是否显示标题,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
*/
showLabel:boolean,
/**
* 错误标签的对齐方式,left,center,right
*/
errorAlign:errorAlignType,
/**
* rules这里提供和与formItem上提供的不冲突都可以校验
* 如果两个名称相同会被校验两次。如果两有一边提供也会校验一次。
*/
rules:Map<string,FORM_RULE[]>,
/**
* 是否开启实时全部字段校验获取当前实时的校验状态,
* 通过vmodel:valid对外输出当前实时的校验值,当字段值改变时会一直监测并监听所有字段并返回结果到对外
* 如果你没有这个需求场景,请保持关闭状态,如果你确实外部需要实时改变并观察提交按钮状态可以打开,请在字段少的场景使用。
*/
watchValidStatus:boolean,
/**
* 当前的校验状态,请不要外部改变此值,此值只对外输出当前校验状态
* 需要设置watchValidStatus为true才会实时监听并检测状态。请通过v-model:valid来得到状态值
*/
modelValid:boolean,
}
const list = ref<FORM_ITEM[]>([])
const top = ref(0)
const isOkTop = ref(false)
const isdestry = ref(false)
const oldFormData = ref<any>({})
const id = ("xForm-" + getUid()) as string
const props = withDefaults(defineProps<xFormPropsType>(), {
labelWidth:"100",
labelDirection:"horizontal",
labelFontColor:"#333333",
errorAutoPage:true,
modelValue:{} as UTSJSONObject,
labelFontSize:"16",
showLabel:true,
errorAlign:'left',
rules: ():Map<string,FORM_RULE[]> => new Map<string,FORM_RULE[]>(),
watchValidStatus:false,
modelValid:false
})
const _modelValue = computed(():any =>props.modelValue)
const _rules = computed(():Map<string,FORM_RULE[]> =>props.rules)
let validTimeid = 12
provide('XFORMITEM_TOP',computed(():number=>top.value))
provide('XFORMITEM_LABEL_WIDTH',computed(():string=>props.labelWidth))
provide('XFORMITEM_LABEL_Direction',computed(():string=>props.labelDirection))
provide('labelFontColor',computed(():string=>props.labelFontColor))
provide('XFORMITEM_LABEL_SCROLL',computed(():boolean=>props.errorAutoPage))
provide('XFORMITEM_LABEL_FontSize',computed(():string=>props.labelFontSize))
provide('XFORMITEM_SHOWLABEL',computed(():boolean=>props.showLabel))
provide('XFORMITEM_ERROR_ALIGN',computed(():string=>props.errorAlign))
/**
* 将嵌套对象拍平为点分隔的键值对
* @param obj 要拍平的对象
* @param prefix 键前缀
* @returns 拍平后的对象
*/
let flattenObject:flatFunCall|null = null;
flattenObject = (obj: UTSJSONObject, prefix: string): UTSJSONObject => {
let flattened: UTSJSONObject = {}
const flatMap = obj.toMap()
for (const aitem of flatMap) {
let item = aitem as Array<any|null>
let key = item[0]! as string;
let value = item[1];
if (flatMap.has(key)) {
const newKey = prefix!='' ? `${prefix}/${key}` : key
if (value !== null && value instanceof UTSJSONObject ) {
// 递归处理嵌套对象
let fun = flattenObject!
flattened = {...flattened,...fun(value, newKey)}
} else {
flattened.set(newKey,value)
}
}
}
return flattened
}
function getNodeInfo() {
uni.createSelectorQuery().in(proxy)
.select(".xForm")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
top.value = nodeinfo.top!;
isOkTop.value = true;
})
}
function actionsRequired(templist : FORMITEM_R[]) {
list.value.forEach((el : FORM_ITEM) => {
let index = templist.findIndex((ele : FORMITEM_R) : boolean => ele.key == el.name)
if (index > -1) {
el.ele.vaildCompele(templist[index].value)
}
})
}
//比较哪个字段有变化
function diffFiled(newvals : any, olds : any) {
if (isdestry.value) return;
if (typeof newvals != 'object' || typeof olds != 'object') return;
let newval = JSON.parseObject(JSON.stringify(newvals)) as UTSJSONObject;
let old = JSON.parseObject(JSON.stringify(olds)) as UTSJSONObject;
newval = flattenObject!(newval,'');
old = flattenObject!(old,'');
let fileds = [] as FORMITEM_R[]
for (let key in old) {
let val : any | null = newval.get(key)
let oldval : any | null = old.get(key)
if (Array.isArray(val) && Array.isArray(oldval)) {
let temval = val.length
let temoldval = oldval.length
if (temval != temoldval) {
fileds.push({
key,
value: val
} as FORMITEM_R)
}
} else if (val != oldval) {
fileds.push({
key,
value: val
} as FORMITEM_R)
}
}
actionsRequired(fileds)
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
}
function _valid(keys : string[],isShowwStatusInTag:boolean = true) : FORM_SUBMIT_RESULT {
let toptemp = 0
let result = {
valid: true,
errorMessage: "",
key: "",
formData: [] as FORM_SUBMIT_OBJECT[]
} as FORM_SUBMIT_RESULT
let modevalue = JSON.parseObject(JSON.stringify(_modelValue.value)) as UTSJSONObject;
modevalue = flattenObject!(modevalue,'');
for (let key in modevalue) {
if (keys.length == 0 || keys.includes(key)) {
let valdata : any | null = modevalue.get(key)
let index = list.value.findIndex((ele : FORM_ITEM) : boolean => ele.name == key)
if (index > -1) {
let resultJg = null as FORM_SUBMIT_OBJECT|null
if(isShowwStatusInTag){
resultJg = list.value[index].ele.vaildCompele(valdata, false) as FORM_SUBMIT_OBJECT
}else{
resultJg = list.value[index].ele.getVaildStatus(valdata) as FORM_SUBMIT_OBJECT
}
if (!resultJg!.valid && result.valid) {
result.valid = false;
result.key = key;
result.errorMessage = resultJg!.errorMessage;
toptemp = list.value[index].top
}
result.formData.push(resultJg!)
}
}
}
if(isShowwStatusInTag){
/**
* 按钮提交表单时触发。
* @param {FORM_SUBMIT_RESULT} result 表单验证结果。
*/
emits('submit', result)
if (props.errorAutoPage && !result.valid && toptemp > 0) {
uni.pageScrollTo({
scrollTop: toptemp,
fail: (_) => {
console.error("当前页面没有放置可滚动的列表,但使用了表单出错滚动功能,请关闭表单属性:error-auto-page")
}
})
}
}
return result;
}
// 全局检测校验整体状态。
function getStatus(){
if(!props.watchValidStatus) return;
clearTimeout(validTimeid)
validTimeid = setTimeout(function() {
const result = _valid([] as string[],false)
console.log(result.valid)
emits('update:modelValid',result.valid)
}, 120);
}
onMounted(()=>{
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
getNodeInfo();
})
onBeforeUnmount(()=>{
clearTimeout(validTimeid)
isdestry.value = true;
list.value = [] as FORM_ITEM[]
oldFormData.value = {} as UTSJSONObject
})
watch(():any=>props.modelValue,(newValue : any)=>{
diffFiled(newValue, oldFormData.value)
if(props.watchValidStatus){
getStatus()
}
},{deep:true})
defineExpose({
pushAdd(item : FORM_ITEM) {
if (isdestry.value) return;
let index = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == item.id);
if (index > -1) {
list.value.splice(index, 1, item)
} else {
list.value.push(item)
}
},
delItem(id : string) {
if (isdestry.value) return;
if (list.value.length == 0) return;
let index : number = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == id);
if (index > -1) {
list.value.splice(index, 1)
}
},
getRules(key:string):FORM_RULE[]{
const _localRules = _rules.value.get(key)
if(_localRules == null) return [] as FORM_RULE[]
return _localRules!
},
/**
* 用来首次同步检测:modelValid,一定要在onready生命期中执行,并且你赋值完表数据后再来个,nextTick中执行本方法,可以确保不会有遗漏。
*/
checkAsyncVaildStatus(){
getStatus();
},
/**
* 手动执行触发校验函数,如果提供空数组,表示校验所有,如果提供了指定值,则表示只校验提供的字段。
* @public
* @param {string[]} keys - 待校验的字段
* @returns {FORM_SUBMIT_RESULT} 校验结果
*/
valid(keys : string[]) : FORM_SUBMIT_RESULT {
return _valid(keys);
},
/**
* 清除校验状态并回到初始状态。
*/
clearValid(){
list.value.forEach((el : FORM_ITEM) => {
el.ele.clearValid()
})
},
submit() : FORM_SUBMIT_RESULT {
return _valid([] as string[]);
}
})
</script>
<template>
<view class="xForm">
<!--
@slot 插槽内只能放置x-form-item子节点组件
-->
<slot></slot>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,221 @@
<script setup lang="ts">
import { ref, computed, inject } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { XGRID_ITEM_INFO } from "../../interface.uts"
/**
* @name 宫格子组件 xGridItem
* @description 不可单独使用,请把放它在x-grid标签内。
* @page /pages/index/grid
* @category 导航组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xGridItem" })
const emits = defineEmits([
/**
* 项目点击时触发
*/
'click'
])
const isHover = ref(false)
type xGridItemPropsType = {
/**
* 背景,默认为空值,读取父xGrid组件统一设置的背景
* 如果这里提供了,以子组件为准。
*/
bgColor: string,
/**
* 项目在列表中的索引,从0开始
* 请务必在循环gridItem时提供order为循环的index
*/
order: number,
/**
* 图标
*/
icon: string,
/**
* 文字
*/
text: string,
/**
* 图标颜色,空值取父xGrid的值
*/
iconColor: string,
/**
* 文字亮系,空值取父xGrid的值
*/
textColor: string,
/**
* 文字暗黑颜色,空值取父xGrid的值
*/
textDarkColor: string,
/**
* 文字大小,空值取父xGrid的值
*/
fontSize: string,
/**
* 图标大小,空值取父xGrid的值
*/
iconSize: string,
/**
* 是否开启链接hover效果
*/
isLink: boolean,
/**
* url链接地址,如果填写,点击会跳转
*/
url: string,
}
const props = withDefaults(defineProps<xGridItemPropsType>(), {
bgColor: 'transparent',
order: -1,
icon: '',
text: '',
iconColor: '',
textColor: '',
textDarkColor: '',
fontSize: '',
iconSize: '',
isLink: true,
url: '',
})
// gird共有几列
const xGridCol = inject('xGridCol', computed(() : number => 1))
const xGridHeight = inject('xGridHeight', computed(() : string => '0px'))
const xGridItemBgColor = inject('xGridItemBgColor', computed(() : string => 'rgba(0,0,0,0)'))
const xGridItemGlobalProptype = inject('xGridItemGlobalProptype', computed(() : XGRID_ITEM_INFO => {
return {
iconSize: '',
fontSize: '',
iconColor: '',
fontColor: '',
fontDarkColor: '',
darkIconColor: ''
} as XGRID_ITEM_INFO
}))
const borderColor = inject('borderColor', computed(() : string => 'rgba(0,0,0,0)'))
const showBorder = inject('showBorder', computed(() : boolean => false))
const _xGridCol = computed(():number => xGridCol.value)
const _xGridColBl = computed(():string => `${100 / _xGridCol.value}%`)
const _bgColor = computed(():string => {
if (props.bgColor == '') return xGridItemBgColor.value
return getDefaultColor(props.bgColor)
})
const _hoverbgColor = computed(():string => {
if (!props.isLink && props.url == '') return _bgColor.value
if (props.bgColor == '') return colorAddDeepen(xGridItemBgColor.value)
return colorAddDeepen(props.bgColor)
})
const _text = computed(():string => props.text)
// gird项目当前的位置索引
const _order = computed(():number => props.order)
const _icon = computed(():string => props.icon)
const _iconColor = computed(():string => {
if (props.iconColor == "") {
if (xConfig.dark == 'dark') {
return xGridItemGlobalProptype.value.darkIconColor
}
return xGridItemGlobalProptype.value.iconColor
}
return getDefaultColor(props.iconColor);
})
const _textColor = computed(():string => {
if (xConfig.dark == 'dark') {
if (props.textDarkColor == "") return xGridItemGlobalProptype.value.fontDarkColor == '' ? '#ffffff' : xGridItemGlobalProptype.value.fontDarkColor
return props.textDarkColor
}
if (props.textColor == "") return xGridItemGlobalProptype.value.fontColor
return getDefaultColor(props.textColor);
})
const _fontSize = computed(():string => {
if (props.fontSize == "") return xGridItemGlobalProptype.value.fontSize
return checkIsCssUnit(props.fontSize, 'px');
})
const _iconSize = computed(():string => {
if (props.iconSize == "") return xGridItemGlobalProptype.value.iconSize
return checkIsCssUnit(props.iconSize, 'px');
})
const borderMaps = computed(():Map<string,any> => {
const borderMap = new Map<string, any>()
// 如果没有开启边线显示,返回空Map
if (!showBorder.value) {
return borderMap
}
// 计算当前项目在第几行第几列
const currentCol = _order.value % _xGridCol.value // 当前列索引(从0开始)
const currentRow = Math.floor(_order.value / _xGridCol.value) // 当前行索引(从0开始)
// 设置边线样式
const borderStyle = `1px solid ${borderColor.value}`
// 上边线:只有非第一行才显示
if (currentRow > 0) {
borderMap.set('border-top', borderStyle)
}
// 右边线:只有非最右列才显示
if (currentCol < _xGridCol.value - 1) {
borderMap.set('border-right', borderStyle)
}
return borderMap
})
const itemClick = () => {
if (props.url != '') {
uni.navigateTo({
url: props.url
})
return;
}
/**
* 项目点击时触发。
*/
emits('click')
}
</script>
<template>
<view @touchend="isHover = false" @touchcancel="isHover = false" @touchstart="isHover = true" @click="itemClick"
class="xGridItem"
:style="[{width:_xGridColBl,height:xGridHeight,backgroundColor:isHover?_hoverbgColor:_bgColor},borderMaps]">
<!--
@slot 默认插槽内容
-->
<slot>
<x-icon v-if="_icon" :name="_icon" :font-size="_iconSize" :color="_iconColor"
style="margin-bottom: 8px;"></x-icon>
<text :style="{
color:_textColor,
fontSize:_fontSize,
textAlign:'center'
}">{{_text}}</text>
</slot>
</view>
</template>
<style scoped>
.xGridItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
/* #ifndef APP */
box-szing:border-box;
/* #endif */
}
</style>
@@ -0,0 +1,168 @@
<script lang="ts" setup>
import { computed, provide } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { XGRID_ITEM_INFO } from "../../interface.uts"
/**
* @name 宫格 xGrid
* @description 内部只可放置x-grid-item。
* @page /pages/index/grid
* @category 导航组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xGrid"})
type xGridPropsType = {
/**
* 显示几列
*/
col: number,
/**
* 项目高度
*/
itemHeight: string,
/**
* 统一设置子组件的背景
*/
itemBgColor: string,
/**
* 整体宫格的背景
*/
bgColor: string,
/**
* 整体宫格的背景暗黑,如果为空,读取全局sheetDark
*/
darkBgColor: string,
/**
* 整体宽度
*/
width: string,
/**
* 图标颜色
*/
iconColor: string,
/**
* 暗黑时图标颜色
*/
darkIconColor: string,
/**
* 文字颜色
*/
textColor: string,
/**
* 文字暗黑颜色
*/
textDarkColor: string,
/**
* 文字大小
*/
fontSize: string,
/**
* 图标大小
*/
iconSize: string,
/**
* 是否显示边框。请务必为每个项目配置order
*/
showBorder: boolean,
borderColor: string,
borderDarkColor: string,
/**
* 圆角
*/
round: string,
}
const props = withDefaults(defineProps<xGridPropsType>(), {
col: 3,
itemHeight: '70',
itemBgColor: 'white',
bgColor: 'transparent',
darkBgColor: 'transparent',
width: 'auto',
iconColor: '#333333',
darkIconColor: '#FFFFFF',
textColor: '#888888',
textDarkColor: '',
fontSize: '13',
iconSize: '25',
showBorder: true,
borderColor: '#f5f5f5',
borderDarkColor: '#333333',
round: '0',
})
const _col = computed(() : number => {
return props.col
})
const _itemHeight = computed(() : string => {
return checkIsCssUnit(props.itemHeight, xConfig.unit)
})
const _width = computed(() : string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _itemBgColor = computed(() : string => {
return getDefaultColor(props.itemBgColor)
})
const _bgColor = computed(() : string => {
if (xConfig.dark == 'dark') {
if (props.darkBgColor != "") return getDefaultColor(props.darkBgColor)
return getDefaultColor(xConfig.sheetDarkColor)
}
return getDefaultColor(props.bgColor)
})
const _itemGloablStyle = computed(() : XGRID_ITEM_INFO =>{
return {
iconColor: getDefaultColor(props.iconColor),
iconSize: checkIsCssUnit(props.iconSize, xConfig.unit),
fontColor: getDefaultColor(props.textColor),
fontDarkColor: getDefaultColor(props.textDarkColor),
fontSize: checkIsCssUnit(props.fontSize, xConfig.unit),
darkIconColor: getDefaultColor(props.darkIconColor),
} as XGRID_ITEM_INFO
})
const _round = computed(():string => checkIsCssUnit(props.round,xConfig.unit))
provide("xGridCol",_col)
provide("xGridHeight",_itemHeight)
provide("xGridItemBgColor",_itemBgColor)
provide("xGridItemGlobalProptype",_itemGloablStyle)
const _borderColor = computed(():string => {
if (xConfig.dark == 'dark') {
if (props.borderDarkColor != "") return getDefaultColor(props.borderDarkColor)
return getDefaultColor(xConfig.inputDarkColor)
}
return getDefaultColor(props.borderColor)
})
const _showBorder = computed(():boolean => props.showBorder)
provide("showBorder",_showBorder)
provide("borderColor",_borderColor)
</script>
<template>
<view class="xGrid" :style="{
width:_width,
backgroundColor:_bgColor,
border:`${_showBorder?('1px solid '+_borderColor):'none'}`,
borderRadius:_round
}">
<!--
@slot 插槽内只可放置x-grid-item
-->
<slot></slot>
</view>
</template>
<style scoped>
.xGrid {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,342 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { getDefaultColor, colorAddDeepen, setBgColorLightByDark, setTextColorLightByDark, isBlackAndWhite } from "../../core/util/xCoreColorUtil.uts"
import { fillArrayCssValue, fillArrayCssValueBycolor, fillArrayCssValueByround, checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xTween } from "../../core/util/xTween.uts"
import { xTweenStatus, xTweenCallbackFunType, xTweenAnimate, xTweenEventCallFunType, xTweenEventCall } from "../../interface.uts"
// #ifdef APP|| WEB
import remixicon from "./remixicon.uts"
// #endif
/**
* @name 图标 xIcon
* @description 图标使用的是开源图标:[https://remixicon.com/](https://remixicon.com/),版本是:4.5.0 ,使用时,不用带ri-前缀。
* @page /pages/index/icon
* @category 常用组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xIcon"})
const emits = defineEmits(['click'])
type xIconPropsType = {
/**
* 图标名称,不带ri-前缀
* 也可以是本地或者远程图片
*/
name: string,
/**
* 图标大小,单位任意,比如"12",12px,12rpx
*/
fontSize: string,
/**
* 自定义图标字体,前提是
* 你要在appuvue中已经安装好字体文件,
* 否则无法使用。
* 要让自定图标生效你需要配合code属性一起使用
*/
fontFamily: string,
/**
* 图标的16进制字符串,注意不含u
* 比如:ea0cea14这种,
* 如果你提供了code优化解析这,那么name将失效,如果你是纯字体图标使用
* 你可以使用这个属性,可以提供性能和自己定义的图标会比较方便。
*/
code: string,
/**
* 图标颜色
*/
color: string,
/**
* 暗黑时的文本颜色,如果你不提供,将自动反转。
* 自动反转是根据亮度反转,色相不变。
*/
darkColor: string,
/**
* 是否旋转动画
*/
spin: boolean,
/**
* 旋转角度
*/
rotation: number,
/**
* 动画时间
*/
duration: number,
}
const props = withDefaults(defineProps<xIconPropsType>(), {
name: "home-3-fill",
fontSize: "16",
fontFamily: "remixicon",
code: "",
color: "black",
darkColor: "",
spin: false,
rotation: 0,
duration: 1500,
})
// data -> refs
const xIcon = ref<Element | null>(null)
const refreshId = ref(1)
const id = ref<string>("xIconspin" + getUid())
const element = ref<UniElement | null>(null)
const rotationDeg = ref<number>(0)
const isLoad = ref<boolean>(false)
const isdestory = ref<boolean>(false)
const status = ref<string>('play')
const tid = ref<number>(0)
const xt = ref<xTween>(new xTween())
const xIcons = ref<UniElement | null>(null)
// computed replacements
const _iconName = computed((): string => {
return props.name
})
const _mpcode = computed((): string => {
let cname = ''
// #ifdef MP
cname = props.code==''?('ri-'+props.name):'';
// #endif
return cname
})
const _isFileImg = computed((): boolean => {
if (props.name.lastIndexOf(".") > -1 ||
props.name.indexOf("ftp:") > -1 ||
props.name.indexOf("https:") > -1 ||
props.name.indexOf("http:") > -1 ||
props.name.indexOf("data:image") > -1
) {
return true;
}
return false;
})
const iconName = computed((): string => {
if (_isFileImg.value) return props.name
let texts = ""
try {
// #ifdef APP||WEB
let codestr = ''
if(props.code==''){
codestr = remixicon.getString(props.name)!
}else{
codestr = props.code
}
// #endif
// #ifdef APP-ANDROID
let codePoint = Integer.parseInt(codestr, 16);
let charArray = Character.toChars(codePoint);
texts = new String(charArray);
// #endif
// #ifdef APP-IOS|| WEB||APP-HARMONY
texts = String.fromCharCode(parseInt(codestr, 16));
// #endif
// #ifdef MP
if(props.code!=''){
texts = String.fromCharCode(parseInt(props.code, 16));
}
// #endif
} catch (e) {
console.error("xicon解析失败。", e)
}
return texts
})
const _fontSize = computed((): string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _color = computed((): string => {
let color = props.color==""?'black':props.color;
if (xConfig.dark == 'dark') {
if (props.darkColor != "") {
color = props.darkColor!
return getDefaultColor(color)
}
return setTextColorLightByDark(color)
}
return getDefaultColor(color);
})
const _spin = computed((): boolean => props.spin)
const _rotation = computed((): number => props.rotation)
function setRadeg() {
try {
element.value = xIcons.value as UniElement
element.value!.style.setProperty("transition-duration", props.duration.toString() + 'ms')
element.value!.style.setProperty("transform", `rotate(${_rotation.value}deg)`)
} catch (e) {
}
}
function playSpin() {
if (!_spin.value || isdestory.value) return;
let elementLocal = xIcons.value as UniElement
// #ifdef APP || WEB
xt.value.pause()
xt.value.destroy()
xt.value.startRender()
xt.value.addAnimate({
loop:-1,
duration:props.duration,
complete:(item:xTweenEventCallFunType)=>{
},
enter:(item:xTweenEventCallFunType)=>{
elementLocal!.style.setProperty("transition-duration", '0ms')
elementLocal!.style.setProperty("transform", `rotate(${360 * item.progress}deg)`)
},
pause:(item:xTweenEventCallFunType)=>{},
} as xTweenAnimate)
xt.value.play()
// #endif
}
function clickListen() {
emits("click")
}
/** app加载的字体方式 */
// https://cdn.tmui.design/public/static/remixicon.ttf
function loadFontByX() {
isLoad.value = true;
// uni.loadFontFace({
// source: "url('static/remixicon.ttf')",
// family: "remixicon",
// success() {
// this.isLoad = true;
// uni.setStorageSync("loadedFontBytmx", "true")
// },
// fail() {
// console.log('loadfontFail.---')
// uni.setStorageSync("loadedFontBytmx", "")
// },
// complete() {
// }
// })
}
// watchers
watch(():boolean => props.spin, () => {
if (_spin.value) {
playSpin();
}else{
xt.value.pause()
xt.value.destroy()
}
})
watch(():number => props.rotation, () => {
if (_spin.value) return;
setRadeg();
})
onBeforeUnmount(() => {
isdestory.value = true;
clearTimeout(tid.value)
xt.value.destroy()
})
onMounted(() => {
loadFontByX();
isdestory.value = false;
if (_spin.value) {
playSpin();
} else {
nextTick(() => {
setRadeg();
})
}
})
</script>
<template>
<!-- #ifdef MP-WEIXIN -->
<view class="faceBox">
<!-- #endif -->
<text
v-if="!_isFileImg"
@click="clickListen"
:id="id"
ref="xIcons"
class="face"
:class="[_spin?'faceSpinIcon':'',_mpcode]"
:style="{
'font-family': fontFamily,
'font-size':_fontSize,
'color':_color,
'width':_fontSize,
'height':_fontSize,
'lineHeight':_fontSize
}">
{{iconName}}
</text>
<image @click="clickListen" :id="id" v-else ref="xIcons" :style="{width:_fontSize,height:_fontSize}" :src="iconName"></image>
<!-- #ifdef MP-WEIXIN -->
</view>
<!-- #endif -->
</template>
<style scoped >
/* #ifdef MP-WEIXIN */
.faceBox{
display: inline-block;
}
/* #endif */
.face {
transition-property: transform;
transition-duration: 0ms;
transition-timing-function: linear;
transform: rotate(0deg);
text-align: center;
/* #ifdef WEB || MP-WEIXIN */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
line-height: 1 !important;
/* #endif */
}
/* #ifdef WEB || MP-WEIXIN */
.faceSpinIcon{
animation:xFontIconRotate 1s linear infinite ;
}
@keyframes xFontIconRotate {
0%{
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
}
/* #endif */
</style>
@@ -0,0 +1,242 @@
<script lang="ts" setup>
import { ref, computed } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
* @name 图集 xImageGroup
* @description 主要是为了一些需要快速图片排版集的展示,比如评论图集,详情列表图集等,快速开发时使用。方便快捷。
* @page /pages/index/image-group
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xImageGroup"})
const emits = defineEmits<{
/**
* 图片项目被点击
* @param item - 项目
*/
click: [item: UTSJSONObject]
}>()
type xImageGroupPropsType = {
/**
* 图片列表
* 只要包含有url字段即可。
* label需要在图片显示的文本字段,如果没有不要出现此字段
* 如果提供了temp缩略图,会优先展示temp字段,预览时采用url原图
* {url:string,label?:string,temp?:string}
*/
list: UTSJSONObject[],
/**
* 显示的模式见:https://doc.dcloud.net.cn/uni-app-x/component/image.html
*/
model: string,
/**
* 图片高,不要使用auto%
*/
height: string,
/**
* 图片宽,不要使用auto,可以%值
*/
width: string,
/**
* 间隙
*/
gutter: string,
/**
* inset表示文本在图片上
* ouuter表示文本在正文展示
* 不要动态修改
*/
labelModel: 'inset' | 'outter',
/**
* 如果有文字显示文字的大小
*/
labelFontSize: string,
/**
* 如果有文字,显示文字的颜色
*/
labelFontColor: string,
/**
* 如果有文字,显示文字的颜色,暗黑时为空时取白
*/
darkLabelFontColor: string,
/**
* 是否预览图片
*/
preview: boolean,
/**
* 圆角
*/
round: string
}
const props = withDefaults(defineProps<xImageGroupPropsType>(), {
list: ():UTSJSONObject[] => [] as UTSJSONObject[],
model: "scaleToFill",
height: "100",
width: "33.33%",
gutter: "2",
labelModel: "inset",
labelFontSize: "14",
labelFontColor: "white",
darkLabelFontColor: "",
preview: true,
round: "0"
})
// 计算属性
const _list = computed((): UTSJSONObject[] => {
return props.list
})
const _labelFontColor = computed((): string => {
if (xConfig.dark == 'dark') {
if (props.darkLabelFontColor != '') return getDefaultColor(props.darkLabelFontColor)
return "#ffffff"
}
return getDefaultColor(props.labelFontColor)
})
const _gutter = computed((): string => {
return checkIsCssUnit(props.gutter, xConfig.unit)
})
const _labelFontSize = computed((): string => {
let fontSize = checkIsCssUnit(props.labelFontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 14
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _width = computed((): string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _height = computed((): string => {
return checkIsCssUnit(props.height, xConfig.unit)
})
const _round = computed((): string => {
return checkIsCssUnit(props.round, xConfig.unit)
})
// 方法
function showUrlList(item: UTSJSONObject): string {
let url = item.getString('url');
url = url == null ? '' : url
let temp = item.getString('temp');
if (temp != null) return temp;
return url;
}
function showLabel(item: UTSJSONObject): string {
let label = item.getString('label');
label = label == null ? '' : label
return label;
}
function onClick(item: UTSJSONObject, index: number): void {
let url = item.getString('url');
url = url == null ? '' : url
let listurl = _list.value.map((el: UTSJSONObject): string => {
let temurl = el.getString('url');
temurl = temurl == null ? '' : temurl
return temurl;
})
if (props.preview) {
uni.previewImage({
current: url,
urls: listurl as string[]
})
}
emits('click', item)
}
</script>
<template>
<view class="xImageGroup">
<view @click="onClick(item,index)" v-for="(item,index) in _list" :key="index" class="xImageGroupWrap"
:style="{padding:_gutter,width:_width,height:_height,borderRadius:_round}">
<view class="xImageGroupWrapBox">
<x-image :round="_round" :ratio="1" width="100%" height="100%" style="height: 100%;width:100%" :model="props.model" :preview="false" :src="showUrlList(item)"></x-image>
</view>
<!-- margin:labelModel=='inset'?_gutter:'0px' -->
<view v-if="showLabel(item)!=''" class="xImageGroupLabelBox" :style="{
position:props.labelModel=='inset'?'absolute':'static',
}">
<view class="xImageGroupLabelBoxPadding" :style="{
margin:props.labelModel=='inset'?_gutter:'0px',
padding: props.labelModel=='inset'?'20rpx':'20rpx 0px',
borderRadius:_round,
'background-image': props.labelModel=='inset'?'linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,0.9))':''
}">
<text :style="{fontSize:_labelFontSize,color:_labelFontColor}"
class="xImageGroupLabelLabel">{{showLabel(item)}}</text>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.xImageGroupLabelBox {
z-index: 2;
left: 0px;
bottom: 0px;
pointer-events: none;
width: 100%;
/* #ifdef MP || WEB */
box-sizing: border-box;
/* #endif */
}
.xImageGroupLabelBoxPadding {
/* background-image: linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,0.9)); */
}
.xImageGroupLabelLabel {
/* color:white; */
lines: 1;
/* #ifdef WEB */
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
/* #endif */
}
.xImageGroupWrap {
display: flex;
flex-direction: column;
align-items: stretch;
position: relative;
/* #ifdef MP || WEB */
box-sizing: border-box;
/* #endif */
}
.xImageGroupWrapBox {
flex: 1;
pointer-events: none;
}
.xImageGroup {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
}
</style>
@@ -0,0 +1,333 @@
export type xSIZE = {
width : number,
height : number,
top : number,
left : number
}
export type xImageInfo = {
width : number,
height : number,
top : number,
left : number,
src : string,
}
export type xSIZEiMage = {
width : number,
height : number,
top : number,
left : number,
scale : number
}
export type xPOINT = {
x : number,
y : number
}
export type xCalculatePinchZoomOpts = {
initialPoints : xPOINT[],
currentPoints : xPOINT[],
originWidth : number, // 原始图片宽度
originHeight : number, // 原始图片高度
cropWidth : number, // 裁剪框宽度
cropHeight : number,
initialScale : number,
width : number,
height : number,
top : number,
left : number,
scale : number
}
export function alignImageToFrame(imageParams : xSIZEiMage, frameParams : xSIZE) : xSIZE {
const {
width: imgWidth,
height: imgHeight,
top: imgTop,
left: imgLeft
} = imageParams;
const {
width: frameWidth,
height: frameHeight,
top: frameTop,
left: frameLeft
} = frameParams;
// 图片是否完全覆盖框(修改为严格大于)
const isWidthCovered = imgWidth > frameWidth;
const isHeightCovered = imgHeight > frameHeight;
// 当图片宽高都大于框时,保持自由拖动
// 同时图片确保覆盖住裁剪框.
if (isWidthCovered
&& isHeightCovered
&& imgLeft < frameLeft
&& imgTop < frameTop
&& (imgLeft + imgWidth) > (frameLeft + frameWidth)
&& (imgTop + imgHeight) > (frameTop + frameHeight)
) {
return {
width: imgWidth,
height: imgHeight,
top: imgTop,
left: imgLeft
};
}
let newTop = imgTop;
let newLeft = imgLeft;
// 图片的左侧是不是小于框的右侧
let isMinLeft = imgLeft < frameLeft
let isMaxRight = imgLeft + imgWidth > frameLeft + frameWidth
let isMinTop = imgTop < frameTop
let isMaxTop = imgTop + imgHeight > frameTop + frameHeight
// 水平对齐逻辑(反向对齐),如果越过左边了要左对齐,
if (imgLeft < frameLeft && !isMaxRight) {
// 图片左侧越界,靠右对齐
newLeft = frameLeft + frameWidth - imgWidth;
} else if (imgLeft + imgWidth > frameLeft + frameWidth && !isMinLeft) {
// 图片右侧越界,靠左对齐
newLeft = frameLeft;
}
// 垂直对齐逻辑(反向对齐)
if (imgTop < frameTop && !isMaxTop) {
// 图片顶部越界,靠底对齐
newTop = frameTop + frameHeight - imgHeight;
} else if (imgTop + imgHeight > frameTop + frameHeight && !isMinTop) {
// 图片底部越界,靠顶对齐
newTop = frameTop;
}
return {
width: imgWidth,
height: imgHeight,
top: newTop,
left: newLeft
};
}
function scaleImageToFitFrame(imageParams : xImageInfo, frameParams : xSIZE) : xSIZEiMage {
// 解构图片和裁剪框的参数
const {
width: imgWidth,
height: imgHeight,
top: imgTop = 0,
left: imgLeft = 0
} = imageParams;
const {
width: frameWidth,
height: frameHeight,
top: frameTop = 0,
left: frameLeft = 0
} = frameParams;
// 计算宽高比
const imgRatio = imgWidth / imgHeight;
const frameRatio = frameWidth / frameHeight;
let newWidth = 0
let newHeight = 0
let scale = 1
// 确保填充整个裁剪框
if (imgRatio > frameRatio) {
// 以高度为基准缩放
newHeight = frameHeight;
newWidth = newHeight * imgRatio;
scale = newHeight / imgHeight;
// 如果宽度不够,再次调整
if (newWidth < frameWidth) {
newWidth = frameWidth;
newHeight = newWidth / imgRatio;
scale = newWidth / imgWidth;
}
} else {
// 以宽度为基准缩放
newWidth = frameWidth;
newHeight = newWidth / imgRatio;
scale = newWidth / imgWidth;
// 如果高度不够,再次调整
if (newHeight < frameHeight) {
newHeight = frameHeight;
newWidth = newHeight * imgRatio;
scale = newHeight / imgHeight;
}
}
// 计算最终定位(考虑裁剪框和图片的初始偏移)
const top = frameTop + (frameHeight - newHeight) / 2 - imgTop * scale;
const left = frameLeft + (frameWidth - newWidth) / 2 - imgLeft * scale;
return {
width: newWidth, // 新的图片宽度
height: newHeight, // 新的图片高度
top, // 垂直方向偏移
left, // 水平方向偏移
scale // 缩放比例
} as xSIZEiMage;
}
export function translateScalePosition(orimg : xImageInfo, img : xSIZEiMage, mask : xSIZE, parent : xSIZE) {
let scaleinfo = scaleImageToFitFrame(orimg, mask)
img.width = scaleinfo.width
img.height = scaleinfo.height
img.top = scaleinfo.top
img.left = scaleinfo.left
img.scale = scaleinfo.scale
}
export function calculatePinchZoom(options : xCalculatePinchZoomOpts) : xSIZEiMage {
const {
initialPoints, // 初始双指坐标 [{x, y}, {x, y}]
currentPoints, // 当前双指坐标 [{x, y}, {x, y}]
originWidth, // 原始图片宽度
originHeight, // 原始图片高度
cropWidth, // 裁剪框宽度
cropHeight, // 裁剪框高度
width, // 当前图片宽度
height, // 当前图片高度
top, // 图片当前顶部位置
left, // 图片当前左侧位置
scale, // 当前缩放比例
initialScale // 初始缩放比例
} = options;
// 阻尼系数,0-1之间,越大越快,越小缩放越慢.
const DAMPING_FACTOR = 0.45;
// 计算初始和当前双指距离
const getDistance = (points : xPOINT[]) : number => {
const p1 = points[0];
const p2 = points[1];
return Math.sqrt(
Math.pow(p2.x - p1.x, 2) +
Math.pow(p2.y - p1.y, 2)
);
};
// 计算双指中心点
const getCenter = (points : xPOINT[]) : xPOINT => {
const p1 = points[0];
const p2 = points[1];
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
} as xPOINT;
};
const initialDistance = getDistance(initialPoints);
const currentDistance = getDistance(currentPoints);
if (initialDistance == currentDistance) return {
width,
height,
left,
top,
scale
} as xSIZEiMage;
// 计算缩放比例(加入阻尼)
const rawScaleChange = currentDistance / initialDistance;
const dampedScaleChange = 1 + (rawScaleChange - 1) * DAMPING_FACTOR;
let newScale = scale * dampedScaleChange;
// 计算最小宽高(确保至少覆盖裁剪框)
const minWidth = Math.max(cropWidth, originWidth * initialScale);
const minHeight = Math.max(cropHeight, originHeight * initialScale);
// 计算最大宽高
const maxWidth = originWidth * 3;
const maxHeight = originHeight * 3;
// 限制缩放范围
const MIN_SCALE = initialScale;
const MAX_SCALE = 3;
// 判断是否超过最大缩放限制
if (newScale > MAX_SCALE) {
return {
width: maxWidth,
height: maxHeight,
left,
top,
scale: MAX_SCALE
} as xSIZEiMage;
}
if (newScale < MIN_SCALE) {
return {
width: minWidth,
height: minHeight,
left,
top,
scale: MIN_SCALE
} as xSIZEiMage;
}
// 计算新的图片宽高
const newWidth = Math.max(minWidth, Math.min(originWidth * newScale, maxWidth));
const newHeight = Math.max(minHeight, Math.min(originHeight * newScale, maxHeight));
// 计算缩放中心点
const initialCenter = getCenter(initialPoints);
const currentCenter = getCenter(currentPoints);
// 计算偏移量(同样应用阻尼)
const rawOffsetX = (initialCenter.x - left) * (dampedScaleChange - 1);
const rawOffsetY = (initialCenter.y - top) * (dampedScaleChange - 1);
// 计算新的位置
const newLeft = left - rawOffsetX;
const newTop = top - rawOffsetY;
return {
width: newWidth,
height: newHeight,
left: newLeft,
top: newTop,
scale: newScale
} as xSIZEiMage;
}
// 根据裁剪框的缩放后的大小,与原始大小的缩放比,确定图片的当前大小的缩放比与位置
export function maskRatioScaleToPhotoSizePosition (oldMaskSize:xSIZEiMage,newMaskSize:xSIZE,oldimg:xImageInfo,nowImgPosSize:xSIZEiMage,index:number){
let difflen = oldMaskSize.width - newMaskSize.width
let scale2 = difflen/oldMaskSize.width
let nwdifflen = scale2*nowImgPosSize.width
let scale = nwdifflen / oldimg.width
if(nowImgPosSize.scale + scale>3){
scale = 3-nowImgPosSize.scale
}
let diffx = oldimg.width*scale
let diffy = oldimg.height*scale
let newWidth = nowImgPosSize.width + diffx
let newHeight = nowImgPosSize.height + diffy
nowImgPosSize.width = newWidth
nowImgPosSize.height = newHeight
// 左上角,应该是左上放大.
if(index == 0){
nowImgPosSize.left = nowImgPosSize.left - diffx
nowImgPosSize.top = nowImgPosSize.top - diffy
}
if(index == 1){
// nowImgPosSize.left = nowImgPosSize.left - diffx
nowImgPosSize.top = nowImgPosSize.top - diffy
}
if(index == 3){
nowImgPosSize.left = nowImgPosSize.left - diffx
// nowImgPosSize.top = nowImgPosSize.top - diffy
}
newMaskSize.width = oldMaskSize.width
newMaskSize.height = oldMaskSize.height
newMaskSize.left = oldMaskSize.left
newMaskSize.top = oldMaskSize.top
nowImgPosSize.scale = nowImgPosSize.scale + scale
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,501 @@
<script lang="ts">
import { type PropType } from "vue"
import { checkIsCssUnit, getUid,getUnit } from "../../core/util/xCoreUtil.uts";
import { xConfig,xProvitae } from "../../config/xConfig.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
type IMG_MODEL = "fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill";
type IMG_SIZE_INFO = {
width : number,
height : number,
}
type IMG_SIZE_INFO_PLACE = {
width : string,
height : string,
}
/**
* @name 图片 xImage
* @description 宽高可以设置,支持百分比,px,rpx
* @page /pages/index/image
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
id: ("xImage-" + getUid()) as string,
idBox: ("xImage-" + getUid()) as string,
/** 是否正在加载,默认为false */
isLoading: true,
/** 是否加载出错,默认为false */
isError: false,
reload: 0,
imgrealWidth: 0,
imgrealHeight: 0,
boxWidth: 0,
boxHeight: 0,
ratioWidth: 0,
ratioHeight: 0,
isLoaded:false,
tid:0,
resizeObserver: null as UniResizeObserver | null,
androidAndWebUrl:"",
dateTime: 0,
_x: 0,
_y: 0,
boxLeft:0,
boxTop:0,
isVisibled:false,
tid2:0,
keyidsf:0
}
},
emits:[
/**
* 图片被点击
*/
'click'],
props: {
/**
* 宽度,默认100%
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
*/
width: {
type: String,
default: "100%"
},
/**
* 高度,auto,%,rpx,px,string number
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
*/
height: {
type: String,
default: "auto"
},
/** 图片源 */
src: {
type: String,
default: ""
},
/**
* 预览的图片源,如果为空则与src同步
*/
previewSrc:{
type:String,
default:""
},
/**
* 模式
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
*
*/
model: {
type: String as PropType<IMG_MODEL>,
default: "fill"
},
/**
* 点击后是否预览图片
*/
preview: {
type: Boolean,
default: true
},
/**
* 预览占位比例
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
* 当图片加载成功后,使用正确的原图片比例设置。
* 默认是5/4=1.25
*/
ratio: {
type: Number,
default: 1.25
},
/**
* 圆角
*/
round: {
type: String,
default: '0'
},
/**
* 加载和失败时的图标大小。
*/
iconSize:{
type:String,
default:"16"
},
/**
* 占位背景色
*/
placeBgColor:{
type:String,
default:"#F5F5F5"
},
/**
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
*/
placeDarkBgColor:{
type:String,
default:""
},
/**
* 是否在安卓上显示过渡动画
*/
fadeShow:{
type:Boolean,
default:false
},
/**
* 用于在scorllview根节点的页面进行懒加载,不可视范围内的不显示.请仅慎使用,不可在list-view中使用.
*/
lazy:{
type:Boolean,
default:false
}
},
mounted() {
let t= this;
// #ifdef APP
this.imgLoad()
// #endif
let ele = uni.getElementById(this.idBox!);
if(ele==null) return;
// #ifdef APP || WEB
if (this.resizeObserver == null) {
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
entries.forEach(entry => {
if (entry.target == ele) {
// #ifdef APP
t.tid = setTimeout(function() {
t.getNodes();
}, 50);
// #endif
// #ifdef WEB
t.getNodes();
// #endif
}
})
})
}
this.resizeObserver!.observe(ele!)
// #ifdef uniVersion >= 4.31 && APP-ANDROID
t.tid = setTimeout(function() {
t.getNodes();
}, 50);
// #endif
// #endif
// #ifdef MP-WEIXIN
this.getNodes();
// #endif
this.setIsVisibledShow()
},
beforeUnmount() {
clearTimeout(this.tid)
clearTimeout(this.tid2)
if(this.resizeObserver!=null){
this.resizeObserver?.disconnect()
}
},
updated() {
// #ifdef MP-WEIXIN
this.getNodes()
// #endif
},
computed: {
_model():string{
return this.model;
},
_placeBgColor():string{
let bgcolor = this.placeBgColor;
if(xConfig.dark=='dark'){
bgcolor = this.placeDarkBgColor
if(this.placeDarkBgColor==''){
bgcolor = xConfig.inputDarkColor
}
}
return getDefaultColor(bgcolor)
},
_round() : string {
return checkIsCssUnit(this.round, xConfig.unit)
},
_src() : string {
return this.src;
},
_previewSrc() : string {
if(this.previewSrc == '') return this.src;
return this.previewSrc;
},
_place_size() : IMG_SIZE_INFO_PLACE {
return {
width: checkIsCssUnit(this.width, xConfig.unit),
height: checkIsCssUnit(this.height, xConfig.unit),
} as IMG_SIZE_INFO_PLACE
},
_scrollTop():number{
return xProvitae.scrollTop;
},
_img_box_size() : IMG_SIZE_INFO_PLACE {
if(this.imgrealHeight>0){
return this._img_size;
}
let _w = this.width;
let _h = this.height;
let us_w = checkIsCssUnit(this.width, xConfig.unit)
let us_h = checkIsCssUnit(this.height, xConfig.unit)
if(this.width.lastIndexOf('%')>-1||this.width=='auto' ){
us_w = '100%'
_w = '100%'
}
if(this.height.lastIndexOf('%')>-1||this.height=='auto' || this.isError){
if(this.boxHeight>=5){
_h = (this.boxHeight).toString()+'px'
}else{
if(this.width.lastIndexOf('%')>-1||this.width=='auto'){
if(this.height=='100%'){
_h = (this.ratio * this.boxHeight).toString()+'px'
}else{
_h = (this.ratio * this.boxWidth).toString()+'px'
}
}else{
_h = (this.ratio * parseFloat(us_w)).toString()+getUnit(us_w)
}
}
return { width: _w, height: _h } as IMG_SIZE_INFO_PLACE
}
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
},
_img_size() : IMG_SIZE_INFO_PLACE {
let us_w = checkIsCssUnit(this.width, xConfig.unit)
let us_h = checkIsCssUnit(this.height, xConfig.unit)
if (!this.isLoaded) {
return { width: "300px", height: "300px" } as IMG_SIZE_INFO_PLACE
}
if (this.boxWidth > 0) {
let ratio = this.boxWidth / this.imgrealWidth;
// 如果图片高为auto,表示自动高。
if((this.height=='auto')){
us_h = (ratio*this.imgrealHeight).toString()+'px'
}
if((this.width=='auto')){
us_w = this.boxWidth.toString()+'px'
}
if((this.height.lastIndexOf('%')>-1)){
us_h = (this.boxHeight).toString()+'px'
}
if((this.width.lastIndexOf('%')>-1)){
us_w = (this.boxWidth).toString()+'px'
}
}
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
},
_styleMap() : Map<string, string> {
let styleMap = new Map<string, string>();
styleMap.set("width", this._img_size.width)
styleMap.set("height", this._img_size.height)
styleMap.set("transform", this.isLoading ? 'scale(0.1)' : 'scale(1)')
styleMap.set("visibility", this.isLoading ? 'visible' : (!this.isError ? 'visible' : 'hidden'))
styleMap.set("opacity", this.isLoading ? '0' : '1')
styleMap.set("border-raiuds", this._round)
return styleMap;
}
},
watch:{
src(){
// #ifdef APP-ANDROID || WEB
this.imgLoad()
// #endif
},
_scrollTop(){
if(!this.lazy) return;
clearTimeout(this.tid2)
let t = this;
this.tid2 = setTimeout(function() {
t.setIsVisibledShow()
}, 10);
}
},
methods: {
setIsVisibledShow(){
let win = uni.getWindowInfo()
let t = this;
// let ele = this?.$parent?.$el as UniElement||null;
uni.createSelectorQuery().in(this)
.select(".xImage")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
let winHeight = win.windowHeight;
// let winWidth = win.windowWidth;
let eleTop = nodeinfo.top!
let eleBotom = nodeinfo.bottom!
t.isVisibled = eleBotom < 0 || eleTop > winHeight ?false:true;
})
},
prevImage() {
/**
* 图片被点击
*/
this.$emit('click')
if (this.preview) {
uni.previewImage({
current: this._previewSrc,
urls: [this._previewSrc]
})
}
},
imgLoad2(evt : ImageLoadEvent) {
this.imgrealWidth = evt.detail.width;
this.imgrealHeight = evt.detail.height;
this.isLoading = false;
this.androidAndWebUrl = this._src
this.isError = false;
this.isLoaded = true;
},
imgLoad() {
let t = this;
uni.getImageInfo({
src:this._src,
fail(error:IMediaError){
console.log('error',error.errMsg)
t.isError = true;
t.isLoading = false
},
success(result:GetImageInfoSuccess){
t.isLoading = false;
t.isError = false;
t.isLoaded = true;
if( result.path!=t.androidAndWebUrl){
t.imgrealWidth = result.width
t.imgrealHeight = result.height
t.androidAndWebUrl = result.path;
t.keyidsf+=1;
}
}
} as GetImageInfoOptions)
},
// evt:ImageErrorEvent
imgError() {
this.isError = true;
this.isLoading = false
},
getNodes() {
let t = this;
// let ele = this?.$parent?.$el as UniElement||null;
uni.createSelectorQuery().in(this)
.select(".xImage")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
t.boxWidth = nodeinfo.width!
t.boxHeight = nodeinfo.height!
})
},
resize(){
this.isLoaded = false
let t = this;
// let ele = this?.$parent?.$el as UniElement||null;
uni.createSelectorQuery().in(this)
.select(".xImage")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
t.boxWidth = nodeinfo.width!
t.boxHeight = nodeinfo.height!
t.isLoaded = true
})
}
},
}
</script>
<template>
<view
:key="keyidsf"
@click="prevImage"
class="xImage" ref="xImage" :id="idBox" :style="{width:_place_size.width,height:_place_size.height}">
<view class="xImageBox"
:style="{width:_img_box_size.width,height:_img_box_size.height,borderRadius:_round,pointerEvent:'none'}">
<!--
@slot 加载中的插槽,插槽内自行给你布局的view宽和高写100%
-->
<slot name="loading" v-if="isLoading">
<view class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
<x-icon :font-size="iconSize" v-if="isLoading" name="loader-2-line" color="primary" :spin="true"></x-icon>
</view>
</slot>
<!--
@slot 加载失败时的插槽,插槽内自行给你布局的view宽和高写100%
-->
<slot name="error" v-if="isError">
<view class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
<x-icon :font-size="iconSize" v-if="isError" color="error" name="landscape-line"></x-icon>
</view>
</slot>
<!-- @error="imgError" -->
<!-- #ifdef APP -->
<image :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
:mode="_model" :style="[_styleMap,{visibility:isVisibled||!lazy?'visible':'hidden'}]" :src="_src" >
</image>
<!-- #endif -->
<!-- #ifndef APP -->
<image @load="imgLoad2" @error="imgError" :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
:mode="_model" :style="[_styleMap,{visibility:isVisibled||!lazy?'visible':'hidden'}]" :src="_src" >
</image>
<!-- #endif -->
</view>
</view>
</template>
<style scoped>
.xImage {
position: relative;
overflow: hidden;
}
.xImageBox{
pointer-events: none;
}
.xImagePlace {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%
}
.xImageImgAbs {
position: absolute;
pointer-events: none;
}
</style>
@@ -0,0 +1,437 @@
<script lang="ts">
import { type PropType } from "vue"
import { checkIsCssUnit, getUid,getUnit } from "../../core/util/xCoreUtil.uts";
import { xConfig } from "../../config/xConfig.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
type IMG_MODEL = "fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill";
type IMG_SIZE_INFO = {
width : number,
height : number,
}
type IMG_SIZE_INFO_PLACE = {
width : string,
height : string,
}
/**
* @name 图片 xImage
* @description 宽高可以设置,支持百分比,px,rpx
* @page /pages/index/image
* @category 展示组件
* @constant 平台兼容
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
*/
export default {
data() {
return {
id: ("xImage-" + getUid()) as string,
idBox: ("xImage-" + getUid()) as string,
/** 是否正在加载,默认为false */
isLoading: true,
/** 是否加载出错,默认为false */
isError: false,
reload: 0,
imgrealWidth: 0,
imgrealHeight: 0,
boxWidth: 0,
boxHeight: 0,
ratioWidth: 0,
ratioHeight: 0,
isLoaded:false,
tid:0,
resizeObserver: null as UniResizeObserver | null,
androidAndWebUrl:"",
dateTime: 0,
_x: 0,
_y: 0,
}
},
emits:[
/**
* 图片被点击
*/
'click'],
props: {
/**
* 宽度,默认100%
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
*/
width: {
type: String,
default: "100%"
},
/**
* 高度,auto,%,rpx,px,string number
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
*/
height: {
type: String,
default: "auto"
},
/** 图片源 */
src: {
type: String,
default: ""
},
/**
* 模式
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
*
*/
model: {
type: String as PropType<IMG_MODEL>,
default: "fill"
},
/**
* 点击后是否预览图片
*/
preview: {
type: Boolean,
default: true
},
/**
* 预览占位比例
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
* 当图片加载成功后,使用正确的原图片比例设置。
* 默认是5/4=1.25
*/
ratio: {
type: Number,
default: 1.25
},
/**
* 圆角
*/
round: {
type: String,
default: '0'
},
/**
* 加载和失败时的图标大小。
*/
iconSize:{
type:String,
default:"16"
},
/**
* 占位背景色
*/
placeBgColor:{
type:String,
default:"#F5F5F5"
},
/**
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
*/
placeDarkBgColor:{
type:String,
default:""
},
fadeShow:{
type:Boolean,
default:true
}
},
mounted() {
let t= this;
this.imgLoad()
let ele = uni.getElementById(this.idBox!);
if(ele==null) return;
if (this.resizeObserver == null) {
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
entries.forEach(entry => {
if (entry.target == ele) {
// #ifdef APP
t.tid = setTimeout(function() {
t.getNodes();
}, 50);
// #endif
// #ifdef WEB
t.getNodes();
// #endif
}
})
})
}
this.resizeObserver!.observe(ele!)
},
beforeUnmount() {
clearTimeout(this.tid)
this.resizeObserver?.disconnect()
// uni.$off('onResize', this.resize)
},
computed: {
_model():string{
return this.model;
},
_placeBgColor():string{
let bgcolor = this.placeBgColor;
if(xConfig.dark=='dark'){
bgcolor = this.placeDarkBgColor
if(this.placeDarkBgColor==''){
bgcolor = xConfig.inputDarkColor
}
}
return getDefaultColor(bgcolor)
},
_round() : string {
return checkIsCssUnit(this.round, xConfig.unit)
},
_src() : string {
return this.src;
},
_place_size() : IMG_SIZE_INFO_PLACE {
return {
width: checkIsCssUnit(this.width, xConfig.unit),
height: checkIsCssUnit(this.height, xConfig.unit),
} as IMG_SIZE_INFO_PLACE
},
_img_box_size() : IMG_SIZE_INFO_PLACE {
if(this.imgrealHeight>0){
return this._img_size;
}
let _w = this.width;
let _h = this.height;
let us_w = checkIsCssUnit(this.width, xConfig.unit)
let us_h = checkIsCssUnit(this.height, xConfig.unit)
if(this.width.lastIndexOf('%')>-1||this.width=='auto' ){
us_w = '100%'
_w = '100%'
}
if(this.height.lastIndexOf('%')>-1||this.height=='auto' || this.isError){
if(this.boxHeight>=5){
_h = (this.boxHeight).toString()+'px'
}else{
if(this.width.lastIndexOf('%')>-1||this.width=='auto'){
if(this.height=='100%'){
_h = (this.ratio * this.boxHeight).toString()+'px'
}else{
_h = (this.ratio * this.boxWidth).toString()+'px'
}
}else{
_h = (this.ratio * parseFloat(us_w)).toString()+getUnit(us_w)
}
}
return { width: _w, height: _h } as IMG_SIZE_INFO_PLACE
}
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
},
_img_size() : IMG_SIZE_INFO_PLACE {
let us_w = checkIsCssUnit(this.width, xConfig.unit)
let us_h = checkIsCssUnit(this.height, xConfig.unit)
if (!this.isLoaded) {
return { width: "300px", height: "300px" } as IMG_SIZE_INFO_PLACE
}
if (this.boxWidth > 0) {
let ratio = this.boxWidth / this.imgrealWidth;
// 如果图片高为auto,表示自动高。
if((this.height=='auto')){
us_h = (ratio*this.imgrealHeight).toString()+'px'
}
if((this.width=='auto')){
us_w = this.boxWidth.toString()+'px'
}
if((this.height.lastIndexOf('%')>-1)){
us_h = (this.boxHeight).toString()+'px'
}
if((this.width.lastIndexOf('%')>-1)){
us_w = (this.boxWidth).toString()+'px'
}
}
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
},
_styleMap() : Map<string, string> {
let styleMap = new Map<string, string>();
styleMap.set("width", this._img_size.width)
styleMap.set("height", this._img_size.height)
styleMap.set("transform", this.isLoading ? 'scale(0.1)' : 'scale(1)')
styleMap.set("visibility", this.isLoading ? 'visible' : (!this.isError ? 'visible' : 'hidden'))
styleMap.set("opacity", this.isLoading ? '0' : '1')
styleMap.set("border-raiuds", this._round)
return styleMap;
}
},
watch:{
src(){
// #ifdef APP-ANDROID || WEB
this.imgLoad()
// #endif
}
},
methods: {
prevImage() {
/**
* 图片被点击
*/
this.$emit('click')
if (this.preview) {
uni.previewImage({
current: this.src,
urls: [this.src]
})
}
},
imgLoad2(evt : ImageLoadEvent) {
this.imgrealWidth = evt.detail.width;
this.imgrealHeight = evt.detail.height;
this.isLoading = false;
this.isError = false;
this.isLoaded = true;
},
imgLoad() {
let t = this;
uni.getImageInfo({
src:this._src,
fail(_:IMediaError){
console.log('error')
t.isError = true;
t.isLoading = false
},
success(result:GetImageInfoSuccess){
console.log('success')
t.isLoading = false;
t.isError = false;
t.isLoaded = true;
if( result.path!=t.androidAndWebUrl){
t.imgrealWidth = result.width
t.imgrealHeight = result.height
t.androidAndWebUrl = result.path;
}
}
} as GetImageInfoOptions)
},
// evt:ImageErrorEvent
imgError() {
this.isError = true;
this.isLoading = false
},
mStart(evt:UniTouchEvent){
this.dateTime = Date.now()
this._x = evt.changedTouches[0].clientX
this._y = evt.changedTouches[0].clientY
},
mEnd(evt:UniTouchEvent){
let diffdate = Date.now() - this.dateTime
let diffx = Math.abs(evt.changedTouches[0].clientX - this._x)
let diffy = Math.abs(evt.changedTouches[0].clientY - this._y)
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
this.prevImage()
}
},
// #ifdef WEB
mmStart(evt:UniMouseEvent){
this.dateTime = Date.now()
this._x = evt.clientX
this._y = evt.clientY
},
mmEnd(evt:UniMouseEvent){
let diffdate = Date.now() - this.dateTime
let diffx = Math.abs(evt.clientX - this._x)
let diffy = Math.abs(evt.clientY - this._y)
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
this.prevImage()
}
},
// #endif
getNodes() {
let t = this;
// let ele = this?.$parent?.$el as UniElement||null;
uni.createSelectorQuery().in(this)
.select(".xImage")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
t.boxWidth = nodeinfo.width!
t.boxHeight = nodeinfo.height!
})
},
resize(){
this.isLoaded = false
let t = this;
// let ele = this?.$parent?.$el as UniElement||null;
uni.createSelectorQuery().in(this)
.select(".xImage")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo;
t.boxWidth = nodeinfo.width!
t.boxHeight = nodeinfo.height!
t.isLoaded = true
})
}
},
}
</script>
<template>
<view
@touchstart="mStart"
@touchend="mEnd"
<!-- #ifdef WEB -->
@mousedown="mmStart"
@mouseup="mmEnd"
<!-- #endif -->
class="xImage" :id="idBox" :style="{width:_place_size.width,height:_place_size.height}">
<view class="xImageBox"
:style="{width:_img_box_size.width,height:_img_box_size.height,borderRadius:_round,pointerEvent:'none'}">
<view v-if="isLoading||isError" class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
<x-icon :font-size="iconSize" v-if="isError" color="error" name="landscape-line"></x-icon>
<x-icon :font-size="iconSize" v-if="isLoading" name="loader-2-line" color="primary" :spin="true"></x-icon>
</view>
<image :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
:mode="_model" :style="_styleMap" :src="_src" @error="imgError">
</image>
</view>
</view>
</template>
<style scoped>
.xImage {
position: relative;
overflow: hidden;
}
.xImageBox{
pointer-events: none;
}
.xImagePlace {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%
}
.xImageImgAbs {
position: absolute;
}
</style>
@@ -0,0 +1,7 @@
export type INDEXBAR_ITEM = {
id : string,
ele : XIndexbarItemComponentPublicInstance,
top:number,
name:string,
height:number
}
@@ -0,0 +1,623 @@
<script lang="ts">
import { SlotsType, PropType } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
type indexbarTYPE = {
index : number,
title : string
}
/**
*
* @name 索引 xIndexbar
* @description 特别提醒:本组件在1.0.9重构不向下兼容,并且删除了子组件:x-indexbar-item,不再需要子组件。请使用对应的动态插槽来渲染
* 数据,demo是526条数据的测试依赖右边索引滑动跟手流畅。
* 虚拟列表有个缺点会在滚动时分页读取数据并复用布局,因此如果你想做带图片的索引,
* 建议进入应用后启用后台缓存已有的头像或者图片数据类似微信那样缓存图片,这样虚拟加载的时候闪烁感就少了
* 重要:插槽内不要使用任何自定组件布局,也不要增加任何额外的节点,能少就少。
* @page /pages/index/indexbar
* @category 导航组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
listIndexBar: [] as indexbarTYPE[],
boxHeight: 0,
boxWidth: 0,
boxTop: 0,
boxLeft: 0,
nowIndex: 0,
currentIndexBYbar: 0,
// 视图内显示的条数。
viewConunt: 0,
windowTop: 0,
scrollTop: 0,
realScrollY:0,
headerDomRectHeight:0,
_y: 0,
tid: 0,
isMoveing: false,
olsShowView: [] as UTSJSONObject[],
isShowOldViewData: false,
dotParentOffsetTop: -1,
dotRight: 0,
tid2: 0,
showDot: false,
sliderDotSize:16,
// #ifdef MP-WEIXIN
mpBgBounds:null as any|null,
sliderBgBounds:null as any|null
// #endif
}
},
slots: Object as SlotsType<{
header : { title : string, index : number },
default : {
current : UTSJSONObject,
currentIndex : number,
index : number
}
}>,
props: {
/**
* 宽
*/
width: {
type: String,
default: "auto"
},
/**
* 高,%,rpx,px单位均可。
*/
height: {
type: String,
default: "100%"
},
/**
* 侧边指示激活时的文字颜色
* 空值是取全局主题值
*/
dotActiveColor: {
type: String,
default: ""
},
/**
* 侧边指示未激活时的文字颜色
*/
dotColor: {
type: String,
default: "#c0c0c0"
},
/**
* 侧边指示的背景颜色
*/
dotBgColor: {
type: String,
default: "white"
},
list: {
type: Array as PropType<UTSJSONObject[]>,
default: () : UTSJSONObject[] => [] as UTSJSONObject[]
},
/**
* 项目的高
* 只能是数字,或者带rpx,px单位
*/
cellHeight: {
type: String,
default: '50'
},
/**
* 项目的标题高
* 只能是数字,或者带rpx,px单位
*/
titleHeight: {
type: String,
default: '32'
},
customSliderBar:{
type:Array as PropType<Array<string>>,
default:():string[] => [] as string[]
}
},
computed: {
_customBarLen():number{
return this.customSliderBar.length;
},
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_cellHeight() : number {
let height = checkIsCssUnit(this.cellHeight, xConfig.unit)
let unit = getUnit(height)
let realheight = parseInt(height)
if (unit == 'rpx') {
realheight = rpx2px(realheight)
}
return realheight;
},
_titleHeight() : number {
let height = checkIsCssUnit(this.titleHeight, xConfig.unit)
let unit = getUnit(height)
let realheight = parseInt(height)
if (unit == 'rpx') {
realheight = rpx2px(realheight)
}
return realheight;
},
_totalHeight() : number {
return this.list.length * this._cellHeight;
},
_viewConunt() : number {
if (this.boxHeight == 0 || this.list.length == 0) return 0;
let itemcount = this.boxHeight / this._cellHeight;
return Math.ceil(itemcount + 1)
},
_dotActiveColor() : string {
if (this.dotActiveColor == "") return getDefaultColor(xConfig.color);
return getDefaultColor(this.dotActiveColor)
},
_dotColor() : string {
return getDefaultColor(this.dotColor)
},
_dotBgColor() : string {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(this.dotBgColor)
},
_viewList() : UTSJSONObject[] {
if (this.isShowOldViewData) return this.olsShowView
let start = this.nowIndex - this._viewConunt;
let end = this.nowIndex + this._viewConunt;
start = Math.max(0, start)
// end = Math.min(this.list.length-1,end)
let oldIndexList = this.list.slice(start, end);
let list = oldIndexList.map((el : UTSJSONObject, index : number) : UTSJSONObject => {
el.set("oldIndex", index + start)
return el;
})
this.olsShowView = list
return list
},
_list() : UTSJSONObject[] {
return this.list;
},
_listTotal() : UTSJSONObject[] {
let customlist = this.customSliderBar.map((el:string,index:number):UTSJSONObject => {
return {
"id": index,
"name": el,
"logo": "",
"index": el
} as UTSJSONObject
})
return [...customlist,...this.list];
}
},
watch: {
list() {
this.scrollTop = 0
this.nowIndex = 0
this.listIndexBar = [] as indexbarTYPE[]
this.olsShowView = [] as UTSJSONObject[]
this.currentIndexBYbar = 0
this.olsShowView = this._viewList
this.setInitData();
}
},
mounted() {
this.windowTop = uni.getWindowInfo().windowTop
this.getNodeInfo();
this.olsShowView = this._viewList
this.setInitData();
// #ifdef MP-WEIXIN
this.mpweixinGetBoundec()
// #endif
},
beforeUnmount() {
clearTimeout(this.tid)
},
methods: {
// #ifdef MP-WEIXIN
async mpweixinGetBoundec(){
let _this = this;
setTimeout(async function() {
let ele = _this.$refs['xIndexBarRightSliderBg'] as UniElement;
_this.mpBgBounds= await ele.getBoundingClientRectAsync()
let parentele = _this.$refs['xIndexBarRightSlider'] as UniElement;
_this.sliderBgBounds = await parentele.getBoundingClientRectAsync()
}, 100);
},
// #endif
getHeaderDom(){
let t = this;
const ele = this.$refs['headerRef']! as UniElement;
ele.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
t.headerDomRectHeight = rect.height
})
.catch(()=>{})
},
setInitData() {
let lst = Date.now()
let list = this._list as UTSJSONObject[]
if (list.length == 0) return
let listbar = [] as indexbarTYPE[]
let listbarTitle = [] as string[];
for (let i = 0; i < this._list.length; i++) {
let current = this._list[i]
let keya = current.getString("index")!;
if (!listbarTitle.includes(keya!)) {
listbarTitle.push(keya)
listbar.push({ index: i+this._customBarLen, title: keya } as indexbarTYPE)
}
}
let customlist = this.customSliderBar.map((el:string,index:number):indexbarTYPE => {
return {
index,
title:el
} as indexbarTYPE
})
this.listIndexBar = [...customlist,...listbar];
console.warn("索引数据处理时间(毫秒)", Date.now() - lst, "如果此值过大,表示sdk有问题,正常在10ms内,安卓dev会在60ms内")
},
sliderOnclik(evt : UniPointerEvent, item : indexbarTYPE, index : number) {
// let customIndex = this.customSliderBar.length + index -1;
this.nowIndex = this.listIndexBar[index].index;
// this.nowIndex = index;
if(index <=this.customSliderBar.length-1){
this.scrollTop = 0
}else{
this.scrollTop = this._cellHeight * this.nowIndex + this.headerDomRectHeight
}
this.currentIndexBYbar = index;
this.transxyByDotTop(evt.clientY)
},
onScrollEvent(evt : UniScrollEvent) {
if (this.isMoveing) return;
this.realScrollY = evt.detail.scrollTop ;
this.transxyend(evt.detail.scrollTop - this._titleHeight - this.headerDomRectHeight)
this.getHeaderDom()
},
ONscrollend(evt : UniScrollEvent) {
// #ifdef APP-IOS||APP-ANDROID
this.scrollTop = evt.detail.scrollTop;
// #endif
},
transxyend(top : number) {
let index = Math.ceil((top) / this._cellHeight)
index = Math.max(0, index)
index = Math.min(this._list.length - 1, index)
if (index == this.nowIndex) return;
this.nowIndex = index;
let title = this._list[index].getString("index")!
if (title == this.listIndexBar[this.currentIndexBYbar].title) return;
let barindexItem = this.listIndexBar.findIndex((el : indexbarTYPE) : boolean => el.title == title)
if (barindexItem == this.currentIndexBYbar) return;
if (barindexItem > -1) {
this.currentIndexBYbar = barindexItem;
}
},
getNodeInfo() {
let t = this;
uni.createSelectorQuery().in(this)
.select(".xIndexBarBox")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
t.boxTop = nodeinfo.top!;
t.boxWidth = nodeinfo.width!;
t.boxHeight = nodeinfo.height!;
t.boxLeft = nodeinfo.left!;
})
this.getHeaderDom();
},
hideDot() {
let t = this;
clearTimeout(this.tid2)
this.tid2 = setTimeout(function () {
t.showDot = false
}, 700);
},
itemMove(evt : UniTouchEvent) {
evt.stopPropagation();
evt.preventDefault();
let t = this;
let y = evt.changedTouches[0].clientY
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
let boundsTop = 0
// #ifdef APP || WEB
boundsTop = ele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop = this.mpBgBounds!.top
// #endif
let diff = y - boundsTop;
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * t.sliderDotSize))
let barIndex = Math.floor(diff / t.sliderDotSize)
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
if (barIndex != this.currentIndexBYbar) {
this.currentIndexBYbar = barIndex;
}
let index = this.listIndexBar[barIndex].index;
if (index == this.nowIndex) return
this.nowIndex = index;
clearTimeout(this.tid)
this.isShowOldViewData = true
t.transxyByDotTop(y)
// 减少处理次数
this.tid = setTimeout(function () {
if(index<=t._customBarLen-1){
t.scrollTop = 0
}else{
t.scrollTop = index * t._cellHeight + t.headerDomRectHeight
}
t.isShowOldViewData = false;
t.realScrollY = t.scrollTop ;
}, 10);
},
itemStart(evt : UniTouchEvent) {
this.isMoveing = true
this._y = evt.changedTouches[0].clientY;
},
transxyByDotTop(top : number) {
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
let boundsTop = 0
// #ifdef APP || WEB
boundsTop = ele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop = this.mpBgBounds!.top
// #endif
let diff = top - boundsTop!
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * this.sliderDotSize))
let barIndex = Math.floor(diff / this.sliderDotSize)
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
let parentele = this.$refs['xIndexBarRightSlider'] as UniElement;
let boundsTop2 = 0
// #ifdef APP || WEB
boundsTop2 = parentele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop2 = this.sliderBgBounds!.top
// #endif
let topdiff = barIndex * this.sliderDotSize + boundsTop - boundsTop2 - (50 - this.sliderDotSize) / 2;
// #ifdef APP-ANDROID
topdiff = -25 + (boundsTop - boundsTop2) + barIndex * this.sliderDotSize + this.sliderDotSize / 2
// #endif
let dotele = this.$refs['xIndexBarDot'] as UniElement;
if (topdiff == this.dotParentOffsetTop) return;
this.dotParentOffsetTop = topdiff
dotele.style.setProperty('transform', `translateY(${topdiff}px) rotate(-45deg)`)
this.showDot = true
this.hideDot();
},
itemEnd() {
this.isMoveing = false
this.dotParentOffsetTop = -1
}
}
}
</script>
<template>
<view class="xIndexBar" :style="{height:_height,width:_width}">
<view :style="{height:_titleHeight+'px',top:realScrollY>=headerDomRectHeight?'0px':(headerDomRectHeight-realScrollY)+'px'}">
<!--
@slot 悬浮的菜单头
@props {title} 当前选中的菜单头
@props {index} 当前选中的菜单头索引
-->
<slot name="header" :title="listIndexBar.length>0?listIndexBar[currentIndexBYbar].title:''"
:index="listIndexBar.length>0?listIndexBar[currentIndexBYbar+(Math.max(customSliderBar.length-1,0))].index:0">
<view style="display: flex;flex-direction: row;align-items: center;height: 100%;">
<x-text v-if="listIndexBar.length>0">{{listIndexBar[currentIndexBYbar<= (_customBarLen-1)?_customBarLen:currentIndexBYbar].title}}</x-text>
</view>
</slot>
</view>
<view class="xIndexBarBox" >
<scroll-view @scrollend="ONscrollend" :show-scrollbar="false" @scroll="onScrollEvent" :scroll-top="scrollTop"
:bounces="false" :enable-back-to-top="true" class="xIndexBar" :style="{height:boxHeight+'px'}">
<view ref="headerRef">
<view style="flex:1">
<!--
@slot 顶部布局,可以自由布局
-->
<slot name="top"></slot>
</view>
<view :style="{height:_titleHeight+'px'}"></view>
</view>
<view :style="{height:_totalHeight+'px',position:'relative'}" v-if="_viewList.length>0">
<template v-for="(item,index) in _viewList" :key="index">
<view class="xIndexBarBoxItem" :style="{
height:_cellHeight+'px',
transform:`translateY(${(_cellHeight*(item.getNumber('oldIndex')!))}px)`
}">
<!--
@slot 动态插槽
@prop {UTSJSONObject} current - 当前的项目信息,可通过此布局当前数据
@prop {number} currentIndex - 右边导航当前的索引
@prop {number} index - 当前索引
-->
<slot :index="(item.getNumber('oldIndex')!)" :currentIndex="nowIndex" :current="item"></slot>
</view>
</template>
</view>
</scroll-view>
<view class="xIndexBarRightSlider" ref="xIndexBarRightSlider">
<view @touchmove="itemMove" @touchstart="itemStart" @touchend="itemEnd" ref="xIndexBarRightSliderBg"
class="xIndexBarRightSliderBg" :style="{background:_dotBgColor}">
<view @click.stop="sliderOnclik($event as UniPointerEvent,item,index)"
v-for="(item,index) in listIndexBar" :key="index" class="xIndexBarRightSliderItem"
:class="[currentIndexBYbar==index?'xIndexBarRightSliderItemHover':'']" :hover-start-time="50"
:hover-stay-time="100" hover-class="xIndexBarRightSliderItemHover" :style="{
backgroundColor:currentIndexBYbar==index?_dotActiveColor:_dotBgColor,
}">
<text :style="{color:currentIndexBYbar==index?'white':_dotColor}" class="xIndexBarRightSliderItemText">{{item.title}}</text>
</view>
</view>
</view>
<view android-layer-type="software" ref="xIndexBarDot" class="xIndexBarDot"
:style="{opacity:!showDot?'0':'1',backgroundColor:_dotActiveColor}">
<text v-if="(_list.length>0)" class="xIndexBarDotText">{{_listTotal[nowIndex]['index']}}</text>
</view>
</view>
</view>
</template>
<style scoped>
.xIndexBar{
display: flex;
flex-direction: column;
}
.xIndexBarBox {
position: relative;
flex: 1;
width:100%;
}
.xIndexBarBoxItem {
position: absolute;
width: 100%;
top: 0px;
}
.xIndexBarDot {
pointer-events: none;
position: absolute;
width: 50px;
height: 50px;
border-radius: 50px 50px 6px 50px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
/* transform: rotate(-45deg); */
/* transition-duration: 100ms;
transition-property: opacity;
transition-timing-function: linear; */
right: 47px;
top: 0;
/* #ifdef WEB */
box-szing: border-box;
/* #endif */
}
.xIndexBarDotText {
transform: rotate(45deg);
font-size: 18px;
color: white;
font-weight: bold;
}
.xIndexBarRightSliderItemHover {
/* background-color: rgba(0, 0, 0, 0.05); */
/* background-color:red; */
border-radius: 30px;
}
.xIndexBarHeaderTitleBox {
pointer-events: none;
}
.xIndexBarHeaderTitle {
height: 32px;
padding-left: 16px;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
/* #ifdef H5 */
position: fixed;
z-index: 5;
font-size: bold;
/* #endif */
}
.xIndexBarRightSlider {
width: 32px;
height: 100%;
position: absolute;
z-index: 8;
right: 5px;
top:0px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.xIndexBarRightSliderItem {
width: 16px;
height: 16px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 21px;
}
.xIndexBarRightSliderItemText {
font-size: 10px;
line-height: 11px;
/* font-weight: bold; */
}
.xIndexBarRightSliderBg {
border-radius: 21px;
/* box-shadow: 0 0px 10px rgba(0, 0, 0, 0.06); */
position: relative;
}
</style>
@@ -0,0 +1,872 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
const i18n = xConfig.i18n;
/**
*
* @name 输入框 xInputNumber
* @description 表单数字输入框,样式可定制化强,允许整数,小数限制,注意配合属性type和inputmodel来实现业务功能体验
* @page /pages/index/input-number
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xInputNumber"})
// 事件定义
const emits = defineEmits([
/**
* 点击整个输入框触发
*/
'click',
/**
* 清空时触发
*/
'clear',
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {number} value - 已输入的数字,如果是空值返回的是NaN
*/
'rightClick',
/**
* 输入法点了确认搜索按钮时触发
* @param {number} value - 已输入的字符串,如果是空值返回的是NaN
*/
'confirm',
/**
* 输入时触发
* @param {number} value - 当前已输入的字符串,如果是空值返回的是NaN
*/
'input',
/**
* 获得焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'focus',
/**
* 失去焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'blur',
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
*/
'keyboardheightchange',
'update:modelValue'
])
export type xInputPropsType = {
/**
* 自定义style
* 标签请写_style,不是-style,插件文档转换问题
*/
_style: string,
/**
* 输入框统一的聚集样式
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
* ['2px','solid','','']
* 全局的配置名称是:inputFocusBorder,可以全局设置.
*/
focusBorder: string[],
/**
* 占位的样式
*/
placeholderStyle: string,
/**
* 自定class
* 标签请写_class,不是-class,插件文档转换问题
*/
_class: string,
/**
* 输入框圆角
*/
round: string,
/**
* 是否显示清除图标
*/
showClear: boolean,
/**
* 右侧文本
*/
rightText: string,
/**
* 左侧文本
*/
leftText: string,
/**
* 双向绑定的输入值,如果是空值返回的是NaN
*/
modelValue: number,
/**
* 修饰符同vmodel.xxx='',比如v-model.trim=''
*/
// modelModifiers: UTSJSONObject,
/**
* 输入框提示语
*/
placeholder: string,
/**
* 左图标的颜色
* 默认空值取全局的主题色。
*/
iconColor: string,
/**
* 清除图标的颜色
*/
clearColor: string,
/**
* 输入框背景
*/
color: string,
/**
* 输入框暗黑背景,空值取全局的配置
* 提供会覆盖全局的配色。默认是透明
*/
darkBgColor: string,
/**
* 输入框的字体颜色
*/
fontColor: string,
/**
* 如果你提供,就会覆盖自动的反转配色。
* 默认是fontColor的反转颜色。
*/
darkFontColor: string,
/**
* 文字大小
*/
fontSize: string,
/**
* 左图标
*/
leftIcon: string,
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
name: string,
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
disabled: boolean,
/**
* 输入类型,数字仅限整数,小数或者整数
*/
type: "number" | "digit",
/**
* numeric:整数,配合type=number时,输入框只允许输入整数,手机会自动切换为整数数字键盘(不带小数点符号)
* decimal:小数,配合type=digit时,输入框允许输入小数或者整数,在手机键盘会自动切换为带小数点的键盘
*/
inputmode: "decimal" | "numeric",
/**
* 当type=digit时,可以控制小数点长度,默认1
*/
decimalLen:number,
/**
* 最大值
*/
max:number,
/**
* 最小值
*/
min:number,
/**
* 是否是密码类型
*/
password: boolean,
/**
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
*/
maxlength: number,
cursorSpacing: number,
cursorColor: string,
autoFocus: boolean,
focus: boolean,
confirmType: "send" | "search" | "next" | "go" | "done",
confirmHold: boolean,
cursor: number,
selectionStart: number,
selectionEnd: number,
adjustPosition: boolean,
/**
* 宽
*/
width: string,
/**
* 高
*/
height: string,
/**
* 自动删除首尾空格?
* 只会在失去焦点时删除.
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
*/
trim: boolean,
/**
* 文本对齐方式
*/
align: 'left' | 'right' | 'center',
/**
* type=textarea时生效
*/
autoHeight: boolean,
/**
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
*/
fixed: boolean,
/**
* 显示底部的注释说明及出错信息。
*/
showFooter: boolean,
/**
* 是否显示字符统计。
*/
showChartCount: boolean,
/**
* 格式就是正常的css格式
* 比如:8rpx 8rpx 0rpx 0rpx
*/
inputPadding: string,
/**
* focus时,点击页面的时候不收起键盘
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
*/
holdKeyboard: boolean
}
const props = withDefaults(defineProps<xInputPropsType>(), {
_style: "",
focusBorder: ():string[] => [] as string[],
placeholderStyle: "",
_class: "",
round: "",
showClear: false,
rightText: "",
leftText: "",
modelValue: NaN,
placeholder: "",//请输入
iconColor: "",
clearColor: "#bfbfbf",
color: "",
darkBgColor: "transparent",
fontColor: "#333333",
darkFontColor: "",
fontSize: "16",
leftIcon: "",
name: "",
disabled: false,
type: "number",
password: false,
maxlength: -1,
cursorSpacing: 0,
cursorColor: "",
autoFocus: false,
focus: false,
confirmType: "next",
confirmHold: false,
cursor: 0,
selectionStart: -1,
selectionEnd: -1,
adjustPosition: true,
width: "auto",
height: "44",
trim: true,
align: "left",
autoHeight: false,
fixed: false,
showFooter: false,
showChartCount: false,
inputPadding: "8px 12px",
inputmode: 'numeric',
decimalLen:1,
max:9999999999,
min:-999999999,
holdKeyboard: false
})
// 响应式数据
const nowValue = ref("")
const seePass = ref(false)
const isFocus = ref(false)
// 获取当前实例
const proxy = getCurrentInstance()?.proxy??null;
// 计算属性
const _focusBorder = computed(():string[] => {
let style = props.focusBorder.slice(0);
if(props.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
style = xConfig.inputFocusBorder.slice(0);
}
if(style.length <4){
return ['0px','solid','transparent'] as string[];
}
let oldcolor = style[2]
let hoverColor = getDefaultColor(style[3])
if(oldcolor==""){
oldcolor = props.color
}
if(hoverColor==""){
hoverColor = getDefaultColor(xConfig.color)
}
return [style[0],style[1],isFocus.value?hoverColor:oldcolor];
})
const _inputLen = computed(() : number => {
return nowValue.value.split("").length
})
const _maxlength = computed(() : number => {
return props.maxlength
})
const _showFooter = computed(() : boolean => {
return props.showFooter
})
const _holdKeyboard = computed(():boolean => {
return props.holdKeyboard
})
const _autoHeight = computed(() : boolean => {
return props.autoHeight
})
const _showChartCount = computed(():boolean => {
return props.showChartCount
})
const _fixed = computed(() : boolean => {
return props.fixed
})
const _width = computed(() : string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _height = computed(() : string => {
return checkIsCssUnit(props.height, xConfig.unit)
})
const _cstyle = computed(() : string => {
return props._style
})
const _placeholderStyle = computed(() : string => {
return props.placeholderStyle==''?xConfig.placeholderStyle:props.placeholderStyle
})
const _cclass = computed(() : string => {
return props._class
})
const _round = computed(() : string => {
if(props.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
return checkIsCssUnit(props.round, xConfig.unit)
})
const _fontSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _fontSizeUnScale = computed(() : string => {
return props.fontSize
})
const _showClear = computed(() : boolean => {
return props.showClear
})
const _rightText = computed(() : string => {
return props.rightText
})
const _leftText = computed(() : string => {
return props.leftText
})
const _confirmType = computed(() : string => {
return props.confirmType
})
const _placeholder = computed(() : string => {
if(props.placeholder=='') return i18n.t("tmui4x.input.placeholder")
return props.placeholder
})
const _iconColor = computed(() : string => {
if(props.iconColor==""){
return getDefaultColor(xConfig.color)
}
return getDefaultColor(props.iconColor)
})
const _color = computed(() : string => {
let color = getDefaultColor(props.color==''?xConfig.inputBgColor:props.color)
if (xConfig.dark == 'dark') {
if (props.darkBgColor == "") {
color = xConfig.inputDarkColor
} else {
color = getDefaultColor(props.darkBgColor)
}
}
return color
})
const _clearColor = computed(():string => {
if(props.clearColor=='') return _iconColor.value
return getDefaultColor(props.clearColor)
})
const _fontColor = computed(() : string => {
let color = getDefaultColor(props.fontColor)
if (xConfig.dark == 'dark') {
if (props.darkFontColor == "") {
color = "#ffffff"
} else {
color = getDefaultColor(props.darkFontColor)
}
}
return color
})
const _cursorColor = computed(():string => {
let color = props.cursorColor
if(props.cursorColor==''){
color = xConfig.color
}
return getDefaultColor(color)
})
const _leftIcon = computed(() : string => {
return props.leftIcon
})
const _disabled = computed(() : boolean => {
return props.disabled
})
const _password = computed(() : boolean => {
return props.password
})
const _autoFocus = computed(() : boolean => {
return props.autoFocus
})
const _focus = computed(() : boolean => {
return props.focus
})
const _adjustPosition = computed(() : boolean => {
return props.adjustPosition
})
const _selectionEnd = computed(() : number => {
return props.selectionEnd
})
const _selectionStart = computed(() : number => {
return props.selectionStart
})
// 方法函数
function getTrimAfterValue(value:string):string{
if(props.trim&& typeof value == 'string'){
return value.trim()
}
return value;
}
function clampValue(value : number) : number {
return Math.min(Math.max(value, props.min), props.max);
}
function getDien(str:string):string{
let v1 = str.split(".")
let result = v1[0]
if(v1.length==0){
result = '0.'
for(let i =0;i<props.decimalLen;i++){
result+='0'
}
}else if(v1.length==1){
result = result + "."
for(let i =0;i<props.decimalLen;i++){
result+='0'
}
}else{
result = result + "." + v1[1].substring(0,props.decimalLen)
}
return result
}
function translaterNumberToModel(val:string|number,isTranVal:boolean = true):number{
if(val==''||val==null) return 0;
let realVal = 0
if(typeof val == 'string'){
realVal = parseFloat(val)
}else if(typeof val == 'number'){
realVal = val
}
if(isNaN(realVal)) return 0;
if(isTranVal){
if(props.type == 'number'){
realVal = Math.floor(realVal)
}
}
return realVal;
}
//处理值
function translaterNumberToInputVal(val:string|number,isTranVal:boolean = true):string{
// #ifndef APP-ANDROID
if(val===''||val===null) return '';
// #endif
// #ifdef APP-ANDROID
if(val==''||val==null) return '';
// #endif
let realVal = ''
if(typeof val == 'string'){
realVal = val
}else if(typeof val == 'number'){
if(!isNaN(val)){
if(props.type == 'number'){
realVal = val.toString()
}else if(props.type == 'digit'){
realVal = val.toFixed(props.decimalLen)
}
}
}
// const tval = translaterNumberToModel(realVal,isTranVal)
// realVal = tval.toString()
return realVal;
}
function inputHndler(evt : UniInputEvent) {
const val = getTrimAfterValue(getDien(evt.detail.value))
let realVal = translaterNumberToInputVal(val)
let modelsnycn = translaterNumberToModel(realVal)
modelsnycn = clampValue(modelsnycn)
realVal = translaterNumberToInputVal(modelsnycn)
/**
* 输入时触发
* @param {string} value 当前已输入的字符串
*/
emits('input', modelsnycn)
// emits('update:modelValue', modelsnycn)
}
function confirm() {
/**
* 输入法点了确认搜索按钮时触发
* @param {string} value 已输入的字符串
*/
emits('confirm', translaterNumberToModel(nowValue.value))
}
function raightCellClick() {
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {string} value 已输入的字符串
*/
emits('rightClick', translaterNumberToModel(nowValue.value))
}
function clearHandler() {
nowValue.value = "";
/**
* 等同v-model
*/
emits('update:modelValue', NaN)
emits('clear', NaN)
}
function onFocus(evt : UniInputFocusEvent) {
/**
* 获取焦点时
* @param {UniInputFocusEvent} evt
*/
emits('focus',evt)
isFocus.value = true;
}
function onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt
*/
emits('keyboardheightchange', evt)
}
function onClick() {
/**
* 点击整个输入框触发
*/
emits('click')
}
type FindParentCall = (parent:VueComponent|null)=> VueComponent|null
let findParent:FindParentCall|null = null;
findParent = (parent:VueComponent|null):VueComponent|null=>{
if(parent == null) return null;
// #ifdef WEB||APP-IOS|| MP-WEIXIN
// @ts-ignore
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
// #endif
// #ifdef APP-HARMONY
if(parent.$parent?.$options?.name?.indexOf('xFormItem')>-1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
// @ts-ignore
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
// #endif
let parents = findParent!(parent.$parent)
// #ifdef WEB||APP-IOS || MP-WEIXIN
// @ts-ignore
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
// #endif
// #ifdef APP-HARMONY
if(parents?.$options?.name?.indexOf('xFormItem')>-1) return parents;
// #endif
// #ifdef APP-ANDROID
// @ts-ignore
if(parents instanceof XFormItemComponentPublicInstance) return parents;
// #endif
return null;
}
function valid(){
let pelement = findParent!(proxy);
if (pelement == null) return;
// @ts-ignore
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
// #ifndef APP-ANDROID
if (typeof parent?.validByblur != 'function') return
// #endif
parent.validByblur(nowValue.value)
}
// 监听器
watch(():number => props.modelValue, (newValue : number) => {
const val = translaterNumberToInputVal(isNaN(newValue)?'':newValue)
if (val == nowValue.value) return;
nowValue.value = val;
})
function onBlur(evt : UniInputBlurEvent) {
// 对内容进行首尾清空
const val = getTrimAfterValue(getDien(evt.detail.value))
let realVal = translaterNumberToInputVal(val)
let modelsnycn = translaterNumberToModel(realVal)
modelsnycn = clampValue(modelsnycn)
realVal = translaterNumberToInputVal(modelsnycn)
nowValue.value = realVal
/**
* 等同v-model
*/
emits('update:modelValue', realVal==''?NaN:modelsnycn)
/**
* 失去焦点时
* @param {InputBlurEvent} evt
*/
emits('blur',evt)
isFocus.value = false;
valid();
}
// 生命周期
onMounted(() => {
nowValue.value = translaterNumberToInputVal(isNaN(props.modelValue)?'':props.modelValue,false);
})
</script>
<template>
<view>
<view @click="onClick" class="xInput"
:style="{width:_width}">
<view class="xInputLeft">
<!--
@slot 左插槽
-->
<slot name="left">
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
style="padding-right: 12px;">{{_leftText}}</x-text>
</slot>
</view>
<view :class="[_cclass]" class="xInputCenter"
:style="[
{
borderRadius:_round,
backgroundColor:_color,
borderWidth:_focusBorder[0],
borderStyle:_focusBorder[1],
borderColor:_focusBorder[2]
},_cstyle]">
<!--
@slot 输入框内的左插槽
-->
<slot name="inputLeft"></slot>
<view v-if="_leftIcon" style="margin-left:12px;">
<x-icon :color="_iconColor" :name="_leftIcon"
:font-size="_fontSizeUnScale"></x-icon>
</view>
<input :inputmode="props.inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
@input="inputHndler" @confirm="confirm" @blur="onBlur"
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
v-model="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="props.type"
:disabled="_disabled" :password="!seePass&&_password" :maxlength="props.maxlength"
:cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
:confirmType="props.confirmType" :confirmHold="props.confirmHold" :cursor="props.cursor"
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
:fixed="_fixed" />
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
style="padding: 0 12px;">
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
</view>
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
</view>
<!--
@slot 输入框内右插槽
-->
<slot name="inputRight"></slot>
</view>
<view class="xInputRight">
<!--
@slot 右插槽
-->
<slot name="right">
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
class="xInputRightText">{{_rightText}}</x-text>
</slot>
</view>
</view>
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
<view>
<!--
@slot 底部提示插槽
-->
<slot v-if="_showFooter" name="footer"></slot>
</view>
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
{{_inputLen}}/{{_maxlength}}
</text>
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
字符数:{{_inputLen}}
</text>
</view>
</view>
</template>
<style scoped>
.xInputFooter {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 8rpx;
}
.xInputMaxLen {
color: #888;
font-size: 12px;
text-align: right;
}
.xInputCenterInput {
flex: 1;
font-size: 16px;
/* padding: 16rpx 24rpx; */
height: 100%;
}
.xInputCenterInputArea {
line-height: 1.6;
/* #ifdef WEB || MP-WEIXIN */
box-sizing: border-box;
/* #endif */
}
.xInput {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xInputCenter {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
flex: 1;
}
.xInputLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
}
.xInputRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
height: 100%;
}
.xInputRightText {
padding-left: 12px;
font-size: 16px;
}
</style>
@@ -0,0 +1,366 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type POSITIONTYPE = "out" | "in"
/**
* @name 标签输入框 xInputTag
* @description 可通过键盘或者按钮,输入框输入字段回车保存标签词
* @page /pages/index/input-tag
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xInputTag"})
const emits = defineEmits<{
/**
* 标签变化时触发
* @param value - 当前的标签数组
*/
change: [value: string[]],
/**
* 等同v-model
* @param value - 当前的标签数组
*/
"update:modelValue": [value: string[]]
}>()
type xInputTagPropsType = {
/**
* 输入框背景及标签背景
*/
bgColor: string,
/**
* 输入框的暗黑背景色
* 空值读取全局的Input暗黑背景色
*/
darkBgColor: string,
/**
* 右边按钮主题色,空取全局主题色
*/
btnColor: string,
/**
* 文本大小
*/
fontSize: string,
/**
* 文本颜色,暗黑时取白
*/
fontColor: string,
/**
* 宽
*/
width: string,
/**
* 高
*/
height: string,
/**
* 圆角
*/
round: string,
/**
* 输入提示词,默认:请输入并回车
*/
placeholder: string,
/**
* 双向绑定
*/
modelValue: string[],
/**
* 标签在内还是在外
*/
postion: POSITIONTYPE,
/**
* postion为in时,可以控制隐藏按钮.
*/
showBtn: boolean,
/**
* 添加按钮的文本,默认:添加标签
*/
btnText: string,
/**
* 设置键盘右下角按钮的文字,仅在 type为text 时生效。
*/
confirmType: string,
/**
* 最佳输入标签数量,只有用户主动输入才会触发此限制
* 你代码赋值不会限制.-1表示不限制
*/
maxCount: number
}
const props = withDefaults(defineProps<xInputTagPropsType>(), {
bgColor: "#f5f5f5",
darkBgColor: "",
btnColor: "",
fontSize: "16",
fontColor: "#1d1d1f",
width: "auto",
height: "40",
round: "",
placeholder: "",
modelValue: (): string[] => [] as string[],
postion: "out",
showBtn: true,
btnText: "",
confirmType: "done",
maxCount: -1
})
// 响应式数据
const nowValue = ref<string[]>([])
const keyword = ref<string>("")
// 计算属性
const _postion = computed((): POSITIONTYPE => {
return props.postion
})
const _maxCount = computed((): number => {
return props.maxCount
})
const _bgColor = computed((): string => {
let color = getDefaultColor(props.bgColor)
if (xConfig.dark == 'dark') {
if (props.darkBgColor == "") {
color = xConfig.inputDarkColor
} else {
color = getDefaultColor(props.darkBgColor)
}
}
return color
})
const _intagBg = computed((): string => {
if (xConfig.dark == 'dark') return 'rgb(44,44,46)'
return "#fff"
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') return '#f5f5f7'
return getDefaultColor(props.fontColor)
})
const _btnColor = computed((): string => {
if (props.btnColor == "") return getDefaultColor(xConfig.color)
return getDefaultColor(props.btnColor)
})
const _round = computed((): string => {
if (props.round == "") return checkIsCssUnit(xConfig.buttonRadius, xConfig.unit)
return checkIsCssUnit(props.round, xConfig.unit)
})
const _fontSize = computed((): string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _width = computed((): string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _height = computed((): string => {
return checkIsCssUnit(props.height, xConfig.unit)
})
const _placeholder = computed((): string => {
if (props.placeholder == '') return xConfig.i18n.t("tmui4x.inputTag.placeholder")
return props.placeholder
})
const _btnText = computed((): string => {
if (props.btnText == '') return xConfig.i18n.t("tmui4x.inputTag.btnText")
return props.btnText
})
// 方法
function okConfirm(): void {
let word = keyword.value.trim()
if (word == "") {
// 不能为空
uni.showToast({ title: xConfig.i18n.t("tmui4x.inputTag.tips"), icon: 'none' })
return;
}
if (_maxCount.value > -1 && nowValue.value.length >= _maxCount.value) {
// 超过限制最大数
uni.showToast({ title: xConfig.i18n.t("tmui4x.inputTag.tips2", _maxCount.value), icon: 'none' })
return;
}
let isKey = nowValue.value.findIndex((el: string): boolean => el == word)
if (isKey == -1) {
if (_postion.value == 'out') {
nowValue.value.unshift(word)
} else {
nowValue.value.push(word)
}
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value)
/**
* 标签变化时触发
* @param value 当前的标签数组
*/
emits('change', nowValue.value)
}
keyword.value = ""
}
function del(index: number): void {
nowValue.value.splice(index, 1)
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value)
/**
* 标签变化时触发
* @param value 当前的标签数组
*/
emits('change', nowValue.value)
}
// 生命周期
onMounted((): void => {
nowValue.value = props.modelValue;
})
// 监听器
watch((): string[] => props.modelValue, (newValue: string[]) => {
if (newValue.join("") == nowValue.value.join("")) return;
nowValue.value = props.modelValue;
})
</script>
<template>
<view v-if="_postion == 'out'">
<view class="xInputTag" :style="{width:_width,height:_height,borderRadius:_round,backgroundColor:_bgColor,}">
<input :confirm-type="props.confirmType" v-model="keyword" @confirm="okConfirm" :placeholder="_placeholder" class="xInputTagWrap"
:style="{color:_fontColor,fontSize:_fontSize,height:_height}" type="text" />
<x-button v-if="props.showBtn" @click="okConfirm" :round="_round" :font-size="props.fontSize" :color="_btnColor" width="30%"
style="min-width:100px;max-width:190px" :height="_height">{{_btnText}}</x-button>
</view>
<view class="xInputTagGroup">
<!--
@slot 标签插槽,如果对标签样式不喜欢可通过此修改。
@prop {string[]} tags - 当前标签组件。
-->
<slot name="tag" :tags="nowValue">
<view v-for="(item,index) in nowValue" :key="index" class="xInputTagTag"
:style="{backgroundColor:_bgColor,borderRadius:_round}">
<view style="flex: 1;">
<x-text :font-size="props.fontSize" class="xInputTagText">{{item}}</x-text>
</view>
<view @click="del(index)" class="xInputTagClose">
<x-icon color="#d0d0d0" font-size="16" name="close-circle-fill"></x-icon>
</view>
</view>
</slot>
</view>
</view>
<view v-if="_postion == 'in'">
<view class="xInputTag xInputTagIntag" :style="{width:_width,minHeight:_height,borderRadius:_round,backgroundColor:_bgColor,}">
<!--
@slot 标签插槽,如果对标签样式不喜欢可通过此修改。
@prop {string[]} tags - 当前标签组件。
-->
<slot name="tag" :tags="nowValue">
<view v-for="(item,index) in nowValue" :key="index" class="xInputTagTag xInputTagTagItemIn"
:style="{backgroundColor:_intagBg,borderRadius:_round}">
<view style="flex: 1;">
<x-text :font-size="props.fontSize" class="xInputTagText">{{item}}</x-text>
</view>
<view @click="del(index)" class="xInputTagClose">
<x-icon color="#d0d0d0" font-size="16" name="close-circle-fill"></x-icon>
</view>
</view>
</slot>
<input :confirm-type="props.confirmType" :confirm-hold="true" v-model="keyword" @confirm="okConfirm" :placeholder="_placeholder" class="xInputTagWrap xInputTagWrapIn"
:style="{color:_fontColor,fontSize:_fontSize,height:_postion == 'in'?'32px':'24px',marginBottom:'8px'}" type="text" />
</view>
</view>
</template>
<style scoped>
.xInputTagIntag{
flex-wrap: wrap;
padding-top: 8px;
padding-left: 8px;
padding-right: 8px;
display: flex;
flex-direction: row;
/* justify-items: flex-start; */
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
}
.xInputTagClose {
padding-left: 5px;
}
.xInputTagText {
/* font-size: 28rpx; */
color: #333;
}
.xInputTagGroupBox {
position: relative;
}
.xInputTagTag {
margin-bottom: 12px;
margin-right: 6px;
padding: 3px 10px;
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.xInputTagTagItemIn{
margin-bottom: 8px;
}
.xInputTagGroup {
margin-top: 16px;
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
}
.xInputTag {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.xInputTagWrap {
padding: 0 12px;
flex: 1;
}
.xInputTagWrapIn{
min-width: 120px;
padding: 0px 0px;
}
</style>
@@ -0,0 +1,848 @@
<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
*
* @name 输入框 xInput
* @description 表单输入框,样式可定制化强
* @page /pages/index/input
* @category 表单组件
* @constant 平台兼容
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
*/
export default {
data() {
return {
nowValue: "",
seePass: false,
isFocus:false
}
},
emits: [
/**
* 点击整个输入框触发
*/
'click',
/**
* 清空时触发
*/
'clear',
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {string} value - 已输入的字符串
*/
'rightClick',
/**
* 输入法点了确认搜索按钮时触发
* @param {string} value - 已输入的字符串
*/
'confirm',
/**
* 输入时触发
* @param {string} value - 当前已输入的字符串
*/
'input',
/**
* 获得焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'focus',
/**
* 失去焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'blur',
/**
* 行高变化时,type=textarea时生效
* @param {UniTextareaLineChangeEvent} evt - 事件对象
*/
'linechange',
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
*/
'keyboardheightchange', 'update:modelValue'],
props: {
/**
* 自定义style
* 标签请写_style,不是-style,插件文档转换问题
*/
_style: {
type: String,
default: ""
},
/**
* 输入框统一的聚集样式
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
* ['2px','solid','','']
* 全局的配置名称是:inputFocusBorder,可以全局设置.
*/
focusBorder:{
type:Array as PropType<string[]>,
default:():string[] => [] as string[]
},
/**
* 占位的样式
*/
placeholderStyle: {
type: String,
default: ""
},
/**
* 自定class
* 标签请写_class,不是-class,插件文档转换问题
*/
_class: {
type: String,
default: ""
},
/**
* 输入框圆角
*/
round: {
type: String,
default: ""
},
/**
* 是否显示清除图标
*/
showClear: {
type: Boolean,
default: false
},
/**
* 右侧文本
*/
rightText: {
type: String,
default: ""
},
/**
* 左侧文本
*/
leftText: {
type: String,
default: ""
},
/**
* 双向绑定的输入值
*/
modelValue: {
type: String,
default: "",
},
/**
* 修饰符同vmodel.xxx='',比如v-model.trim=''
*/
// modelModifiers:{
// type: Object as PropType<UTSJSONObject>,
// default: ():UTSJSONObject => ({})
// },
/**
* 输入框提示语
*/
placeholder: {
type: String,
default: "请输入",
},
/**
* 左图标的颜色
* 默认空值取全局的主题色。
*/
iconColor: {
type: String,
default: "",
},
/**
* 清除图标的颜色
*/
clearColor: {
type: String,
default: "#bfbfbf",
},
/**
* 输入框背景
*/
color: {
type: String,
default: "",
},
/**
* 输入框暗黑背景,空值取全局的配置
* 提供会覆盖全局的配色。默认是透明
*/
darkBgColor: {
type: String,
default: "transparent",
},
/**
* 输入框的字体颜色
*/
fontColor: {
type: String,
default: "#333333",
},
/**
* 如果你提供,就会覆盖自动的反转配色。
* 默认是fontColor的反转颜色。
*/
darkFontColor: {
type: String,
default: "",
},
/**
* 文字大小
*/
fontSize: {
type: String,
default: "16",
},
/**
* 左图标
*/
leftIcon: {
type: String,
default: "",
},
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
name: {
type: String,
default: "",
},
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
disabled: {
type: Boolean,
default: false,
},
/**
* 类型
* "text"|"number"|"digit"|"tel"
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
* 不管你是number,还是digit或者tel是可以让用户按规范填写的只能是数字
* 但你定义modelValue类型时,只能是string,请特别注意。
*/
type: {
type: String as PropType<"text" | "number" | "digit" | "tel" | "textarea">,
default: "text",
},
/**
* 是否是密码类型
*/
password: {
type: Boolean,
default: false,
},
/**
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
*/
maxlength: {
type: Number,
default: -1
},
cursorSpacing: {
type: Number,
default: 0
},
cursorColor: {
type: String,
default: "",
},
autoFocus: {
type: Boolean,
default: false,
},
focus: {
type: Boolean,
default: false,
},
confirmType: {
type: String as PropType<"send" | "search" | "next" | "go" | "done">,
default: "next",
},
confirmHold: {
type: Boolean,
default: false,
},
cursor: {
type: Number,
default: 0
},
selectionStart: {
type: Number,
default: -1
},
selectionEnd: {
type: Number,
default: -1
},
adjustPosition: {
type: Boolean,
default: true,
},
/**
* 宽
*/
width: {
type: String,
default: "auto"
},
/**
* 高
*/
height: {
type: String,
default: "44"
},
/**
* 自动删除首尾空格?
* 只会在失去焦点时删除.
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
*/
trim: {
type: Boolean,
default: true,
},
/**
* 文本对齐方式
*/
align: {
type: String as PropType<'left' | 'right' | 'center'>,
default: "left"
},
/**
* type=textarea时生效
*/
autoHeight: {
type: Boolean,
default: false
},
/**
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
*/
fixed: {
type: Boolean,
default: false
},
/**
* 显示底部的注释说明及出错信息。
*/
showFooter: {
type: Boolean,
default: false
},
/**
* 是否显示字符统计。
*/
showChartCount:{
type: Boolean,
default: false
},
/**
* 格式就是正常的css格式
* 比如:8rpx 8rpx 0rpx 0rpx
*/
inputPadding: {
type: String,
default: "8px 12px"
},
inputmode:{
type:String,
default:'text'
},
holdKeyboard:{
type:Boolean,
default:false
}
},
computed: {
_focusBorder():string[]{
let style = this.focusBorder.slice(0);
if(this.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
style = xConfig.inputFocusBorder.slice(0);
}
if(style.length <4){
return ['0px','solid','transparent'] as string[];
}
let oldcolor = style[2]
let hoverColor = getDefaultColor(style[3])
if(oldcolor==""){
oldcolor = this._color
}
if(hoverColor==""){
hoverColor = getDefaultColor(xConfig.color)
}
return [style[0],style[1],this.isFocus?hoverColor:oldcolor];
},
_inputLen() : number {
return this.nowValue.split("").length
},
_maxlength() : number {
return this.maxlength
},
_showFooter() : boolean {
return this.showFooter
},
_holdKeyboard():boolean{
return this.holdKeyboard
},
_autoHeight() : boolean {
return this.autoHeight
},
_showChartCount():boolean{
return this.showChartCount
},
_fixed() : boolean {
return this.fixed
},
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_height() : string {
if (this.autoHeight&&this.type == 'textarea') return "auto"
return checkIsCssUnit(this.height, xConfig.unit)
},
_cstyle() : string {
return this._style
},
_placeholderStyle() : string {
return this.placeholderStyle==''?xConfig.placeholderStyle:this.placeholderStyle
},
_cclass() : string {
return this._class
},
_round() : string {
if(this.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
return checkIsCssUnit(this.round, xConfig.unit)
},
_fontSize() : string {
let fontSize = checkIsCssUnit(this.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
},
_fontSizeUnScale() : string {
return this.fontSize
},
_showClear() : boolean {
return this.showClear
},
_rightText() : string {
return this.rightText
},
_leftText() : string {
return this.leftText
},
_confirmType() : string {
return this.confirmType
},
_placeholder() : string {
return this.placeholder
},
_iconColor() : string {
if(this.iconColor==""){
return getDefaultColor(xConfig.color)
}
return getDefaultColor(this.iconColor)
},
_color() : string {
let color = getDefaultColor(this.color==''?xConfig.inputBgColor:this.color)
if (xConfig.dark == 'dark') {
if (this.darkBgColor == "") {
color = xConfig.inputDarkColor
} else {
color = getDefaultColor(this.darkBgColor)
}
}
return color
},
_clearColor():string{
if(this.clearColor=='') return this._iconColor
return getDefaultColor(this.clearColor)
},
_fontColor() : string {
let color = getDefaultColor(this.fontColor)
if (xConfig.dark == 'dark') {
if (this.darkFontColor == "") {
color = "#ffffff"
} else {
color = getDefaultColor(this.darkFontColor)
}
}
return color
},
_cursorColor():string{
let color = this.cursorColor
if(this.cursorColor==''){
color = xConfig.color
}
return getDefaultColor(color)
},
_leftIcon() : string {
return this.leftIcon
},
_disabled() : boolean {
return this.disabled
},
_password() : boolean {
return this.password
},
_autoFocus() : boolean {
return this.autoFocus
},
_focus() : boolean {
return this.focus
},
_adjustPosition() : boolean {
return this.adjustPosition
},
_selectionEnd() : number {
return this.selectionEnd
},
_selectionStart() : number {
return this.selectionStart
},
},
watch: {
modelValue(newValue : string) {
if (newValue == this.nowValue) return;
this.nowValue = newValue;
}
},
mounted() {
this.nowValue = this.modelValue;
},
methods: {
getTrimAfterValue(value:string):string{
if(this.trim) return value.trim()
return value;
},
confirm() {
/**
* 输入法点了确认搜索按钮时触发
* @param {string} value 已输入的字符串
*/
this.$emit('confirm', this.getTrimAfterValue(this.nowValue))
},
inputHndler(evt : UniInputEvent) {
this.nowValue = this.getTrimAfterValue(evt.detail.value)
/**
* 输入时触发
* @param {string} value 当前已输入的字符串
*/
this.$emit('input', this.nowValue)
/**
* 等同v-model
*/
this.$emit('update:modelValue', this.nowValue)
this.$forceUpdate()
},
raightCellClick() {
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {string} value 已输入的字符串
*/
this.$emit('rightClick', this.nowValue)
},
clearHandler() {
this.nowValue = "";
/**
* 等同v-model
*/
this.$emit('update:modelValue', "")
this.$emit('clear', "")
},
onBlur(evt : UniInputBlurEvent) {
// 对内容进行首尾清空
let newVal = this.getTrimAfterValue(this.nowValue)
if(newVal != this.nowValue){
this.nowValue = newVal
this.$emit('update:modelValue', this.nowValue)
}
/**
* 失去焦点时
* @param {InputBlurEvent} evt
*/
this.$emit('blur',evt)
this.isFocus =false;
this.valid();
},
onFocus(evt : UniInputFocusEvent) {
/**
* 获取焦点时
* @param {UniInputFocusEvent} evt
*/
this.$emit('focus',evt)
this.isFocus = true;
},
onAreaBlur(evt : UniTextareaBlurEvent) {
let newVal = this.getTrimAfterValue(this.nowValue)
if(newVal != this.nowValue){
this.nowValue = newVal
this.$emit('update:modelValue', this.nowValue)
}
/**
* 失去焦点时
* @param {InputBlurEvent} evt
*/
this.$emit('blur',evt)
this.isFocus =false;
this.valid();
},
onAreaFocus(evt : UniTextareaFocusEvent) {
/**
* 获取焦点时
* @param {UniInputFocusEvent} evt
*/
this.$emit('focus',evt)
this.isFocus = true;
},
onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt
*/
this.$emit('keyboardheightchange', evt)
},
onLinechange(evt : UniTextareaLineChangeEvent) {
/**
* 键盘高度变化时触发
* @param {UniTextareaLineChangeEvent} evt
*/
this.$emit('linechange', evt)
},
onClick() {
/**
* 点击整个输入框触发
*/
this.$emit('click')
},
// #ifdef MP-WEIXIN
inpuUnfocusChange(evt){
let value = this.getTrimAfterValue(evt.detail.value);
if(value!=this.modelValue&&value!=this.nowValue){
this.nowValue = value;
this.$emit('update:modelValue', value)
}
},
// #endif
valid(){
let pelement = this.findParent(this);
if (pelement == null) return;
// @ts-ignore
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
// #ifndef APP-ANDROID
if (typeof parent?.validByblur != 'function') return
// #endif
parent.validByblur(this.nowValue)
},
findParent(parent:VueComponent|null):VueComponent|null{
if(parent == null) return null;
// #ifdef WEB||APP-IOS|| MP-WEIXIN
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
// #endif
let parents = this.findParent(parent.$parent)
// #ifdef WEB||APP-IOS || MP-WEIXIN
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
// #endif
// #ifdef APP-ANDROID
if(parents instanceof XFormItemComponentPublicInstance) return parents;
// #endif
return null;
}
},
}
</script>
<template>
<view>
<view @click="onClick" class="xInput"
:style="{width:_width}">
<view class="xInputLeft">
<!--
@slot 左插槽
-->
<slot name="left">
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
style="padding-right: 12px;">{{_leftText}}</x-text>
</slot>
</view>
<view :class="[_cclass]" class="xInputCenter"
:style="[
{
borderRadius:_round,
backgroundColor:_color,
borderWidth:_focusBorder[0],
borderStyle:_focusBorder[1],
borderColor:_focusBorder[2]
},_cstyle]">
<!--
@slot 输入框内的左插槽
-->
<slot name="inputLeft"></slot>
<view v-if="_leftIcon" style="margin-left:12px;">
<x-icon :color="_iconColor" :name="_leftIcon"
:font-size="_fontSizeUnScale"></x-icon>
</view>
<input v-if="type!='textarea'" :inputmode="inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
<!-- #ifdef MP-WEIXIN -->
@change="inpuUnfocusChange"
<!-- #endif -->
:style="{color:_fontColor,fontSize:_fontSize,textAlign:align,padding:inputPadding,height:_height}"
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onBlur"
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
:value="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="type"
:disabled="_disabled" :password="!seePass&&_password" :maxlength="maxlength"
:cursorSpacing="cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
:confirmType="confirmType" :confirmHold="confirmHold" :cursor="cursor"
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
:fixed="_fixed" />
<textarea v-if="type=='textarea'" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
:style="{color:_fontColor,fontSize:_fontSize,textAlign:align,padding:inputPadding,height:_height}"
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onAreaBlur"
@keyboardheightchange="onkeyboardheightchange" @focus="onAreaFocus" :value="nowValue"
:placeholder="_placeholder" class="xInputCenterInput xInputCenterInputArea" :disabled="_disabled"
:maxlength="maxlength" :cursorSpacing="cursorSpacing" :cursor-color="_cursorColor"
:autoFocus="_autoFocus" :focus="_focus" :confirmHold="confirmHold" :cursor="cursor"
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
:fixed="_fixed" :autoHeight="_autoHeight"></textarea>
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
style="padding: 0 12px;">
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
</view>
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
</view>
<!--
@slot 输入框内右插槽
-->
<slot name="inputRight"></slot>
</view>
<view class="xInputRight">
<!--
@slot 右插槽
-->
<slot name="right">
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
class="xInputRightText">{{_rightText}}</x-text>
</slot>
</view>
</view>
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
<view>
<!--
@slot 底部提示插槽
-->
<slot v-if="_showFooter" name="footer"></slot>
</view>
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
{{_inputLen}}/{{_maxlength}}
</text>
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
字符数:{{_inputLen}}
</text>
</view>
</view>
</template>
<style scoped>
.xInputFooter {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 8rpx;
}
.xInputMaxLen {
color: #888;
font-size: 12px;
text-align: right;
}
.xInputCenterInput {
flex: 1;
font-size: 16px;
/* padding: 16rpx 24rpx; */
height: 100%;
}
.xInputCenterInputArea {
line-height: 1.6;
/* #ifdef WEB || MP-WEIXIN */
box-sizing: border-box;
/* #endif */
}
.xInput {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xInputCenter {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
flex: 1;
}
.xInputLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
}
.xInputRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
height: 100%;
}
.xInputRightText {
padding-left: 12px;
font-size: 16px;
}
</style>
@@ -0,0 +1,847 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
const i18n = xConfig.i18n;
/**
*
* @name 输入框 xInput
* @description 表单输入框,样式可定制化强
* @page /pages/index/input
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xInput"})
// 事件定义
const emits = defineEmits([
/**
* 点击整个输入框触发
*/
'click',
/**
* 清空时触发
*/
'clear',
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {string} value - 已输入的字符串
*/
'rightClick',
/**
* 输入法点了确认搜索按钮时触发
* @param {string} value - 已输入的字符串
*/
'confirm',
/**
* 输入时触发
* @param {string} value - 当前已输入的字符串
*/
'input',
/**
* 获得焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'focus',
/**
* 失去焦点时
* @param {UniInputBlurEvent} evt - 事件对象
*/
'blur',
/**
* 行高变化时,type=textarea时生效
* @param {UniTextareaLineChangeEvent} evt - 事件对象
*/
'linechange',
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
*/
'keyboardheightchange', 'update:modelValue'])
export type xInputPropsType = {
/**
* 自定义style
* 标签请写_style,不是-style,插件文档转换问题
*/
_style: string,
/**
* 输入框统一的聚集样式
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
* ['2px','solid','','']
* 全局的配置名称是:inputFocusBorder,可以全局设置.
*/
focusBorder: string[],
/**
* 占位的样式
*/
placeholderStyle: string,
/**
* 自定class
* 标签请写_class,不是-class,插件文档转换问题
*/
_class: string,
/**
* 输入框圆角
*/
round: string,
/**
* 是否显示清除图标
*/
showClear: boolean,
/**
* 右侧文本
*/
rightText: string,
/**
* 左侧文本
*/
leftText: string,
/**
* 双向绑定的输入值
*/
modelValue: string,
/**
* 修饰符同vmodel.xxx='',比如v-model.trim=''
*/
// modelModifiers: UTSJSONObject,
/**
* 输入框提示语
*/
placeholder: string,
/**
* 左图标的颜色
* 默认空值取全局的主题色。
*/
iconColor: string,
/**
* 清除图标的颜色
*/
clearColor: string,
/**
* 输入框背景
*/
color: string,
/**
* 输入框暗黑背景,空值取全局的配置
* 提供会覆盖全局的配色。默认是透明
*/
darkBgColor: string,
/**
* 输入框的字体颜色
*/
fontColor: string,
/**
* 如果你提供,就会覆盖自动的反转配色。
* 默认是fontColor的反转颜色。
*/
darkFontColor: string,
/**
* 文字大小
*/
fontSize: string,
/**
* 左图标
*/
leftIcon: string,
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
name: string,
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
*/
disabled: boolean,
/**
* 类型
* "text"|"number"|"digit"|"tel"
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
* 不管你是number,还是digit或者tel是可以让用户按规范填写的只能是数字
* 但你定义modelValue类型时,只能是string,请特别注意。
*/
type: "text" | "number" | "idcard" | "digit" | "tel" | "safe-password" | "nickname" | "textarea",
/**
* 是否是密码类型
*/
password: boolean,
/**
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
*/
maxlength: number,
cursorSpacing: number,
cursorColor: string,
autoFocus: boolean,
focus: boolean,
confirmType: "send" | "search" | "next" | "go" | "done",
confirmHold: boolean,
cursor: number,
selectionStart: number,
selectionEnd: number,
adjustPosition: boolean,
/**
* 宽
*/
width: string,
/**
* 高
*/
height: string,
/**
* 自动删除首尾空格?
* 只会在失去焦点时删除.
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
*/
trim: boolean,
/**
* 文本对齐方式
*/
align: 'left' | 'right' | 'center',
/**
* type=textarea时生效
*/
autoHeight: boolean,
/**
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
*/
fixed: boolean,
/**
* 显示底部的注释说明及出错信息。
*/
showFooter: boolean,
/**
* 是否显示字符统计。
*/
showChartCount: boolean,
/**
* 格式就是正常的css格式
* 比如:8rpx 8rpx 0rpx 0rpx
*/
inputPadding: string,
/**
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
*/
inputmode: "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url",
/**
* focus时,点击页面的时候不收起键盘
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
*/
holdKeyboard: boolean,
/**
* 是否作为可点击指示。会在右边显示图标指示可以点击的链接状态
*/
isLink:boolean;
rightIcon: string,
}
const props = withDefaults(defineProps<xInputPropsType>(), {
_style: "",
focusBorder: ():string[] => [] as string[],
placeholderStyle: "",
_class: "",
round: "",
showClear: false,
rightText: "",
leftText: "",
modelValue: "",
placeholder: "",//请输入
iconColor: "",
clearColor: "#bfbfbf",
color: "",
darkBgColor: "transparent",
fontColor: "#333333",
darkFontColor: "",
fontSize: "16",
leftIcon: "",
name: "",
disabled: false,
type: "text",
password: false,
maxlength: -1,
cursorSpacing: 0,
cursorColor: "",
autoFocus: false,
focus: false,
confirmType: "next",
confirmHold: false,
cursor: 0,
selectionStart: -1,
selectionEnd: -1,
adjustPosition: true,
width: "auto",
height: "44",
trim: true,
align: "left",
autoHeight: false,
fixed: false,
showFooter: false,
showChartCount: false,
inputPadding: "8px 12px",
inputmode: 'text',
holdKeyboard: false,
isLink:false,
rightIcon:""
})
// 响应式数据
const nowValue = ref("")
const seePass = ref(false)
const isFocus = ref(false)
// 获取当前实例
const proxy = getCurrentInstance()?.proxy??null;
// 计算属性
const _focusBorder = computed(():string[] => {
let style = props.focusBorder.slice(0);
if(props.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
style = xConfig.inputFocusBorder.slice(0);
}
if(style.length <4){
return ['0px','solid','transparent'] as string[];
}
let oldcolor = style[2]
let hoverColor = getDefaultColor(style[3])
if(oldcolor==""){
oldcolor = props.color
}
if(hoverColor==""){
hoverColor = getDefaultColor(xConfig.color)
}
return [style[0],style[1],isFocus.value?hoverColor:oldcolor];
})
const _inputLen = computed(() : number => {
return nowValue.value.split("").length
})
const _maxlength = computed(() : number => {
return props.maxlength
})
const _showFooter = computed(() : boolean => {
return props.showFooter
})
const _holdKeyboard = computed(():boolean => {
return props.holdKeyboard
})
const _autoHeight = computed(() : boolean => {
return props.autoHeight
})
const _isLink = computed(() : boolean => {
return props.isLink
})
const _rightIcon = computed(() : string => {
return props.rightIcon
})
const _showChartCount = computed(():boolean => {
return props.showChartCount
})
const _fixed = computed(() : boolean => {
return props.fixed
})
const _width = computed(() : string => {
return checkIsCssUnit(props.width, xConfig.unit)
})
const _height = computed(() : string => {
if (props.autoHeight&&props.type == 'textarea') return "auto"
return checkIsCssUnit(props.height, xConfig.unit)
})
const _cstyle = computed(() : string => {
return props._style
})
const _placeholderStyle = computed(() : string => {
return props.placeholderStyle==''?xConfig.placeholderStyle:props.placeholderStyle
})
const _cclass = computed(() : string => {
return props._class
})
const _round = computed(() : string => {
if(props.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
return checkIsCssUnit(props.round, xConfig.unit)
})
const _fontSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _fontSizeUnScale = computed(() : string => {
return props.fontSize
})
const _showClear = computed(() : boolean => {
return props.showClear
})
const _rightText = computed(() : string => {
return props.rightText
})
const _leftText = computed(() : string => {
return props.leftText
})
const _confirmType = computed(() : string => {
return props.confirmType
})
const _placeholder = computed(() : string => {
if(props.placeholder=='') return i18n!.t("tmui4x.input.placeholder")
return props.placeholder
})
const _iconColor = computed(() : string => {
if(props.iconColor==""){
return getDefaultColor(xConfig.color)
}
return getDefaultColor(props.iconColor)
})
const _color = computed(() : string => {
let color = getDefaultColor(props.color==''?xConfig.inputBgColor:props.color)
if (xConfig.dark == 'dark') {
if (props.darkBgColor == "") {
color = xConfig.inputDarkColor
} else {
color = getDefaultColor(props.darkBgColor)
}
}
return color
})
const _clearColor = computed(():string => {
if(props.clearColor=='') return _iconColor.value
return getDefaultColor(props.clearColor)
})
const _fontColor = computed(() : string => {
let color = getDefaultColor(props.fontColor)
if (xConfig.dark == 'dark') {
if (props.darkFontColor == "") {
color = "#ffffff"
} else {
color = getDefaultColor(props.darkFontColor)
}
}
return color
})
const _cursorColor = computed(():string => {
let color = props.cursorColor
if(props.cursorColor==''){
color = xConfig.color
}
return getDefaultColor(color)
})
const _leftIcon = computed(() : string => {
return props.leftIcon
})
const _disabled = computed(() : boolean => {
return props.disabled
})
const _password = computed(() : boolean => {
return props.password
})
const _autoFocus = computed(() : boolean => {
return props.autoFocus
})
const _focus = computed(() : boolean => {
return props.focus
})
const _adjustPosition = computed(() : boolean => {
return props.adjustPosition
})
const _selectionEnd = computed(() : number => {
return props.selectionEnd
})
const _selectionStart = computed(() : number => {
return props.selectionStart
})
// 方法函数
function getTrimAfterValue(value:string):string{
if(props.trim) return value.trim()
return value;
}
function confirm() {
/**
* 输入法点了确认搜索按钮时触发
* @param {string} value 已输入的字符串
*/
emits('confirm', getTrimAfterValue(nowValue.value))
}
function inputHndler(evt : UniInputEvent) {
nowValue.value = getTrimAfterValue(evt.detail.value)
/**
* 输入时触发
* @param {string} value 当前已输入的字符串
*/
emits('input', nowValue.value)
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value)
}
function raightCellClick() {
/**
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
* @param {string} value 已输入的字符串
*/
emits('rightClick', nowValue.value)
}
type FindParentCall = (parent:VueComponent|null)=> VueComponent|null
let findParent:FindParentCall|null = null;
findParent = (parent:VueComponent|null):VueComponent|null =>{
if(parent == null) return null;
// #ifdef WEB||APP-IOS|| MP-WEIXIN
// @ts-ignore
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
// #endif
// #ifdef APP-HARMONY
if(parent.$parent?.$options?.name?.indexOf('xFormItem')>-1) return parent.$parent;
// #endif
// #ifdef APP-ANDROID
// @ts-ignore
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
// #endif
let parents = findParent!(parent.$parent)
// #ifdef WEB||APP-IOS || MP-WEIXIN
// @ts-ignore
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
// #endif
// #ifdef APP-HARMONY
if(parents?.$options?.name?.indexOf('xFormItem')>-1) return parents;
// #endif
// #ifdef APP-ANDROID
// @ts-ignore
if(parents instanceof XFormItemComponentPublicInstance) return parents;
// #endif
return null;
}
function valid(){
let pelement = findParent!(proxy);
if (pelement == null) return;
// @ts-ignore
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
// #ifndef APP-ANDROID
if (typeof parent?.validByblur != 'function') return
// #endif
parent.validByblur(nowValue.value)
}
function clearHandler() {
nowValue.value = "";
/**
* 等同v-model
*/
emits('update:modelValue', "")
emits('clear', "")
}
function onBlur(evt : UniInputBlurEvent) {
// 对内容进行首尾清空
let newVal = getTrimAfterValue(nowValue.value)
if(newVal != nowValue.value){
nowValue.value = newVal
emits('update:modelValue', nowValue.value)
}
/**
* 失去焦点时
* @param {InputBlurEvent} evt
*/
emits('blur',evt)
isFocus.value = false;
valid();
}
function onFocus(evt : UniInputFocusEvent) {
/**
* 获取焦点时
* @param {UniInputFocusEvent} evt
*/
emits('focus',evt)
isFocus.value = true;
}
function onAreaBlur(evt : UniTextareaBlurEvent) {
let newVal = getTrimAfterValue(nowValue.value)
if(newVal != nowValue.value){
nowValue.value = newVal
emits('update:modelValue', nowValue.value)
}
/**
* 失去焦点时
* @param {InputBlurEvent} evt
*/
emits('blur',evt)
isFocus.value = false;
valid();
}
function onAreaFocus(evt : UniTextareaFocusEvent) {
/**
* 获取焦点时
* @param {UniInputFocusEvent} evt
*/
emits('focus',evt)
isFocus.value = true;
}
function onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
/**
* 键盘高度变化时触发
* @param {UniInputKeyboardHeightChangeEvent} evt
*/
emits('keyboardheightchange', evt)
}
function onLinechange(evt : UniTextareaLineChangeEvent) {
/**
* 键盘高度变化时触发
* @param {UniTextareaLineChangeEvent} evt
*/
emits('linechange', evt)
}
function onClick() {
/**
* 点击整个输入框触发
*/
emits('click')
}
// #ifdef MP-WEIXIN
function inpuUnfocusChange(evt){
let value = getTrimAfterValue(evt.detail.value);
if(value!=props.modelValue&&value!=nowValue.value){
nowValue.value = value;
emits('update:modelValue', value)
}
}
// #endif
// 监听器
watch(():string => props.modelValue, (newValue : string) => {
if (newValue == nowValue.value) return;
nowValue.value = newValue;
})
// 生命周期
onMounted(() => {
nowValue.value = props.modelValue;
})
</script>
<template>
<view>
<view @click="onClick" class="xInput"
:style="{width:_width}">
<view class="xInputLeft">
<!--
@slot 左插槽
-->
<slot name="left">
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
style="padding-right: 12px;">{{_leftText}}</x-text>
</slot>
</view>
<view :class="[_cclass]" class="xInputCenter"
:style="[
{
borderRadius:_round,
backgroundColor:_color,
borderWidth:_focusBorder[0],
borderStyle:_focusBorder[1],
borderColor:_focusBorder[2]
},_cstyle]">
<!--
@slot 输入框内的左插槽
-->
<slot name="inputLeft"></slot>
<view v-if="_leftIcon" style="margin-left:12px;">
<x-icon :color="_iconColor" :name="_leftIcon"
:font-size="_fontSizeUnScale"></x-icon>
</view>
<input v-if="props.type!='textarea'" :inputmode="props.inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
<!-- #ifdef MP-WEIXIN -->
@change="inpuUnfocusChange"
<!-- #endif -->
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onBlur"
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
:value="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="props.type"
:disabled="_disabled" :password="!seePass&&_password" :maxlength="props.maxlength"
:cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
:confirmType="props.confirmType" :confirmHold="props.confirmHold" :cursor="props.cursor"
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
:fixed="_fixed" />
<textarea v-if="props.type=='textarea'" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onAreaBlur"
@keyboardheightchange="onkeyboardheightchange" @focus="onAreaFocus" :value="nowValue"
:placeholder="_placeholder" class="xInputCenterInput xInputCenterInputArea" :disabled="_disabled"
:maxlength="props.maxlength" :cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor"
:autoFocus="_autoFocus" :focus="_focus" :confirmHold="props.confirmHold" :cursor="props.cursor"
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
:fixed="_fixed" :autoHeight="_autoHeight"></textarea>
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
style="padding: 0 12px;">
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
</view>
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
</view>
<!--
@slot 输入框内右插槽
-->
<slot name="inputRight"></slot>
<view v-if="_isLink||_rightIcon!=''" style="padding: 0 12px;">
<x-icon :color="_iconColor" name="arrow-right-s-line"></x-icon>
</view>
</view>
<view class="xInputRight">
<!--
@slot 右插槽
-->
<slot name="right">
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
class="xInputRightText">{{_rightText}}</x-text>
</slot>
</view>
</view>
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
<view>
<!--
@slot 底部提示插槽
-->
<slot v-if="_showFooter" name="footer"></slot>
</view>
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
{{_inputLen}}/{{_maxlength}}
</text>
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
字符数:{{_inputLen}}
</text>
</view>
</view>
</template>
<style scoped>
.xInputFooter {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 8rpx;
}
.xInputMaxLen {
color: #888;
font-size: 12px;
text-align: right;
}
.xInputCenterInput {
flex: 1;
font-size: 16px;
/* padding: 16rpx 24rpx; */
height: 100%;
}
.xInputCenterInputArea {
line-height: 1.6;
/* #ifdef WEB || MP-WEIXIN */
box-sizing: border-box;
/* #endif */
}
.xInput {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xInputCenter {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
flex: 1;
}
.xInputLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 100%;
}
.xInputRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
height: 100%;
}
.xInputRightText {
padding-left: 12px;
font-size: 16px;
}
</style>
@@ -0,0 +1,411 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
/**
* @name 车牌键盘 xKeyboardCar
* @description 功能齐全的车牌快速输入键盘。
* @page /pages/index/keyboard
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xKeyboardCar"})
const i18n = xConfig.i18n
defineSlots<{
default(props:{show:boolean}): any
}>()
const emits = defineEmits([
/**
* 值变化时触发
* @param {string} value - 当前值
*/
'change',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 确认时触发
* @param {string} value - 当前值
*/
'confirm',
/**
* 关闭取消时触发
* @param {string} value - 当前值
*/
'cancel',
'update:modelValue'
])
type xKeyboardCarPropsType = {
/**
* 当前输入的值
*/
modelValue: string,
/**
* 最大长度
*/
maxLen: number,
/**
* 当前打开的状态。
* 等同v-model:model-show
*/
modelShow: boolean,
/**
* 顶部标题,默认:安全键盘请放心输入
*/
title: string,
/**
* 主按钮色,空值取全局主题
*/
color: string,
/**
* 按钮背景
*/
btnColor: string,
/**
* 键盘背景
*/
bgColor: string,
/**
* 文字颜色
*/
fontColor: string,
/**
* 点击确认是否保持键盘不收起
*/
hold: boolean
}
const props = withDefaults(defineProps<xKeyboardCarPropsType>(), {
modelValue: "",
maxLen: 8,
modelShow: false,
title: "",
color: "",
btnColor: 'white',
bgColor: 'info',
fontColor: '#3b3b3b',
hold: false
})
// 响应式数据
const show = ref(false)
const nowValue = ref("")
const model = ref<'省份' | 'abc'>('省份')
const abcList = ref([
['京', '沪', '津', '渝', '鲁', '冀', '晋','蒙','辽','吉' ],
['黑', '苏', '浙', '皖', '闽','赣','豫', '湘', '鄂','粤'],
['桂', '琼', '川', '贵','云', '藏', '陕', '甘', '青', '宁'],
['新', '港', '澳', '台', '警', '使', '学', '教'],
['abc', 'del', '确认'],
])
const numList = ref([
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K'],
['L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V'],
['省份','W', 'X', 'Y', 'Z', 'del', '确认'],
])
const isShift = ref(false)
const tid = ref(0)
// 计算属性
const _hold = computed((): boolean => props.hold)
const _color = computed((): string => {
if (props.color == '') return getDefaultColor(xConfig.color)
return getDefaultColor(props.color)
})
const _btnColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(props.btnColor)
})
const _btnBorderColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return "#f5f5f5"
})
const _bgColor = computed((): string => {
return getDefaultColor(props.bgColor)
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') return "#ffffff"
return getDefaultColor(props.fontColor)
})
const _title = computed((): string => {
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
return props.title
})
const _carvalue = computed((): string[] => {
if(nowValue.value.length<=2){
return [nowValue.value.substring(0,2),'']
}
return [nowValue.value.substring(0,2),nowValue.value.substring(2)]
})
// 方法
function getFontSize(k: string): string {
return checkIsCssUnit(k, xConfig.unit)
}
function openShow(): void {
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', true)
}
function onCancel(): void {
/**
* 关闭取消时触发
*/
emits('cancel', nowValue.value);
}
function onClose(): void {
emits('update:modelShow', false)
}
function ok(): void {
/**
* 点击确认时触发
*/
emits('confirm', nowValue.value);
if(!_hold.value){
show.value = false;
emits('update:modelShow', false)
}
}
function del(): void {
if (nowValue.value.split('').length == 0) return;
let stp = nowValue.value.split('');
stp = stp.slice(0, stp.length - 1)
nowValue.value = stp.join("")
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
if(nowValue.value.length==0){
model.value = '省份'
}
}
function getFontColor(value: string): string {
if (value == "确认") return 'white'
if (value == 'del') return _color.value
if (value == 'shift' && isShift.value) return _color.value
return _fontColor.value
}
function itemClick(value: string): void {
let value_convaer = value
if (value == 'abc') {
model.value = 'abc'
return;
} else if (value == '省份') {
model.value = '省份'
return;
} else if (value == 'shift') {
isShift.value = !isShift.value
return;
} else if (value == 'del') {
del()
return;
} else if (value == '确认') {
ok()
return;
} else if (value == '空格') {
value_convaer = " "
}
if (isShift.value) {
value_convaer = value_convaer.toLocaleUpperCase()
}
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
if (isMaxvalu) {
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '个字符', icon: 'error' })
return;
}
nowValue.value = nowValue.value + value_convaer;
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
if(model.value=='省份'){
model.value = 'abc'
}
}
// 监听器
watch((): string => props.modelValue, (newvalue: string) => {
if (newvalue == nowValue.value) return;
nowValue.value = newvalue
})
watch((): boolean => props.modelShow, (newValue: boolean) => {
if (newValue == show.value) return;
show.value = newValue
})
// 生命周期
onMounted(() => {
nowValue.value = props.modelValue
if (props.modelShow) {
tid.value = setTimeout(function () {
show.value = props.modelShow
}, 200);
}
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@prop {boolean} show - 控制打开关闭状态
-->
<slot :show="show"></slot>
</view>
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
@cancel="onCancel" v-model:show="show" :show-close="true">
<template v-slot:title>
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<text v-if="nowValue.split('').length==0" :style="{fontSize:getFontSize('12'),color:_fontColor}">{{_title}}</text>
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor}">{{_carvalue[0]}}</text>
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor,margin: '0 8px'}">·</text>
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor}">{{_carvalue[1]}}</text>
</view>
</template>
<template v-slot:default>
<view v-if="model=='省份'" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in abcList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)"
v-for="(item2,index2) in item"
:key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover"
:style="{
backgroundColor:item2=='确认'?_color:_btnColor,
flex:item2=='空格'?'2':'1',
border:`1px solid ${_btnBorderColor}`
}">
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
class="xKeyboardText">
{{item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):(isShift?item2.toLocaleUpperCase():item2)}}
</text>
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
font-size="19"></x-icon>
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
font-size="19" color="white"></x-icon>
</view>
</view>
</view>
</view>
<view v-if="model=='abc'" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in numList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{
backgroundColor:item2=='确认'?_color:_btnColor,
flex:item2=='确认'||item2=='abc'?'2':'1',
border:`1px solid ${_btnBorderColor}`
}">
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
class="xKeyboardText">
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):item2)}}
</text>
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
font-size="19"></x-icon>
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
font-size="19" color="white"></x-icon>
</view>
</view>
</view>
</view>
<view style="height:12px"></view>
</template>
</x-drawer>
</template>
<style scoped>
.xKeyboardText {
font-weight: bold;
font-size: 16px;
}
.xKeyboardHover {
opacity: 0.6;
}
.xKeyboardLeftLine {
display: flex;
flex-direction: row;
}
.xKeyboardNumber {
display: flex;
flex-direction: row;
}
.xKeyboardLeft {
flex: 1;
}
.xKeyboardItem {
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemNoright {
margin-right: 0px;
}
.xKeyboardRight {
width: 70px;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,130 @@
export function validateIdCard(idCard: string):boolean {
let vcity = new Map<number,string>([
[11, "北京"], [12, "天津"], [13, "河北"], [14, "山西"], [15, "内蒙古"],
[21, "辽宁"], [22, "吉林"], [23, "黑龙江"], [31, "上海"], [32, "江苏"],
[33, "浙江"], [34, "安徽"], [35, "福建"], [36, "江西"], [37, "山东"], [41, "河南"],
[42, "湖北"], [43, "湖南"], [44, "广东"], [45, "广西"], [46, "海南"], [50, "重庆"],
[51, "四川"], [52, "贵州"], [53, "云南"], [54, "西藏"], [61, "陕西"], [62, "甘肃"],
[63, "青海"], [64, "宁夏"], [65, "新疆"], [71, "台湾"], [81, "香港"], [82, "澳门"], [91, "国外"]
]);;
//是否为空
if (idCard === '') {
return false;
}
//校验长度,类型
if (isCardNo(idCard) === false) {
return false;
}
//检查省份
if (checkProvince(idCard, vcity) === false) {
return false;
}
//校验生日
if (checkBirthday(idCard) === false) {
return false;
}
//检验位的检测
if (checkParity(idCard) === false) {
return false;
}
return true;
}
function isCardNo(card:string):boolean {
//身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X
let reg = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/;
if (reg.test(card) === false) {
return false;
}
return true;
}
function checkProvince(card:string, vcity:Map<number,string>):boolean {
let province = card.substring(0,2)
if (!vcity.has(parseInt(province))) {
return false;
}
return true;
};
function checkBirthday(card:string):boolean {
let len = card.length;
//身份证15位时,次序为省(3位)市(3位)年(2位)月(2位)日(2位)校验位(3位),皆为数字
if (len == 15) {
let re_fifteen = /^(\d{6})(\d{2})(\d{2})(\d{2})(\d{3})$/;
let arr_data = card.match(re_fifteen);
if(arr_data==null) return false
let year = arr_data[2];
let month = arr_data[3];
let day = arr_data[4];
if(year==null||month==null||day==null) return false
let birthday = new Date('19' + year + '/' + month + '/' + day);
return verifyBirthday(parseInt('19' + year), parseInt(month), parseInt(day), birthday);
}
//身份证18位时,次序为省(3位)市(3位)年(4位)月(2位)日(2位)校验位(4位),校验位末尾可能为X
if (len == 18) {
let re_eighteen = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X|x)$/;
let arr_data = card.match(re_eighteen);
if(arr_data==null) return false
let year = arr_data[2];
let month = arr_data[3];
let day = arr_data[4];
if(year==null||month==null||day==null) return false
let birthday = new Date(year + '/' + month + '/' + day);
return verifyBirthday(parseInt(year), parseInt(month), parseInt(day), birthday);
}
return false;
};
function verifyBirthday(year:number, month:number, day:number, birthday:Date):boolean {
let now = new Date();
let now_year = now.getFullYear();
//年月日是否合理
if (birthday.getFullYear() == year && (birthday.getMonth() + 1) == month && birthday.getDate() == day) {
//判断年份的范围(0岁到100岁之间)
let time = now_year - year;
if (time >= 0 && time <= 100) {
return true;
}
return false;
}
return false;
}
function checkParity(card:string):boolean {
//15位转18位
card = changeFivteenToEighteen(card);
let len = card.length;
if (len == 18) {
let arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
let arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
let cardTemp = 0
let i =0;
let valnum = '';
for (i = 0; i < 17; i++) {
cardTemp += parseInt(card.charAt(i)) * arrInt[i];
}
valnum = arrCh[cardTemp % 11];
if (valnum == card.substring(17).toLocaleUpperCase()) {
return true;
}
return false;
}
return false;
}
function changeFivteenToEighteen(card:string):string {
if (card.length == 15) {
let arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
let arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
let cardTemp = 0
let i =0;
card = card.substring(0, 6) + '19' + card.substring(6, card.length - 6);
for (i = 0; i < 17; i++) {
cardTemp += parseInt(card.substring(i, 1)) * arrInt[i];
}
card += arrCh[cardTemp % 11];
return card;
}
return card;
}
@@ -0,0 +1,356 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
import { validateIdCard } from "./idcard.uts"
/**
* @name 身份证键盘 xKeyboardIdcard
* @description 自动解析第二代身份证号,验证用户输入的是否正确,事件中能得到当前输入值及校验值。
* @page /pages/index/keyboard
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xKeyboardIdcard"})
const i18n = xConfig.i18n
defineSlots<{
default(props:{show:boolean}): any
}>()
const emits = defineEmits([
/**
* 值变化时触发
* @param {string} value - 当前值
*/
'change',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 确认时触发
* @param {string} value - 当前值
* @param {boolean} pass - 是否验证通过
*/
'confirm',
/**
* 关闭取消时触发
* @param {string} value - 当前值
*/
'cancel',
'update:modelValue'
])
type xKeyboardIdcardPropsType = {
/**
* 当前输入的值
*/
modelValue: string,
/**
* 最大长度
*/
maxLen: number,
/**
* 当前打开的状态。
* 等同v-model:model-show
*/
modelShow: boolean,
/**
* 顶部标题,默认:安全键盘请放心输入
*/
title: string,
/**
* 主按钮色,空值取全局主题
*/
color: string,
/**
* 按钮背景,暗黑时会取二级inputDarkbg灰
*/
btnColor: string,
/**
* 键盘背景
*/
bgColor: string,
/**
* 文字颜色,暗黑是会取白。
*/
fontColor: string,
/**
* 点击确认是否保持键盘不收起
*/
hold: boolean
}
const props = withDefaults(defineProps<xKeyboardIdcardPropsType>(), {
modelValue: "",
maxLen: 18,
modelShow: false,
title: "",
color: "",
btnColor: 'white',
bgColor: 'info',
fontColor: '#3b3b3b',
hold: false
})
// 响应式数据
const show = ref(false)
const nowValue = ref("")
const numbList = ref([
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
["00", "0", "x"]
])
const tid = ref(0)
// 计算属性
const _hold = computed((): boolean => props.hold)
const _color = computed((): string => {
if (props.color == '') return getDefaultColor(xConfig.color)
return getDefaultColor(props.color)
})
const _btnColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(props.btnColor)
})
const _bgColor = computed((): string => {
return getDefaultColor(props.bgColor)
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') return "#ffffff"
return getDefaultColor(props.fontColor)
})
const _title = computed((): string => {
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
return props.title
})
const _isPass = computed((): boolean => {
return validateIdCard(nowValue.value)
})
// 方法
function getFontSize(k: string): string {
return checkIsCssUnit(k, xConfig.unit)
}
function openShow(): void {
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', true)
}
function onCancel(): void {
/**
* 关闭取消时触发
*/
emits('cancel', nowValue.value);
}
function onClose(): void {
emits('update:modelShow', false)
}
function ok(): void {
/**
* 点击确认时触发
*/
emits('confirm', nowValue.value, validateIdCard(nowValue.value));
if(!_hold.value){
show.value = false;
emits('update:modelShow', false)
}
}
function del(): void {
if (nowValue.value.split('').length == 0) return;
let stp = nowValue.value.split('');
stp = stp.slice(0, stp.length - 1)
nowValue.value = stp.join("")
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
function itemClick(value: string): void {
let isDem = nowValue.value.lastIndexOf('.') > -1;
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
if (isMaxvalu) {
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '位数', icon: 'error' })
return;
}
if (isDem && value == 'x') return;
if ((nowValue.value.split('').length == 0 && value == '00') || (nowValue.value.split('').length == 0 && value == 'x')) return
nowValue.value = nowValue.value + value;
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
// 监听器
watch((): string => props.modelValue, (newvalue: string) => {
if (newvalue == nowValue.value) return;
nowValue.value = newvalue
})
watch((): boolean => props.modelShow, (newValue: boolean) => {
if (newValue == show.value) return;
show.value = newValue
})
// 生命周期
onMounted(() => {
nowValue.value = props.modelValue
if (props.modelShow) {
tid.value = setTimeout(function () {
show.value = props.modelShow
}, 200);
}
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@prop {boolean} show - 控制打开关闭状态
-->
<slot :show="show"></slot>
</view>
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
@cancel="onCancel" v-model:show="show" :show-close="true">
<template v-slot:title >
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<x-icon style="margin-right: 5px;" v-if="_isPass" name="checkbox-circle-fill" color="success"></x-icon>
<x-icon style="margin-right: 5px;" v-if="!_isPass&&nowValue.length>0" color="warn" name="error-warning-fill"></x-icon>
<text :style="{fontWeight:'bold',fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_isPass?'rgb(9, 204, 84)':_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
</view>
</template>
<template v-slot:default>
<view class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in numbList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
</view>
</view>
</view>
<view class="xKeyboardRight">
<view :style="{backgroundColor:_btnColor,height:'50px'}" @click="del" class="xKeyboardItemDel xKeyboardItemNoright"
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
</view>
<view @click="ok" :style="{backgroundColor:_color}" class="xKeyboardItem xKeyboardItemNoright"
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
<text style="color: white;font-size: 16px;">
<!-- 确认 -->
{{i18n.t('tmui4x.keyboard.confirm')}}
</text>
</view>
</view>
</view>
<view style="height:12px"></view>
</template>
</x-drawer>
</template>
<style scoped>
.xKeyboardText {
font-weight: bold;
font-size: 20px;
}
.xKeyboardHover {
opacity: 0.2;
}
.xKeyboardLeftLine {
display: flex;
flex-direction: row;
}
.xKeyboardNumber {
display: flex;
flex-direction: row;
}
.xKeyboardLeft {
flex: 1;
}
.xKeyboardItem {
flex: 1;
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemDel {
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemNoright {
margin-right: 0px;
}
.xKeyboardRight {
width: 140rpx;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,412 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
/**
* @name 数字键盘 xKeyboardNumber
* @description 数字键盘,如果你要密码键盘见x-keyboard
* @page /pages/index/keyboard
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xKeyboardNumber"})
const i18n = xConfig.i18n
defineSlots<{
default(props:{show:boolean}): any
}>()
const emits = defineEmits([
/**
* 值变化时触发
* @param {string} value - 当前值
*/
'change',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 确认时触发
* @param {string} value - 当前值
*/
'confirm',
/**
* 关闭取消时触发
* @param {string} value - 当前值
*/
'cancel',
'update:modelValue'
])
type xKeyboardNumberPropsType = {
/**
* 当前输入的值
*/
modelValue: string,
/**
* 最大长度
*/
maxLen: number,
/**
* 当前打开的状态。
* 等同v-model:model-show
*/
modelShow: boolean,
/**
* 顶部标题,默认:安全键盘请放心输入
*/
title: string,
/**
* 主按钮色,空值取全局主题
*/
color: string,
/**
* 按钮背景,暗黑时会取二级inputDarkbg灰
*/
btnColor: string,
/**
* 键盘背景
*/
bgColor: string,
/**
* 文字颜色,暗黑是会取白。
*/
fontColor: string,
/**
* 输入的最大值
* 默认0表示不限制。
*/
max: number,
/**
* 是否显示小数及00填充
*/
digit: boolean,
/**
* 模式
* number 数字键盘
* password 密码键盘,作为密码用,那就不会限制数字模式就当字符串使用。
*/
mode: string,
/**
* 点击确认是否保持键盘不收起
*/
hold: boolean
}
const props = withDefaults(defineProps<xKeyboardNumberPropsType>(), {
modelValue: "",
maxLen: 9,
modelShow: false,
title: "",
color: "",
btnColor: 'transparent',
bgColor: 'info',
fontColor: '#3b3b3b',
max: 0,
digit: true,
mode: "number",
hold: false
})
// 响应式数据
const show = ref(false)
const nowValue = ref("")
const numbList = ref([
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
["00", "0", "."]
])
const numbList2 = ref([
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9']
])
const tid = ref(0)
// 计算属性
const _hold = computed((): boolean => props.hold)
const _color = computed((): string => {
if (props.color == '') return getDefaultColor(xConfig.color)
return getDefaultColor(props.color)
})
const _btnColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(props.btnColor)
})
const _bgColor = computed((): string => {
return getDefaultColor(props.bgColor)
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') return "#ffffff"
return getDefaultColor(props.fontColor)
})
const _title = computed((): string => {
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
return props.title
})
const _max = computed((): number => {
return props.max;
})
// 方法
function getFontSize(k: string): string {
return checkIsCssUnit(k, xConfig.unit)
}
function openShow(): void {
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', true)
}
function onCancel(): void {
/**
* 关闭取消时触发
*/
emits('cancel', nowValue.value);
}
function onClose(): void {
emits('update:modelShow', false)
}
function ok(): void {
/**
* 点击确认时触发
*/
emits('confirm', nowValue.value);
if(!_hold.value){
show.value = false;
emits('update:modelShow', false)
}
}
function del(): void {
if (nowValue.value.split('').length == 0) return;
let stp = nowValue.value.split('');
stp = stp.slice(0, stp.length - 1)
nowValue.value = stp.join("")
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
function itemClick(value: string): void {
let isDem = nowValue.value.lastIndexOf('.') > -1;
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
if (isMaxvalu) {
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '位数', icon: 'error' })
return;
}
if(props.mode=='number'){
if (isDem && value == '.') return;
if ((nowValue.value.split('').length == 0 && value == '00') || (nowValue.value.split('').length == 0 && value == '.')) return
if (nowValue.value.substring(0, 1) == '0' && nowValue.value.split('').length == 1 && value != '.') return;
let totalvalue = parseFloat(nowValue.value + value);
if(totalvalue>_max.value&&_max.value!=0){
uni.showToast({ title: '应该小于' + _max.value.toString(), icon: 'error' })
return;
}
}
nowValue.value = nowValue.value + value;
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
// 监听器
watch((): string => props.modelValue, (newvalue: string) => {
if (newvalue == nowValue.value) return;
nowValue.value = newvalue
})
watch((): boolean => props.modelShow, (newValue: boolean) => {
if (newValue == show.value) return;
show.value = newValue
})
// 生命周期
onMounted(() => {
nowValue.value = props.modelValue
if (props.modelShow) {
tid.value = setTimeout(function () {
show.value = props.modelShow
}, 200);
}
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@prop {boolean} show - 控制打开关闭状态
-->
<slot :show="show"></slot>
</view>
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
@cancel="onCancel" v-model:show="show" :show-close="true">
<template v-slot:title>
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<text
:style="{fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
</view>
</template>
<template v-slot:default>
<view v-if="digit" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in numbList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
</view>
</view>
</view>
<view class="xKeyboardRight">
<view :style="{backgroundColor:_btnColor,height:'50px'}" @click="del" class="xKeyboardItemDel xKeyboardItemNoright"
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
</view>
<view @click="ok" :style="{backgroundColor:_color}" class="xKeyboardItem xKeyboardItemNoright"
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
<text style="color: white;font-size: 16px;">
<!-- 确认 -->
{{i18n.t('tmui4x.keyboard.confirm')}}
</text>
</view>
</view>
</view>
<view v-if="!digit" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in numbList2" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
</view>
</view>
<view class="xKeyboardLeftLine">
<view @click="itemClick('0')"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
<text :style="{color:_fontColor}" class="xKeyboardText">0</text>
</view>
<view @click="del"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
</view>
<view @click="ok" :style="{backgroundColor:_color,'margin-right': '5px'}" class="xKeyboardItem xKeyboardItemNoright"
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
<text style="color: white;font-size: 16px;">
<!-- 确认 -->
{{i18n.t('tmui4x.keyboard.confirm')}}
</text>
</view>
</view>
</view>
</view>
<view style="height:12px"></view>
</template>
</x-drawer>
</template>
<style scoped>
.xKeyboardText {
font-weight: bold;
font-size: 20px;
}
.xKeyboardHover {
opacity: 0.6;
}
.xKeyboardLeftLine {
display: flex;
flex-direction: row;
}
.xKeyboardNumber {
display: flex;
flex-direction: row;
}
.xKeyboardLeft {
flex: 1;
}
.xKeyboardItem {
flex: 1;
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemDel {
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemNoright {
margin-right: 0px;
}
.xKeyboardRight {
width: 140rpx;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,406 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
/**
* @name 密码键盘 xKeyboard
* @description 密码键盘,如果你只是要单纯的数字键盘见x-keyboard-number
* @page /pages/index/keyboard
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xKeyboard"})
const i18n = xConfig.i18n
defineSlots<{
default(props:{show:boolean}): any
}>()
const emits = defineEmits([
/**
* 值变化时触发
* @param {string} value - 当前值
*/
'change',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 确认时触发
* @param {string} value - 当前值
*/
'confirm',
/**
* 关闭取消时触发
* @param {string} value - 当前值
*/
'cancel',
'update:modelValue'
])
type xKeyboardPropsType = {
/**
* 当前输入的值
*/
modelValue: string,
/**
* 最大长度
*/
maxLen: number,
/**
* 当前打开的状态。
* 等同v-model:model-show
*/
modelShow: boolean,
/**
* 顶部标题,默认:安全键盘请放心输入
*/
title: string,
/**
* 主按钮色,空值取全局主题
*/
color: string,
/**
* 按钮背景
*/
btnColor: string,
/**
* 键盘背景
*/
bgColor: string,
/**
* 文字颜色
*/
fontColor: string,
/**
* 点击确认是否保持键盘不收起
*/
hold: boolean
}
const props = withDefaults(defineProps<xKeyboardPropsType>(), {
modelValue: "",
maxLen: 9,
modelShow: false,
title: "",
color: "",
btnColor: 'white',
bgColor: 'info',
fontColor: '#3b3b3b',
hold: false
})
// 响应式数据
const show = ref(false)
const nowValue = ref("")
const model = ref<'number' | 'abc'>('abc')
const abcList = ref([
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'del'],
['123', '空格', '确认'],
])
const numList = ref([
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['#', '/', ':', ';', '(', ')', '^', '*', '+'],
['-', '=', '\\', '|', '~', '$', '&', '.', ',', 'del'],
['abc', '%', '?', '!', '{', '}', '确认'],
])
const isShift = ref(false)
const tid = ref(0)
// 计算属性
const _hold = computed((): boolean => props.hold)
const _color = computed((): string => {
if (props.color == '') return getDefaultColor(xConfig.color)
return getDefaultColor(props.color)
})
const _btnColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(props.btnColor)
})
const _btnBorderColor = computed((): string => {
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
return "#f5f5f5"
})
const _bgColor = computed((): string => {
return getDefaultColor(props.bgColor)
})
const _fontColor = computed((): string => {
if (xConfig.dark == 'dark') return "#ffffff"
return getDefaultColor(props.fontColor)
})
const _title = computed((): string => {
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
return props.title
})
// 方法
function getFontSize(k: string): string {
return checkIsCssUnit(k, xConfig.unit)
}
function openShow(): void {
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emits('update:modelShow', true)
}
function onCancel(): void {
/**
* 关闭取消时触发
*/
emits('cancel', nowValue.value);
}
function onClose(): void {
emits('update:modelShow', false)
}
function ok(): void {
/**
* 点击确认时触发
*/
emits('confirm', nowValue.value);
if(!_hold.value){
show.value = false;
emits('update:modelShow', false)
}
}
function del(): void {
if (nowValue.value.split('').length == 0) return;
let stp = nowValue.value.split('');
stp = stp.slice(0, stp.length - 1)
nowValue.value = stp.join("")
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
function itemClick(value: string): void {
let value_convaer = value
if (value == 'abc') {
model.value = 'abc'
return;
} else if (value == '123') {
model.value = 'number'
return;
} else if (value == 'shift') {
isShift.value = !isShift.value
return;
} else if (value == 'del') {
del()
return;
} else if (value == '确认') {
ok()
return;
} else if (value == '空格') {
value_convaer = " "
}
if (isShift.value) {
value_convaer = value_convaer.toLocaleUpperCase()
}
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
if (isMaxvalu) {
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '个字符', icon: 'error' })
return;
}
nowValue.value = nowValue.value + value_convaer;
/**
* 等同v-model
*/
emits('update:modelValue', nowValue.value);
/**
* 值变化时触发
* @paramt {string} value
*/
emits('change', nowValue.value);
}
function getFontColor(value: string): string {
if (value == "确认") return 'white'
if (value == 'del') return _color.value
if (value == 'shift' && isShift.value) return _color.value
return _fontColor.value
}
// 监听器
watch((): string => props.modelValue, (newvalue: string) => {
if (newvalue == nowValue.value) return;
nowValue.value = newvalue
})
watch((): boolean => props.modelShow, (newValue: boolean) => {
if (newValue == show.value) return;
show.value = newValue
})
// 生命周期
onMounted(() => {
nowValue.value = props.modelValue
if (props.modelShow) {
tid.value = setTimeout(function () {
show.value = props.modelShow
}, 200);
}
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
})
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@prop {boolean} show - 控制打开关闭状态
-->
<slot :show="show"></slot>
</view>
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
@cancel="onCancel" v-model:show="show" :show-close="true">
<template v-slot:title>
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<text :style="{fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
</view>
</template>
<template v-slot:default>
<view v-if="model=='abc'" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in abcList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)"
v-for="(item2,index2) in item"
:key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover"
:style="{
backgroundColor:item2=='确认'?_color:_btnColor,
flex:item2=='空格'?'2':'1',
border:`1px solid ${_btnBorderColor}`
}">
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
class="xKeyboardText">
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):'')}}
{{(item2!='确认'&&item2!='空格'?(isShift?item2.toLocaleUpperCase():item2):'')}}
{{(item2=='空格'?i18n.t('tmui4x.keyboard.space'):'')}}
</text>
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
font-size="19"></x-icon>
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
font-size="19" ></x-icon>
</view>
</view>
</view>
</view>
<view v-if="model=='number'" class="xKeyboardNumber">
<view class="xKeyboardLeft">
<view v-for="(item,index) in numList" :key="index" class="xKeyboardLeftLine">
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
hover-class="xKeyboardHover" :style="{
backgroundColor:item2=='确认'?_color:_btnColor,
flex:item2=='确认'||item2=='abc'?'2':'1',
border:`1px solid ${_btnBorderColor}`
}">
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
class="xKeyboardText">
<!-- #ifdef WEB -->
{{(item2=='\\'?'\\\\':'')}}
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):'')}}
{{(item2!='确认'&&item2!='\\'?item2:'')}}
<!-- #endif -->
<!-- #ifdef APP || MP-WEIXIN -->
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):item2)}}
<!-- #endif -->
</text>
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
font-size="19"></x-icon>
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
font-size="19" ></x-icon>
</view>
</view>
</view>
</view>
<view style="height:12px"></view>
</template>
</x-drawer>
</template>
<style scoped>
.xKeyboardText {
font-weight: bold;
font-size: 16px;
}
.xKeyboardHover {
opacity: 0.6;
}
.xKeyboardLeftLine {
display: flex;
flex-direction: row;
}
.xKeyboardNumber {
display: flex;
flex-direction: row;
}
.xKeyboardLeft {
flex: 1;
}
.xKeyboardItem {
height: 50px;
/* background-color: white; */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 6px;
}
.xKeyboardItemNoright {
margin-right: 0px;
}
.xKeyboardRight {
width: 70px;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,127 @@
<script lang="ts" setup>
import { getDefaultColor,setBgColorLightByDark,isBlackAndWhite } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
/**
* @name 占位排版 xLayout
* @description 这是一个占位排版布局组件,需要配合x-layout-item组件实现占位排版
* 具体表现为:当组件在可视区域外时或者你指定延迟渲染或者你指定不渲染时,会以一个空的view来占位,内容不渲染.以此来达到排版不塌陷,同时又提高渲染速度.
* 场景:非常适配个人中心/首页,或者需要占位,渲染较多的情况下使用
* @page /pages/index/row
* @category 其它组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xLayout"})
const props = defineProps({
/**
* 默认是false自动判断要不要显示内容
* 如果你强制设置为true那内部不再判断,直接显示内容
* 并且不可动态更新,只有初始设置有效.
*/
show:{
type:Boolean,
default:false
},
/**
* 显示时,是否需要过渡.
*/
fade:{
type:Boolean,
default:true
},
customStyle:{
type:Object as PropType<UTSJSONObject>,
default:()=>{
return {} as UTSJSONObject
}
},
/**
* 占位时的背景
*/
color:{
type:String,
default:"transparent"
},
/**
* 暗黑时的占位背景.
*/
darkColor:{
type:String,
default:"transparent"
}
})
const xLayoutRef = ref<UniElement|null>(null)
const show = ref<boolean>(props.show)
const _customStyle = computed(():any=>props.customStyle)
const _isshowInit = ref(false)
let domTop = 0 as number|null
let winHeight = 0
let scrollTop = 0
const _bgColor = computed(():string=>{
let color = getDefaultColor(props.color)
if (xConfig.dark == 'dark' && props.darkColor != '') {
color = getDefaultColor(props.darkColor)
}
if (xConfig.dark == 'dark' && props.darkColor == '') {
if (isBlackAndWhite(color)) {
color = xConfig.sheetDarkColor;
} else {
color = setBgColorLightByDark(color)
}
}
return color;
})
const setDomShow = ()=>{
if(xLayoutRef.value==null||winHeight==0||domTop==null||props.show||show.value) return;
let dtop = domTop!;
if(dtop>0&&dtop<winHeight){
show.value = true
}
}
const onInitDom = ()=>{
if(xLayoutRef.value!=null){
let ele = xLayoutRef.value!;
ele.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
domTop= rect.top;
_isshowInit.value = true;
setDomShow();
})
}
}
// onResize(()=>{
// winHeight = uni.getWindowInfo().windowHeight
// onInitDom();
// })
// onPageScroll((evt)=>{
// if(!_isshowInit.value) return;
// scrollTop = evt.scrollTop;
// onInitDom()
// })
uni.$once("onReady",()=>{
winHeight = uni.getWindowInfo().windowHeight
// onInitDom();
// show.value = true;
})
</script>
<template>
<view ref="xLayoutRef" v-if="show"
:style="[{backgroundColor:_bgColor}]"
>
<view :style="_customStyle">
<slot></slot>
</view>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,216 @@
<template>
<view @click="onclick" class="xLink"
:style="{color:_color,fontSize:_fontSize,'text-decoration-line':props.line?'underline':'none'}">
<text v-if="_prefix!=''" :style="{
'font-family': 'remixicon',
'font-size':_fontSize,
'color':_color,
paddingRight:'5px'
}">{{_prefix}}</text>
<text
:style="{color:_color,fontSize:_fontSize}"
>
<!--
@slot 默认插槽,仅可放置文本
-->
<slot></slot>
</text>
<text v-if="_suffix!=''" :style="{
'font-family': 'remixicon',
'font-size':_fontSize,
'color':_color,
paddingLeft:'5px'
}">{{_suffix}}</text>
</view>
</template>
<script setup lang="ts">
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { PropType } from "vue"
import { xConfig } from "../../config/xConfig.uts"
import { NAVIGATE_TYPE } from "../../interface.uts"
import remixicon from "../x-icon/remixicon.uts"
import {openWeb} from "@/uni_modules/x-openweb"
/**
* @name 链接 xLink
* @page /pages/index/link
* @category 展示组件
* @description 链接可以打开指定的页面,也可以打开外链(打开外链依赖于x-openweb插件,加密用户请联系发你源码自行源码编译)
* 微信小程序无法打开外链,微信小程序正式版本pc版本可以打开外链,真机手机仅可打开应用内页面.
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xLink"})
const emits = defineEmits(
[
/**
* 点击事件
*/
'click'
])
const props = defineProps({
/**
* 需要打开的链接,可以是页面地址也可以是网页链接地址.
*/
href: {
type: String,
default: ""
},
/**
* 空值时取全局主题
*/
color: {
type: String,
default: ""
},
/**
* 字号,rpx,px,单位均可
*/
fontSize: {
type: String,
default: "15"
},
/**
* 是否需要下划线
*/
line: {
type: Boolean,
default: false
},
/**
* 打开方式,如果是网页链接将启动新的窗口打开.
*/
openType: {
type: String as PropType<NAVIGATE_TYPE>,
default: "navigate"
},
/**
* 前缀图标名称
*/
prefix: {
type: String,
default: "links-line"
},
/**
* 后缀图标名称
*/
suffix: {
type: String,
default: ""
}
})
const _color = computed(() : string => {
if (props.color == "") {
return getDefaultColor(xConfig.color)
}
return getDefaultColor(props.color)
})
const getIcon = (icon : string) : string => {
let texts = ""
try {
let code = ''
// #ifdef APP-ANDROID
code = remixicon[icon] as string;
let codePoint = Integer.parseInt(code, 16);
let charArray = Character.toChars(codePoint);
texts = new String(charArray);
// #endif
// #ifndef APP-ANDROID
code = remixicon[icon] as string;
texts = String.fromCharCode(parseInt(code, 16));
// #endif
} catch (e) {
console.error("xicon解析失败。", e)
}
return texts
}
const _fontSize = computed(() : string => {
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
if (xConfig.fontScale == 1) return fontSize;
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 14
}
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
})
const _prefix = computed(() : string => {
if (props.prefix == '') return ''
return getIcon(props.prefix)
})
const _suffix = computed(() : string => {
if (props.suffix == '') return ''
return getIcon(props.suffix)
})
const onclick = () => {
console.log('click')
emits('click')
if (props.href == '') return;
let isHttpRef = props.href.indexOf(':')
let href = props.href;
if (isHttpRef > -1) {
openWeb(href)
return;
}
switch (props.openType) {
case 'navigateBack':
uni.navigateBack({})
break;
case 'navigate':
uni.navigateTo({
url: href
})
break;
case 'reLaunch':
uni.reLaunch({
url: href
})
break;
case 'redirect':
uni.redirectTo({
url: href
})
break;
case 'switchTab':
uni.switchTab({
url: href
})
break;
default: {
uni.navigateTo({
url: href
} as NavigateToOptions)
break;
}
}
}
</script>
<style scoped>
.xLink {
display: flex;
flex-direction: row;
/* #ifdef WEB */
cursor: pointer;
/* #endif */
}
/* #ifdef WEB */
.xLink:hover {
opacity: 0.7;
}
/* #endif */
</style>
@@ -0,0 +1,113 @@
<script lang="ts" setup>
import { computed } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
/**
* @name 加载中 xLoading
* @description 加载中占位符,场景用到页面加载前。请在标签内写上你的文本, 如果不写,默认显示"加载中..."
* @page /pages/index/loading
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({name:"xLoading"})
const i18n = xConfig.i18n
defineSlots<{
default(): any
}>()
type xLoadingPropsType = {
/**
* 图标颜色
*/
color: string,
/**
* 文字颜色
*/
textColor: string,
/**
* 文字大小
*/
textSize: string,
/**
* 图标大小
*/
iconSize: string,
/**
* 是否垂直,默认是水平
*/
vertical: boolean,
/**
* 图标
*/
icon: string,
/**
* 隐藏加载文本插槽
*/
hideText: boolean,
/**
* 加载中的文本,加载中...
*/
label: string
}
const props = withDefaults(defineProps<xLoadingPropsType>(), {
color: "#8b8b8b",
textColor: "#8b8b8b",
textSize: "12",
iconSize: "21",
vertical: true,
icon: "loader-line",
hideText: false,
label: ""
})
// 计算属性
const _label = computed((): string => {
if(props.label == '') return i18n.t("tmui4x.xloading.label");
return props.label;
})
const _icon = computed((): string => {
return props.icon
})
const _hideText = computed((): boolean => {
return props.hideText
})
const _color = computed((): string => {
return getDefaultColor(props.color)
})
const _textColor = computed((): string => {
return getDefaultColor(props.textColor)
})
const _textSize = computed((): string => {
return checkIsCssUnit(props.textSize, xConfig.unit)
})
const _iconSize = computed((): string => {
return checkIsCssUnit(props.iconSize, xConfig.unit)
})
</script>
<template>
<view class="xLoading" :style="{'flex-direction':vertical?'column':'row'}">
<x-icon :font-size="_iconSize" :color="_color" :name="_icon" :spin="true"></x-icon>
<text v-if="!_hideText" :style="{'font-size': _textSize,'color': _textColor,marginLeft:vertical?'0px':'5px',marginTop:vertical?'8px':'0px',lineHeight:'1.1'}">
<!--
@slot 请在插槽内写上你的文本, 如果不写,默认显示"加载中..."
-->
<slot>{{_label}}</slot>
</text>
</view>
</template>
<style scoped>
.xLoading {
display: flex;
/* flex-direction: column; */
align-items: center;
justify-content: center;
}
</style>
@@ -0,0 +1,9 @@
/*!
Theme: Default
Description: Original highlight.js style
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/
License: see project LICENSE
Touched: 2021
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
Binary file not shown.
File diff suppressed because it is too large Load Diff

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