Image.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <template>
  2. <view
  3. class="nana-image-wrapper"
  4. :style="style"
  5. @click="handleClick"
  6. >
  7. <image
  8. :style="{
  9. width: style.width,
  10. height: style.height,
  11. }"
  12. :mode="($attrs.mode as any)"
  13. :lazyLoad="$attrs.lazyLoad"
  14. :fadeShow="$attrs.fadeShow"
  15. :webp="$attrs.webp"
  16. :show-menu-by-longpress="$attrs.showMenuByLongpress"
  17. :draggable="$attrs.draggable"
  18. :src="isErrorState ? failedImage : (src || defaultImage)"
  19. @loadstart="isLoadState = true"
  20. @load="isLoadState = false"
  21. @error="isErrorState = true; isLoadState = false"
  22. />
  23. <view v-if="showFailed && isErrorState" class="inner-view error">
  24. <!-- todo: failed -->
  25. <Text color="second" :text="src ? '暂无图片' : '加载失败'" />
  26. </view>
  27. <view v-if="showLoading && isLoadState" class="inner-view loading">
  28. <ActivityIndicator
  29. :color="themeContext.resolveThemeColor(loadingColor)"
  30. :size="themeContext.resolveThemeSize(loadingSize)"
  31. />
  32. </view>
  33. </view>
  34. </template>
  35. <script setup lang="ts">
  36. import { computed, onMounted, ref, watch } from 'vue';
  37. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  38. import ActivityIndicator from './ActivityIndicator.vue';
  39. import Text from './Text.vue';
  40. export interface ImageProps {
  41. /**
  42. * 图片地址
  43. */
  44. src?: string,
  45. /**
  46. * 加载失败图片地址
  47. */
  48. failedImage?: string,
  49. /**
  50. * 为空时图片地址
  51. */
  52. defaultImage?: string,
  53. /**
  54. * 是否显示加载中提示,默认是
  55. */
  56. showLoading?: boolean,
  57. /**
  58. * 是否显示加载失败提示,默认是
  59. */
  60. showFailed?: boolean,
  61. /**
  62. * 是否显示灰色占位,默认是
  63. */
  64. showGrey?: boolean,
  65. width?: string|number,
  66. height?: string|number,
  67. /**
  68. * 是否可以点击预览图片
  69. */
  70. clickPreview?: boolean,
  71. /**
  72. * 初始加载中状态
  73. */
  74. loading?: boolean,
  75. /**
  76. * 加载中圆圈颜色
  77. */
  78. loadingColor?: string,
  79. /**
  80. * 加载中圆圈颜色
  81. */
  82. loadingSize?: string|number,
  83. /**
  84. * 指定图片是否可以点击,默认否
  85. */
  86. touchable?: boolean,
  87. /**
  88. * 图片是否有圆角
  89. */
  90. round?: boolean,
  91. /**
  92. * 当round为true的圆角大小,默认是50%
  93. */
  94. radius?: string|number,
  95. /**
  96. * 内部样式
  97. */
  98. innerStyle?: object;
  99. }
  100. defineOptions({
  101. options: {
  102. virtualHost: true
  103. }
  104. })
  105. const props = withDefaults(defineProps<ImageProps>(), {
  106. src: '',
  107. failedImage: '',
  108. defaultImage: '',
  109. showLoading: true,
  110. showFailed: true,
  111. showGrey: () => propGetThemeVar('ImageShowGrey', false),
  112. loading: false,
  113. loadingColor: () => propGetThemeVar('ImageLoadingColor', 'border.default'),
  114. loadingSize: () => propGetThemeVar('ImageLoadingSize', 50),
  115. touchable: false,
  116. round: () => propGetThemeVar('ImageRound', false),
  117. radius: () => propGetThemeVar('ImageRadius', '50%'),
  118. })
  119. const emit = defineEmits([ 'click' ]);
  120. const isErrorState = ref(false);
  121. const isLoadState = ref(true);
  122. const themeContext = useTheme();
  123. const style = computed(() => {
  124. const o : Record<string, any> = {
  125. borderRadius: props.round ? themeContext.resolveThemeSize(props.radius) : '',
  126. backgroundColor: isErrorState.value || props.showGrey ? themeContext.resolveThemeColor('background.imageBox') : 'transparent',
  127. overflow: 'hidden',
  128. width: themeContext.resolveThemeSize(props.width),
  129. height: themeContext.resolveThemeSize(props.height),
  130. ...props.innerStyle,
  131. }
  132. return o;
  133. });
  134. function handleClick() {
  135. if (props.clickPreview) {
  136. uni.previewImage({
  137. urls: [ props.src ],
  138. })
  139. }
  140. emit('click');
  141. }
  142. function loadSrcState() {
  143. if (props.src) {
  144. isErrorState.value = false;
  145. isLoadState.value = true;
  146. } else {
  147. isErrorState.value = true;
  148. isLoadState.value = false;
  149. }
  150. }
  151. watch(() => props.src, (newVal, oldVal) => {
  152. if (newVal) {
  153. isErrorState.value = true;
  154. isLoadState.value = false;
  155. } else
  156. isErrorState.value = false;
  157. })
  158. onMounted(() => {
  159. loadSrcState();
  160. })
  161. </script>
  162. <style lang="scss">
  163. .nana-image-wrapper {
  164. position: relative;
  165. flex-shrink: 0;
  166. .inner-view {
  167. position: absolute;
  168. left: 0;
  169. right: 0;
  170. top: 0;
  171. bottom: 0;
  172. display: flex;
  173. align-items: center;
  174. justify-content: center;
  175. }
  176. }
  177. </style>