Image.vue 4.9 KB

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