cl-image.uvue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. <template>
  2. <view
  3. class="cl-image"
  4. :class="[pt.className]"
  5. :style="{
  6. width: parseRpx(width!),
  7. height: parseRpx(height!)
  8. }"
  9. >
  10. <view
  11. class="cl-image__error"
  12. :class="[
  13. {
  14. 'is-dark': isDark
  15. },
  16. pt.error?.className
  17. ]"
  18. v-if="isError"
  19. >
  20. <slot name="error">
  21. <cl-icon
  22. :name="pt.error?.name ?? 'close-line'"
  23. :size="pt.error?.size ?? 40"
  24. :pt="{
  25. className: parseClass(['!text-surface-400', pt.error?.className])
  26. }"
  27. ></cl-icon>
  28. </slot>
  29. </view>
  30. <view
  31. class="cl-image__loading"
  32. :class="[
  33. {
  34. 'is-dark': isDark
  35. },
  36. pt.loading?.className
  37. ]"
  38. v-else-if="isLoading && showLoading"
  39. >
  40. <slot name="loading">
  41. <cl-loading
  42. :loading="true"
  43. :pt="{ icon: { className: '!text-surface-400' } }"
  44. ></cl-loading>
  45. </slot>
  46. </view>
  47. <image
  48. class="cl-image__inner"
  49. :class="[pt.inner?.className]"
  50. :src="src"
  51. :mode="mode"
  52. :lazy-load="lazyLoad"
  53. :webp="webp"
  54. :show-menu-by-longpress="showMenuByLongpress"
  55. @load="onLoad"
  56. @error="onError"
  57. @tap="onTap"
  58. />
  59. <slot></slot>
  60. </view>
  61. </template>
  62. <script setup lang="ts">
  63. import { computed, ref, type PropType } from "vue";
  64. import type { PassThroughProps } from "../../types";
  65. import { isDark, isEmpty, parseClass, parsePt, parseRpx } from "@/cool";
  66. import type { ClIconProps } from "../cl-icon/props";
  67. defineOptions({
  68. name: "cl-image"
  69. });
  70. const props = defineProps({
  71. // 透传样式
  72. pt: {
  73. type: Object,
  74. default: () => ({})
  75. },
  76. // 图片源
  77. src: {
  78. type: String,
  79. default: ""
  80. },
  81. // 图片裁剪、缩放的模式
  82. mode: {
  83. type: String as PropType<
  84. | "scaleToFill"
  85. | "aspectFit"
  86. | "aspectFill"
  87. | "widthFix"
  88. | "heightFix"
  89. | "top"
  90. | "bottom"
  91. | "center"
  92. | "left"
  93. | "right"
  94. | "top left"
  95. | "top right"
  96. | "bottom left"
  97. | "bottom right"
  98. >,
  99. default: "aspectFill"
  100. },
  101. // 是否显示边框
  102. border: {
  103. type: Boolean,
  104. default: false
  105. },
  106. // 是否预览
  107. preview: {
  108. type: Boolean,
  109. default: false
  110. },
  111. // 预览图片列表
  112. previewList: {
  113. type: Array as PropType<string[]>,
  114. default: () => []
  115. },
  116. // 图片高度
  117. height: {
  118. type: [String, Number] as PropType<string | number>,
  119. default: 120
  120. },
  121. // 图片宽度
  122. width: {
  123. type: [String, Number] as PropType<string | number>,
  124. default: 120
  125. },
  126. // 是否显示加载状态
  127. showLoading: {
  128. type: Boolean,
  129. default: true
  130. },
  131. // 是否懒加载
  132. lazyLoad: {
  133. type: Boolean,
  134. default: false
  135. },
  136. // 图片显示动画效果
  137. fadeShow: {
  138. type: Boolean,
  139. default: false
  140. },
  141. // 是否解码webp格式
  142. webp: {
  143. type: Boolean,
  144. default: false
  145. },
  146. // 是否长按显示菜单
  147. showMenuByLongpress: {
  148. type: Boolean,
  149. default: false
  150. }
  151. });
  152. // 事件定义
  153. const emit = defineEmits(["load", "error"]);
  154. // 透传样式类型
  155. type PassThrough = {
  156. className?: string;
  157. inner?: PassThroughProps;
  158. error?: ClIconProps;
  159. loading?: PassThroughProps;
  160. };
  161. // 解析透传样式
  162. const pt = computed(() => parsePt<PassThrough>(props.pt));
  163. // 加载状态
  164. const isLoading = ref(true);
  165. // 加载失败状态
  166. const isError = ref(false);
  167. // 图片加载成功
  168. function onLoad(e: UniEvent) {
  169. isLoading.value = false;
  170. isError.value = false;
  171. emit("load", e);
  172. }
  173. // 图片加载失败
  174. function onError(e: UniEvent) {
  175. isLoading.value = false;
  176. isError.value = true;
  177. emit("error", e);
  178. }
  179. // 图片点击
  180. function onTap() {
  181. if (props.preview) {
  182. const urls = isEmpty(props.previewList) ? [props.src] : props.previewList;
  183. uni.previewImage({
  184. urls,
  185. current: props.src
  186. });
  187. }
  188. }
  189. </script>
  190. <style lang="scss" scoped>
  191. .cl-image {
  192. @apply relative flex flex-row items-center justify-center;
  193. &__inner {
  194. @apply w-full h-full rounded-xl;
  195. }
  196. &__loading,
  197. &__error {
  198. @apply absolute h-full w-full bg-surface-200 rounded-xl;
  199. @apply flex flex-col items-center justify-center;
  200. &.is-dark {
  201. @apply bg-surface-700;
  202. }
  203. }
  204. }
  205. </style>