Container.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <template>
  2. <view class="container">
  3. <view
  4. class="title"
  5. :style="{
  6. paddingTop: `${safeArea.top}px`,
  7. background: bgColor,
  8. }"
  9. v-if="showTitle"
  10. >
  11. <uni-nav-bar
  12. :left-icon="showBack ? 'left' : ''"
  13. @clickLeft="onBack"
  14. :border="false"
  15. :title="title"
  16. fixed
  17. :backgroundColor="bgColor"
  18. />
  19. </view>
  20. <scroll-view
  21. :scroll-y="scrollY"
  22. :scroll-with-animation="true"
  23. :enable-flex="true"
  24. :enable-passive="true"
  25. :enhanced="true"
  26. :paging-enabled="true"
  27. :scroll-anchoring="true"
  28. :show-scrollbar="false"
  29. :scroll-x="scrollX"
  30. :scroll-into-view="scrollIntoView"
  31. enable-back-to-top
  32. scroll-anchoring
  33. >
  34. <view
  35. :class="['scroll-view', scrollX ? 'topic-scroll' : '']"
  36. :style="{
  37. height: `${safeArea.height}px`,
  38. whiteSpace: scrollX ? 'nowrap' : 'normal',
  39. ...scrollStyle,
  40. }"
  41. >
  42. <slot></slot>
  43. </view>
  44. <!-- 底部文字 -->
  45. <view v-if="showBottom && bottomText" class="bottom-text">
  46. {{ bottomText }}
  47. </view>
  48. </scroll-view>
  49. <view
  50. v-if="$slots.footer"
  51. class="bottom-button"
  52. :style="{
  53. width: `${safeArea.width}px`,
  54. background: bgColor,
  55. }"
  56. >
  57. <slot name="footer" :footer-height="footerHeight"></slot>
  58. </view>
  59. </view>
  60. </template>
  61. <script setup>
  62. import pages from "@/pages.json";
  63. import { ref, onMounted, watchEffect } from "vue";
  64. import { getRoute, router } from "../../utils/router";
  65. import { getRect } from "../../utils";
  66. import { getCurrentInstance } from "vue";
  67. const props = defineProps({
  68. showBottom: {
  69. type: Boolean,
  70. default: false,
  71. },
  72. bottomText: {
  73. type: String,
  74. default: "~已经到底了~",
  75. },
  76. scrollX: {
  77. type: Boolean,
  78. default: false,
  79. },
  80. scrollY: {
  81. type: Boolean,
  82. default: true,
  83. },
  84. className: {
  85. type: String,
  86. default: "",
  87. },
  88. scrollIntoView: {
  89. type: String,
  90. default: "",
  91. },
  92. scrollStyle: {
  93. type: Object,
  94. default: () => ({}),
  95. },
  96. onBack: Function,
  97. showBack: {
  98. type: Boolean,
  99. default: true,
  100. },
  101. showTitle: {
  102. type: Boolean,
  103. default: true,
  104. },
  105. bgColor: {
  106. type: String,
  107. default: "#fff",
  108. },
  109. title: String,
  110. });
  111. const emit = defineEmits(["onSafeAreaChange"]);
  112. const onBack = async () => {
  113. if (props.onBack) {
  114. await props.onBack();
  115. router.back();
  116. return;
  117. }
  118. router.back();
  119. };
  120. const safeArea = ref({
  121. footer: {},
  122. title: {},
  123. });
  124. const footerHeight = ref(0);
  125. onMounted(() => {
  126. const systemInfo = uni.getWindowInfo();
  127. // 判断是否为tabbar页面
  128. safeArea.value = {
  129. ...systemInfo.safeArea,
  130. source: systemInfo.safeArea,
  131. };
  132. const isTarbarPage = pages.tabBar.list
  133. .map((item) => item.pagePath)
  134. .includes(getRoute().routeList[0].path);
  135. if (isTarbarPage) {
  136. // 高度再减掉tabbar高度
  137. safeArea.value.height -= 50;
  138. // 减去系统导航栏高度
  139. safeArea.value.height -= systemInfo.statusBarHeight - 18;
  140. }
  141. // 24是内边距
  142. safeArea.value.width -= 24;
  143. const instance = getCurrentInstance();
  144. // 获取头部高度
  145. getRect({
  146. name: ".title",
  147. onSuccess(res) {
  148. safeArea.value.title = res;
  149. safeArea.value.height -= props.showTitle
  150. ? safeArea.value.title?.height
  151. : 0;
  152. },
  153. instance,
  154. });
  155. // 获取底部高度
  156. getRect({
  157. name: ".bottom-button",
  158. onSuccess(res) {
  159. safeArea.value.footer = res;
  160. safeArea.value.height -= safeArea.value.footer?.height;
  161. safeArea.value.height += 22; // 剩余高度
  162. },
  163. instance,
  164. });
  165. });
  166. watchEffect(() => {
  167. emit("onSafeAreaChange", safeArea.value);
  168. });
  169. defineExpose({
  170. safeArea: safeArea.value,
  171. });
  172. </script>
  173. <style lang="scss" scoped>
  174. @import "@/uni.scss";
  175. .title {
  176. position: sticky;
  177. top: 0;
  178. z-index: 9999;
  179. }
  180. .container {
  181. display: flex;
  182. flex-direction: column;
  183. color: #333;
  184. box-sizing: content-box;
  185. position: relative;
  186. height: 100vh;
  187. }
  188. .scroll-view {
  189. position: relative;
  190. padding: 24rpx 24rpx 0;
  191. box-sizing: content-box;
  192. }
  193. .bottom-text {
  194. display: flex;
  195. align-items: center;
  196. justify-content: center;
  197. width: 100%;
  198. }
  199. .bottom-button {
  200. width: 100%;
  201. padding: 24rpx;
  202. position: absolute;
  203. bottom: 0;
  204. }
  205. .topic-scroll {
  206. white-space: nowrap;
  207. display: inline-flex;
  208. }
  209. </style>