From 25ad02a8c091d12627b10d93d3019e30b5208f86 Mon Sep 17 00:00:00 2001 From: sheepluo Date: Mon, 27 Feb 2023 11:23:13 +0800 Subject: [PATCH] feat(image): support .avif and .webp (#2182) --- src/_common | 2 +- src/config-provider/_example/others.vue | 9 ++ src/config-provider/config-provider.en-US.md | 3 +- src/config-provider/config-provider.md | 3 +- src/config-provider/type.ts | 5 + src/image/__tests__/vitest-image.test.jsx | 41 ++++-- src/image/_example/avif.vue | 15 +++ src/image/image.en-US.md | 1 + src/image/image.md | 1 + src/image/image.tsx | 49 ++++--- src/image/props.ts | 4 + src/image/type.ts | 9 ++ test/snap/__snapshots__/csr.test.js.snap | 128 +++++++++++++++++++ test/snap/__snapshots__/ssr.test.js.snap | 4 +- 14 files changed, 243 insertions(+), 31 deletions(-) create mode 100644 src/image/_example/avif.vue diff --git a/src/_common b/src/_common index 760156c44..c80805436 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 760156c445e3e2d16d58bffa4943607692fcfc99 +Subproject commit c8080543627898df25bd917254eb7903c56e493a diff --git a/src/config-provider/_example/others.vue b/src/config-provider/_example/others.vue index c4eb6dead..76b9866d3 100644 --- a/src/config-provider/_example/others.vue +++ b/src/config-provider/_example/others.vue @@ -77,6 +77,8 @@

+ + @@ -138,6 +140,13 @@ export default { return { // 全局特性配置,可以引入英文默认配置 enConfig,还可以在默认配置的基础上进行自定义配置 globalConfig: merge(enConfig, { + image: { + // 全局替换图片地址 + replaceImageSrc(params) { + console.log(params); + return 'https://tdesign.gtimg.com/demo/demo-image-1.png'; + }, + }, form: { requiredMark: false, }, diff --git a/src/config-provider/config-provider.en-US.md b/src/config-provider/config-provider.en-US.md index 064350c9f..3ebab529d 100644 --- a/src/config-provider/config-provider.en-US.md +++ b/src/config-provider/config-provider.en-US.md @@ -292,7 +292,7 @@ copyText | String | - | \- | N name | type | default | description | required -- | -- | -- | -- | -- -`MessageOptions` | \- | - | \- | N +`MessageOptions` | \- | - | extends `MessageOptions` | N ### ImageConfig @@ -300,6 +300,7 @@ name | type | default | description | required -- | -- | -- | -- | -- errorText | String | - | loading text, default value is "Error" | N loadingText | String | - | loading text, default value is "loading" | N +replaceImageSrc | Function | - | replace all `src` attribute of images。Typescript:`(params: ImageProps) => string`,[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/config-provider/type.ts) | N ### ImageViewerConfig diff --git a/src/config-provider/config-provider.md b/src/config-provider/config-provider.md index 88edb0eae..2c38813dc 100644 --- a/src/config-provider/config-provider.md +++ b/src/config-provider/config-provider.md @@ -326,7 +326,7 @@ copyText | String | - | 语言配置,“复制链接” 描述文本 | N 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- -`MessageOptions` | \- | - | 继承 `MessageOptions` 中的全部 API | N +`MessageOptions` | \- | - | 继承 `MessageOptions` 中的全部属性 | N ### ImageConfig @@ -334,6 +334,7 @@ copyText | String | - | 语言配置,“复制链接” 描述文本 | N -- | -- | -- | -- | -- errorText | String | - | 图片加载失败显示的文本,中文默认为“图片无法显示” | N loadingText | String | - | 图片加载中显示的文本,中文默认为“图片加载中” | N +replaceImageSrc | Function | - | 统一替换图片 `src` 地址,参数为组件的全部属性,返回值为新的图片地址。TS 类型:`(params: ImageProps) => string`,[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/config-provider/type.ts) | N ### ImageViewerConfig diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index 47d43363b..2749cc8be 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -9,6 +9,7 @@ import { CalendarController } from '../calendar'; import { ButtonProps } from '../button'; import { FormErrorMessage } from '../form'; import { MessageOptions } from '../message'; +import { ImageProps } from '../image'; import { TNode } from '../common'; export interface GlobalConfigProvider { @@ -833,6 +834,10 @@ export interface ImageConfig { * @default '' */ loadingText?: string; + /** + * 统一替换图片 `src` 地址,参数为组件的全部属性,返回值为新的图片地址 + */ + replaceImageSrc?: (params: ImageProps) => string; } export interface ImageViewerConfig { diff --git a/src/image/__tests__/vitest-image.test.jsx b/src/image/__tests__/vitest-image.test.jsx index 618de8e77..e71cd1c7a 100644 --- a/src/image/__tests__/vitest-image.test.jsx +++ b/src/image/__tests__/vitest-image.test.jsx @@ -14,7 +14,7 @@ describe('Image Component', () => { it('props.alt works fine', () => { const wrapper = mount({ render() { - return text image load failed; + return {'text; }, }).find('img'); expect(wrapper.attributes('alt')).toBe('text image load failed'); @@ -24,7 +24,7 @@ describe('Image Component', () => { const wrapper = mount({ render() { return ( - TNode} src="https://this.is.an.error.img.com"> + TNode} src={'https://this.is.an.error.img.com'}> ); }, }); @@ -40,7 +40,7 @@ describe('Image Component', () => { return ( TNode }} - src="https://this.is.an.error.img.com" + src={'https://this.is.an.error.img.com'} > ); }, @@ -140,7 +140,7 @@ describe('Image Component', () => { it('slots.overlay-content works fine', () => { const wrapper = mount({ render() { - return TNode }}>; + return TNode }}>; }, }); expect(wrapper.find('.custom-node').exists()).toBeTruthy(); @@ -156,7 +156,7 @@ describe('Image Component', () => { wrapper.find('.t-image__wrapper').trigger('mouseenter'); await wrapper.vm.$nextTick(); expect(wrapper.find('.t-image__overlay-content').exists()).toBeTruthy(); - expect(wrapper.findAll('.t-image__overlay-content--hidden').length).toBe(0); + expect(wrapper.find('.t-image__overlay-content--hidden').exists()).toBeFalsy(); wrapper.find('.t-image__wrapper').trigger('mouseleave'); await wrapper.vm.$nextTick(); expect(wrapper.find('.t-image__overlay-content--hidden').exists()).toBeTruthy(); @@ -213,11 +213,30 @@ describe('Image Component', () => { }); }); + it('props.srcset is equal to {\'image/avif\': \'https://tdesign.gtimg.com/img/tdesign-image.avif\',\'image/webp\': \'https://tdesign.gtimg.com/img/tdesign-image.webp\'}', () => { + const wrapper = mount({ + render() { + return ( + + ); + }, + }); + const domWrapper = wrapper.find('picture > source'); + expect(domWrapper.attributes('srcset')).toBe('https://tdesign.gtimg.com/img/tdesign-image.avif'); + const domWrapper1 = wrapper.find('picture > source:nth-child(2)'); + expect(domWrapper1.attributes('srcset')).toBe('https://tdesign.gtimg.com/img/tdesign-image.webp'); + }); + it('events.error works fine', async () => { const onErrorFn = vi.fn(); const wrapper = mount({ render() { - return ; + return ; }, }); const imgDom = wrapper.find('img').element; @@ -225,7 +244,7 @@ describe('Image Component', () => { await wrapper.vm.$nextTick(); expect(wrapper.find('.t-image__error').exists()).toBeTruthy(); expect(wrapper.find('.t-icon-image-error').exists()).toBeTruthy(); - expect(onErrorFn).toHaveBeenCalled(1); + expect(onErrorFn).toHaveBeenCalled(); expect(onErrorFn.mock.calls[0][0].e.type).toBe('error'); }); @@ -233,14 +252,14 @@ describe('Image Component', () => { const onLoadFn1 = vi.fn(); const wrapper = mount({ render() { - return ; + return ; }, }); await wrapper.vm.$nextTick(); - const imgDom = wrapper.find('img').element; - simulateImageEvent(imgDom, 'load'); + const imgDom1 = wrapper.find('img').element; + simulateImageEvent(imgDom1, 'load'); await wrapper.vm.$nextTick(); - expect(onLoadFn1).toHaveBeenCalled(1); + expect(onLoadFn1).toHaveBeenCalled(); expect(onLoadFn1.mock.calls[0][0].e.type).toBe('load'); }); }); diff --git a/src/image/_example/avif.vue b/src/image/_example/avif.vue new file mode 100644 index 000000000..7f7b420a4 --- /dev/null +++ b/src/image/_example/avif.vue @@ -0,0 +1,15 @@ + diff --git a/src/image/image.en-US.md b/src/image/image.en-US.md index 86c765457..392de9324 100644 --- a/src/image/image.en-US.md +++ b/src/image/image.en-US.md @@ -17,6 +17,7 @@ placeholder | String / Slot / Function | - | Typescript:`string \| TNode`。[s position | String | center | \- | N shape | String | square | options:circle/round/square | N src | String | - | \- | N +srcset | Object | - | for `.avif` and `.webp` image url。Typescript:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/image/type.ts) | N onError | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image load failed | N onLoad | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image loaded | N diff --git a/src/image/image.md b/src/image/image.md index 313909c85..b399e9ccc 100644 --- a/src/image/image.md +++ b/src/image/image.md @@ -17,6 +17,7 @@ placeholder | String / Slot / Function | - | 占位元素,展示层级低于 ` position | String | center | 等同于原生的 object-position 属性,可选值为 top right bottom left 或 string,可以自定义任何单位,px 或者 百分比 | N shape | String | square | 图片圆角类型。可选项:circle/round/square | N src | String | - | 图片链接 | N +srcset | Object | - | 图片地址,支持特殊格式的图片,如 `.avif` 和 `.webp`。TS 类型:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/image/type.ts) | N onError | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
图片加载失败时触发 | N onLoad | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
图片加载完成时触发 | N diff --git a/src/image/image.tsx b/src/image/image.tsx index 1b03887ae..bb9afb861 100644 --- a/src/image/image.tsx +++ b/src/image/image.tsx @@ -1,5 +1,8 @@ -import { defineComponent, ref, watch } from '@vue/composition-api'; +import { + computed, defineComponent, ref, watch, +} from '@vue/composition-api'; import omit from 'lodash/omit'; +import isFunction from 'lodash/isFunction'; import { ImageErrorIcon, ImageIcon } from 'tdesign-icons-vue'; import observe from '../_common/js/utils/observe'; import { useConfig } from '../config-provider/useConfig'; @@ -39,13 +42,14 @@ export default defineComponent({ const { classPrefix, globalConfig } = useConfig('image'); const imageRef = ref(null); - const imageSrc = ref(props.src); + // replace image url + const imageSrc = computed(() => isFunction(globalConfig.value.replaceImageSrc) ? globalConfig.value.replaceImageSrc(props) : props.src); + watch( () => props.src, () => { hasError.value = false; isLoaded.value = false; - imageSrc.value = props.src; }, ); @@ -77,8 +81,15 @@ export default defineComponent({ } }; + const imageClasses = computed(() => [ + `${classPrefix.value}-image`, + `${classPrefix.value}-image--fit-${props.fit}`, + `${classPrefix.value}-image--position-${props.position}`, + ]); + return { imageRef, + imageClasses, handleLoadImage, classPrefix, globalConfig, @@ -127,6 +138,23 @@ export default defineComponent({ ); }, + + renderImageSrcset() { + return ( + + {Object.entries(this.srcset).map(([type, url]) => ( + + ))} + {this.src && this.renderImage(this.src)} + + ); + }, + + renderImage(url: string) { + return ( + {this.alt} + ); + }, }, render() { @@ -148,19 +176,8 @@ export default defineComponent({ {this.renderGalleryShadow()} {(this.hasError || !this.shouldLoad) &&
} - {!(this.hasError || !this.shouldLoad) && ( - {this.alt} - )} + {!(this.hasError || !this.shouldLoad) + && (this.srcset && Object.keys(this.srcset).length ? this.renderImageSrcset() : this.renderImage(this.imageSrc))} {!(this.hasError || !this.shouldLoad) && !this.isLoaded && (
{renderTNodeJSX(this, 'loading') || ( diff --git a/src/image/props.ts b/src/image/props.ts index dfe8b620d..3e745b897 100644 --- a/src/image/props.ts +++ b/src/image/props.ts @@ -70,6 +70,10 @@ export default { type: String, default: '', }, + /** 图片地址,支持特殊格式的图片,如 `.avif` 和 `.webp` */ + srcset: { + type: Object as PropType, + }, /** 图片加载失败时触发 */ onError: Function as PropType, /** 图片加载完成时触发 */ diff --git a/src/image/type.ts b/src/image/type.ts index 8d3d57172..1ae646f7a 100644 --- a/src/image/type.ts +++ b/src/image/type.ts @@ -63,6 +63,10 @@ export interface TdImageProps { * @default '' */ src?: string; + /** + * 图片地址,支持特殊格式的图片,如 `.avif` 和 `.webp` + */ + srcset?: ImageSrcset; /** * 图片加载失败时触发 */ @@ -72,3 +76,8 @@ export interface TdImageProps { */ onLoad?: (context: { e: ImageEvent }) => void; } + +export interface ImageSrcset { + 'image/avif': string; + 'image/webp': string; +} diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap index 7d0a7f515..4ee07c50f 100644 --- a/test/snap/__snapshots__/csr.test.js.snap +++ b/test/snap/__snapshots__/csr.test.js.snap @@ -35940,6 +35940,56 @@ exports[`csr snapshot test > csr test ./src/config-provider/_example/others.vue
+
+ +
+
+
+ + + + +
+
+ 图片加载中 +
+
+
+
`; @@ -57151,6 +57201,84 @@ exports[`csr snapshot test > csr test ./src/icon/_example/single.vue 1`] = `
`; +exports[`csr snapshot test > csr test ./src/image/_example/avif.vue 1`] = ` +
+
+
+ + + + + +
+
+
+ + + + +
+
+ 图片加载中 +
+
+
+
+
+
+ + .avif / .webp + +
+
+`; + exports[`csr snapshot test > csr test ./src/image/_example/extra-always.vue 1`] = `
renders ./src/config-provider/_example/global.vue c exports[`ssr snapshot test > renders ./src/config-provider/_example/input.vue correctly 1`] = `"
"`; -exports[`ssr snapshot test > renders ./src/config-provider/_example/others.vue correctly 1`] = `"


0 / 20
0 / 0
Empty Data





















Feature TagFeature TagFeature TagFeature Tag



Department A
Department B



First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.


"`; +exports[`ssr snapshot test > renders ./src/config-provider/_example/others.vue correctly 1`] = `"


0 / 20
0 / 0
Empty Data





















Feature TagFeature TagFeature TagFeature Tag



Department A
Department B



First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.


\\"\\"
图片加载中
"`; exports[`ssr snapshot test > renders ./src/config-provider/_example/pagination.vue correctly 1`] = `"
Total 36 items
10 / page
  • 1
  • 2
  • 3
  • 4
jump to
/ 4
"`; @@ -450,6 +450,8 @@ exports[`ssr snapshot test > renders ./src/icon/_example/iconfont-enhanced.vue c exports[`ssr snapshot test > renders ./src/icon/_example/single.vue correctly 1`] = `"
"`; +exports[`ssr snapshot test > renders ./src/image/_example/avif.vue correctly 1`] = `"
\\"\\"
图片加载中
.avif / .webp
"`; + exports[`ssr snapshot test > renders ./src/image/_example/extra-always.vue correctly 1`] = `"
有遮罩
\\"\\"
图片加载中
高清
无遮罩
\\"\\"
图片加载中
高清
"`; exports[`ssr snapshot test > renders ./src/image/_example/extra-hover.vue correctly 1`] = `"
\\"\\"
图片加载中
预览
"`;