-
Notifications
You must be signed in to change notification settings - Fork 5
feat(image): 实现 Image 图片组件 #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for skiyee-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
总体概述此更改引入了新的 SkImage 组件,为 Uni-app 提供包括懒加载、图片预览、占位符和错误处理在内的高级图像显示功能,并添加了七个展示各项功能的示例页面。 变更明细
序列图sequenceDiagram
participant User
participant SkImage
participant ImageElement
participant PreviewService
User->>SkImage: 挂载组件(带图片源)
SkImage->>SkImage: 初始化状态<br/>(loading=true, error=false)
alt 启用懒加载
SkImage->>ImageElement: 应用 lazy-load 属性
else 禁用懒加载
SkImage->>ImageElement: 立即加载图片
end
ImageElement-->>SkImage: `@load` 事件触发
SkImage->>SkImage: 更新状态<br/>(loading=false, error=false)
SkImage->>User: 触发 load 事件
alt 加载失败
ImageElement-->>SkImage: `@error` 事件触发
SkImage->>SkImage: 更新状态<br/>(error=true)
SkImage->>User: 触发 error 事件
SkImage->>User: 显示错误内容<br/>或自定义插槽
else 加载成功
SkImage->>User: 显示图片<br/>(带渐显动画)
end
alt 用户点击图片且启用预览
User->>SkImage: 点击图片
SkImage->>PreviewService: 打开预览<br/>(previewSrcList)
PreviewService->>User: 显示图片预览库
end
代码审查工作量评估🎯 3 (中等复杂) | ⏱️ ~25 分钟 需要重点关注的区域:
诗句
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/skiyee-uni-ui/src/types/common.ts (1)
42-42: 建议补充 'none' 字面量当前 RadiusSize 类型允许 'small' | 'medium' | 'large' | 'full' | number | string,但 SkImageUcv 组件还定义了 'none' 变体。为保持类型定义的完整性和一致性,建议在 RadiusSize 中补充 'none' 字面量。
可应用以下修改:
-export type RadiusSize = 'small' | 'medium' | 'large' | 'full' | number | string +export type RadiusSize = 'none' | 'small' | 'medium' | 'large' | 'full' | number | stringexamples/uni/src/pages-basic/image/mode.vue (1)
1-107: 图片模式展示有效页面展示了常用的 8 种图片显示模式,针对不同模式选择了合适的源图比例(400x200、400x400)以更好地体现模式效果。widthFix 和 heightFix 的示例正确地仅设置了一个维度。
当前示例覆盖了 8 种模式,ImageMode 类型还定义了 6 种位置模式(left、right、top left、top right、bottom left、bottom right)。如果希望示例更完整,可以考虑补充这些模式的展示。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
examples/uni/src/pages-basic/image/base.vue(1 hunks)examples/uni/src/pages-basic/image/error.vue(1 hunks)examples/uni/src/pages-basic/image/lazy.vue(1 hunks)examples/uni/src/pages-basic/image/mode.vue(1 hunks)examples/uni/src/pages-basic/image/placeholder.vue(1 hunks)examples/uni/src/pages-basic/image/preview.vue(1 hunks)examples/uni/src/pages-basic/image/radius.vue(1 hunks)examples/uni/src/pages.json(1 hunks)examples/uni/types/components.d.ts(1 hunks)examples/uni/types/pages.d.ts(1 hunks)packages/skiyee-uni-ui/src/components/sk-image.vue(1 hunks)packages/skiyee-uni-ui/src/styles/index.ts(2 hunks)packages/skiyee-uni-ui/src/styles/sk-image.ts(1 hunks)packages/skiyee-uni-ui/src/types/common.ts(1 hunks)packages/skiyee-uni-ui/src/utils/format.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/skiyee-uni-ui/src/styles/sk-image.ts (1)
packages/skiyee-uni-ui/src/styles/index.ts (2)
SkImageUcv(28-28)SkImageUcvProps(27-27)
🪛 ESLint
packages/skiyee-uni-ui/src/components/sk-image.vue
[error] 26-26: 'RadiusSize' is defined but never used.
(unused-imports/no-unused-imports)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Redirect rules - skiyee-ui
- GitHub Check: Header rules - skiyee-ui
- GitHub Check: Pages changed - skiyee-ui
🔇 Additional comments (7)
packages/skiyee-uni-ui/src/utils/format.ts (1)
9-9: 代码改进合理显式使用模板字符串确保返回值统一为字符串类型,提升了代码的类型一致性和可读性。
packages/skiyee-uni-ui/src/styles/sk-image.ts (1)
20-34: UCV 组件定义规范样式组件定义清晰,圆角变体映射合理。基础样式包含了图片容器所需的关键属性(相对定位、行内块、溢出隐藏、背景色),圆角变体覆盖了常用场景。
packages/skiyee-uni-ui/src/types/common.ts (1)
23-37: ImageMode 类型定义完整类型定义涵盖了 uni-app image 组件的所有 mode 属性值,注释清晰说明了每个模式的行为,便于开发者理解和使用。
examples/uni/src/pages.json (1)
126-153: 页面路由配置正确新增的 7 个图片示例页面配置格式规范,路径命名清晰,与现有页面结构保持一致。
examples/uni/src/pages-basic/image/radius.vue (1)
1-103: 示例页面实现清晰页面结构良好,完整展示了 SkImage 组件的圆角功能。各个示例都配有清晰的文字说明,圆形头像组合的展示也很实用。
examples/uni/src/pages-basic/image/base.vue (1)
1-53: 基础示例覆盖全面页面展示了图片组件的基础用法、尺寸设置和全宽布局三个核心场景,示例简洁明了,便于开发者快速上手。
examples/uni/src/pages-basic/image/error.vue (1)
1-119: 错误处理示例完善页面全面展示了图片加载失败的各种处理方式,包括默认错误提示、自定义错误图、自定义插槽内容、隐藏错误提示、批量错误处理和空 src 场景。使用无效 URL 可靠地触发错误状态,示例实用且易于理解。
| SkSwitch: typeof import('@skiyee/uni-ui/components/sk-switch.vue')['default'] | ||
| SkTextarea: typeof import('@skiyee/uni-ui/components/sk-textarea.vue')['default'] | ||
| SkToast: typeof import('@skiyee/uni-ui/components/sk-toast.vue')['default'] | ||
| SkImage: typeof import('@skiyee/uni-ui/components/sk-image.vue')['default'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
验证移除的组件是否仍在使用中
根据 AI 摘要,此次变更移除了 21 个组件的类型声明(SkBadge、SkButton、SkCheckbox 等),仅保留了新增的 SkImage。由于这是自动生成的文件,这意味着 vite-plugin-uni-components 插件不再检测到这些组件。
如果示例应用中仍在使用这些被移除的组件,将导致类型错误和运行时故障。请确认:
- 这些组件是否已从项目中完全移除?
- 如果仍需要这些组件,插件配置是否需要调整?
运行以下脚本验证被移除的组件是否仍在代码中被引用:
🏁 Script executed:
#!/bin/bash
# 描述:检查被移除的组件是否仍在 examples/uni 中被使用
# 定义被移除的组件列表
removed_components=(
"SkBadge" "SkButton" "SkCheckbox" "SkCheckboxGroup"
"SkDialog" "SkField" "SkForm" "SkIcon" "SkInput"
"SkNumberInput" "SkOverlay" "SkPicker" "SkRadio"
"SkRadioGroup" "SkRoller" "SkRollerGroup" "SkSlider"
"SkSpinner" "SkSwitch" "SkTextarea" "SkToast"
)
echo "=== 检查被移除的组件是否仍在使用 ==="
for component in "${removed_components[@]}"; do
echo -e "\n--- 搜索 $component ---"
# 在 Vue 文件和 TS 文件中搜索组件使用
rg -n --type=vue --type=ts -C2 "<$component" examples/uni/ || echo "未找到 $component 的使用"
done
echo -e "\n=== 检查 vite-plugin-uni-components 配置 ==="
# 查找插件配置
fd -e ts -e js "vite.config|uno.config" examples/uni/ --exec cat {}Length of output: 2975
需要改进搜索方法,使用更稳健的查询方式重新验证。
🏁 Script executed:
#!/bin/bash
# 改进的验证脚本:检查被移除的组件是否在 examples/uni 中被使用
echo "=== 第一步:检查 examples/uni 目录结构 ==="
ls -la examples/uni/src/ 2>/dev/null | head -20 || echo "目录不存在或为空"
echo -e "\n=== 第二步:查找所有 .vue 文件 ==="
fd -e vue examples/uni/ 2>/dev/null | head -20
echo -e "\n=== 第三步:在所有文件中搜索被移除组件的使用 ==="
# 不使用 --type 标志,直接搜索
removed_components="SkBadge|SkButton|SkCheckbox|SkCheckboxGroup|SkDialog|SkField|SkForm|SkIcon|SkInput|SkNumberInput|SkOverlay|SkPicker|SkRadio|SkRadioGroup|SkRoller|SkRollerGroup|SkSlider|SkSpinner|SkSwitch|SkTextarea|SkToast"
echo "搜索组件使用:<(${removed_components})"
rg -n -C2 "<(${removed_components})" examples/uni/ 2>/dev/null || echo "未在 examples/uni 中找到组件标签使用"
echo -e "\n=== 第四步:检查是否有从 @skiyee/uni-ui 的组件导入 ==="
rg -n "from.*@skiyee/uni-ui" examples/uni/ 2>/dev/null || echo "未找到相关导入"
echo -e "\n=== 第五步:检查生成文件前后的组件声明 ==="
# 检查现在生成的 components.d.ts 文件
if [ -f examples/uni/types/components.d.ts ]; then
echo "当前 components.d.ts 中的组件:"
grep -o "Sk[A-Za-z]*" examples/uni/types/components.d.ts | sort -u
fiLength of output: 155261
例项目中大量使用已移除的组件,导致严重的类型安全问题
验证确认了一个严重问题:
已移除的 21+ 个组件(SkButton、SkToast、SkTextarea、SkSwitch、SkIcon、SkRadio、SkRadioGroup、SkSlider、SkSpinner、SkCheckbox、SkCheckboxGroup、SkNumberInput、SkDialog、SkOverlay、SkRoller、SkRollerGroup、SkField、SkForm、SkInput、SkPicker、SkBadge)在 examples/uni 代码库中仍被大量使用(共 300+ 处引用)。
例如:
- SkButton 在 50+ 个文件中使用
- SkSwitch、SkCheckbox、SkRadio 各 20+ 处
- SkInput、SkField 等表单组件 30+ 处
- 其他组件分布在 pages-basic、pages-form、pages-feedback 等目录
然而,当前 components.d.ts(第 10 行)的 GlobalComponents 接口中仅保留了 SkImage。这导致:
- 类型错误:所有已移除组件的使用都失去了 TypeScript 类型定义
- 运行时风险:组件可能无法正确注册或被识别
- 示例应用破损:整个 examples/uni 项目处于非功能状态
需要在合并前采取以下行动之一:
- 如果需要保留示例应用,需要恢复 components.d.ts 中的组件声明
- 如果确实要移除这些组件,需要从示例应用中删除所有相关的使用代码
- 检查 vite-plugin-uni-components 配置是否正确及插件是否需要重新构建
🤖 Prompt for AI Agents
In examples/uni/types/components.d.ts around line 10, GlobalComponents currently
only declares SkImage but the examples/uni codebase still uses 21+ removed
components (SkButton, SkToast, SkTextarea, SkSwitch, SkIcon, SkRadio,
SkRadioGroup, SkSlider, SkSpinner, SkCheckbox, SkCheckboxGroup, SkNumberInput,
SkDialog, SkOverlay, SkRoller, SkRollerGroup, SkField, SkForm, SkInput,
SkPicker, SkBadge) causing widespread type and runtime breakage; fix by either
(A) restoring TypeScript declarations for all these components into
GlobalComponents in this file (add each component mapping to the correct import
default from '@skiyee/uni-ui/components/...'), or (B) if removal is intended,
remove all usages of these components across examples/uni (search &
delete/replace ~300 references), and finally verify vite-plugin-uni-components
configuration and rebuild the plugin so auto-registration matches your
declarations.
| import type { ImageMode, RadiusSize } from '../types' | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
移除未使用的 RadiusSize 引用
ESLint(unused-imports/no-unused-imports)已将该导入标记为报错,当前会导致 lint 失败,请清理。
-import type { ImageMode, RadiusSize } from '../types'
+import type { ImageMode } from '../types'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { ImageMode, RadiusSize } from '../types' | |
| import type { ImageMode } from '../types' | |
🧰 Tools
🪛 ESLint
[error] 26-26: 'RadiusSize' is defined but never used.
(unused-imports/no-unused-imports)
🤖 Prompt for AI Agents
In packages/skiyee-uni-ui/src/components/sk-image.vue around lines 26-27, the
import statement imports an unused type RadiusSize which causes eslint
unused-imports/no-unused-imports to fail; remove RadiusSize from the import
(i.e., import only ImageMode) or eliminate the entire import if ImageMode is
also unused, then run lint to verify the error is resolved.
| import { computed, onMounted, ref } from 'vue' | ||
| import { SkImageUcv } from '../styles' | ||
| import { formatSize } from '../utils' | ||
| defineOptions({ | ||
| inheritAttrs: false, | ||
| options: { | ||
| virtualHost: true, | ||
| }, | ||
| }) | ||
| const props = withDefaults(defineProps<SkImageProps>(), { | ||
| mode: 'aspectFill', | ||
| radius: 'none', | ||
| isLazy: false, | ||
| isShowLoading: true, | ||
| isShowError: true, | ||
| isFade: true, | ||
| isPreview: false, | ||
| }) | ||
| const emits = defineEmits<SkImageEmits>() | ||
| defineSlots<SkImageSlots>() | ||
| // 状态管理 | ||
| const loading = ref(true) | ||
| const error = ref(false) | ||
| const imageLoaded = ref(false) | ||
| // 计算样式 | ||
| const containerStyle = computed(() => { | ||
| const styles: Record<string, string> = {} | ||
| if (props.width) { | ||
| styles.width = formatSize(props.width) | ||
| } | ||
| if (props.height) { | ||
| styles.height = formatSize(props.height) | ||
| } | ||
| return styles | ||
| }) | ||
| const classes = computed(() => { | ||
| return SkImageUcv({ | ||
| radius: props.radius, | ||
| clax: props.clax, | ||
| }) | ||
| }) | ||
| // 图片样式 - 处理淡入动画 | ||
| const imageStyle = computed(() => { | ||
| const styles: Record<string, string> = { | ||
| width: '100%', | ||
| height: '100%', | ||
| } | ||
| // 淡入动画 | ||
| if (props.isFade && imageLoaded.value) { | ||
| styles.opacity = '1' | ||
| styles.transition = 'opacity 0.3s ease-in-out' | ||
| } | ||
| else if (props.isFade) { | ||
| styles.opacity = '0' | ||
| } | ||
| return styles | ||
| }) | ||
| // 图片加载完成 | ||
| function handleLoad(event: any) { | ||
| loading.value = false | ||
| error.value = false | ||
| imageLoaded.value = true | ||
| emits('load', event) | ||
| } | ||
| // 图片加载失败 | ||
| function handleError(event: any) { | ||
| loading.value = false | ||
| error.value = true | ||
| emits('error', event) | ||
| } | ||
| // 点击图片 | ||
| function handleClick() { | ||
| emits('click') | ||
| if (props.isPreview && !error.value) { | ||
| const urls = props.previewSrcList || [props.src] | ||
| const current = props.previewSrcList ? props.previewSrcList.indexOf(props.src) : 0 | ||
| uni.previewImage({ | ||
| current: current >= 0 ? current : 0, | ||
| urls, | ||
| }) | ||
| } | ||
| } | ||
| // 初始化 | ||
| onMounted(() => { | ||
| // 如果没有 src,直接设置为错误状态 | ||
| if (!props.src) { | ||
| loading.value = false | ||
| error.value = true | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
修复 src 变更后状态不会复位的问题
当初始 src 为空或加载失败后(例如 placeholder 示例中先渲染空地址,再异步赋值),onMounted 会把 error 置为 true,但后续 src 更新没有任何逻辑重置 loading/error/imageLoaded,实际图片永远不会渲染。请在 props.src 变化时重新拉起加载状态。
-import { computed, onMounted, ref } from 'vue'
+import { computed, onMounted, ref, watch } from 'vue'
@@
const imageLoaded = ref(false)
+watch(
+ () => props.src,
+ (newSrc) => {
+ if (!newSrc) {
+ loading.value = false
+ error.value = true
+ imageLoaded.value = false
+ return
+ }
+
+ loading.value = true
+ error.value = false
+ imageLoaded.value = false
+ }
+)
+
// 初始化
onMounted(() => {
// 如果没有 src,直接设置为错误状态📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { computed, onMounted, ref } from 'vue' | |
| import { SkImageUcv } from '../styles' | |
| import { formatSize } from '../utils' | |
| defineOptions({ | |
| inheritAttrs: false, | |
| options: { | |
| virtualHost: true, | |
| }, | |
| }) | |
| const props = withDefaults(defineProps<SkImageProps>(), { | |
| mode: 'aspectFill', | |
| radius: 'none', | |
| isLazy: false, | |
| isShowLoading: true, | |
| isShowError: true, | |
| isFade: true, | |
| isPreview: false, | |
| }) | |
| const emits = defineEmits<SkImageEmits>() | |
| defineSlots<SkImageSlots>() | |
| // 状态管理 | |
| const loading = ref(true) | |
| const error = ref(false) | |
| const imageLoaded = ref(false) | |
| // 计算样式 | |
| const containerStyle = computed(() => { | |
| const styles: Record<string, string> = {} | |
| if (props.width) { | |
| styles.width = formatSize(props.width) | |
| } | |
| if (props.height) { | |
| styles.height = formatSize(props.height) | |
| } | |
| return styles | |
| }) | |
| const classes = computed(() => { | |
| return SkImageUcv({ | |
| radius: props.radius, | |
| clax: props.clax, | |
| }) | |
| }) | |
| // 图片样式 - 处理淡入动画 | |
| const imageStyle = computed(() => { | |
| const styles: Record<string, string> = { | |
| width: '100%', | |
| height: '100%', | |
| } | |
| // 淡入动画 | |
| if (props.isFade && imageLoaded.value) { | |
| styles.opacity = '1' | |
| styles.transition = 'opacity 0.3s ease-in-out' | |
| } | |
| else if (props.isFade) { | |
| styles.opacity = '0' | |
| } | |
| return styles | |
| }) | |
| // 图片加载完成 | |
| function handleLoad(event: any) { | |
| loading.value = false | |
| error.value = false | |
| imageLoaded.value = true | |
| emits('load', event) | |
| } | |
| // 图片加载失败 | |
| function handleError(event: any) { | |
| loading.value = false | |
| error.value = true | |
| emits('error', event) | |
| } | |
| // 点击图片 | |
| function handleClick() { | |
| emits('click') | |
| if (props.isPreview && !error.value) { | |
| const urls = props.previewSrcList || [props.src] | |
| const current = props.previewSrcList ? props.previewSrcList.indexOf(props.src) : 0 | |
| uni.previewImage({ | |
| current: current >= 0 ? current : 0, | |
| urls, | |
| }) | |
| } | |
| } | |
| // 初始化 | |
| onMounted(() => { | |
| // 如果没有 src,直接设置为错误状态 | |
| if (!props.src) { | |
| loading.value = false | |
| error.value = true | |
| } | |
| }) | |
| import { computed, onMounted, ref, watch } from 'vue' | |
| import { SkImageUcv } from '../styles' | |
| import { formatSize } from '../utils' | |
| defineOptions({ | |
| inheritAttrs: false, | |
| options: { | |
| virtualHost: true, | |
| }, | |
| }) | |
| const props = withDefaults(defineProps<SkImageProps>(), { | |
| mode: 'aspectFill', | |
| radius: 'none', | |
| isLazy: false, | |
| isShowLoading: true, | |
| isShowError: true, | |
| isFade: true, | |
| isPreview: false, | |
| }) | |
| const emits = defineEmits<SkImageEmits>() | |
| defineSlots<SkImageSlots>() | |
| // 状态管理 | |
| const loading = ref(true) | |
| const error = ref(false) | |
| const imageLoaded = ref(false) | |
| // 计算样式 | |
| const containerStyle = computed(() => { | |
| const styles: Record<string, string> = {} | |
| if (props.width) { | |
| styles.width = formatSize(props.width) | |
| } | |
| if (props.height) { | |
| styles.height = formatSize(props.height) | |
| } | |
| return styles | |
| }) | |
| const classes = computed(() => { | |
| return SkImageUcv({ | |
| radius: props.radius, | |
| clax: props.clax, | |
| }) | |
| }) | |
| // 图片样式 - 处理淡入动画 | |
| const imageStyle = computed(() => { | |
| const styles: Record<string, string> = { | |
| width: '100%', | |
| height: '100%', | |
| } | |
| // 淡入动画 | |
| if (props.isFade && imageLoaded.value) { | |
| styles.opacity = '1' | |
| styles.transition = 'opacity 0.3s ease-in-out' | |
| } | |
| else if (props.isFade) { | |
| styles.opacity = '0' | |
| } | |
| return styles | |
| }) | |
| // 图片加载完成 | |
| function handleLoad(event: any) { | |
| loading.value = false | |
| error.value = false | |
| imageLoaded.value = true | |
| emits('load', event) | |
| } | |
| // 图片加载失败 | |
| function handleError(event: any) { | |
| loading.value = false | |
| error.value = true | |
| emits('error', event) | |
| } | |
| // 点击图片 | |
| function handleClick() { | |
| emits('click') | |
| if (props.isPreview && !error.value) { | |
| const urls = props.previewSrcList || [props.src] | |
| const current = props.previewSrcList ? props.previewSrcList.indexOf(props.src) : 0 | |
| uni.previewImage({ | |
| current: current >= 0 ? current : 0, | |
| urls, | |
| }) | |
| } | |
| } | |
| watch( | |
| () => props.src, | |
| (newSrc) => { | |
| if (!newSrc) { | |
| loading.value = false | |
| error.value = true | |
| imageLoaded.value = false | |
| return | |
| } | |
| loading.value = true | |
| error.value = false | |
| imageLoaded.value = false | |
| } | |
| ) | |
| // 初始化 | |
| onMounted(() => { | |
| // 如果没有 src,直接设置为错误状态 | |
| if (!props.src) { | |
| loading.value = false | |
| error.value = true | |
| } | |
| }) |
🤖 Prompt for AI Agents
In packages/skiyee-uni-ui/src/components/sk-image.vue around lines 124 to 233,
the component sets error=true in onMounted when src is empty but never resets
state when props.src later changes, so the image never re-renders; add a watch
on props.src (importing watch from 'vue') that when src becomes a truthy new
value sets loading=true, error=false, imageLoaded=false (and when src becomes
empty sets loading=false,error=true), so loads are retriggered and preview/click
logic work correctly.
#35
Summary by CodeRabbit