Image.vue 5.0 KB

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