dynamic-demo-template.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. <template>
  2. <view class="dynamic-demo">
  3. <!-- 效果预览窗口 -->
  4. <view v-if="!noDemo" class="demo-container" :class="{'demo-container--full': full}">
  5. <view class="demo">
  6. <slot></slot>
  7. </view>
  8. <!-- 提示信息 -->
  9. <view v-if="haveTips">
  10. <view class="demo__tips__icon" @click="demoTipsClick">
  11. <view class="icon tn-icon-help"></view>
  12. </view>
  13. <view class="demo__tips__content"
  14. :class="[showContentTips ? 'demo__tips__content--show' : 'demo__tips__content--hide']">
  15. <view v-for="(item,index) in tipsData" :key="index" class="demo__tips__content--item">{{ item }}</view>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- 模式切换 -->
  20. <view v-if="multiMode" class="mode-switch">
  21. <view class="mode-switch__container">
  22. <view v-for="(item, index) in sectionModeListInfos" :key="index" class="mode-switch__item"
  23. :class="[`mode-switch-item-${index}`,{'mode-switch__item--active': modeIndex === index}]"
  24. @click="switchMode(index)">{{ item.name }}</view>
  25. <!-- 滑块样式 -->
  26. <view class="mode-switch__slider" :style="[modeSwitchSliderStyle]"></view>
  27. </view>
  28. </view>
  29. <!-- 组件对应可选项容器 -->
  30. <view class="section-container">
  31. <scroll-view
  32. class="section__scroll-view"
  33. :class="{'section__scroll-view--auto': sectionScrollViewStyle.height === 'auto'}"
  34. :style="[sectionScrollViewStyle]"
  35. :scroll-y="sectionScrollViewStyle.height !== 'auto'"
  36. >
  37. <block v-for="(item,index) in btnsList" :key="index">
  38. <view class="section__content" :class="{'section__content--visible': item.show}">
  39. <view class="section__content__title">
  40. <view class="section__content__title__left-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
  41. <view class="section__content__title--text tn-text-ellipsis" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]">{{ item.title }}</view>
  42. <view class="section__content__title__right-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
  43. </view>
  44. <view class="section__content__btns">
  45. <view v-for="(section_btn,section_index) in item.optional" :key="section_index"
  46. class="section__content__btns__item" :class="[`tn-main-gradient-${tuniaoColorList[index]}--light`]" @click="sectionBtnClick(index, section_index)">
  47. <view class="section__content__btns__item__bg"
  48. :class="[`tn-main-gradient-${tuniaoColorList[index]}`, {'section__content__btns__item__bg--active':sectionIndex[modeIndex][index]['value'] === section_index}]"></view>
  49. <view class="section__content__btns__item--text tn-text-ellipsis"
  50. :class="[sectionIndex[modeIndex][index]['value'] === section_index ? 'section__content__btns__item--text--active' : `tn-color-${tuniaoColorList[index]}`]">{{ section_btn }}</view>
  51. </view>
  52. </view>
  53. </view>
  54. </block>
  55. </scroll-view>
  56. </view>
  57. </view>
  58. </template>
  59. <script>
  60. export default {
  61. name: 'dynamic-demo-template',
  62. props: {
  63. // 可选项列表数据
  64. sectionList: {
  65. type: Array,
  66. default() {
  67. return []
  68. }
  69. },
  70. // 提示信息
  71. tips: {
  72. type: [String, Array],
  73. default: ''
  74. },
  75. // 演示框的内容是否为铺满
  76. full: {
  77. type: Boolean,
  78. default: false
  79. },
  80. // 是否使用了自定义顶部导航栏
  81. customBar: {
  82. type: Boolean,
  83. default: true
  84. },
  85. // 是否全屏滚动
  86. fullWindowsScroll: {
  87. type: Boolean,
  88. default: false
  89. },
  90. // 没有演示内容
  91. noDemo: {
  92. type: Boolean,
  93. default: false
  94. }
  95. },
  96. computed: {
  97. tipsData() {
  98. if (typeof this.tips === 'string') {
  99. return [this.tips]
  100. }
  101. return this.tips
  102. },
  103. haveTips() {
  104. return this.tips && this.tips.length > 0
  105. },
  106. multiMode() {
  107. return this.sectionList.length > 1
  108. },
  109. sectionModeList() {
  110. return this.sectionList.map((item) => {
  111. return item.name
  112. })
  113. }
  114. },
  115. data() {
  116. return {
  117. // 图鸟颜色列表
  118. tuniaoColorList: this.$t.color.getTuniaoColorList(),
  119. // 保存选项列表信息(由于prop中的数据时不能被修改的)
  120. _sectionList: [],
  121. // 模式列表信息
  122. sectionModeListInfos: [],
  123. // 所选模式的序号
  124. modeIndex: 0,
  125. // 模式选择滑块样式
  126. modeSwitchSliderStyle: {
  127. width: 0,
  128. left: 0
  129. },
  130. // 显示组件相关提示信息
  131. showContentTips: false,
  132. // 可选项滚动容器样式
  133. sectionScrollViewStyle: {
  134. height: 0
  135. },
  136. // 按钮列表信息
  137. btnsList: [],
  138. // 标记当前所选按钮
  139. sectionIndex: [],
  140. // 标记选项按钮是否可以滑动(使用scroll-view进行包裹)
  141. sectionScrollFlag: true
  142. }
  143. },
  144. watch: {
  145. sectionList: {
  146. handler(value) {
  147. // 如果sectionList发生改变,重新初始化选项列表信息
  148. this.initSectionBtns()
  149. },
  150. deep: true
  151. },
  152. sectionScrollFlag(value) {
  153. if (!value) {
  154. this.sectionScrollViewStyle.height = 'auto'
  155. }
  156. },
  157. fullWindowsScroll: {
  158. handler(value) {
  159. if (value) {
  160. this.sectionScrollViewStyle.height = 'auto'
  161. }
  162. },
  163. immediate: true
  164. }
  165. },
  166. created() {
  167. // 初始化可选项模式列表
  168. this.sectionModeListInfos = this.sectionModeList.map((item) => {
  169. return {
  170. name: item
  171. }
  172. })
  173. // 初始化选项按钮默认信息
  174. this.initSectionBtns()
  175. },
  176. mounted() {
  177. // 等待加载组件完成
  178. // setTimeout(() => {
  179. // // 计算出底部scroll-view的高度
  180. // this.initSectionScrollView()
  181. // if (this.multiMode) {
  182. // // 获取模式切换标签的信息
  183. // this.getModeTabsInfo()
  184. // }
  185. // }, 10)
  186. this.$nextTick(() => {
  187. // 计算出底部scroll-view的高度
  188. this.initSectionScrollView()
  189. if (this.multiMode) {
  190. // 获取模式切换标签的信息
  191. this.getModeTabsInfo()
  192. }
  193. })
  194. },
  195. methods: {
  196. // 初始化选项滑动窗口的高度
  197. initSectionScrollView() {
  198. // 全屏滚动时不进行任何的操作
  199. if (this.fullWindowsScroll) {
  200. return
  201. }
  202. // 获取屏幕的高度
  203. uni.getSystemInfo({
  204. success: (systemInfo) => {
  205. // 通过当前屏幕的安全高度减去上一个元素的底部和距离上一个元素的外边距,然后减获取到的值减去标题栏的高度即可
  206. const navBarHeight = this.customBar ? 0 : this.vuex_custom_bar_height
  207. if (this.multiMode) {
  208. uni.createSelectorQuery().in(this).select('.mode-switch').boundingClientRect(data => {
  209. if (data.bottom >= systemInfo.safeArea.height) {
  210. this.sectionScrollFlag = false
  211. } else {
  212. this.sectionScrollFlag = true
  213. const containerBaseHeight = systemInfo.safeArea.height - data.bottom
  214. this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
  215. }
  216. }).exec()
  217. } else {
  218. if (!this.noDemo) {
  219. uni.createSelectorQuery().in(this).select('.demo-container').boundingClientRect(data => {
  220. if (data.bottom >= systemInfo.safeArea.height) {
  221. this.sectionScrollFlag = false
  222. } else {
  223. this.sectionScrollFlag = true
  224. const containerBaseHeight = systemInfo.safeArea.height - data.bottom
  225. this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
  226. }
  227. }).exec()
  228. } else {
  229. this.sectionScrollFlag = false
  230. }
  231. }
  232. }
  233. })
  234. },
  235. // 更新选项滑动容器的高度
  236. updateSectionScrollView() {
  237. this.$nextTick(() => {
  238. this.initSectionScrollView()
  239. })
  240. },
  241. // 获取各个模式tab的节点信息
  242. getModeTabsInfo() {
  243. let view = uni.createSelectorQuery().in(this)
  244. for (let i = 0; i < this.sectionModeListInfos.length; i++) {
  245. view.select('.mode-switch-item-' + i).boundingClientRect()
  246. }
  247. view.exec(res => {
  248. // 如果没有获取到,则重新获取
  249. if (!res.length) {
  250. setTimeout(() => {
  251. this.getModeTabsInfo()
  252. }, 10)
  253. return
  254. }
  255. // 将每个模式的宽度放入list中
  256. res.map((item, index) => {
  257. this.sectionModeListInfos[index].width = item.width
  258. })
  259. // 初始化滑块的宽度
  260. this.modeSwitchSliderStyle.width = this.sectionModeListInfos[0].width + 'px'
  261. // 初始化滑块的位置
  262. this.modeSliderPosition()
  263. })
  264. },
  265. // 设置模式滑块的位置
  266. modeSliderPosition() {
  267. let left = 0
  268. // 计算当前所选模式选项到组件左边的距离
  269. this.sectionModeListInfos.map((item, index) => {
  270. if (index < this.modeIndex) left += item.width
  271. })
  272. this.modeSwitchSliderStyle.left = left + 'px'
  273. },
  274. // 切换模式
  275. switchMode(index) {
  276. // 不允许点击当前激活的选项
  277. if (index === this.modeIndex) return
  278. this.modeIndex = index
  279. this.modeSliderPosition()
  280. this.updateSectionBtns()
  281. this.$emit('modeClick', {
  282. index: index
  283. })
  284. },
  285. // 点击内容提示信息
  286. demoTipsClick() {
  287. this.showContentTips = !this.showContentTips
  288. },
  289. // 初始化被选中选项按钮
  290. initSectionBtns() {
  291. this.sectionIndex = []
  292. this.sectionIndex = this.sectionList.map((item) => {
  293. if (item.hasOwnProperty('section') && item.section.length > 0) {
  294. return Array(item.section.length).fill({
  295. value: 0,
  296. change: false
  297. })
  298. } else {
  299. return []
  300. }
  301. })
  302. this._sectionList = this.$t.deepClone(this.sectionList)
  303. // 给本地选项按钮列表给默认show属性
  304. this._sectionList.map((item) => {
  305. const section = item.section.map((section_item) => {
  306. if (!section_item.hasOwnProperty('show')) {
  307. section_item.show = true
  308. }
  309. return section_item
  310. })
  311. item.section = section
  312. return item
  313. })
  314. // 更新按钮信息
  315. this.updateSectionBtns()
  316. },
  317. // 跟新选项按钮信息
  318. updateSectionBtns(sectionIndex = -1, showState = true) {
  319. let sectionOptional = this._sectionList[this.modeIndex]['section']
  320. this.btnsList = sectionOptional.map((item, index) => {
  321. // 判断是否已经修改了对应的值
  322. let changeValue = this.sectionIndex[this.modeIndex][index]['change'] || false
  323. let currentSectionIndexValue = this.sectionIndex[this.modeIndex][index]['value'] || 0
  324. // 取出默认值(如果是已经修改过的选项,则使用之前的选项信息)
  325. let indexValue = changeValue ? currentSectionIndexValue : item.hasOwnProperty('current') ? item.current : 0
  326. // 取出是否显示当前选项
  327. let show = (sectionIndex !== -1 && sectionIndex === index) ? showState : item.hasOwnProperty('show') ? item.show : true
  328. // 处理最大最小值
  329. if (indexValue < 0) {
  330. indexValue = 0
  331. }
  332. if (indexValue >= item.optional.length) {
  333. indexValue = item.optional.length
  334. }
  335. // this.sectionIndex[this.modeIndex][index]['value'] = indexValue
  336. this.$set(this.sectionIndex[this.modeIndex], index, {value: indexValue, change: changeValue})
  337. item.show = show
  338. return item
  339. })
  340. },
  341. // 更新选项按钮状态信息
  342. updateSectionBtnsState(sectionIndex = -1, showState = true) {
  343. // 判断sectionIndex是否为数组
  344. if (this.$t.array.isArray(sectionIndex)) {
  345. if (sectionIndex.length === 0) {
  346. return
  347. }
  348. sectionIndex = sectionIndex.filter((item) => item >= 0 && item < this.sectionList[this.modeIndex]['section'].length)
  349. sectionIndex.map((item) => {
  350. this.btnsList[item]['show'] = showState
  351. this._sectionList[this.modeIndex]['section'][item]['show'] = showState
  352. })
  353. } else {
  354. if (sectionIndex < 0 || sectionIndex >= this.sectionList[this.modeIndex]['section'].length) {
  355. return
  356. }
  357. // 将按键的对应显示状态设置为对应的状态
  358. this.btnsList[sectionIndex]['show'] = showState
  359. this._sectionList[this.modeIndex]['section'][sectionIndex]['show'] = showState
  360. }
  361. },
  362. // 更新选项按钮选中信息
  363. updateSectionBtnsValue(modeIndex = 0, sectionIndex = -1, value = 0) {
  364. if (sectionIndex < 0 || sectionIndex >= this.sectionList[modeIndex]['section'].length) {
  365. return
  366. }
  367. // 如果showState为false则移除对应的选项按钮,否则往对应的位置添加上对应的选项按钮
  368. this.sectionIndex[modeIndex][sectionIndex] = {
  369. value,
  370. change: true
  371. }
  372. },
  373. // 选项按钮点击事件
  374. sectionBtnClick(index, sectionIndex) {
  375. // if (this.sectionIndex[this.modeIndex][index] === sectionIndex) {
  376. // return
  377. // }
  378. this.$set(this.sectionIndex[this.modeIndex], index, {value: sectionIndex, change: true})
  379. this.$emit('click', {
  380. methods: this.btnsList[index]['methods'],
  381. index: sectionIndex,
  382. name: this.btnsList[index]['optional'][sectionIndex]
  383. })
  384. }
  385. }
  386. }
  387. </script>
  388. <style lang="scss" scoped>
  389. .dynamic-demo {
  390. padding-top: 78rpx;
  391. /* 顶部模式切换start */
  392. .mode-switch {
  393. width: 100%;
  394. display: flex;
  395. align-items: center;
  396. justify-content: center;
  397. margin-top: 75rpx;
  398. padding: 0 30rpx;
  399. &__container {
  400. position: relative;
  401. display: flex;
  402. flex-direction: row;
  403. align-items: center;
  404. width: 476rpx;
  405. height: 62rpx;
  406. background-color: #FFFFFF;
  407. box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
  408. border-radius: 31rpx;
  409. }
  410. &__item {
  411. flex: 1;
  412. height: 62rpx;
  413. width: 100%;
  414. line-height: 62rpx;
  415. text-align: center;
  416. font-size: 28rpx;
  417. color: $tn-font-sub-color;
  418. z-index: 2;
  419. transition: all 0.3s;
  420. &--active {
  421. color: #FFFFFF;
  422. font-weight: bold;
  423. }
  424. }
  425. &__slider {
  426. position: absolute;
  427. height: 62rpx;
  428. border-radius: 31rpx;
  429. // background-image: linear-gradient(-86deg, #FF8359 0%, #FFDF40 100%);
  430. background-image: linear-gradient(-86deg, #00C3FF 0%, #58FFF5 100%);
  431. box-shadow: 1rpx 10rpx 24rpx 0rpx #00C3FF77;
  432. z-index: 1;
  433. transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  434. }
  435. }
  436. /* 顶部模式切换end */
  437. /* 演示内容展示start */
  438. .demo-container {
  439. min-height: 327rpx;
  440. width: calc(100% - 60rpx);
  441. background-color: #FFFFFF;
  442. box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
  443. margin: 0 30rpx 5rpx 30rpx;
  444. border-radius: 20rpx;
  445. position: relative;
  446. display: flex;
  447. justify-content: center;
  448. align-items: center;
  449. &--full {
  450. display: inline-block;
  451. padding-bottom: 20rpx;
  452. min-height: 0rpx;
  453. padding: 10rpx 20rpx 30rpx;
  454. }
  455. .demo {
  456. padding-top: 70rpx;
  457. &__tips {
  458. &__icon {
  459. position: absolute;
  460. top: 20rpx;
  461. right: 16rpx;
  462. width: 39rpx;
  463. height: 39rpx;
  464. line-height: 39rpx;
  465. font-size: 39rpx;
  466. .icon {
  467. background: linear-gradient(-45deg, #FF8359 0%, #FFDF40 100%);
  468. -webkit-background-clip: text;
  469. color: transparent;
  470. text-shadow: 0rpx 10rpx 10rpx rgba(255, 156, 82, 0.2);
  471. }
  472. }
  473. &__content {
  474. position: absolute;
  475. top: 65rpx;
  476. right: 16rpx;
  477. font-size: 20rpx;
  478. margin-left: 20rpx;
  479. word-wrap: normal;
  480. display: flex;
  481. flex-direction: column;
  482. background-color: #E6E6E6;
  483. padding: 20rpx;
  484. border-radius: 10rpx;
  485. transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1);
  486. transform-origin: 0 0;
  487. z-index: 999999;
  488. &--hide {
  489. transform: scaleY(0);
  490. }
  491. &--show {
  492. transform: scaleY(100%);
  493. &::after {
  494. content: "";
  495. width: 0px;
  496. height: 0px;
  497. border-width: 4px;
  498. border-style: solid;
  499. border-color: transparent transparent rgba(149, 149, 149, 0.1) transparent;
  500. position: absolute;
  501. top: -8px;
  502. right: 6px;
  503. }
  504. }
  505. }
  506. }
  507. }
  508. }
  509. /* 演示内容展示end */
  510. /* 可选项start */
  511. .section-container {
  512. width: 100%;
  513. height: auto;
  514. margin-top: 70rpx;
  515. .section {
  516. &__content {
  517. margin-top: 70rpx;
  518. display: none;
  519. &--visible {
  520. display: block;
  521. &:last-child {
  522. padding-bottom: calc(70rpx + env(safe-area-inset-bottom));
  523. }
  524. }
  525. &:nth-child(1) {
  526. margin-top: 0rpx;
  527. }
  528. &__title {
  529. display: flex;
  530. justify-content: center;
  531. align-items: center;
  532. margin: 0 30rpx;
  533. text-align: center;
  534. &__left-line,
  535. &__right-line {
  536. width: 100rpx;
  537. height: 2rpx;
  538. position: relative;
  539. }
  540. &__left-line {
  541. &::after {
  542. content: '';
  543. background: inherit;
  544. width: 12rpx;
  545. height: 12rpx;
  546. position: absolute;
  547. top: -12rpx;
  548. right: 0rpx;
  549. border-radius: 50%;
  550. transform: translateY(50%);
  551. }
  552. }
  553. &__right-line {
  554. &::after {
  555. content: '';
  556. background: inherit;
  557. width: 12rpx;
  558. height: 12rpx;
  559. position: absolute;
  560. top: -12rpx;
  561. left: 0rpx;
  562. border-radius: 50%;
  563. transform: translateY(50%);
  564. }
  565. }
  566. &--text {
  567. -webkit-background-clip: text;
  568. color: transparent;
  569. min-width: 124rpx;
  570. height: 30rpx;
  571. font-size: 32rpx;
  572. line-height: 1;
  573. margin: 0 35rpx;
  574. }
  575. }
  576. &__btns {
  577. width: calc(100% - 60rpx);
  578. margin: 0 30rpx;
  579. margin-top: 29rpx;
  580. padding: 50rpx 30rpx 0rpx 0rpx;
  581. background-color: #FFFFFF;
  582. box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
  583. border-radius: 20rpx;
  584. display: flex;
  585. flex-direction: row;
  586. align-items: center;
  587. justify-content: flex-start;
  588. flex-wrap: wrap;
  589. &__item {
  590. max-width: 30%;
  591. padding: 17rpx 36rpx;
  592. border-radius: 10rpx;
  593. margin-bottom: 40rpx;
  594. margin-left: 40rpx;
  595. position: relative;
  596. z-index: 1;
  597. // &::before {
  598. // content: " ";
  599. // position: absolute;
  600. // top: 10rpx;
  601. // left: 1rpx;
  602. // width: 100%;
  603. // height: 100%;
  604. // background: inherit;
  605. // filter: blur(24rpx);
  606. // opacity: 1;
  607. // z-index: -1;
  608. // }
  609. &__bg {
  610. position: absolute;
  611. top: 0;
  612. left: 0;
  613. width: 100%;
  614. height: 100%;
  615. border-radius: inherit;
  616. z-index: -1;
  617. opacity: 0;
  618. transform: scale(0);
  619. transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  620. &--active {
  621. opacity: 1;
  622. transform: scale(1);
  623. }
  624. }
  625. &--text {
  626. font-size: 24rpx;
  627. line-height: 1.2em;
  628. transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  629. &--active {
  630. color: #FFFFFF;
  631. }
  632. }
  633. }
  634. }
  635. }
  636. }
  637. }
  638. /* 可选项end */
  639. }
  640. </style>