cl-image.uvue 4.0 KB

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