From 216fc21b1393015b99d064269832d43e2a75c449 Mon Sep 17 00:00:00 2001 From: kryst4l <690039872@qq.com> Date: Tue, 2 Aug 2022 19:36:26 +0800 Subject: [PATCH 1/2] feat:new component image-viewer --- src/image-viewer/_example/background.jsx | 34 +++ src/image-viewer/_example/base.jsx | 36 +++ src/image-viewer/_example/imageList.jsx | 46 +++ src/image-viewer/_example/index.jsx | 34 +++ src/image-viewer/_example/initialIndex.jsx | 34 +++ src/image-viewer/_example/style/index.css | 9 + src/image-viewer/_example/style/index.less | 11 + src/image-viewer/image-viewer.md | 22 ++ src/image-viewer/imageViewer.tsx | 337 +++++++++++++++++++++ src/image-viewer/index.ts | 9 + src/image-viewer/style/css.js | 1 + src/image-viewer/style/index.js | 1 + src/image-viewer/type.ts | 52 ++++ 13 files changed, 626 insertions(+) create mode 100644 src/image-viewer/_example/background.jsx create mode 100644 src/image-viewer/_example/base.jsx create mode 100644 src/image-viewer/_example/imageList.jsx create mode 100644 src/image-viewer/_example/index.jsx create mode 100644 src/image-viewer/_example/initialIndex.jsx create mode 100644 src/image-viewer/_example/style/index.css create mode 100644 src/image-viewer/_example/style/index.less create mode 100644 src/image-viewer/image-viewer.md create mode 100644 src/image-viewer/imageViewer.tsx create mode 100644 src/image-viewer/index.ts create mode 100644 src/image-viewer/style/css.js create mode 100644 src/image-viewer/style/index.js create mode 100644 src/image-viewer/type.ts diff --git a/src/image-viewer/_example/background.jsx b/src/image-viewer/_example/background.jsx new file mode 100644 index 00000000..69a41801 --- /dev/null +++ b/src/image-viewer/_example/background.jsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import ImageViewer from '../ImageViewer'; +import Button from '../../button/Button'; + +// 图片预览素材 +const IMAGE_SOURCE = ['https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview2.png']; + +function Index() { + // 是否显示图片预览 + const [imageViewerVisible, setImageViewerVisible] = useState(false); + + return ( +
+ {/* 预览 */} + + {/* 控制 */} + +
+ ); +} + +export default Index; diff --git a/src/image-viewer/_example/base.jsx b/src/image-viewer/_example/base.jsx new file mode 100644 index 00000000..19c1e2f9 --- /dev/null +++ b/src/image-viewer/_example/base.jsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import ImageViewer from '../ImageViewer'; +import Button from '../../button/Button'; +import Toast from '../../toast/Toast'; + +// 图片预览素材 +const IMAGE_SOURCE = ['https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview1.png']; + +function Base() { + // 是否显示图片预览 + const [imageViewerVisible, setImageViewerVisible] = useState(false); + + return ( +
+ {/* 预览 */} + Toast({ message: `翻到第 ${index} 页` })} + > + {/* 控制 */} + +
+ ); +} + +export default Base; diff --git a/src/image-viewer/_example/imageList.jsx b/src/image-viewer/_example/imageList.jsx new file mode 100644 index 00000000..7649c176 --- /dev/null +++ b/src/image-viewer/_example/imageList.jsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { Toast } from 'tdesign-mobile-react'; +import ImageViewer from '../ImageViewer'; +import Button from '../../button/Button'; + +// 图片预览素材 +const IMAGE_SOURCE = [ + 'https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview1.png', + 'https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview3.png', + 'https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview2.png', +]; + +function ImageList() { + // 是否显示图片预览 + const [imageViewerVisible, setImageViewerVisible] = useState(false); + + return ( +
+ {/* 预览 */} + Toast({ message: `翻到第 ${index} 页` })} + onClose={(trigger, visible, index) => console.log('close', trigger, visible, index)} + onDelete={(index) => Toast({ message: `删除第 ${index} 页` })} + > + {/* 控制 */} + +
+ ); +} + +export default ImageList; diff --git a/src/image-viewer/_example/index.jsx b/src/image-viewer/_example/index.jsx new file mode 100644 index 00000000..fa523dbc --- /dev/null +++ b/src/image-viewer/_example/index.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import TDemoBlock from '../../../site/mobile/components/DemoBlock'; +import TDemoHeader from '../../../site/mobile/components/DemoHeader'; +import Base from './base'; +import ImageList from './imageList'; +import InitialIndex from './initialIndex'; +import Background from './background'; + +import './style/index.less'; + +export default function ImageViewerDemo() { + return ( +
+ + + +
+ {/* 基础图片预览 */} + + {/* 有删除操作 */} + + {/* 图片超高情况 */} + + {/* 图片超宽情况 */} + +
+
+
+
+ ); +} diff --git a/src/image-viewer/_example/initialIndex.jsx b/src/image-viewer/_example/initialIndex.jsx new file mode 100644 index 00000000..7e4b62a4 --- /dev/null +++ b/src/image-viewer/_example/initialIndex.jsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import ImageViewer from '../ImageViewer'; +import Button from '../../button/Button'; + +// 图片预览素材 +const IMAGE_SOURCE = ['https://oteam-tdesign-1258344706.cos.ap-guangzhou.myqcloud.com/miniprogram/images/preview3.png']; + +function Index() { + // 是否显示图片预览 + const [imageViewerVisible, setImageViewerVisible] = useState(false); + + return ( +
+ {/* 预览 */} + + {/* 控制 */} + +
+ ); +} + +export default Index; diff --git a/src/image-viewer/_example/style/index.css b/src/image-viewer/_example/style/index.css new file mode 100644 index 00000000..57f5e006 --- /dev/null +++ b/src/image-viewer/_example/style/index.css @@ -0,0 +1,9 @@ +.tdesign-mobile-demo { + background-color: #f6f6f6; +} +.image-viewer-block { + padding: 0 16px; +} +.image-viewer-block .t-button { + margin-bottom: 16px; +} diff --git a/src/image-viewer/_example/style/index.less b/src/image-viewer/_example/style/index.less new file mode 100644 index 00000000..5118333f --- /dev/null +++ b/src/image-viewer/_example/style/index.less @@ -0,0 +1,11 @@ +.tdesign-mobile-demo { + background-color: #f6f6f6; +} + +.image-viewer-block { + padding: 0 16px; + + .t-button { + margin-bottom: 16px; + } +} \ No newline at end of file diff --git a/src/image-viewer/image-viewer.md b/src/image-viewer/image-viewer.md new file mode 100644 index 00000000..7b34f5d8 --- /dev/null +++ b/src/image-viewer/image-viewer.md @@ -0,0 +1,22 @@ +:: BASE_DOC :: + +## API + +| 属性 | 类型 | 默认值 | 必传 | 说明 | +| --------------- | --------------- | ----------------- | ---- | -------------- | +| images | `Array` | [] | Y | 图片数组 | +| visible | Boolean | false | N | 隐藏/显示预览 | +| showIndex | Boolean | true | N | 是否显示页码 | +| initialIndex | Number | 0 | N | 默认展示第几项 | +| backgroundColor | String | rgba(0, 0, 0, .6) | N | 遮罩的背景颜色 | +| closeBtn | Boolean | false | N | 是否显示关闭按钮 | +| deleteBtn | Boolean | false | N | 是否显示删除按钮 | + +## Events + +| 事件名称 | 参数 | 说明 | +| -------- | ---- | ---------- | +| onVisibleChange | - | 显示/隐藏回调 | +| onChange | - | 翻页时回调 | +| onClose | - | 关闭时回调 | +| onDelete | - | 删除时回调 | \ No newline at end of file diff --git a/src/image-viewer/imageViewer.tsx b/src/image-viewer/imageViewer.tsx new file mode 100644 index 00000000..adbecb00 --- /dev/null +++ b/src/image-viewer/imageViewer.tsx @@ -0,0 +1,337 @@ +import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { CloseIcon, DeleteIcon } from 'tdesign-icons-react'; +import useConfig from '../_util/useConfig'; +import { TdImageViewerProps } from './type'; + +export type ImageViewerProps = TdImageViewerProps; + +interface sourceItem { + url: string; + imgProportion: number; +} + +const ImageViewer: React.FC = ({ + images = [], + visible = false, + showIndex = true, + closeBtn = false, + deleteBtn = false, + initialIndex = 1, + backgroundColor, + onVisibleChange, + onChange, + onClose, + onDelete, +}) => { + const PAGING_DURATION = 300; + const PAGING_SCALE = 0.5; + // 统一配置信息 + const { classPrefix } = useConfig(); + // prefix + const prefix = useMemo(() => `${classPrefix}`, [classPrefix]); + // 视图元素 + const element = useRef(null); + const [source, setSource] = useState>([]); + // 图片总数 + const [count, setCount] = useState(0); + // 当前第几张 + const [index, setIndex] = useState(1); + // 当前偏移量 + const [offset, setOffSet] = useState(0); + // 容器大小 + const [clientSize, setClientSize] = useState({ + width: 0, + height: 0, + }); + // 坐标 + const [dragState, setDrageState] = useState({ + startTime: new Date().getTime(), // 拖拽时间 + startLeft: -1, // 初始 x 坐标 + startTop: -1, // 初始 y 坐标 + itemWidth: -1, // 视图节点 宽度 + itemHeight: -1, // 视图节点 高度 + currentLeft: -1, // 最终 x 坐标 + currentTop: -1, // 最终 y 坐标 + }); + // 是否做动画 + const [isMove, setIsMove] = useState(false); + // 动画时间 + const [dragTime, setDragTime] = useState(0); + // 是否锁定拖拽 + const [moveLock, setMoveLock] = useState(true); + + const getPoint = (event: React.TouchEvent) => { + const point = event.changedTouches[0]; + return point; + }; + + const handleTouchStart = (event: React.TouchEvent) => { + const point = getPoint(event); + + // 初始化坐标信息 + setDrageState({ + ...dragState, + startTime: new Date().getTime(), + startLeft: point.pageX, + startTop: point.pageY, + itemWidth: element.current.offsetWidth, + itemHeight: element.current.offsetHeight, + }); + + if (count <= 1 || !moveLock) return; + + setMoveLock(false); + }; + const handleTouchMove = (event: React.TouchEvent) => { + if (count <= 1 || moveLock) return; + + const point = getPoint(event); + + // 实时记录 x y 坐标 + const currentLeft = point.pageX; + const currentTop = point.pageY; + // 计算偏移量 + const offsetLeft = currentLeft - dragState.startLeft; + // const offsetTop = currentTop - dragState.startTop; + + // 滚动穿透 + // event.preventDefault(); + + const newOffsetLeft = Math.min(Math.max(-dragState.itemWidth + 1, offsetLeft), dragState.itemWidth - 1); + const newOffset = newOffsetLeft - dragState.itemWidth * index; + setOffSet(newOffset); + + setDrageState({ + ...dragState, + currentLeft, + currentTop, + }); + }; + const handleTouchEnd = (event: React.TouchEvent) => { + const point = getPoint(event); + + // 点击事件 + if (dragState.startLeft === point.pageX && !closeBtn) { + setMoveLock(true); + onVisibleChange && onVisibleChange(false); + + onClose && onClose('overlay', false, index); + return; + } + if (count <= 1 || moveLock) return; + setMoveLock(true); + + const dragDuration = Math.min(new Date().getTime() - dragState.startTime, 500); + const isFastDrag = dragDuration < PAGING_DURATION; + setDragTime(dragDuration); + if (isFastDrag && dragState.currentLeft === -1) return; + + let action = ''; + const offsetLeft = dragState.currentLeft - dragState.startLeft; + const { itemWidth } = dragState; + + // 做动画 + setIsMove(true); + + // 偏移量是否大于一半 + if (Math.abs(offsetLeft) > itemWidth * PAGING_SCALE || isFastDrag) { + if (offsetLeft < 0) { + // 下一页 + const newOffset = -itemWidth * (index + 1); + setOffSet(newOffset); + action = 'next'; + } else { + // 上一页 + const newOffset = -itemWidth * (index - 1); + setOffSet(newOffset); + action = 'prev'; + } + } else { + // 复位 + const newOffset = -itemWidth * index; + setOffSet(newOffset); + } + + // 复位 + setTimeout(() => { + setIsMove(false); + + if (action) { + let currentIndex = index; + if (action === 'next') { + currentIndex += 1; + } else { + currentIndex -= 1; + } + + if (currentIndex === count + 1) { + currentIndex = 1; + } else if (currentIndex === 0) { + currentIndex = count; + } + + setIndex(currentIndex); + const newOffset = -itemWidth * currentIndex; + setOffSet(newOffset); + + onChange && onChange(currentIndex); + } + // context.emit('change', index.value - 1); + }, dragDuration); + + // 初始化 + setDrageState({ + startTime: new Date().getTime(), // 拖拽时间 + startLeft: -1, // 初始 x 坐标 + startTop: -1, // 初始 y 坐标 + itemWidth: -1, // 视图节点 宽度 + itemHeight: -1, // 视图节点 高度 + currentLeft: -1, // 最终 x 坐标 + currentTop: -1, // 最终 y 坐标 + }); + }; + + const handleClose = () => { + if (!closeBtn) return; + onVisibleChange(false); + + onClose && onClose('button', false, index); + }; + const handleDelete = () => { + if (!deleteBtn) return; + onDelete && onDelete(index); + }; + + const handleInitialIndex = useCallback( + (index: number) => { + let defaultIndex = 0; + if (index <= 0) { + defaultIndex = 1; + } else if (index > 0 && index >= images.length) { + defaultIndex = images.length; + } else if (index > 0 && index < images.length) { + defaultIndex = index; + } + setIndex(defaultIndex); + setOffSet(-element.current.offsetWidth * defaultIndex || 0); + }, + [images], + ); + const handleWidth = useCallback( + (imgProportion: number) => { + const clientProportion = clientSize.width === 0 ? 0 : clientSize.width / clientSize.height; + + if (imgProportion < clientProportion) { + const imgWidthStyle = imgProportion * clientSize.height; + return `${imgWidthStyle}px`; + } + return '100%'; + }, + [clientSize], + ); + const handleSource = (images: Array): Array => { + const frontItem = images[0]; + const endItem = images[images.length - 1]; + + // 首尾图片 + const newImages = [...images]; + newImages.push(frontItem); + newImages.unshift(endItem); + + return newImages.map((m) => ({ url: m, imgProportion: 0 })); + }; + const handleLoaded = (e, key: number) => { + const { target } = e; + + const imgWidth = target.naturalWidth; + const imgHeight = target.naturalHeight; + const imgProportion = imgWidth / imgHeight; + + source[key].imgProportion = imgProportion; + }; + + useEffect(() => { + setClientSize({ width: element.current.offsetWidth, height: element.current.offsetHeight }); + handleInitialIndex(initialIndex); + }, [handleInitialIndex, initialIndex, visible]); + + useEffect(() => { + setCount(images.length); + + const newImages = handleSource(images); + setSource(newImages); + }, [images]); + + return ( + +
+ {/* 蒙版 */} +
+
+ {/* 关闭 */} + {closeBtn ? ( + + ) : ( +
+ )} + {/* 页码 */} + {showIndex ? ( +
+ {index}/{count} +
+ ) : ( +
+ )} + {/* 删除 */} + {deleteBtn ? ( + + ) : ( +
+ )} +
+ {/* 内容 */} +
+
+
+
+ {source.map((item, key) => ( +
+
+ handleLoaded(e, key)} + /> +
+
+ ))} +
+
+
+
+
+
+ ); +}; + +export default ImageViewer; diff --git a/src/image-viewer/index.ts b/src/image-viewer/index.ts new file mode 100644 index 00000000..a29713aa --- /dev/null +++ b/src/image-viewer/index.ts @@ -0,0 +1,9 @@ +import _ImageViewer from './ImageViewer'; + +import './style/index.js'; + +export type { ImageViewerProps } from './ImageViewer'; +export * from './type'; + +export const ImageViewer = _ImageViewer; +export default ImageViewer; diff --git a/src/image-viewer/style/css.js b/src/image-viewer/style/css.js new file mode 100644 index 00000000..6a9a4b13 --- /dev/null +++ b/src/image-viewer/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/image-viewer/style/index.js b/src/image-viewer/style/index.js new file mode 100644 index 00000000..b9e0f3db --- /dev/null +++ b/src/image-viewer/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/mobile/components/image-viewer/_index.less'; diff --git a/src/image-viewer/type.ts b/src/image-viewer/type.ts new file mode 100644 index 00000000..5211d767 --- /dev/null +++ b/src/image-viewer/type.ts @@ -0,0 +1,52 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +export interface TdImageViewerProps { + /** + * 数据来源,image 列表 + */ + images: Array; + /** + * 是否显示 + */ + visible: boolean; + /** + * 是否显示页码 + */ + showIndex?: boolean; + /** + * 是否显示关闭按钮 + */ + closeBtn?: boolean; + /** + * 是否显示删除按钮 + */ + deleteBtn?: boolean; + /** + * 当前显示第几张 + */ + initialIndex?: number; + /** + * 背景色 + */ + backgroundColor?: string; + /** + * 是否显示回调 + */ + onVisibleChange: (visible: boolean) => void; + /** + * 翻页时回调 + */ + onChange: (index: number) => void; + /** + * 关闭回调 + */ + onClose: (trigger: 'overlay' | 'button', visible: Boolean, index: Number) => void; + /** + * 删除回调 + */ + onDelete: (index: number) => void; +} From 401767c5763571f83aa5aa068c3ebef22916471e Mon Sep 17 00:00:00 2001 From: kryst4l <690039872@qq.com> Date: Tue, 2 Aug 2022 19:49:50 +0800 Subject: [PATCH 2/2] feat(add component image-viewer): add component image-viewer add component image-viewer feat #259 --- site/mobile/mobile.config.js | 5 +++++ site/web/site.config.js | 12 ++++++------ src/index.ts | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index cae84078..40b36a66 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -35,6 +35,11 @@ export default { name: 'image', component: () => import('tdesign-mobile-react/image/_example/index.jsx'), }, + { + title: 'ImageViewer 图片预览', + name: 'image-viewer', + component: () => import('tdesign-mobile-react/image-viewer/_example/index.jsx'), + }, { title: 'Popup 弹出层', name: 'popup', diff --git a/site/web/site.config.js b/site/web/site.config.js index 4a577158..80569198 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -245,12 +245,12 @@ export default { // path: '/mobile-react/components/list', // component: () => import('tdesign-mobile-react/list/list.md'), // }, - // { - // title: 'ImageViewer 图片预览', - // name: 'image-viewer', - // path: '/mobile-react/components/image-viewer', - // component: () => import('tdesign-mobile-react/image-viewer/image-viewer.md'), - // }, + { + title: 'ImageViewer 图片预览', + name: 'image-viewer', + path: '/mobile-react/components/image-viewer', + component: () => import('tdesign-mobile-react/image-viewer/image-viewer.md'), + }, { title: 'Skeleton 骨架屏', name: 'skeleton', diff --git a/src/index.ts b/src/index.ts index b9f37c7d..b19d22c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ export * from './cell-group'; export * from './count-down'; export * from './grid'; export * from './image'; +export * from './image-viewer'; export * from './skeleton'; export * from './sticky'; export * from './swiper';