Image.vue 4.3 KB

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