tm-bottomnavigation.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <!-- 导航工具栏 -->
  2. <template>
  3. <view :style="{
  4. width: position != 'static' ? wininfo.windowWidth - offsetLeft * 2 + 'px' : 'auto',
  5. left: position != 'static' ? offsetLeft + 'px' : '0px',
  6. top: position == 'top' ? top_px + 'px' : 'auto',
  7. bottom: position == 'bottom' ? bottom_px + 'px' : 'auto'
  8. }" class="tm-bottomnavigation " :class="[
  9. black_tmeme ? 'grey-darken-5' : '',
  10. black_tmeme ? 'bk' : '',
  11. 'round-' + round,
  12. bgTheme,
  13. position,
  14. border === 'top' ? 'border-t-1' : '',
  15. border === 'bottom' ? 'border-b-1' : '',
  16. 'sheetIDX'
  17. ]">
  18. <view :style="{ background: bgColor }" class=" flex-between py-10">
  19. <block v-for="(item, index) in listDate" :key="index">
  20. <view :style="{
  21. width: listLen + '%'
  22. }" class="flex-center" :key="index" :class="[active_selecindex == index && ani == true ? ` ani ` : ``]">
  23. <tm-button :key="index" titl height="100%" :width="btnwidth"
  24. :iconSize="item['iconSize'] ? item['iconSize'] : 38"
  25. :fontSize="item['fontSize'] ? item['fontSize'] : 20" :vertical="vertical" :icon="active_selecindex==index?item.icon:(item['noIcon']||item.icon)"
  26. :label="item.value" block :theme="active_selecindex == index ? iconColor : iconColorGrey"
  27. :black="black_tmeme" @click.stop="onclick($event, index)" item-class="noGutter "
  28. :font-color="active_selecindex == index ? iconColor : iconColorGrey">
  29. <template v-slot:icon="{ data }">
  30. <!-- #ifdef MP -->
  31. <text v-if="false">{{offsetLeft}}</text>
  32. <!-- #endif -->
  33. <view style="height: 52rpx;">
  34. <view v-if="!item['custom']" >
  35. <view v-if="item.showDot" class="relative fulled" style="z-index: 10;">
  36. <tm-badges :color="item.dot['color']||iconColor" v-if="!item.dot['dot']&&!item.dot['icon']" :label="item.dot.num"
  37. :offset="[10, 0]" :dot="item.dot.dot"></tm-badges>
  38. <tm-badges :color="item.dot['color']||iconColor" v-if="item.dot['dot']&&!item.dot['icon']"></tm-badges>
  39. <tm-badges :color="item.dot['color']||iconColor" :offset="[10,0]" :icon="item.dot['icon']" v-if="item.dot['icon']"></tm-badges>
  40. </view>
  41. <tm-icons :black="black_tmeme"
  42. :color="active_selecindex == index ? iconColor : iconColorGrey" :size="data.size"
  43. :name="data.icon"></tm-icons>
  44. </view>
  45. <view v-if="item['custom'] === true"
  46. class="tm-bottomnavigation-custom absolute flex-center" :style="{
  47. width: '100%',
  48. height: '100%',
  49. minHeight: '100%',
  50. top: '-50%',
  51. left: '0',
  52. flexShrink: 0
  53. }">
  54. <view
  55. :class="[item['customColor'] ? item['customColor'] : (bgColor?'':'red'), black_tmeme ? 'bk' : '']"
  56. :style="{width: '90rpx',height: '90rpx',background:bgColor?bgColor:''}" class="rounded flex-center">
  57. <tm-icons :black="black_tmeme" :color="item['customFontColor'] || iconColorGrey"
  58. :size="data.size" :name="data.icon"></tm-icons>
  59. </view>
  60. </view>
  61. </view>
  62. </template>
  63. <template v-slot:default="{data}">
  64. <text :class="[active_selecindex == index ? 'ani' : '']">{{ data }}</text>
  65. </template>
  66. </tm-button>
  67. </view>
  68. </block>
  69. </view>
  70. <!-- 安全区域的高度。 -->
  71. <view v-if="safe" :style="{ height: safeBottomeHeight + 'px',background: bgColor }"></view>
  72. </view>
  73. <!--
  74. list:[{...}],基础属性为:{icon:"图标或者图片",value:"标题"}
  75. 全部属性为(除了上述基本两个属性,其它全为可选。):
  76. {
  77. icon: 'icon-user-fill',
  78. noIcon:'',//未选中时的图标,不提供默认使用相同的图标icon
  79. value: '个人中心',
  80. iconSize: 38,
  81. fontSize: 20,
  82. showDot:false,//是否显示角标。
  83. dot:{
  84. dot:false,//是否显示点。如果是点,则不显示Num
  85. num:"",//是否显示数字。
  86. ico:"",//是否显示图标,优先级高于dot,num.
  87. },
  88. path: ''
  89. }
  90. -->
  91. </template>
  92. <script>
  93. /**
  94. * 底部导航工具条
  95. * @property {Array} list = [] 默认:[],基本属性:{icon:"图标或者图片",value:"标题"},
  96. * @property {String} bg-theme = [] 默认:"white",背景颜色主题名称
  97. * @property {String | Boolean} black = [true|false] 默认:null,暗黑模式。
  98. * @property {String} bg-color = [] 默认:'',自定义背景颜色。
  99. * @property {String} icon-color = [] 默认:使用主题色,项目默认激活色。
  100. * @property {String} icon-color-grey = [] 默认:使用主题色,项目失去焦点时的颜色。
  101. * @property {String} position = [bottom|top|static] 默认:bottom,可选位置:bottom|top|static
  102. * @property {Number|String} top = [] 默认:0,距离顶部的距离:只有在position=='top'使用
  103. * @property {Number|String} bottom = [] 默认:0,距离底部的距离:只有在position=='bottom'使用
  104. * @property {String} border = [top|bottom] 默认:top,显示上边线还是下边线。可选top / bottom
  105. * @property {String|Boolean} vertical = [true|false] 默认:true,文字方向:默认是竖向。否则为横向。
  106. * @property {String|Number} offset-left = [] 默认:0,偏移量。即离左边的间距。如果提供,自动居中出现两边间隙。
  107. * @property {String|Boolean} safe = [true|false] 默认:true,// 是否开启底部安全区域。
  108. * @property {String|Boolean} auto-selected = [true|false] 默认:true,// 是否开启自动匹配页面选中底部按钮。
  109. * @property {Function} change 切换按钮时触发。
  110. *
  111. */
  112. import tmButton from "@/tm-vuetify/components/tm-button/tm-button.vue"
  113. import tmIcons from "@/tm-vuetify/components/tm-icons/tm-icons.vue"
  114. import tmBadges from "@/tm-vuetify/components/tm-badges/tm-badges.vue"
  115. export default {
  116. components: {
  117. tmButton,
  118. tmIcons,
  119. tmBadges
  120. },
  121. name: 'tm-bottomnavigation',
  122. props: {
  123. list: {
  124. Array,
  125. default: () => {
  126. return [];
  127. }
  128. },
  129. // 背景颜色主题名称
  130. bgTheme: {
  131. type: String,
  132. default: 'black'
  133. },
  134. // 是否启用暗黑主题。
  135. black: {
  136. type: String | Boolean,
  137. default: null
  138. },
  139. // 背景颜色,自定义。
  140. bgColor: {
  141. type: String,
  142. default: ''
  143. },
  144. // 自定义项目文字默认激活色。
  145. iconColor: {
  146. type: String,
  147. default: 'primary'
  148. },
  149. // 自定义项目文字默认失去焦点色。
  150. iconColorGrey: {
  151. type: String,
  152. default: 'grey-lighten-1'
  153. },
  154. // 可选bootom / top / static
  155. position: {
  156. type: String,
  157. default: 'bottom'
  158. },
  159. // 距离顶部的距离。默认是0,只有在position=='top'使用
  160. top: {
  161. type: Number | String,
  162. default: 0
  163. },
  164. // 距离顶部的距离。默认是0,只有在position=='bottom'使用
  165. bottom: {
  166. type: Number | String,
  167. default: 0
  168. },
  169. // 显示上边线还是下边线。可选top / bottom
  170. border: {
  171. type: String,
  172. default: 'top'
  173. },
  174. // 文字方向:默认是竖向。否则为横向。
  175. vertical: {
  176. type: String | Boolean,
  177. default: true
  178. },
  179. // 只支持圆角主题。如round-5
  180. round: {
  181. type: String | Number,
  182. default: 0
  183. },
  184. // 偏移量。即离左边的间距。如果提供,整个navbar的宽度会是100% - offsetLeft*2。
  185. offsetLeft: {
  186. type: String | Number,
  187. default: 0
  188. },
  189. // 是否开启底部安全区域。
  190. safe: {
  191. type: String | Boolean,
  192. default: true
  193. },
  194. // 是否开启点按动画,默认开启。
  195. ani: {
  196. type: String | Boolean,
  197. default: true
  198. },
  199. // 是否自动匹配页面选中底部按钮?
  200. autoSelected: {
  201. type: String | Boolean,
  202. default: true
  203. },
  204. activeIndex:{
  205. type:Number,
  206. default:0
  207. }
  208. },
  209. computed: {
  210. black_tmeme: function() {
  211. if (this.black !== null) return this.black;
  212. return this.$tm.vx.state().tmVuetify.black;
  213. },
  214. top_px: function() {
  215. return this.top;
  216. },
  217. bottom_px: function() {
  218. return this.bottom;
  219. },
  220. wininfo: function() {
  221. return uni.getSystemInfoSync();
  222. },
  223. listLen: function() {
  224. if (this.listDate.length == 0) return 1;
  225. return 100 / this.listDate.length;
  226. },
  227. active_selecindex:function(){
  228. if(!this.autoSelected) return this.slectedIndx;
  229. return this.$tm.vx.state().tmVuetify.tmVueTifly_pagesIndex;
  230. }
  231. },
  232. created() {
  233. uni.hideTabBar({animation:false})
  234. },
  235. data() {
  236. return {
  237. slectedIndx: -1, //默认激活的值。
  238. btnwidth: 0,
  239. safeBottomeHeight: 0,
  240. listDate: [],
  241. pages:[]
  242. };
  243. },
  244. watch:{
  245. list:{
  246. deep:true,
  247. handler(){
  248. let t = this;
  249. this.$nextTick(function() {
  250. let qr = uni.createSelectorQuery().in(this);
  251. qr.select('.sheetIDX')
  252. .boundingClientRect()
  253. .exec(e => {
  254. t.btnwidth = e[0].width / 5 + 'px';
  255. t.listItem();
  256. });
  257. });
  258. }
  259. },
  260. activeIndex:function(){
  261. if(!this.autoSelected) this.slectedIndx = this.activeIndex;
  262. }
  263. },
  264. mounted() {
  265. if(!this.autoSelected) this.slectedIndx = this.activeIndex;
  266. setTimeout(function(){
  267. uni.hideTabBar({animation:false})
  268. },350)
  269. let t = this;
  270. this.$nextTick(function() {
  271. uni.hideTabBar({animation:false})
  272. let qr = uni.createSelectorQuery().in(this);
  273. qr.select('.sheetIDX')
  274. .boundingClientRect()
  275. .exec(e => {
  276. t.btnwidth = e[0].width / 5 + 'px';
  277. t.listItem();
  278. });
  279. });
  280. },
  281. methods: {
  282. autoxz(){
  283. let sy = uni.getSystemInfoSync();
  284. // #ifdef MP
  285. this.safeBottomeHeight = sy.screenHeight - sy.safeArea.bottom;
  286. // #endif
  287. // #ifdef H5
  288. this.safeBottomeHeight = sy.windowHeight - sy.safeArea.height;
  289. // #endif
  290. // #ifdef APP
  291. this.safeBottomeHeight = Math.abs(sy.safeArea.bottom - sy.safeArea.height);
  292. // #endif
  293. if(!this.autoSelected){
  294. return;
  295. }
  296. let pageRoute = this.$tm.vx.state().tmVuetify.tmVueTifly_pages;
  297. let index = this.listDate.findIndex((el,index)=>{
  298. let url = el?.path||"";
  299. url = url.split('?')[0]||"";
  300. return url == pageRoute
  301. })
  302. if(index>-1){
  303. this.slectedIndx = index;
  304. this.$tm.vx.commit('setPageNowIndex',index)
  305. this.$emit('change', {
  306. index: index,
  307. item: this.listDate[index]
  308. });
  309. }
  310. },
  311. listItem() {
  312. let mod = {
  313. icon: 'icon-user-fill',
  314. value: '个人中心',
  315. iconSize: 38,
  316. fontSize: 20,
  317. showDot: false, //是否显示角标。
  318. dot: {
  319. dot: false, //是否显示点。如果是点,则不显示Num
  320. num: '', //是否显示数字。
  321. ico: '' //是否显示图标,优先级高于dot,num.
  322. },
  323. path: '',
  324. openType:'switchTab'
  325. };
  326. this.$nextTick(function() {
  327. let tm = [];
  328. let ls = this.$tm.deepClone(this.list);
  329. ls.forEach((item, index) => {
  330. tm.push({
  331. ...mod,
  332. ...item
  333. });
  334. });
  335. this.listDate = tm;
  336. this.autoxz();
  337. });
  338. },
  339. onclick(e, index) {
  340. let t = this;
  341. this.slectedIndx = index;
  342. this.$tm.vx.commit('setPageNowIndex',index)
  343. let item = this.listDate[index];
  344. this.$emit('change', {
  345. index: index,
  346. item: item
  347. });
  348. if (item?.path) {
  349. let oldPath = item['path']||"";
  350. oldPath = oldPath.split("?")[0]||"";
  351. let pages = getCurrentPages();
  352. let url = pages[pages.length-1].route;
  353. if(url[0]!='/') url = '/' + url;
  354. url = url.split('?')[0]||"";
  355. if(url==oldPath) return;
  356. // #ifdef MP || APP
  357. try{
  358. uni.vibrateShort()
  359. }catch(e){
  360. // ...
  361. }
  362. // #endif
  363. switch (item['openType']) {
  364. case 'switchTab':
  365. uni.switchTab({
  366. url: item.path,
  367. fail: (e) => {
  368. console.log(e);
  369. }
  370. })
  371. break;
  372. case 'redirect':
  373. uni.redirectTo({
  374. url: item.path
  375. })
  376. break;
  377. case 'reLaunch':
  378. uni.reLaunch({
  379. url: item.path
  380. })
  381. break;
  382. case 'navigateBack':
  383. uni.navigateBack({
  384. url: item.path
  385. })
  386. break;
  387. default:
  388. uni.navigateTo({
  389. url: item.path
  390. })
  391. break;
  392. }
  393. }
  394. }
  395. }
  396. };
  397. </script>
  398. <style>
  399. page,
  400. body {
  401. /* padding-bottom: 167rpx; */
  402. height: 100%;
  403. }
  404. </style>
  405. <style lang="scss" scoped>
  406. .tm-bottomnavigation {
  407. // animation: scalse 0.4s linear;
  408. &.bottom {
  409. position: fixed;
  410. bottom: 0;
  411. left: 0;
  412. z-index: 450;
  413. }
  414. &.top {
  415. position: fixed;
  416. z-index: 450;
  417. top: 0;
  418. left: 0;
  419. }
  420. }
  421. .ani {
  422. animation: scalse 0.4s linear;
  423. }
  424. @keyframes scalse {
  425. 0% {
  426. transform: scale(0.9);
  427. }
  428. 50% {
  429. transform: scale(1.1);
  430. }
  431. 100% {
  432. transform: scale(1);
  433. }
  434. }
  435. </style>