tn-fab.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template>
  2. <view class="tn-fab-class tn-fab" @touchmove.stop.prevent>
  3. <view
  4. class="tn-fab__box"
  5. :class="{'tn-fab--right': left === 'auto'}"
  6. :style="{
  7. left: $t.string.getLengthUnitValue(fabLeft || left),
  8. right: $t.string.getLengthUnitValue(fabRight || right),
  9. bottom: $t.string.getLengthUnitValue(fabBottom || bottom),
  10. zIndex: elZIndex
  11. }"
  12. >
  13. <view
  14. v-if="visibleSync"
  15. class="tn-fab__btns"
  16. :class="[`tn-fab__btns__animation--${animationType}`,
  17. showFab ? `tn-fab__btns--visible--${animationType}` : ''
  18. ]"
  19. >
  20. <view
  21. v-for="(item, index) in btnList"
  22. :key="index"
  23. class="tn-fab__item"
  24. :class="[
  25. `tn-fab__item__animation--${animationType}`,
  26. {'tn-fab__item--left': right === 'auto'}
  27. ]"
  28. :style="[fabItemStyle(index)]"
  29. @tap.stop="handleClick(index)"
  30. >
  31. <!-- 带图标或者图片时显示的文字信息 -->
  32. <view
  33. v-if="animationType !== 'around' && (item.imgUrl || item.icon)"
  34. :class="[left === 'auto' ? 'tn-fab__item__text--right' : 'tn-fab__item__text--left']"
  35. :style="{
  36. color: item.textColor || '#FFF',
  37. fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
  38. }"
  39. >{{ item.text || '' }}</view>
  40. <!-- 带图片或者图标时的图片、图标信息 -->
  41. <view
  42. class="tn-fab__item__btn"
  43. :class="[!item.bgColor ? backgroundColorClass : '']"
  44. :style="{
  45. width: $t.string.getLengthUnitValue(width),
  46. height: $t.string.getLengthUnitValue(height),
  47. lineHeight: $t.string.getLengthUnitValue(height),
  48. backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF',
  49. borderRadius: $t.string.getLengthUnitValue(radius)
  50. }"
  51. >
  52. <!-- 无图片和无图标时只显示文字 -->
  53. <view
  54. v-if="!item.imgUrl && !item.icon"
  55. class="tn-fab__item__btn__title"
  56. :style="{
  57. color: item.textColor || '#fff',
  58. fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
  59. }"
  60. >{{ item.text || '' }}</view>
  61. <!-- 图标 -->
  62. <view
  63. v-if="item.icon"
  64. class="tn-fab__item__btn__icon"
  65. :class="[`tn-icon-${item.icon}`]"
  66. :style="{
  67. color: item.iconColor || '#fff',
  68. fontSize: $t.string.getLengthUnitValue(item.iconSize || iconSize || 64)
  69. }"
  70. ></view>
  71. <!-- 图片 -->
  72. <image
  73. v-else-if="!item.icon && item.imgUrl"
  74. class="tn-fab__item__btn__image"
  75. :style="{
  76. width: $t.string.getLengthUnitValue(item.imgWidth || 64),
  77. height: $t.string.getLengthUnitValue(item.imgHeight || 64),
  78. }"
  79. :src="item.imgUrl"
  80. ></image>
  81. </view>
  82. </view>
  83. </view>
  84. <view
  85. class="tn-fab__item__btn tn-fab__item__btn--fab"
  86. :class="[backgroundColorClass, fontColorClass, {'tn-fab__item__btn--active': showFab}]"
  87. :style="{
  88. width: $t.string.getLengthUnitValue(width),
  89. height: $t.string.getLengthUnitValue(height),
  90. backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#01BEFF' : '',
  91. color: fontColorStyle || '#fff',
  92. borderRadius: $t.string.getLengthUnitValue(radius),
  93. zIndex: elZIndex - 1
  94. }"
  95. @tap.stop="fabClick"
  96. >
  97. <slot>
  98. <view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $t.string.getLengthUnitValue(iconSize || 64)}"></view>
  99. </slot>
  100. </view>
  101. </view>
  102. <view v-if="visibleSync && showMask" class="tn-fab__mask" :class="{'tn-fab__mask--visible': showFab}" :style="{zIndex: elZIndex - 3}" @tap="clickMask()"></view>
  103. </view>
  104. </template>
  105. <script>
  106. import componentsColorMixin from '../../libs/mixin/components_color.js'
  107. export default {
  108. name: 'tn-fab',
  109. mixins: [componentsColorMixin],
  110. props: {
  111. // 按钮列表
  112. // {
  113. // // 背景颜色
  114. // bgColor: '#fff',
  115. // // 图片地址
  116. // imgUrl: '',
  117. // // 图片宽度
  118. // imgWidth: 60,
  119. // // 图片高度
  120. // imgHeight: 60,
  121. // // 图标名称
  122. // icon: '',
  123. // // 图标尺寸
  124. // iconSize: 60,
  125. // // 图标颜色
  126. // iconColor: '#fff',
  127. // // 提示文字
  128. // text: '',
  129. // // 文字大小
  130. // textSize: 30,
  131. // // 字体颜色
  132. // textColor: '#fff'
  133. // }
  134. btnList: {
  135. type: Array,
  136. default() {
  137. return []
  138. }
  139. },
  140. // 自定义悬浮按钮内容
  141. customBtn: {
  142. type: Boolean,
  143. default: false
  144. },
  145. // 悬浮按钮的宽度
  146. width: {
  147. type: [String, Number],
  148. default: 88
  149. },
  150. // 悬浮按钮的高度
  151. height: {
  152. type: [String, Number],
  153. default: 88
  154. },
  155. // 图标大小
  156. iconSize: {
  157. type: [String, Number],
  158. default: 64
  159. },
  160. // 图标名称
  161. icon: {
  162. type: String,
  163. default: 'open'
  164. },
  165. // 按钮圆角
  166. radius: {
  167. type: [String, Number],
  168. default: '50%'
  169. },
  170. // 按钮距离左边的位置
  171. left: {
  172. type: [String, Number],
  173. default: 'auto'
  174. },
  175. // 按钮距离右边的位置
  176. right: {
  177. type: [String, Number],
  178. default: 'auto'
  179. },
  180. // 按钮距离底部的位置
  181. bottom: {
  182. type: [String, Number],
  183. default: 100
  184. },
  185. // 展示动画类型 up 往上展示 around 环绕
  186. animationType: {
  187. type: String,
  188. default: 'up'
  189. },
  190. // 当动画为圆环时,每个弹出按钮之间的距离, 单位px
  191. aroundBtnDistance: {
  192. type: Number,
  193. default: 10
  194. },
  195. zIndex: {
  196. type: Number,
  197. default: 0
  198. },
  199. // 显示遮罩
  200. showMask: {
  201. type: Boolean,
  202. default: true
  203. },
  204. // 点击遮罩是否可以关闭
  205. maskCloseable: {
  206. type: Boolean,
  207. default: true
  208. }
  209. },
  210. data() {
  211. return {
  212. showFab: false,
  213. visibleSync: false,
  214. timer: null,
  215. fabLeft: 0,
  216. fabRight: 0,
  217. fabBottom: 0,
  218. fabBtnInfo: {
  219. width: 0,
  220. height: 0,
  221. left: 0,
  222. right: 0,
  223. bottom: 0
  224. },
  225. systemInfo: {
  226. width: 0,
  227. height: 0
  228. },
  229. updateProps: false
  230. }
  231. },
  232. computed: {
  233. elZIndex() {
  234. return this.zIndex || this.$t.zIndex.fab
  235. },
  236. propsData() {
  237. return [this.width, this.height, this.left, this.right, this.bottom]
  238. },
  239. fabItemStyle() {
  240. return (index) => {
  241. let style = {
  242. zIndex: this.elZIndex - 2
  243. }
  244. if (this.animationType === 'up' || !this.showFab) {
  245. return style
  246. }
  247. let base = 1
  248. if (this.left === 'auto') {
  249. base = 1
  250. } else if (this.right === 'auto') {
  251. base = -1
  252. }
  253. style.transform = `rotate(${base * index * 60}deg) translateX(${(this.aroundBtnDistance + this.fabBtnInfo.width) * (-(base))}px)`
  254. return style
  255. }
  256. }
  257. },
  258. watch: {
  259. propsData() {
  260. // 更新按钮信息
  261. this.updateProps = true
  262. }
  263. },
  264. mounted() {
  265. this.$nextTick(() => {
  266. this.getFabBtnRectInfo()
  267. })
  268. },
  269. beforeDestroy() {
  270. if (this.timer) {
  271. clearTimeout(this.timer)
  272. }
  273. },
  274. methods: {
  275. // 按钮点击事件
  276. handleClick(index) {
  277. this.close()
  278. this.$emit('click', {index: index})
  279. },
  280. // 点击悬浮按钮
  281. fabClick() {
  282. if (this.showFab) {
  283. this.close()
  284. } else {
  285. // console.log(this.visibleSync);
  286. if (this.visibleSync) {
  287. this.visibleSync = false
  288. return
  289. }
  290. this.open()
  291. }
  292. },
  293. // 点击遮罩
  294. clickMask() {
  295. if (!this.showMask || !this.maskCloseable) return
  296. this.close()
  297. },
  298. open() {
  299. this.change('visibleSync', 'showFab', true)
  300. this.translateFabPosition()
  301. },
  302. close() {
  303. this.change('showFab', 'visibleSync', false)
  304. this.fabLeft = 0
  305. this.fabRight = 0
  306. this.fabBottom = 0
  307. },
  308. // 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
  309. // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
  310. change(param1, param2, status) {
  311. this[param1] = status
  312. if (status) {
  313. // #ifdef H5 || MP
  314. this.timer = setTimeout(() => {
  315. this[param2] = status
  316. this.$emit(status ? 'open' : 'close')
  317. clearTimeout(this.timer)
  318. }, 10)
  319. // #endif
  320. // #ifndef H5 || MP
  321. this.$nextTick(() => {
  322. this[param2] = status
  323. this.$emit(status ? 'open' : 'close')
  324. })
  325. // #endif
  326. } else {
  327. this.timer = setTimeout(() => {
  328. this[param2] = status
  329. this.$emit(status ? 'open' : 'close')
  330. clearTimeout(this.timer)
  331. }, 250)
  332. }
  333. },
  334. /******************** 旋转动画相关函数 ********************/
  335. // 获取按钮的信息
  336. async getFabBtnRectInfo() {
  337. const systemInfo = uni.getSystemInfoSync()
  338. const btnRectInfo = await this._tGetRect('.tn-fab__item__btn--fab')
  339. if (!btnRectInfo) {
  340. setTimeout(() => {
  341. this.getFabBtnRectInfo()
  342. }, 10)
  343. return
  344. }
  345. console.log(btnRectInfo);
  346. this.systemInfo = {
  347. width: systemInfo.windowWidth,
  348. height: systemInfo.windowHeight
  349. }
  350. this.fabBtnInfo.width = btnRectInfo.width
  351. this.fabBtnInfo.height = btnRectInfo.height
  352. this.fabBtnInfo.left = btnRectInfo.left
  353. this.fabBtnInfo.right = btnRectInfo.right
  354. this.fabBtnInfo.bottom = btnRectInfo.bottom
  355. },
  356. // 更新悬浮按钮的位置
  357. translateFabPosition() {
  358. if (this.updateProps) {
  359. this.getFabBtnRectInfo()
  360. this.updateProps = false
  361. }
  362. if (this.animationType === 'up') return
  363. // 按钮组的宽度
  364. const btnGroupWidth = this.fabBtnInfo.width + this.aroundBtnDistance + 10
  365. // 判断当前按钮是在左边还是右边
  366. if (this.left === 'auto' && btnGroupWidth > this.systemInfo.width - this.fabBtnInfo.right) {
  367. // 距离不够需要移动
  368. this.fabRight = btnGroupWidth + 'px'
  369. } else if (this.right === 'auto' && btnGroupWidth > this.fabBtnInfo.left) {
  370. this.fabLeft = btnGroupWidth + 'px'
  371. }
  372. if (btnGroupWidth > this.systemInfo.height - this.fabBtnInfo.bottom) {
  373. this.fabBottom = btnGroupWidth + 'px'
  374. }
  375. }
  376. }
  377. }
  378. </script>
  379. <style lang="scss" scoped>
  380. .tn-fab {
  381. &__box {
  382. display: flex;
  383. justify-content: center;
  384. align-items: flex-start;
  385. flex-direction: column;
  386. position: fixed;
  387. transition: all 0.25s ease-in-out;
  388. }
  389. &--right {
  390. align-items: flex-end;
  391. }
  392. &__btns {
  393. transition: all 0.25s cubic-bezier(0,.13,0,1.43);
  394. transform-origin: 80% bottom;
  395. &__animation--up {
  396. opacity: 0;
  397. transform: translateY(100%);
  398. }
  399. &__animation--around {
  400. position: absolute;
  401. top: 0;
  402. left: 0;
  403. }
  404. &--visible--up {
  405. // visibility: visible;
  406. opacity: 1;
  407. transform: translateY(0);
  408. }
  409. &--visible--around {
  410. // visibility: visible;
  411. // opacity: 1;
  412. }
  413. }
  414. &__item {
  415. display: flex;
  416. justify-content: flex-end;
  417. align-items: center;
  418. padding-bottom: 20rpx;
  419. &__animation--around {
  420. position: absolute;
  421. top: 0;
  422. left: 0;
  423. transition: transform 0.25s ease-in-out;
  424. transform-origin: 50% 50%;
  425. padding-bottom: 0 !important;
  426. }
  427. &--left {
  428. flex-flow: row-reverse;
  429. }
  430. &__text {
  431. &--left {
  432. padding-left: 14rpx;
  433. }
  434. &--right {
  435. padding-right: 14rpx;
  436. }
  437. }
  438. &__btn {
  439. display: flex;
  440. align-items: center;
  441. justify-content: center;
  442. box-shadow: 0 0 5rpx 2rpx rgba(0, 0, 0, 0.07);
  443. transition: all 0.2s linear;
  444. &--active {
  445. animation-name: fab-button-animation;
  446. animation-duration: 0.2s;
  447. animation-timing-function: cubic-bezier(0,.13,0,1.43);
  448. }
  449. &__title {
  450. width: 90%;
  451. text-align: center;
  452. white-space: nowrap;
  453. overflow: hidden;
  454. text-overflow: ellipsis;
  455. }
  456. &__icon {
  457. text-align: center;
  458. font-size: 64rpx;
  459. }
  460. &__image {
  461. display: block;
  462. }
  463. }
  464. }
  465. &__mask {
  466. position: fixed;
  467. top: 0;
  468. left: 0;
  469. right: 0;
  470. bottom: 0;
  471. background-color: $tn-mask-bg-color;
  472. transition: all 0.2s ease-in-out;
  473. opacity: 0;
  474. &--visible {
  475. opacity: 1;
  476. }
  477. }
  478. }
  479. @keyframes fab-button-animation {
  480. 0% {
  481. transform: scale(0.6);
  482. }
  483. // 20% {
  484. // transform: scale(1.8);
  485. // }
  486. // 40% {
  487. // transform: scale(0.4);
  488. // }
  489. // 50% {
  490. // transform: scale(1.4);
  491. // }
  492. // 80% {
  493. // transform: scale(0.8);
  494. // }
  495. 100% {
  496. transform: scale(1);
  497. }
  498. }
  499. </style>