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) => (
+
+
+
![]({item.url})
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';