Skip to content

feat(image-viewer): add image-viewer #607

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

Merged
merged 30 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4ae42ec
feat(image-viewer): add image-viewer
novlan1 Apr 8, 2025
3f7ab77
feat(image-viewer): add transition
novlan1 Apr 9, 2025
13d67b4
feat(image-viewer): try add drag events
novlan1 Apr 9, 2025
50c1fb1
chore: update spell
novlan1 Apr 9, 2025
03830d7
chore: resolve conflict
novlan1 Apr 21, 2025
96a9eee
chore: merge develop
novlan1 May 15, 2025
e451ab0
feat(image-viewer): optimize viewer
novlan1 May 15, 2025
88435b7
chore: lint
novlan1 May 15, 2025
39c1c01
chore: update snapshot
novlan1 May 15, 2025
d1aeab0
chore: merge develop
github-actions[bot] May 16, 2025
58a350a
chore: update snapshot
github-actions[bot] May 16, 2025
86f58ac
feat(image-viewer): 优化preview
novlan1 May 22, 2025
c7a89ec
feat(image-viewer): 优化preview
novlan1 May 22, 2025
ee7483f
feat(image-viewer): 优化preview
novlan1 May 22, 2025
65ffbb9
feat(image-viewer): 优化preview
novlan1 May 22, 2025
e068d1e
feat(image-viewer): 优化preview
novlan1 May 22, 2025
29c0ccd
feat(image-viewer): 优化preview
novlan1 May 23, 2025
eb0e28e
feat(image-viewer): 优化preview
novlan1 May 23, 2025
dcaef5f
feat(image-viewer): 优化preview
novlan1 May 23, 2025
bf2c922
feat(image-viewer): 优化preview
novlan1 May 23, 2025
97e9887
feat(image-viewer): 优化preview
novlan1 May 23, 2025
8d620ca
feat(image-viewer): 优化preview
novlan1 May 23, 2025
8ca9d7c
feat(image-viewer): 优化preview
novlan1 May 23, 2025
5b35ada
feat(image-viewer): 优化preview
novlan1 May 23, 2025
c191015
feat(image-viewer): 优化preview
novlan1 May 23, 2025
493cd02
chore: test preview
novlan1 May 26, 2025
1f606ca
chore: test preview
novlan1 May 26, 2025
6d7328b
chore: test preview
novlan1 May 26, 2025
74673f5
chore: test preview
novlan1 May 26, 2025
7b0b7e9
feat(viewer): 优化放大
novlan1 May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"concurrently": "^6.4.0",
"cross-env": "^7.0.3",
"cssnano": "^5.0.12",
"csstype": "^3.1.3",
"cz-conventional-changelog": "^3.3.0",
"dom-parser": "^0.1.6",
"eslint": "^8.4.1",
Expand Down
5 changes: 5 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export default {
name: 'image',
component: () => import('tdesign-mobile-react/image/_example/index.tsx'),
},
{
title: 'ImageViewer 图片预览',
name: 'image-viewer',
component: () => import('tdesign-mobile-react/image-viewer/_example/index.tsx'),
},
{
title: 'Overlay 遮罩层',
name: 'overlay',
Expand Down
14 changes: 8 additions & 6 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,14 @@ export const docs = [
component: () => import('tdesign-mobile-react/image/image.md'),
componentEn: () => import('tdesign-mobile-react/image/image.en-US.md'),
},
// {
// title: 'ImageViewer 图片预览',
// name: 'image-viewer',
// path: '/mobile-react/components/image-viewer',
// component: () => import('tdesign-mobile-react/image-viewer/image-viewer.md'),
// },
{
title: 'ImageViewer 图片预览',
titleEn: 'ImageViewer',
name: 'ImageViewer',
path: '/mobile-react/components/image-viewer',
component: () => import('tdesign-mobile-react/image-viewer/image-viewer.md'),
componentEn: () => import('tdesign-mobile-react/image-viewer/image-viewer.en-US.md'),
},
{
title: 'List 列表',
name: 'list',
Expand Down
9 changes: 8 additions & 1 deletion src/_util/useSwipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface UseSwipeOptions {
* 滑动结束时的回调函数
*/
onSwipeEnd?: (e: TouchEvent) => void;

/**
* 是否禁用
*/
disabled?: boolean;
}

/**
Expand All @@ -48,7 +53,7 @@ export function useSwipe(target: EventTarget | null | undefined, options = {} as
const coordsStart = useRef<Position>({ x: 0, y: 0 }); // 用于存储触摸起始位置的坐标
const coordsEnd = useRef<Position>({ x: 0, y: 0 }); // 用于存储触摸结束位置的坐标
const coordsOffset = useRef<Position>({ x: 0, y: 0 }); // 用于存储滑动偏移量
const { threshold = 0, onSwipe, onSwipeEnd, onSwipeStart, listenerOptions = { passive: true } } = options;
const { threshold = 0, onSwipe, onSwipeEnd, onSwipeStart, listenerOptions = { passive: true }, disabled } = options;

const updateOffset = useCallback(() => {
coordsOffset.current = {
Expand Down Expand Up @@ -95,6 +100,7 @@ export function useSwipe(target: EventTarget | null | undefined, options = {} as
const onTouchStart = useCallback(
(e: TouchEvent) => {
if (e.touches.length !== 1) return;
if (disabled) return;
if (
listenerOptions === true ||
(isObject(listenerOptions) && listenerOptions.capture && listenerOptions.passive)
Expand All @@ -106,6 +112,7 @@ export function useSwipe(target: EventTarget | null | undefined, options = {} as
updateCoordsEnd(x, y);
onSwipeStart?.(e);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[listenerOptions, updateCoordsStart, updateCoordsEnd, onSwipeStart],
);

Expand Down
2 changes: 1 addition & 1 deletion src/cascader/Cascader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useDeepCompareEffect } from 'ahooks';
import classNames from 'classnames';
import last from 'lodash/last';
import last from 'lodash-es/last';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { CloseIcon, ChevronRightIcon } from 'tdesign-icons-react';
import useDefault from '../_util/useDefault';
Expand Down
2 changes: 1 addition & 1 deletion src/dropdown-menu/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useClickAway } from 'ahooks';
import cx from 'classnames';
import uniqueId from 'lodash/uniqueId';
import uniqueId from 'lodash-es/uniqueId';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { CaretDownSmallIcon, CaretUpSmallIcon } from 'tdesign-icons-react';
import { Button, Checkbox, Popup, RadioGroup } from 'tdesign-mobile-react';
Expand Down
2 changes: 1 addition & 1 deletion src/dropdown-menu/DropdownMenuContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import noop from 'lodash/noop';
import noop from 'lodash-es/noop';
import React from 'react';
import { dropdownMenuDefaultProps } from './defaultProps';

Expand Down
31 changes: 31 additions & 0 deletions src/image-viewer/_example/align.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState } from 'react';
import { ImageViewer, Button, type ImageInfo } from 'tdesign-mobile-react';

const images: ImageInfo[] = [
{
url: 'https://tdesign.gtimg.com/mobile/demos/swiper1.png',
align: 'start',
},
{
url: 'https://tdesign.gtimg.com/mobile/demos/swiper2.png',
align: 'end',
},
{
url: 'https://tdesign.gtimg.com/mobile/demos/swiper2.png',
align: 'center',
},
];

export default function AlignDemo() {
const [visible, setVisible] = useState(false);

return (
<div className="image-example">
<Button block size="large" variant="outline" theme="primary" onClick={() => setVisible(true)}>
基础图片预览 + 对齐方式
</Button>

<ImageViewer images={images} visible={visible} onClose={() => setVisible(false)} />
</div>
);
}
25 changes: 25 additions & 0 deletions src/image-viewer/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react';
import { ImageViewer, Button } from 'tdesign-mobile-react';

const images = [
'https://tdesign.gtimg.com/mobile/demos/swiper1.png',
'https://tdesign.gtimg.com/mobile/demos/swiper2.png',
];

export default function BaseDemo() {
const [visible, setVisible] = useState(false);

const onIndexChange = (...args) => {
console.log('[onIndexChange]', args);
};

return (
<div className="image-example">
<Button block size="large" variant="outline" theme="primary" onClick={() => setVisible(true)}>
基础图片预览
</Button>

<ImageViewer images={images} visible={visible} onClose={() => setVisible(false)} onIndexChange={onIndexChange} />
</div>
);
}
27 changes: 27 additions & 0 deletions src/image-viewer/_example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import BaseDemo from './base';
import AlignDemo from './align';
import OperationDemo from './operation';
import './style/index.less';

export default function ImageViewerDemo() {
return (
<div className="tdesign-mobile-demo">
<TDemoHeader
title="ImageViewer 图片预览"
summary="图片全屏放大预览效果,包含全屏背景色、页码位置样式、增加操作等规范"
/>
<TDemoBlock title="01 组件类型" summary="图片预览类型" padding={true}>
<BaseDemo />
</TDemoBlock>
<TDemoBlock title="02 组件类型" summary="图片预览类型,可设置垂直对齐方式" padding={true}>
<AlignDemo />
</TDemoBlock>
<TDemoBlock title="03 组件类型" summary="带操作图片预览" padding={true}>
<OperationDemo />
</TDemoBlock>
</div>
);
}
27 changes: 27 additions & 0 deletions src/image-viewer/_example/operation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { ImageViewer, Button } from 'tdesign-mobile-react';

const images = [
'https://tdesign.gtimg.com/mobile/demos/swiper1.png',
'https://tdesign.gtimg.com/mobile/demos/swiper2.png',
];

export default function OperationDemo() {
const [visible, setVisible] = useState(false);

return (
<div className="image-example">
<Button block size="large" variant="outline" theme="primary" onClick={() => setVisible(true)}>
带操作图片预览
</Button>

<ImageViewer
images={images}
visible={visible}
showIndex={true}
deleteBtn={true}
onClose={() => setVisible(false)}
/>
</div>
);
}
4 changes: 4 additions & 0 deletions src/image-viewer/_example/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.tdesign-mobile-demo {
background-color: #fff;
min-height: 100%;
}
15 changes: 15 additions & 0 deletions src/image-viewer/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */

import { TdImageViewerProps } from './type';

export const imageViewerDefaultProps: TdImageViewerProps = {
closeBtn: true,
deleteBtn: false,
images: [],
defaultIndex: 0,
maxZoom: 3,
showIndex: false,
defaultVisible: false,
};
62 changes: 62 additions & 0 deletions src/image-viewer/getFixScaleEleTransPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getClientSize } from './util';

function fixPoint(key: 'x' | 'y', start: number, width: number, clientWidth: number) {
const startAddWidth = start + width;
const offsetStart = key === 'x' ? (width - clientWidth) / 2 : 0;
// const offsetEnd = key === 'x' ? -offsetStart : clientWidth - width;

if (width > clientWidth) {
if (start > 0) {
return {
[key]: offsetStart,
};
}
if (start < 0 && startAddWidth < clientWidth) {
return {
[key]: -offsetStart,
};
}
} else if (start < 0 || startAddWidth > clientWidth) {
return {
[key]: start < 0 ? offsetStart : -offsetStart,
};
}
return {};
}

/**
* Fix position x,y point when
*
* Ele width && height < client
* - Back origin
*
* - Ele width | height > clientWidth | clientHeight
* - left | top > 0 -> Back 0
* - left | top + width | height < clientWidth | clientHeight -> Back left | top + width | height === clientWidth | clientHeight
*
* Regardless of other
*/
export default function getFixScaleEleTransPosition(
width: number,
height: number,
left: number,
top: number,
): null | { x: number; y: number } {
const { width: clientWidth, height: clientHeight } = getClientSize();

let fixPos = null;

if (width <= clientWidth && height <= clientHeight) {
fixPos = {
x: 0,
y: 0,
};
} else if (width > clientWidth || height > clientHeight) {
fixPos = {
...fixPoint('x', left, width, clientWidth),
...fixPoint('y', top, height, clientHeight),
};
}

return fixPos;
}
22 changes: 22 additions & 0 deletions src/image-viewer/image-viewer.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:: BASE_DOC ::

## API

### ImageViewer Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
closeBtn | TNode | true | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
deleteBtn | TNode | false | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
images | Array | [] | Typescript:`Array<string \| ImageInfo>` `interface ImageInfo { url: string; align: 'start' \| 'center' \| 'end' }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/image-viewer/type.ts) | N
index | Number | 0 | \- | N
defaultIndex | Number | 0 | uncontrolled property | N
maxZoom | Number | 3 | Typescript:`number` | N
showIndex | Boolean | false | \- | N
visible | Boolean | false | hide or show image viewer | N
defaultVisible | Boolean | false | hide or show image viewer。uncontrolled property | N
onClose | Function | | Typescript:`(context: { trigger: 'overlay' \| 'close-btn', visible: boolean, index: number }) => void`<br/> | N
onDelete | Function | | Typescript:`(index: number) => void`<br/> | N
onIndexChange | Function | | Typescript:`(index: number, context: { trigger: 'prev' \| 'next' }) => void`<br/> | N
22 changes: 22 additions & 0 deletions src/image-viewer/image-viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:: BASE_DOC ::

## API

### ImageViewer Props

名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
className | String | - | 类名 | N
style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
closeBtn | TNode | true | 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以完全自定义关闭按钮。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
deleteBtn | TNode | false | 是否显示删除操作,前提需要开启页码。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
images | Array | [] | 图片数组。TS 类型:`Array<string \| ImageInfo>` `interface ImageInfo { url: string; align: 'start' \| 'center' \| 'end' }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/image-viewer/type.ts) | N
index | Number | 0 | 当前预览图片所在的下标 | N
defaultIndex | Number | 0 | 当前预览图片所在的下标。非受控属性 | N
maxZoom | Number | 3 | 【开发中】最大放大比例。TS 类型:`number` | N
showIndex | Boolean | false | 是否显示页码 | N
visible | Boolean | false | 隐藏/显示预览 | N
defaultVisible | Boolean | false | 隐藏/显示预览。非受控属性 | N
onClose | Function | | TS 类型:`(context: { trigger: 'overlay' \| 'close-btn', visible: boolean, index: number }) => void`<br/>关闭时触发 | N
onDelete | Function | | TS 类型:`(index: number) => void`<br/>点击删除操作按钮时触发 | N
onIndexChange | Function | | TS 类型:`(index: number, context: { trigger: 'prev' \| 'next' }) => void`<br/>预览图片切换时触发,`context.prev` 切换到上一张图片,`context.next` 切换到下一张图片 | N
Loading