x-skeleton.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <template>
  2. <view class="x-skeleton" :style="variableStr">
  3. <!-- 骨架屏 -->
  4. <view v-if="skeletonLoading" class="x-skeleton__wrapper" :class="[ startFadeOut && 'fade-out' ]"
  5. :style="{ padding: skeletonConfigs.padding, background: background }">
  6. <view v-for="(row, rowIndex) in gridRowsArr" :key="rowIndex" class="x-skeleton__wrapper__rows"
  7. :style="{ marginBottom: rowIndex < gridRowsArr.length - 1 ? skeletonConfigs.gridRowsGap : 0 }">
  8. <view v-for="(column, columnIndex) in gridColumnsArr" :key="columnIndex" class="x-skeleton__wrapper__columns"
  9. :style="{
  10. flexDirection: skeletonConfigs.itemDirection,
  11. alignItems: skeletonConfigs.itemAlign,
  12. marginRight: columnIndex < gridColumnsArr.length - 1 ? skeletonConfigs.gridColumnsGap : 0,
  13. }">
  14. <view v-if="skeletonConfigs.headShow" class="x-skeleton__wrapper__head" :class="[ animate && 'animate' ]"
  15. :style="{
  16. width: skeletonConfigs.headWidth,
  17. height: skeletonConfigs.headHeight,
  18. borderRadius: skeletonConfigs.headBorderRadius,
  19. marginRight: (skeletonConfigs.itemDirection == 'row' && skeletonConfigs.textShow) ? skeletonConfigs.itemGap : 0,
  20. marginBottom: (skeletonConfigs.itemDirection == 'column' && skeletonConfigs.textShow) ? skeletonConfigs.itemGap : 0
  21. }"></view>
  22. <view v-if="skeletonConfigs.textShow" class="x-skeleton__wrapper__text" :style="skeletonConfigs.textRowStyle">
  23. <view v-for="(text, textIndex) in textRowsArr" :key="textIndex" class="x-skeleton__wrapper__text__row"
  24. :class="[animate && 'animate']" :style="{
  25. width: text.width,
  26. height: text.height,
  27. flex: skeletonConfigs.textItmesFlex,
  28. borderRadius: skeletonConfigs.textBorderRadius,
  29. marginLeft: textIndex > 0 ? skeletonConfigs.textItmesDivide : 0,
  30. marginBottom: textIndex < textRowsArr.length - 1 ? skeletonConfigs.textRowsGap : 0
  31. }"></view>
  32. </view>
  33. </view>
  34. </view>
  35. </view>
  36. <!-- 插槽 -->
  37. <view v-else>
  38. <slot></slot>
  39. </view>
  40. </view>
  41. </template>
  42. <script>
  43. import XSkeletonConfigs from './x-skeleton-configs.js'
  44. export default {
  45. name: "x-skeleton",
  46. mixins: [XSkeletonConfigs],
  47. props: {
  48. // 骨架屏类型
  49. type: {
  50. type: String,
  51. default: '' //banner轮播图、info个人信息、text段落、menu菜单、list列表、waterfall瀑布流
  52. },
  53. // 是否展示骨架组件
  54. loading: {
  55. type: Boolean,
  56. default: true
  57. },
  58. // 是否开启动画效果
  59. animate: {
  60. type: Boolean,
  61. default: true
  62. },
  63. // 动画效果持续时间,单位秒
  64. animateTime: {
  65. type: [Number, String],
  66. default: 1.8
  67. },
  68. // 是否开启淡出动画
  69. fadeOut: {
  70. type: Boolean,
  71. default: true
  72. },
  73. // 淡出效果持续时间,单位秒
  74. fadeOutTime: {
  75. type: [Number, String],
  76. default: 0.3
  77. },
  78. // 骨架的背景色
  79. bgColor: {
  80. type: String,
  81. default: '#EAEDF5'
  82. },
  83. background: {
  84. type: String,
  85. default: '#FFFFFF'
  86. },
  87. // 骨架的动画高亮背景色
  88. highlightBgColor: {
  89. type: String,
  90. default: '#F9FAFF'
  91. },
  92. // 自定义配置
  93. configs: {
  94. type: Object,
  95. default: () => {
  96. return {
  97. // padding: '30rpx', //内边距
  98. // gridRows: 3, //行数
  99. // gridColumns: 2, //列数
  100. // gridRowsGap: '40rpx', //行间隔
  101. // gridColumnsGap: '24rpx', //竖间距
  102. // itemDirection: 'column', //head与text之间的排列方向(row、column)
  103. // itemGap: '16rpx', //head与text之间的间隔
  104. // itemAlign: 'center', //head与text之间的纵轴对齐方式(center、flex-start、flex-end、baseline)
  105. // headShow: true, //head是否展示
  106. // headWidth: '100%', //head宽度,支持百分比
  107. // headHeight: '400rpx', //head高度
  108. // headBorderRadius: '12rpx', //head圆角,支持百分比
  109. // textShow: true, //文本是否展示
  110. // textRows: 3, //文本的行数
  111. // textRowsGap: '12rpx', //文本间距
  112. // textWidth: ['40%', '85%', '60%'], //文本的宽度,可以为百分比,数值,带单位字符串等,可通过数组传入指定每个段落行的宽度
  113. // textHeight: ['30rpx', '20rpx', '20rpx'], //文本的高度,可以为数值,带单位字符串等,可通过数组传入指定每个段落行的高度
  114. // textBorderRadius: '6rpx', //文本的圆角,支持百分比
  115. }
  116. }
  117. }
  118. },
  119. computed: {
  120. gridRowsArr() {
  121. return new Array(Number(this.skeletonConfigs?.gridRows || []));
  122. },
  123. gridColumnsArr() {
  124. return new Array(Number(this.skeletonConfigs?.gridColumns || []));
  125. },
  126. textRowsArr() {
  127. if (!this.skeletonConfigs?.textShow) return [];
  128. if (/%$/.test(this.skeletonConfigs.textHeight)) {
  129. console.error('x-skeleton: textHeight参数不支持百分比单位');
  130. }
  131. const rows = []
  132. for (let i = 0; i < this.skeletonConfigs.textRows; i++) {
  133. const {
  134. gridRows,
  135. textWidth,
  136. textHeight
  137. } = this.skeletonConfigs;
  138. let item = {},
  139. // 需要预防超出数组边界的情况
  140. rowWidth = this.isArray(textWidth) ? (textWidth[i] || (i === gridRows - 1 ? '70%' : '100%')) : i ===
  141. gridRows - 1 ? '70%' : textWidth,
  142. rowHeight = this.isArray(textHeight) ? (textHeight[i] || '30rpx') : textHeight
  143. // 非百分比的宽度时,调整像素单位
  144. if (/%$/.test(rowWidth)) {
  145. item.width = rowWidth;
  146. } else {
  147. item.width = this.addUnit(rowWidth)
  148. }
  149. item.height = this.addUnit(rowHeight)
  150. rows.push(item)
  151. }
  152. return rows
  153. },
  154. variableStr() {
  155. let keys = ['animateTime', 'fadeOutTime', 'bgColor', 'highlightBgColor'];
  156. let str = keys.map(item => {
  157. if (item.indexOf('Time') > -1) {
  158. return `--${item}:${this[item]}s`
  159. } else {
  160. return `--${item}:${this[item]}`
  161. }
  162. }).join(";");
  163. return str;
  164. }
  165. },
  166. watch: {
  167. loading: {
  168. immediate: true,
  169. handler(value) {
  170. if (value) {
  171. this.skeletonLoading = true;
  172. } else {
  173. if (this.fadeOut) {
  174. this.startFadeOut = true;
  175. setTimeout(() => {
  176. this.skeletonLoading = false;
  177. this.startFadeOut = false;
  178. }, this.fadeOutTime * 1000);
  179. } else {
  180. this.skeletonLoading = false;
  181. }
  182. }
  183. }
  184. },
  185. type: {
  186. immediate: true,
  187. handler(value) {
  188. if (value === 'banner') {
  189. this.skeletonConfigs = this.bannerConfigs();
  190. } else if (value === 'info') {
  191. this.skeletonConfigs = this.infoConfigs();
  192. } else if (value === 'text') {
  193. this.skeletonConfigs = this.textConfigs();
  194. } else if (value === 'menu') {
  195. this.skeletonConfigs = this.menuConfigs();
  196. } else if (value === 'list') {
  197. this.skeletonConfigs = this.listConfigs();
  198. } else if (value === 'waterfall') {
  199. this.skeletonConfigs = this.waterfallConfigs();
  200. } else {
  201. this.skeletonConfigs = this.configs || {};
  202. }
  203. }
  204. }
  205. },
  206. data() {
  207. return {
  208. skeletonConfigs: this.configs || {},
  209. skeletonLoading: this.loading,
  210. startFadeOut: false,
  211. width: 0
  212. };
  213. },
  214. mounted() {
  215. },
  216. methods: {
  217. /**
  218. * @description 是否为数组
  219. * @param {object} value 需要判断的对象
  220. */
  221. isArray(value) {
  222. if (typeof Array.isArray === 'function') {
  223. return Array.isArray(value)
  224. }
  225. return Object.prototype.toString.call(value) === '[object Array]'
  226. },
  227. /**
  228. * @description 添加单位,如果有rpx,upx,%,px等单位结尾或者值为auto,直接返回,否则加上px单位结尾
  229. * @param {string|number} value 需要添加单位的值
  230. * @param {string} unit 添加的单位名 比如px
  231. */
  232. addUnit(value = 'auto', unit = 'px') {
  233. value = String(value);
  234. // 用uView内置验证规则中的number判断是否为数值
  235. return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value) ? `${value}${unit}` : value;
  236. }
  237. }
  238. }
  239. </script>
  240. <style lang="scss" scoped>
  241. @mixin background {
  242. background: linear-gradient(90deg, var(--bgColor) 25%, var(--highlightBgColor) 37%, var(--bgColor) 50%);
  243. background-size: 400% 100%;
  244. }
  245. .x-skeleton {
  246. width: 100%;
  247. box-sizing: border-box;
  248. .x-skeleton__wrapper {
  249. display: flex;
  250. flex-direction: column;
  251. &__rows {
  252. display: flex;
  253. align-items: center;
  254. justify-content: space-between;
  255. }
  256. &__columns {
  257. display: flex;
  258. align-items: center;
  259. flex: 1;
  260. }
  261. &__head {
  262. width: 100%;
  263. @include background;
  264. }
  265. &__text {
  266. flex: 1;
  267. width: 100%;
  268. &__row {
  269. @include background;
  270. }
  271. }
  272. }
  273. .fade-out {
  274. opacity: 0;
  275. animation: fadeOutAnim var(--fadeOutTime);
  276. }
  277. @keyframes fadeOutAnim {
  278. from {
  279. opacity: 1;
  280. }
  281. to {
  282. opacity: 0;
  283. }
  284. }
  285. .animate {
  286. animation: skeletonAnim var(--animateTime) ease infinite;
  287. }
  288. @keyframes skeletonAnim {
  289. 0% {
  290. background-position: 100% 50%;
  291. }
  292. 100% {
  293. background-position: 0 50%;
  294. }
  295. }
  296. }
  297. </style>