Repository: Tencent/tdesign-mobile-vue
Pull request #259 (2022-07-31 13:47:05)

feat(image-viewer): 重构 image-viewer 组件
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [ ] 日常 bug 修复
- [x] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- fix(组件名称): 处理问题或特性描述 ...

- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [x] 文档已补充或无须补充
- [x] 代码演示已提供或无须提供
- [x] TypeScript 定义已补充或无须补充
- [x] Changelog 已提供或无须提供



Patch:
diff --git a/src/image-viewer/demos/base.vue b/src/image-viewer/demos/base.vue
index a92d04e8..73a19817 100644
--- a/src/image-viewer/demos/base.vue
+++ b/src/image-viewer/demos/base.vue
@@ -1,11 +1,9 @@
 <template>
   <div>
-    <t-cell-group class="dialog-type-title" title="显示和隐藏">
+    <t-cell-group class="dialog-type-title" title="基础用法">
       <t-cell value-align="left">
-        <t-button theme="primary" @click="toggle">
-          {{ showViewer ? '隐藏' : '显示' }}
-        </t-button>
-        <t-image-viewer v-model="showViewer" :images="images" :initial-index="0"></t-image-viewer>
+        <t-button block size="large" variant="outline" @click="tapShow"> 基础用法 </t-button>
+        <t-image-viewer v-model:images="images" v-model:visible="showViewer"></t-image-viewer>
       </t-cell>
     </t-cell-group>
   </div>
@@ -15,15 +13,17 @@ import { ref, defineComponent, Ref } from 'vue';
 
 export default defineComponent({
   setup() {
-    const images: Ref<Array<string>> = ref([]);
+    const images: Ref<Array<string>> = ref([
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+    ]);
     const showViewer = ref(false);
-    function toggle() {
-      showViewer.value = !showViewer.value;
-      images.value = [
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-      ];
+
+    function tapShow() {
+      showViewer.value = true;
     }
-    return { images, toggle, showViewer };
+    return { images, showViewer, tapShow };
   },
 });
 </script>
diff --git a/src/image-viewer/demos/closeBtn.vue b/src/image-viewer/demos/closeBtn.vue
new file mode 100644
index 00000000..ff7c1577
--- /dev/null
+++ b/src/image-viewer/demos/closeBtn.vue
@@ -0,0 +1,29 @@
+<template>
+  <div>
+    <t-cell-group class="dialog-type-title" title="不展示关闭按钮">
+      <t-cell value-align="left">
+        <t-button block size="large" variant="outline" @click="tapShow">不展示关闭按钮</t-button>
+        <t-image-viewer v-model:images="images" v-model:visible="showViewer" :close-btn="false"></t-image-viewer>
+      </t-cell>
+    </t-cell-group>
+  </div>
+</template>
+<script lang="ts">
+import { ref, defineComponent, Ref } from 'vue';
+
+export default defineComponent({
+  setup() {
+    const images: Ref<Array<string>> = ref([
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+    ]);
+    const showViewer = ref(false);
+
+    function tapShow() {
+      showViewer.value = true;
+    }
+    return { images, showViewer, tapShow };
+  },
+});
+</script>
diff --git a/src/image-viewer/demos/closeEvent.vue b/src/image-viewer/demos/closeEvent.vue
new file mode 100644
index 00000000..1bdd9094
--- /dev/null
+++ b/src/image-viewer/demos/closeEvent.vue
@@ -0,0 +1,33 @@
+<template>
+  <div>
+    <t-cell-group class="dialog-type-title" title="监听关闭事件">
+      <t-cell value-align="left">
+        <t-button block size="large" variant="outline" @click="tapShow">监听关闭事件</t-button>
+        <t-image-viewer v-model:images="images" v-model:visible="showViewer" @close="onClose"></t-image-viewer>
+      </t-cell>
+    </t-cell-group>
+  </div>
+</template>
+<script lang="ts">
+import { ref, defineComponent, Ref } from 'vue';
+
+export default defineComponent({
+  setup() {
+    const images: Ref<Array<string>> = ref([
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+    ]);
+    const showViewer = ref(false);
+
+    function tapShow() {
+      showViewer.value = true;
+    }
+
+    function onClose(context: { trigger: 'close-btn' | 'overlay' | 'esc'; e: MouseEvent | KeyboardEvent }) {
+      console.log('onClose: ', context);
+    }
+    return { images, showViewer, tapShow, onClose };
+  },
+});
+</script>
diff --git a/src/image-viewer/demos/mobile.vue b/src/image-viewer/demos/mobile.vue
index dceaf014..afe0ca10 100644
--- a/src/image-viewer/demos/mobile.vue
+++ b/src/image-viewer/demos/mobile.vue
@@ -4,68 +4,47 @@
     <p class="summary">图片全屏放大预览效果，包含全屏背景色、页码位置样式、增加操作等规范</p>
     <tdesign-demo-block title="01 类型" summary="图片预览类型">
       <div class="image-viewer-demo">
-        <t-button block size="large" variant="outline" @click="tapShow"> 显示 </t-button>
-        <t-button block size="large" variant="outline" @click="tapImgList"> 图片列表 </t-button>
-        <t-button block size="large" variant="outline" @click="tapInitIndex"> 默认显示第二张图片 </t-button>
-        <t-button block size="large" variant="outline" @click="tapBackGround"> 显示 </t-button>
+        <t-button block size="large" variant="outline" @click="tapShowBasic"> 基础用法 </t-button>
+        <t-button block size="large" variant="outline" @click="tapShowCloseBtn"> 不展示关闭按钮 </t-button>
+        <t-button block size="large" variant="outline" @click="tapShowCloseEvent"> 监听关闭事件 </t-button>
       </div>
     </tdesign-demo-block>
-    <t-image-viewer
-      v-model:images="images"
-      v-model="showViewer"
-      :initial-index="initialIndex"
-      :background-color="color"
-      @change="change"
-    ></t-image-viewer>
+    <t-image-viewer v-model:images="images" v-model:visible="showViewer"></t-image-viewer>
+    <t-image-viewer v-model:images="images" v-model:visible="showViewer2" :close-btn="false"></t-image-viewer>
+    <t-image-viewer v-model:images="images" v-model:visible="showViewer3" @close="onClose"></t-image-viewer>
   </div>
 </template>
 <script lang="ts">
 import { ref, defineComponent, Ref } from 'vue';
+import { TdImageViewerProps } from '../type';
 
 export default defineComponent({
   setup() {
-    const images: Ref<Array<string>> = ref([]);
+    const images: Ref<Array<string>> = ref([
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+      'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
+    ]);
     const showViewer = ref(false);
-    const initialIndex = ref(0);
-    const color = ref('rgba(0, 0, 0, .6)');
+    const showViewer2 = ref(false);
+    const showViewer3 = ref(false);
 
-    function tapShow() {
+    function tapShowBasic() {
       showViewer.value = true;
-      images.value = [
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-      ];
     }
-    function tapImgList() {
-      showViewer.value = true;
-      initialIndex.value = 0;
-      images.value = [
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-      ];
+    function tapShowCloseBtn() {
+      showViewer2.value = true;
     }
 
-    function tapInitIndex() {
-      showViewer.value = true;
-      images.value = [
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-      ];
-      initialIndex.value = 1;
-    }
-    function tapBackGround() {
-      showViewer.value = true;
-      images.value = [
-        'https://imgcache.qq.com/open_proj/proj_qcloud_v2/rocket_images/1606728019829_yw760ok1jmpbep14i.png',
-      ];
-      color.value = 'rgba(0, 0, 0, 1)';
+    function tapShowCloseEvent() {
+      showViewer3.value = true;
     }
-    /** 翻页页数变化 */
-    function change(page: number) {
-      console.log(`当前页数${page}`);
+
+    function onClose(context: any) {
+      console.log('onClose: ', context);
     }
-    return { tapShow, tapImgList, tapInitIndex, tapBackGround, color, initialIndex, images, showViewer, change };
+
+    return { images, showViewer, showViewer2, showViewer3, tapShowBasic, tapShowCloseBtn, tapShowCloseEvent, onClose };
   },
 });
 </script>
diff --git a/src/image-viewer/image-viewer.md b/src/image-viewer/image-viewer.md
index 0f1d5612..59f4b1ba 100644
--- a/src/image-viewer/image-viewer.md
+++ b/src/image-viewer/image-viewer.md
@@ -1,17 +1,23 @@
 :: BASE_DOC ::
 
 ## API
+### ImageViewer Props
 
-| 属性            | 类型            | 默认值            | 必传 | 说明           |
-| --------------- | --------------- | ----------------- | ---- | -------------- |
-| images          | `Array<String>` | []                | Y    | 图片数组       |
-| visible         | Boolean         | false             | N    | 隐藏/显示预览  |
-| showIndex       | Boolean         | true              | N    | 是否显示页码   |
-| initialIndex    | Number          | 0                 | N    | 默认展示第几项 |
-| backgroundColor | String          | rgba(0, 0, 0, .6) | N    | 遮罩的背景颜色 |
+名称 | 类型 | 默认值 | 说明 | 必传
+-- | -- | -- | -- | --
+closeBtn | Boolean / Slot / Function | true | 是否展示关闭按钮，值为 `true` 显示默认关闭按钮；值为 `false` 则不显示关闭按钮；也可以完全自定义关闭按钮。TS 类型：`boolean | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+images | Array | [] | 图片数组。TS 类型：`Array<string>` | N
+initialIndex | Number | 0 | 初始化页码。TS 类型：`Number` | N
+maxZoom | Number | 3 | 最大放大比例。TS 类型：`Number` | N
+showIndex | Boolean | false | 是否显示页码 | N
+visible | Boolean | false | 隐藏/显示预览。支持语法糖 `v-model` 或 `v-model:visible` | N
+defaultVisible | Boolean | false | 隐藏/显示预览。非受控属性 | N
+onClose | Function |  | TS 类型：`(context: { trigger: 'close-btn' | 'overlay' | 'esc'; e: MouseEvent | KeyboardEvent }) => void`<br/>关闭时触发，事件参数包含触发关闭的来源：关闭按钮、遮罩层、ESC 键 | N
+onIndexChange | Function |  | TS 类型：`(index: number, context: { trigger: 'prev' | 'next' }) => void`<br/>预览图片切换时触发，`context.prev` 切换到上一张图片，`context.next` 切换到下一张图片 | N
 
-## Events
+### ImageViewer Events
 
-| 事件名称 | 参数 | 说明       |
-| -------- | ---- | ---------- |
-| change   | -    | 翻页时回调 |
+名称 | 参数 | 描述
+-- | -- | --
+close | `(context: { trigger: 'close-btn' | 'overlay' | 'esc'; e: MouseEvent | KeyboardEvent })` | 关闭时触发，事件参数包含触发关闭的来源：关闭按钮、遮罩层、ESC 键
+index-change | `(index: number, context: { trigger: 'prev' | 'next' })` | 预览图片切换时触发，`context.prev` 切换到上一张图片，`context.next` 切换到下一张图片
diff --git a/src/image-viewer/image-viewer.vue b/src/image-viewer/image-viewer.vue
index 301823d8..2abe8b07 100644
--- a/src/image-viewer/image-viewer.vue
+++ b/src/image-viewer/image-viewer.vue
@@ -1,241 +1,250 @@
 <template>
-  <div v-show="showViewer" :class="`${prefix}-image-viewer`">
-    <div :class="`${prefix}-image-mask`" :style="{ background: backgroundColor }"></div>
-    <div class="image-viewer-container">
-      <div
-        :ref="myRef"
-        :class="classes"
-        @touchstart="onTouchstart"
-        @touchmove="onTouchMove"
-        @touchend="onTouchEnd"
-        @touchcancel="onTouchEnd"
-      >
-        <div :class="`${prefix}-swiper-box`">
-          <div :class="`${prefix}-swiper-container`">
-            <div
-              v-for="(item, itemIndex) in images"
-              :key="itemIndex"
-              :class="`${prefix}-swiper-item image-viewer-item-wrap`"
-            >
-              <div class="item">
-                <img :src="item" />
-              </div>
-            </div>
-          </div>
-        </div>
+  <t-overlay :class="`${prefix}-image-viewer`" :visible="visible">
+    <template v-if="lazyVisible">
+      <div :class="`${name}__close-icon`" @click="handleClose($event, 'close-btn')">
+        <t-node v-if="!(typeof closeBtnTNode === 'boolean')" :content="closeBtnTNode"></t-node>
+        <close-circle-filled-icon v-else-if="typeof closeBtn === 'boolean' && closeBtn" />
       </div>
-      <div v-if="showIndex" class="image-viewer-index">{{ index }}/{{ count > 2 ? count - 2 : count }}</div>
-    </div>
-  </div>
+      <t-swiper
+        :autoplay="false"
+        :class="`${name}__swipe`"
+        :default-current="initialIndex"
+        :navigation="navigation"
+        :on-change="onSwiperChange"
+      >
+        <t-swiper-item
+          v-for="(image, index) in images"
+          :key="image + index"
+          :class="`${name}__swipe-item`"
+          @touchstart="onTouchStart"
+          @touchmove="onTouchMove"
+          @touchend="onTouchEnd"
+        >
+          <img :src="image" :style="imageStyle" :class="`${name}__image`" />
+        </t-swiper-item>
+      </t-swiper>
+    </template>
+  </t-overlay>
 </template>
 
 <script lang="ts">
-import { computed, toRefs, ref, defineComponent, reactive, watch, nextTick, PropType } from 'vue';
+import {
+  computed,
+  toRefs,
+  ref,
+  defineComponent,
+  reactive,
+  watch,
+  nextTick,
+  PropType,
+  getCurrentInstance,
+  CSSProperties,
+  SetupContext,
+} from 'vue';
+import { CloseCircleFilledIcon } from 'tdesign-icons-vue-next';
 import config from '../config';
-import { DragState } from './image-viewer.interface';
+import ImageViewerProps from './props';
+import { renderTNode, TNode, useEmitEvent, useDefault, useTouch } from '../shared';
+import { TdImageViewerProps } from './type';
+import { Toast } from '..';
 
+export type TriggerType = 'close-btn' | 'overlay' | 'esc';
 const { prefix } = config;
 const name = `${prefix}-image-viewer`;
 
+/*
+initialIndex, 因 swiper 不支持 defaultCurrent
+
+
+onIndexChange, 因 swiper 未提供 'prev' | 'next'， 所以没有 context
+*/
+const getDistance = (touches: TouchList) =>
+  Math.sqrt((touches[0].clientX - touches[1].clientX) ** 2 + (touches[0].clientY - touches[1].clientY) ** 2);
+
 export default defineComponent({
   name,
-  props: {
-    modelValue: {
-      type: Boolean,
-      default: false,
-    },
-    images: {
-      type: Array as PropType<Array<string>>,
-      default: () => [],
-    },
-    visible: {
-      type: Boolean,
-      default: false,
-    },
-    showIndex: {
-      type: Boolean,
-      default: true,
-    },
-    initialIndex: {
-      type: Number,
-      default: 0,
-    },
-    backgroundColor: {
-      type: String,
-      default: 'rgba(0, 0, 0, 0.6)',
-    },
+  components: {
+    CloseCircleFilledIcon,
+    TNode,
   },
-  emits: ['update:modelValue', 'change'],
-  setup(props, context) {
-    const classes = computed(() => [`${name}`]);
-    const dragState: DragState = reactive({
-      startTime: new Date().getTime(),
-      startLeft: -1,
-      startTop: -1,
-      itemWidth: -1,
-      itemHeight: -1,
-      currentLeft: -1,
-      currentTop: -1,
+  props: ImageViewerProps,
+  emits: ['close', 'index-change', 'update:visible', 'update:modelValue', 'change'],
+  setup(props, context: SetupContext) {
+    const state = reactive({
+      zooming: false,
+      scale: 1,
+    });
+    const emitEvent = useEmitEvent(props, context.emit);
+    const [visible, setVisible] = useDefault<TdImageViewerProps['visible'], TdImageViewerProps>(
+      props,
+      context.emit,
+      'visible',
+      'change',
+    );
+    // 因 Overlay 未提供lazy属性，先暂时自行实现
+    const lazyVisible = ref(visible.value);
+    const touch = useTouch();
+    const internalInstance = getCurrentInstance();
+    const closeBtnTNode = computed(() => {
+      return renderTNode(internalInstance, 'closeBtn');
+    });
+    const navigation = computed(() => {
+      if (props.showIndex) {
+        return { type: 'fraction' };
+      }
+      return { type: 'dots', showSlideBtn: false };
     });
 
-    let eleSwiper: HTMLDivElement;
-    const images = ref(props.images);
-    const index = ref(Math.min(props.initialIndex, images.value.length - 1) + 1);
-    const count = ref(images.value.length);
-    const PAGING_DURATION = 300;
-    const PAGING_SCALE = 0.5;
-
-    let element: HTMLDivElement;
+    const imageStyle = computed(() => {
+      const { scale, zooming } = state;
+      const style: CSSProperties = {
+        transitionDuration: zooming ? '0s' : '.3s',
+      };
 
-    const myRef = (el: any) => {
-      element = el;
-    };
-    const showViewer = computed(() => props.modelValue || props.visible);
-    watch([() => props.images, () => props.initialIndex], () => {
-      images.value = props.images;
-      index.value = Math.min(props.initialIndex, images.value.length - 1) + 1;
-      if (!showViewer.value) {
-        return;
+      if (scale !== 1) {
+        style.transform = `scale(${scale}, ${scale})`;
       }
-      nextTick(() => {
-        eleSwiper = element.querySelector(`.${prefix}-swiper-container`) as HTMLDivElement;
-        initNode();
-      });
+
+      return style;
     });
-    function initNode() {
-      const clientWidth = `${element.clientWidth}px`;
-      const container = element.querySelector(`.${prefix}-swiper-container`) as HTMLDivElement;
-      const children = container.querySelectorAll(`.${prefix}-swiper-item`) as NodeListOf<HTMLDivElement>;
-      children.forEach((el) => {
-        // eslint-disable-next-line no-param-reassign
-        el.style.width = clientWidth;
-      });
-      if (children.length >= 1) {
-        const firstNodeCopy = container.querySelector(`.${prefix}-swiper-item-first-copy`);
-        const lastNodeCopy = container.querySelector(`.${prefix}-swiper-item-last-copy`);
-        if (firstNodeCopy && container.lastElementChild === firstNodeCopy) {
-          container.removeChild(firstNodeCopy);
-        }
-        if (lastNodeCopy && container.firstElementChild === lastNodeCopy) {
-          container.removeChild(lastNodeCopy);
-        }
-        let firstNode = container.firstElementChild as HTMLDivElement;
-        firstNode = firstNode.cloneNode(true) as HTMLDivElement;
-        let lastNode = container.lastElementChild as HTMLDivElement;
-        lastNode = lastNode.cloneNode(true) as HTMLDivElement;
-        firstNode.classList.add(`${prefix}-swiper-item-first-copy`);
-        lastNode.classList.add(`${prefix}-swiper-item-last-copy`);
-        firstNode.style.width = clientWidth;
-        lastNode.style.width = clientWidth;
-        container.appendChild(firstNode);
-        container.insertBefore(lastNode, container.firstElementChild);
-        count.value = images.value.length + 2;
-        setTransform(-element.clientWidth * index.value);
-      }
-    }
-    function setTransform(left: number) {
-      eleSwiper.style.transform = `translate3d(${left}px, 0px, 0px) scale(1)`;
-    }
-    function getPoint(event: TouchEvent) {
-      const point: Touch = (event as TouchEvent).changedTouches[0];
-      return point;
-    }
-    let moveLock = true;
-    const onTouchstart = (event: TouchEvent) => {
-      const point = getPoint(event);
-      dragState.startTime = new Date().getTime();
-      dragState.startLeft = point.pageX;
-      dragState.startTop = point.pageY;
-      dragState.itemWidth = element.offsetWidth;
-      dragState.itemHeight = element.offsetHeight;
-      if (count.value <= 1) {
-        return;
+
+    const handleClose = (e: Event, trigger: TriggerType) => {
+      setVisible(false);
+      emitEvent('close', { trigger, e });
+    };
+
+    const onSwiperChange = (index: number, context: any) => {
+      emitEvent('index-change', index);
+    };
+
+    let fingerNum: number;
+    let startScale: number;
+    let startDistance: number;
+    let doubleTapTimer: number | null;
+    let touchStartTime: number;
+    const onTouchStart = (event: TouchEvent) => {
+      event.preventDefault();
+      event.stopPropagation();
+      const { touches } = event;
+      const { offsetX } = touch;
+
+      touch.start(event);
+
+      fingerNum = touches.length;
+      touchStartTime = Date.now();
+      state.zooming = fingerNum === 2;
+      if (state.zooming) {
+        startScale = state.scale;
+        startDistance = getDistance(event.touches);
       }
-      moveLock = false;
-      eleSwiper.style.transition = 'none';
     };
+
     const onTouchMove = (event: TouchEvent) => {
-      if (count.value <= 3 || moveLock) {
-        return;
-      }
-      const point = getPoint(event);
-      dragState.currentLeft = point.pageX;
-      dragState.currentTop = point.pageY;
-      const offsetLeft = dragState.currentLeft - dragState.startLeft;
-      const offsetTop = dragState.currentTop - dragState.startTop;
+      const { touches } = event;
+
+      touch.move(event);
       event.preventDefault();
-      const newOffsetLeft = Math.min(Math.max(-dragState.itemWidth + 1, offsetLeft), dragState.itemWidth - 1);
-      const offset = newOffsetLeft - dragState.itemWidth * index.value;
-      setTransform(offset);
-    };
-    const onTouchEnd = (event: TouchEvent) => {
-      const point = getPoint(event);
-      if (dragState.startLeft === point.pageX) {
-        moveLock = true;
-        context.emit('update:modelValue', false);
-        return;
+      event.stopPropagation();
+      if (state.zooming) {
+        event.preventDefault();
+        event.stopPropagation();
       }
-      if (count.value <= 3 || moveLock) {
-        return;
+      if (state.zooming && touches.length === 2) {
+        const distance = getDistance(touches);
+        const scale = (startScale * distance) / startDistance;
+
+        setScale(scale);
+      }
+    };
+
+    const setScale = (scale: number) => {
+      scale = Math.min(scale, +props.maxZoom + 1);
+
+      if (scale !== state.scale) {
+        state.scale = scale;
       }
-      moveLock = true;
-      const dragDuration = Math.min(new Date().getTime() - dragState.startTime, 500);
-      const offsetLeft = dragState.currentLeft - dragState.startLeft;
-      const { itemWidth } = dragState;
-      const isFastDrag = dragDuration < PAGING_DURATION;
-      let action = '';
-      if (isFastDrag && dragState.currentLeft === -1) {
+    };
+    const resetScale = () => {
+      setScale(1);
+    };
+
+    const toggleScale = () => {
+      const scale = state.scale > 1 ? 1 : 2;
+
+      setScale(scale);
+    };
+
+    const checkTap = (event: Event) => {
+      if (fingerNum > 1) {
         return;
       }
-      if (Math.abs(offsetLeft) > itemWidth * PAGING_SCALE || isFastDrag) {
-        if (offsetLeft < 0) {
-          setTransform(-element.clientWidth * (index.value + 1));
-          // index.value += 1;
-          action = 'next';
+
+      const { offsetX, offsetY } = touch;
+      const deltaTime = Date.now() - touchStartTime;
+      const TAP_TIME = 250;
+      const TAP_OFFSET = 5;
+
+      if (offsetX.value < TAP_OFFSET && offsetY.value < TAP_OFFSET && deltaTime < TAP_TIME) {
+        if (doubleTapTimer) {
+          clearTimeout(doubleTapTimer);
+          doubleTapTimer = null;
+          toggleScale();
         } else {
-          setTransform(-element.clientWidth * (index.value - 1));
-          // index.value -= 1;
-          action = 'prev';
+          doubleTapTimer = window.setTimeout(() => {
+            handleClose(event, 'overlay');
+            doubleTapTimer = null;
+          }, TAP_TIME);
         }
-      } else {
-        setTransform(-element.clientWidth * index.value);
       }
-      eleSwiper.style.transition = `${dragDuration / 1000}s`;
+    };
 
-      setTimeout(() => {
-        if (action) {
-          action === 'next' ? (index.value += 1) : (index.value -= 1);
-        }
-        // 翻到最后一个
-        if (index.value === count.value - 1) {
-          index.value = 1;
-          // 翻到第一个
-        } else if (index.value === 0) {
-          index.value = count.value - 2;
+    const onTouchEnd = (event: TouchEvent) => {
+      // eliminate tap delay on safari
+      event.preventDefault();
+
+      if (state.zooming) {
+        event.stopPropagation();
+        if (!event.touches.length) {
+          if (state.zooming) {
+            state.zooming = false;
+          }
+          startScale = 1;
+          if (state.scale < 1) {
+            resetScale();
+          }
+          if (state.scale > props.maxZoom) {
+            state.scale = +props.maxZoom;
+          }
         }
-        context.emit('change', index.value - 1);
-        setTransform(-element.clientWidth * index.value);
-        eleSwiper.style.transition = 'none';
-      }, dragDuration);
-      Object.assign(dragState, {
-        startTime: new Date().getTime(),
-        startLeft: 0,
-        startTop: 0,
-        itemWidth: 0,
-        itemHeight: 0,
-        currentLeft: 0,
-        currentTop: 0,
-      });
+      }
+
+      checkTap(event);
+      touch.reset();
     };
+
+    watch(
+      () => visible.value,
+      (value) => {
+        if (!value) {
+          resetScale();
+        }
+        setTimeout(() => {
+          lazyVisible.value = value;
+        }, 300);
+      },
+    );
     return {
+      name,
       prefix,
-      showViewer,
-      index,
-      count,
-      myRef,
-      classes,
+      closeBtnTNode,
+      navigation,
+      imageStyle,
+      lazyVisible,
       ...toRefs(props),
-      onTouchstart,
+      visible,
+      handleClose,
+      onSwiperChange,
+      onTouchStart,
       onTouchMove,
       onTouchEnd,
     };
diff --git a/src/image-viewer/props.ts b/src/image-viewer/props.ts
new file mode 100644
index 00000000..7166cf2e
--- /dev/null
+++ b/src/image-viewer/props.ts
@@ -0,0 +1,48 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdImageViewerProps } from './type';
+import { PropType } from 'vue';
+
+export default {
+  /** 是否展示关闭按钮，值为 `true` 显示默认关闭按钮；值为 `false` 则不显示关闭按钮；也可以完全自定义关闭按钮 */
+  closeBtn: {
+    type: [Boolean, Function] as PropType<TdImageViewerProps['closeBtn']>,
+    default: true,
+  },
+  /** 图片数组 */
+  images: {
+    type: Array as PropType<TdImageViewerProps['images']>,
+    default: (): TdImageViewerProps['images'] => [],
+  },
+  /** 初始化页码 */
+  initialIndex: {
+    type: Number,
+    default: 0,
+  },
+  /** 最大放大比例 */
+  maxZoom: {
+    type: Number,
+    default: 3,
+  },
+  /** 是否显示页码 */
+  showIndex: Boolean,
+  /** 隐藏/显示预览 */
+  visible: {
+    type: Boolean,
+    default: undefined,
+  },
+  modelValue: {
+    type: Boolean,
+    default: undefined,
+  },
+  /** 隐藏/显示预览，非受控属性 */
+  defaultVisible: Boolean,
+  /** 关闭时触发，事件参数包含触发关闭的来源：关闭按钮、遮罩层、ESC 键 */
+  onClose: Function as PropType<TdImageViewerProps['onClose']>,
+  /** 预览图片切换时触发，`context.prev` 切换到上一张图片，`context.next` 切换到下一张图片 */
+  onIndexChange: Function as PropType<TdImageViewerProps['onIndexChange']>,
+};
diff --git a/src/image-viewer/type.ts b/src/image-viewer/type.ts
new file mode 100644
index 00000000..10131eea
--- /dev/null
+++ b/src/image-viewer/type.ts
@@ -0,0 +1,58 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件，请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TNode } from '../common';
+
+export interface TdImageViewerProps {
+  /**
+   * 是否展示关闭按钮，值为 `true` 显示默认关闭按钮；值为 `false` 则不显示关闭按钮；也可以完全自定义关闭按钮
+   * @default true
+   */
+  closeBtn?: boolean | TNode;
+  /**
+   * 图片数组
+   * @default []
+   */
+  images?: Array<string>;
+  /**
+   * 初始化页码
+   * @default 0
+   */
+  initialIndex?: Number;
+  /**
+   * 最大放大比例
+   * @default 3
+   */
+  maxZoom?: Number;
+  /**
+   * 是否显示页码
+   * @default false
+   */
+  showIndex?: boolean;
+  /**
+   * 隐藏/显示预览
+   * @default false
+   */
+  visible?: boolean;
+  /**
+   * 隐藏/显示预览，非受控属性
+   * @default false
+   */
+  defaultVisible?: boolean;
+  /**
+   * 隐藏/显示预览
+   * @default false
+   */
+  modelValue?: boolean;
+  /**
+   * 关闭时触发，事件参数包含触发关闭的来源：关闭按钮、遮罩层、ESC 键
+   */
+  onClose?: (context: { trigger: 'close-btn' | 'overlay' | 'esc'; e: MouseEvent | KeyboardEvent }) => void;
+  /**
+   * 预览图片切换时触发，`context.prev` 切换到上一张图片，`context.next` 切换到下一张图片
+   */
+  onIndexChange?: (index: number, context: { trigger: 'prev' | 'next' }) => void;
+}
diff --git a/src/shared/index.ts b/src/shared/index.ts
index 0cf46e94..4a2f7270 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -12,3 +12,4 @@ export * from './useDefault';
 export * from './useEmitEvent';
 export * from './useChildSlots';
 export * from './useVModel';
+export * from './useTouch';
diff --git a/src/shared/useTouch/index.ts b/src/shared/useTouch/index.ts
new file mode 100644
index 00000000..2c77635f
--- /dev/null
+++ b/src/shared/useTouch/index.ts
@@ -0,0 +1,70 @@
+import { ref } from 'vue';
+
+type Direction = '' | 'vertical' | 'horizontal';
+
+function getDirection(x: number, y: number) {
+  if (x > y) {
+    return 'horizontal';
+  }
+  if (y > x) {
+    return 'vertical';
+  }
+  return '';
+}
+
+export function useTouch() {
+  const startX = ref(0);
+  const startY = ref(0);
+  const deltaX = ref(0);
+  const deltaY = ref(0);
+  const offsetX = ref(0);
+  const offsetY = ref(0);
+  const direction = ref<Direction>('');
+
+  const isVertical = () => direction.value === 'vertical';
+  const isHorizontal = () => direction.value === 'horizontal';
+
+  const reset = () => {
+    deltaX.value = 0;
+    deltaY.value = 0;
+    offsetX.value = 0;
+    offsetY.value = 0;
+    direction.value = '';
+  };
+
+  const start = ((event: TouchEvent) => {
+    reset();
+    startX.value = event.touches[0].clientX;
+    startY.value = event.touches[0].clientY;
+  }) as EventListener;
+
+  const move = ((event: TouchEvent) => {
+    const touch = event.touches[0];
+    // safari back will set clientX to negative number
+    deltaX.value = (touch.clientX < 0 ? 0 : touch.clientX) - startX.value;
+    deltaY.value = touch.clientY - startY.value;
+    offsetX.value = Math.abs(deltaX.value);
+    offsetY.value = Math.abs(deltaY.value);
+
+    // lock direction when distance is greater than a certain value
+    const LOCK_DIRECTION_DISTANCE = 10;
+    if (!direction.value || (offsetX.value < LOCK_DIRECTION_DISTANCE && offsetY.value < LOCK_DIRECTION_DISTANCE)) {
+      direction.value = getDirection(offsetX.value, offsetY.value);
+    }
+  }) as EventListener;
+
+  return {
+    move,
+    start,
+    reset,
+    startX,
+    startY,
+    deltaX,
+    deltaY,
+    offsetX,
+    offsetY,
+    direction,
+    isVertical,
+    isHorizontal,
+  };
+}
diff --git a/src/swiper/swiper.vue b/src/swiper/swiper.vue
index a2f84b03..569cf207 100644
--- a/src/swiper/swiper.vue
+++ b/src/swiper/swiper.vue
@@ -33,7 +33,7 @@
         </span>
       </span>
     </template>
-    <template v-if="computedNavigation !== undefined">
+    <template v-else-if="computedNavigation !== undefined">
       <t-node :content="computedNavigation" :style="{}"></t-node>
     </template>
   </div>
@@ -214,7 +214,7 @@ export default defineComponent({
       }, props?.interval);
     };
     const emitCurrentChange = (index: number) => {
-      if (!state.isControl) return false;
+      // if (!state.isControl) return false;
       let resultIndex = index;
       if (index >= state.itemLength) resultIndex = 0;
       if (index < 0) resultIndex = state.itemLength - 1;


Test patch:
diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap
index fc426951..7ea62b04 100644
--- a/test/snap/__snapshots__/csr.test.js.snap
+++ b/test/snap/__snapshots__/csr.test.js.snap
@@ -27639,38 +27639,18 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/background.vue 1`
               
             </span>
           </button>
-          <div
+          <transition-stub
+            background-color="rgba(0, 0, 0, 1)"
             class="t-image-viewer"
-            style="display: none;"
           >
             <div
-              class="t-image-mask"
-              style="background: rgb(0, 0, 0);"
-            />
-            <div
-              class="image-viewer-container"
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
             >
-              <div
-                class="t-image-viewer"
-              >
-                <div
-                  class="t-swiper-box"
-                >
-                  <div
-                    class="t-swiper-container"
-                  >
-                    
-                    
-                  </div>
-                </div>
-              </div>
-              <div
-                class="image-viewer-index"
-              >
-                0/0
-              </div>
+              
+              
             </div>
-          </div>
+          </transition-stub>
           
         </div>
         <!--v-if-->
@@ -27690,7 +27670,7 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/base.vue 1`] = `
       class="t-cell-group__title"
     >
       
-      显示和隐藏
+      基础用法
       
     </div>
     <div
@@ -27714,7 +27694,7 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/base.vue 1`] = `
           
           <button
             aria-disabled="false"
-            class="t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle"
+            class="t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block"
             role="button"
           >
             <!---->
@@ -27722,42 +27702,153 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/base.vue 1`] = `
               class="t-button__text"
             >
               
-              显示
+               基础用法 
               
             </span>
           </button>
-          <div
+          <transition-stub
             class="t-image-viewer"
-            style="display: none;"
           >
             <div
-              class="t-image-mask"
-              style="background: rgba(0, 0, 0, 0.6);"
-            />
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
+            >
+              
+              
+            </div>
+          </transition-stub>
+          
+        </div>
+        <!--v-if-->
+      </div>
+      
+    </div>
+  </div>
+</div>
+`;
+
+exports[`csr snapshot test > csr test ./src/image-viewer/demos/closeBtn.vue 1`] = `
+<div>
+  <div
+    class="t-cell-group dialog-type-title"
+  >
+    <div
+      class="t-cell-group__title"
+    >
+      
+      不展示关闭按钮
+      
+    </div>
+    <div
+      class="t-cell-group__container"
+    >
+      
+      <div
+        class="t-cell t-cell--middle t-cell--bordered"
+        value-align="left"
+      >
+        <div
+          class="t-cell__left-icon"
+        >
+          <!--v-if-->
+          <!--v-if-->
+        </div>
+        <!--v-if-->
+        <div
+          class="t-cell__note"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block"
+            role="button"
+          >
+            <!---->
+            <span
+              class="t-button__text"
+            >
+              
+              不展示关闭按钮
+              
+            </span>
+          </button>
+          <transition-stub
+            class="t-image-viewer"
+          >
             <div
-              class="image-viewer-container"
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
             >
-              <div
-                class="t-image-viewer"
-              >
-                <div
-                  class="t-swiper-box"
-                >
-                  <div
-                    class="t-swiper-container"
-                  >
-                    
-                    
-                  </div>
-                </div>
-              </div>
-              <div
-                class="image-viewer-index"
-              >
-                0/0
-              </div>
+              
+              
             </div>
-          </div>
+          </transition-stub>
+          
+        </div>
+        <!--v-if-->
+      </div>
+      
+    </div>
+  </div>
+</div>
+`;
+
+exports[`csr snapshot test > csr test ./src/image-viewer/demos/closeEvent.vue 1`] = `
+<div>
+  <div
+    class="t-cell-group dialog-type-title"
+  >
+    <div
+      class="t-cell-group__title"
+    >
+      
+      监听关闭事件
+      
+    </div>
+    <div
+      class="t-cell-group__container"
+    >
+      
+      <div
+        class="t-cell t-cell--middle t-cell--bordered"
+        value-align="left"
+      >
+        <div
+          class="t-cell__left-icon"
+        >
+          <!--v-if-->
+          <!--v-if-->
+        </div>
+        <!--v-if-->
+        <div
+          class="t-cell__note"
+        >
+          
+          <button
+            aria-disabled="false"
+            class="t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block"
+            role="button"
+          >
+            <!---->
+            <span
+              class="t-button__text"
+            >
+              
+              监听关闭事件
+              
+            </span>
+          </button>
+          <transition-stub
+            class="t-image-viewer"
+          >
+            <div
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
+            >
+              
+              
+            </div>
+          </transition-stub>
           
         </div>
         <!--v-if-->
@@ -27813,38 +27904,17 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/imageList.vue 1`]
               
             </span>
           </button>
-          <div
+          <transition-stub
             class="t-image-viewer"
-            style="display: none;"
           >
             <div
-              class="t-image-mask"
-              style="background: rgba(0, 0, 0, 0.6);"
-            />
-            <div
-              class="image-viewer-container"
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
             >
-              <div
-                class="t-image-viewer"
-              >
-                <div
-                  class="t-swiper-box"
-                >
-                  <div
-                    class="t-swiper-container"
-                  >
-                    
-                    
-                  </div>
-                </div>
-              </div>
-              <div
-                class="image-viewer-index"
-              >
-                0/0
-              </div>
+              
+              
             </div>
-          </div>
+          </transition-stub>
           
         </div>
         <!--v-if-->
@@ -27900,38 +27970,17 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/initialIndex.vue
               
             </span>
           </button>
-          <div
+          <transition-stub
             class="t-image-viewer"
-            style="display: none;"
           >
             <div
-              class="t-image-mask"
-              style="background: rgba(0, 0, 0, 0.6);"
-            />
-            <div
-              class="image-viewer-container"
+              class="t-overlay"
+              style="z-index: 1000; transition-duration: 300ms; display: none;"
             >
-              <div
-                class="t-image-viewer"
-              >
-                <div
-                  class="t-swiper-box"
-                >
-                  <div
-                    class="t-swiper-container"
-                  >
-                    
-                    
-                  </div>
-                </div>
-              </div>
-              <div
-                class="image-viewer-index"
-              >
-                0/0
-              </div>
+              
+              
             </div>
-          </div>
+          </transition-stub>
           
         </div>
         <!--v-if-->
@@ -27989,7 +28038,7 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/mobile.vue 1`] =
           class="t-button__text"
         >
           
-           显示 
+           基础用法 
           
         </span>
       </button>
@@ -28004,7 +28053,7 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/mobile.vue 1`] =
           class="t-button__text"
         >
           
-           图片列表 
+           不展示关闭按钮 
           
         </span>
       </button>
@@ -28019,61 +28068,49 @@ exports[`csr snapshot test > csr test ./src/image-viewer/demos/mobile.vue 1`] =
           class="t-button__text"
         >
           
-           默认显示第二张图片 
-          
-        </span>
-      </button>
-      <button
-        aria-disabled="false"
-        class="t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block"
-        data-v-ef8302fa=""
-        role="button"
-      >
-        <!---->
-        <span
-          class="t-button__text"
-        >
-          
-           显示 
+           监听关闭事件 
           
         </span>
       </button>
     </div>
     
   </div>
-  <div
+  <transition-stub
     class="t-image-viewer"
     data-v-ef8302fa=""
-    style="display: none;"
   >
     <div
-      class="t-image-mask"
-      style="background: rgba(0, 0, 0, 0.6);"
-    />
+      class="t-overlay"
+      style="z-index: 1000; transition-duration: 300ms; display: none;"
+    >
+      
+      
+    </div>
+  </transition-stub>
+  <transition-stub
+    class="t-image-viewer"
+    data-v-ef8302fa=""
+  >
     <div
-      class="image-viewer-container"
+      class="t-overlay"
+      style="z-index: 1000; transition-duration: 300ms; display: none;"
     >
-      <div
-        class="t-image-viewer"
-      >
-        <div
-          class="t-swiper-box"
-        >
-          <div
-            class="t-swiper-container"
-          >
-            
-            
-          </div>
-        </div>
-      </div>
-      <div
-        class="image-viewer-index"
-      >
-        0/0
-      </div>
+      
+      
     </div>
-  </div>
+  </transition-stub>
+  <transition-stub
+    class="t-image-viewer"
+    data-v-ef8302fa=""
+  >
+    <div
+      class="t-overlay"
+      style="z-index: 1000; transition-duration: 300ms; display: none;"
+    >
+      
+      
+    </div>
+  </transition-stub>
 </div>
 `;
 
@@ -58422,7 +58459,6 @@ exports[`csr snapshot test > csr test ./src/swiper/demos/base.vue 1`] = `
         
       </div>
       <!--v-if-->
-      <!--v-if-->
     </div>
   </div>
 </div>
@@ -67654,38 +67690,17 @@ exports[`csr snapshot test > csr test ./src/upload/demos/baseMultiple.vue 1`] =
           multiple=""
           type="file"
         />
-        <div
+        <transition-stub
           class="t-image-viewer"
-          style="display: none;"
         >
           <div
-            class="t-image-mask"
-            style="background: rgba(0, 0, 0, 0.6);"
-          />
-          <div
-            class="image-viewer-container"
+            class="t-overlay"
+            style="z-index: 1000; transition-duration: 300ms; display: none;"
           >
-            <div
-              class="t-image-viewer"
-            >
-              <div
-                class="t-swiper-box"
-              >
-                <div
-                  class="t-swiper-container"
-                >
-                  
-                  
-                </div>
-              </div>
-            </div>
-            <div
-              class="image-viewer-index"
-            >
-              0/0
-            </div>
+            
+            
           </div>
-        </div>
+        </transition-stub>
       </div>
       
     </div>
@@ -67752,38 +67767,17 @@ exports[`csr snapshot test > csr test ./src/upload/demos/baseSingle.vue 1`] = `
           hidden=""
           type="file"
         />
-        <div
+        <transition-stub
           class="t-image-viewer"
-          style="display: none;"
         >
           <div
-            class="t-image-mask"
-            style="background: rgba(0, 0, 0, 0.6);"
-          />
-          <div
-            class="image-viewer-container"
+            class="t-overlay"
+            style="z-index: 1000; transition-duration: 300ms; display: none;"
           >
-            <div
-              class="t-image-viewer"
-            >
-              <div
-                class="t-swiper-box"
-              >
-                <div
-                  class="t-swiper-container"
-                >
-                  
-                  
-                </div>
-              </div>
-            </div>
-            <div
-              class="image-viewer-index"
-            >
-              0/0
-            </div>
+            
+            
           </div>
-        </div>
+        </transition-stub>
       </div>
       
     </div>
@@ -67868,38 +67862,17 @@ exports[`csr snapshot test > csr test ./src/upload/demos/mobile.vue 1`] = `
           hidden=""
           type="file"
         />
-        <div
+        <transition-stub
           class="t-image-viewer"
-          style="display: none;"
         >
           <div
-            class="t-image-mask"
-            style="background: rgba(0, 0, 0, 0.6);"
-          />
-          <div
-            class="image-viewer-container"
+            class="t-overlay"
+            style="z-index: 1000; transition-duration: 300ms; display: none;"
           >
-            <div
-              class="t-image-viewer"
-            >
-              <div
-                class="t-swiper-box"
-              >
-                <div
-                  class="t-swiper-container"
-                >
-                  
-                  
-                </div>
-              </div>
-            </div>
-            <div
-              class="image-viewer-index"
-            >
-              0/0
-            </div>
+            
+            
           </div>
-        </div>
+        </transition-stub>
       </div>
       <div
         class="upload-title"
@@ -67943,38 +67916,17 @@ exports[`csr snapshot test > csr test ./src/upload/demos/mobile.vue 1`] = `
           multiple=""
           type="file"
         />
-        <div
+        <transition-stub
           class="t-image-viewer"
-          style="display: none;"
         >
           <div
-            class="t-image-mask"
-            style="background: rgba(0, 0, 0, 0.6);"
-          />
-          <div
-            class="image-viewer-container"
+            class="t-overlay"
+            style="z-index: 1000; transition-duration: 300ms; display: none;"
           >
-            <div
-              class="t-image-viewer"
-            >
-              <div
-                class="t-swiper-box"
-              >
-                <div
-                  class="t-swiper-container"
-                >
-                  
-                  
-                </div>
-              </div>
-            </div>
-            <div
-              class="image-viewer-index"
-            >
-              0/0
-            </div>
+            
+            
           </div>
-        </div>
+        </transition-stub>
       </div>
     </div>
     
diff --git a/test/snap/__snapshots__/ssr.test.js.snap b/test/snap/__snapshots__/ssr.test.js.snap
index 013a0c88..643ca36e 100644
--- a/test/snap/__snapshots__/ssr.test.js.snap
+++ b/test/snap/__snapshots__/ssr.test.js.snap
@@ -223,15 +223,19 @@ exports[`ssr snapshot test > ssr test ./src/image/demos/position.vue 1`] = `"<di
 
 exports[`ssr snapshot test > ssr test ./src/image/demos/shape.vue 1`] = `"<div data-v-5f48b8f6><div class=\\"t-image t-image--circle image-container\\" data-v-5f48b8f6><div class=\\"t-image__status\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-ellipsis\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M3 9a1 1 0 110-2 1 1 0 010 2zM7 8a1 1 0 102 0 1 1 0 00-2 0zM12 8a1 1 0 102 0 1 1 0 00-2 0z\\" fill-opacity=\\"0.9\\"></path></svg></div><img class=\\"t-image__img\\" style=\\"object-fit:fill;object-position:center;\\" src=\\"https://tdesign.gtimg.com/site/upload1.png\\" alt></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/background.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->backgroundColor: 设置遮罩的背景颜色<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 显示 <!--]--></span></button><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 1);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/background.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->backgroundColor: 设置遮罩的背景颜色<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 显示 <!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\" background-color=\\"rgba(0, 0, 0, 1)\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/base.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->显示和隐藏<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[-->显示<!--]--></span></button><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/base.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->基础用法<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 基础用法 <!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/imageList.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->图片数组：images<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 图片列表 <!--]--></span></button><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/closeBtn.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->不展示关闭按钮<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[-->不展示关闭按钮<!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/initialIndex.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->initialIndex: 默认展示第二张图片<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 默认展示 <!--]--></span></button><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/closeEvent.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->监听关闭事件<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[-->监听关闭事件<!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/mobile.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\" data-v-ef8302fa><h1 class=\\"title\\" data-v-ef8302fa>ImageViewer 图片预览</h1><p class=\\"summary\\" data-v-ef8302fa>图片全屏放大预览效果，包含全屏背景色、页码位置样式、增加操作等规范</p><div class=\\"tdesign-mobile-demo-block\\" data-v-ef8302fa><h2 class=\\"tdesign-mobile-demo-block__title\\">01 类型</h2><p class=\\"tdesign-mobile-demo-block__summary\\">图片预览类型</p><!--[--><div class=\\"image-viewer-demo\\" data-v-ef8302fa><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 显示 <!--]--></span></button><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 图片列表 <!--]--></span></button><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 默认显示第二张图片 <!--]--></span></button><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 显示 <!--]--></span></button></div><!--]--></div><div class=\\"t-image-viewer\\" style=\\"display:none;\\" data-v-ef8302fa><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, .6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/imageList.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->图片数组：images<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 图片列表 <!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/initialIndex.vue 1`] = `"<div><div class=\\"t-cell-group dialog-type-title\\"><div class=\\"t-cell-group__title\\"><!--[-->initialIndex: 默认展示第二张图片<!--]--></div><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"t-cell t-cell--middle t-cell--bordered\\" value-align=\\"left\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><!--v-if--><div class=\\"t-cell__note\\"><!--[--><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[--> 默认展示 <!--]--></span></button><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div><!--]--></div><!--v-if--></div><!--]--></div></div></div>"`;
+
+exports[`ssr snapshot test > ssr test ./src/image-viewer/demos/mobile.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\" data-v-ef8302fa><h1 class=\\"title\\" data-v-ef8302fa>ImageViewer 图片预览</h1><p class=\\"summary\\" data-v-ef8302fa>图片全屏放大预览效果，包含全屏背景色、页码位置样式、增加操作等规范</p><div class=\\"tdesign-mobile-demo-block\\" data-v-ef8302fa><h2 class=\\"tdesign-mobile-demo-block__title\\">01 类型</h2><p class=\\"tdesign-mobile-demo-block__summary\\">图片预览类型</p><!--[--><div class=\\"image-viewer-demo\\" data-v-ef8302fa><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 基础用法 <!--]--></span></button><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 不展示关闭按钮 <!--]--></span></button><button class=\\"t-button t-size-l t-button--outline t-button--default t-button--shape-rectangle t-is-block\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-ef8302fa><!----><span class=\\"t-button__text\\"><!--[--> 监听关闭事件 <!--]--></span></button></div><!--]--></div><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\" data-v-ef8302fa><!--[--><!--]--></div><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\" data-v-ef8302fa><!--[--><!--]--></div><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\" data-v-ef8302fa><!--[--><!--]--></div></div>"`;
 
 exports[`ssr snapshot test > ssr test ./src/indexes/demos/base.vue 1`] = `"<div class=\\"component-wrap\\"><div style=\\"\\" class=\\"t-indexes\\"><div class=\\"t-indexes__sidebar\\"><!--[--><div class=\\"t-indexes__sidebar-item\\" data-index=\\"A\\">A <!--v-if--></div><div class=\\"t-indexes__sidebar-item\\" data-index=\\"B\\">B <!--v-if--></div><!--]--></div><!--[--><div data-index=\\"A\\"><div class=\\"t-indexes-anchor\\" style=\\"height:0px;\\"><div class=\\"t-indexes__anchor\\" style=\\"\\"><!--[-->A开头<!--]--></div></div><div class=\\"t-indexes__group\\"><!--[--><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"阿坝\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">阿坝<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"阿拉善\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">阿拉善<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"阿里\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">阿里<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"安康\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">安康<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"安庆\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">安庆<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"鞍山\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">鞍山<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"安顺\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">安顺<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"安阳\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">安阳<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"澳门\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">澳门<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><!--]--></div></div><div data-index=\\"B\\"><div class=\\"t-indexes-anchor\\" style=\\"height:0px;\\"><div class=\\"t-indexes__anchor\\" style=\\"\\"><!--[-->B开头<!--]--></div></div><div class=\\"t-indexes__group\\"><!--[--><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"北京\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">北京<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"白银\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">白银<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"保定\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">保定<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"宝鸡\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">宝鸡<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"保山\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">保山<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"包头\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">包头<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"巴中\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">巴中<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"北海\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">北海<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"蚌埠\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">蚌埠<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"本溪\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">本溪<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"毕节\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">毕节<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"滨州\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">滨州<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"百色\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">百色<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><div class=\\"t-cell t-cell--middle t-indexes-cell\\" data-key value=\\"亳州\\" link=\\"true\\"><div class=\\"t-cell__left-icon\\"><!--v-if--><!--v-if--></div><div class=\\"t-cell__title\\">亳州<!--v-if--><!--v-if--></div><!--v-if--><!--v-if--></div><!--]--></div></div><!--]--></div></div>"`;
 
@@ -483,7 +487,7 @@ exports[`ssr snapshot test > ssr test ./src/swipe-cell/demos/right-menu.vue 1`]
 
 exports[`ssr snapshot test > ssr test ./src/swiper/demos/async.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\"><div style=\\"padding:0 16px;\\"><div style=\\"height:undefinedpx;overflow:hidden;\\" class=\\"t-swiper\\"><div class=\\"t-swiper__container\\" style=\\"height:undefinedpx;flex-direction:row;\\"><!--[--><!--]--></div><!--[--><span><span class=\\"t-swiper__btn btn-prev\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-chevron-left\\" style=\\"font-size:20px;\\"><path fill=\\"currentColor\\" d=\\"M9.54 3.54l.92.92L6.92 8l3.54 3.54-.92.92L5.08 8l4.46-4.46z\\" fill-opacity=\\"0.9\\"></path></svg></span><span class=\\"t-swiper__btn btn-next\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-chevron-right\\" style=\\"font-size:20px;\\"><path fill=\\"currentColor\\" d=\\"M6.46 12.46l-.92-.92L9.08 8 5.54 4.46l.92-.92L10.92 8l-4.46 4.46z\\" fill-opacity=\\"0.9\\"></path></svg></span></span><span class=\\"t-swiper__pagination t-swiper__pagination-dots-bar\\"><!--[--><!--]--><!--v-if--></span><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/swiper/demos/base.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\"><div style=\\"padding:0 16px;\\"><div style=\\"height:undefinedpx;overflow:hidden;\\" class=\\"t-swiper\\"><div class=\\"t-swiper__container\\" style=\\"height:undefinedpx;flex-direction:row;\\"><!--[--><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/01.png\\" class=\\"img\\"><!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/02.png\\" class=\\"img\\"><!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/03.png\\" class=\\"img\\"><!--]--></div><!--]--></div><!--v-if--><!--v-if--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/swiper/demos/base.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\"><div style=\\"padding:0 16px;\\"><div style=\\"height:undefinedpx;overflow:hidden;\\" class=\\"t-swiper\\"><div class=\\"t-swiper__container\\" style=\\"height:undefinedpx;flex-direction:row;\\"><!--[--><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/01.png\\" class=\\"img\\"><!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/02.png\\" class=\\"img\\"><!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\"><!--[--><img src=\\"https://tdesign.gtimg.com/site/swiper/03.png\\" class=\\"img\\"><!--]--></div><!--]--></div><!--v-if--></div></div></div>"`;
 
 exports[`ssr snapshot test > ssr test ./src/swiper/demos/control.vue 1`] = `"<div data-v-7617ffc0><div style=\\"height:undefinedpx;overflow:hidden;\\" class=\\"t-swiper\\" data-v-7617ffc0><div class=\\"t-swiper__container\\" style=\\"height:undefinedpx;flex-direction:row;\\"><!--[--><div class=\\"t-swiper-item swiper-item--demo\\" data-v-7617ffc0><!--[--> item1 <!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\" style=\\"background:blue;\\" data-v-7617ffc0><!--[--> item2 <!--]--></div><div class=\\"t-swiper-item swiper-item--demo\\" style=\\"background:green;\\" data-v-7617ffc0><!--[--> item3 <!--]--></div><!--]--></div><!--[--><span><span class=\\"t-swiper__btn btn-prev\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-chevron-left\\" style=\\"font-size:20px;\\"><path fill=\\"currentColor\\" d=\\"M9.54 3.54l.92.92L6.92 8l3.54 3.54-.92.92L5.08 8l4.46-4.46z\\" fill-opacity=\\"0.9\\"></path></svg></span><span class=\\"t-swiper__btn btn-next\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-chevron-right\\" style=\\"font-size:20px;\\"><path fill=\\"currentColor\\" d=\\"M6.46 12.46l-.92-.92L9.08 8 5.54 4.46l.92-.92L10.92 8l-4.46 4.46z\\" fill-opacity=\\"0.9\\"></path></svg></span></span><span class=\\"t-swiper__pagination t-swiper__pagination-dots-bar\\"><!--[--><!--]--><!--v-if--></span><!--]--></div><p class=\\"summary\\" data-v-7617ffc0> 手动指定页数： <button class=\\"t-button t-size-s t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-7617ffc0><!----><span class=\\"t-button__text\\"><!--[-->第一页<!--]--></span></button><button class=\\"t-button t-size-s t-button--base t-button--default t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-7617ffc0><!----><span class=\\"t-button__text\\"><!--[-->第二页<!--]--></span></button><button class=\\"t-button t-size-s t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\" data-v-7617ffc0><!----><span class=\\"t-button__text\\"><!--[-->第三页<!--]--></span></button></p><p class=\\"summary\\" data-v-7617ffc0>当前页 1</p></div>"`;
 
@@ -584,8 +588,8 @@ exports[`ssr snapshot test > ssr test ./src/toast/demos/position.vue 1`] = `"<di
 
 exports[`ssr snapshot test > ssr test ./src/toast/demos/text.vue 1`] = `"<div class=\\"tdesign-demo-block\\"><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[-->文字提示<!--]--></span></button><button class=\\"t-button t-size-m t-button--base t-button--primary t-button--shape-rectangle\\" role=\\"button\\" aria-disabled=\\"false\\"><!----><span class=\\"t-button__text\\"><!--[-->文字长提示<!--]--></span></button></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/upload/demos/baseMultiple.vue 1`] = `"<div data-v-0c024305><div class=\\"t-cell-group dialog-type-title\\" data-v-0c024305><!--v-if--><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"title\\" data-v-0c024305>多选上传图片</div><div data-v-0c024305><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" multiple hidden accept><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div></div><!--]--></div></div><!--v-if--></div>"`;
+exports[`ssr snapshot test > ssr test ./src/upload/demos/baseMultiple.vue 1`] = `"<div data-v-0c024305><div class=\\"t-cell-group dialog-type-title\\" data-v-0c024305><!--v-if--><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"title\\" data-v-0c024305>多选上传图片</div><div data-v-0c024305><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" multiple hidden accept><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div></div><!--]--></div></div><!--v-if--></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/upload/demos/baseSingle.vue 1`] = `"<div data-v-1b7b387d><div class=\\"t-cell-group dialog-type-title\\" data-v-1b7b387d><!--v-if--><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"title\\" data-v-1b7b387d>单选上传图片</div><div data-v-1b7b387d><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" hidden accept><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div></div><!--]--></div></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/upload/demos/baseSingle.vue 1`] = `"<div data-v-1b7b387d><div class=\\"t-cell-group dialog-type-title\\" data-v-1b7b387d><!--v-if--><div class=\\"t-cell-group__container\\"><!--[--><div class=\\"title\\" data-v-1b7b387d>单选上传图片</div><div data-v-1b7b387d><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" hidden accept><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div></div><!--]--></div></div></div>"`;
 
-exports[`ssr snapshot test > ssr test ./src/upload/demos/mobile.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\" data-v-f557e7b4><h1 class=\\"title\\" data-v-f557e7b4>Upload上传</h1><p class=\\"summary\\" data-v-f557e7b4>上传图片（单张上传、多张上传</p><div class=\\"tdesign-mobile-demo-block\\" data-v-f557e7b4><h2 class=\\"tdesign-mobile-demo-block__title\\">01 类型</h2><p class=\\"tdesign-mobile-demo-block__summary\\">上传图片</p><!--[--><div class=\\"upload-demo\\" data-v-f557e7b4><div class=\\"upload-title\\" data-v-f557e7b4>单选上传图片</div><div data-v-f557e7b4><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" hidden accept><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div></div><div class=\\"upload-title\\" data-v-f557e7b4>多选上传图片</div><div data-v-f557e7b4><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" multiple hidden accept><div class=\\"t-image-viewer\\" style=\\"display:none;\\"><div class=\\"t-image-mask\\" style=\\"background:rgba(0, 0, 0, 0.6);\\"></div><div class=\\"image-viewer-container\\"><div class=\\"t-image-viewer\\"><div class=\\"t-swiper-box\\"><div class=\\"t-swiper-container\\"><!--[--><!--]--></div></div></div><div class=\\"image-viewer-index\\">0/0</div></div></div></div></div><!--]--></div></div>"`;
+exports[`ssr snapshot test > ssr test ./src/upload/demos/mobile.vue 1`] = `"<div class=\\"tdesign-mobile-demo\\" data-v-f557e7b4><h1 class=\\"title\\" data-v-f557e7b4>Upload上传</h1><p class=\\"summary\\" data-v-f557e7b4>上传图片（单张上传、多张上传</p><div class=\\"tdesign-mobile-demo-block\\" data-v-f557e7b4><h2 class=\\"tdesign-mobile-demo-block__title\\">01 类型</h2><p class=\\"tdesign-mobile-demo-block__summary\\">上传图片</p><!--[--><div class=\\"upload-demo\\" data-v-f557e7b4><div class=\\"upload-title\\" data-v-f557e7b4>单选上传图片</div><div data-v-f557e7b4><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" hidden accept><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div></div><div class=\\"upload-title\\" data-v-f557e7b4>多选上传图片</div><div data-v-f557e7b4><ul class=\\"t-upload__card\\"><!--[--><!--]--><li class=\\"t-upload__card-item\\"><div class=\\"t-upload__card-container t-upload__card-box\\"><svg fill=\\"none\\" viewbox=\\"0 0 16 16\\" width=\\"1em\\" height=\\"1em\\" class=\\"t-icon t-icon-add\\" style=\\"\\"><path fill=\\"currentColor\\" d=\\"M7.35 8.65v3.85h1.3V8.65h3.85v-1.3H8.65V3.5h-1.3v3.85H3.5v1.3h3.85z\\" fill-opacity=\\"0.9\\"></path></svg></div></li></ul><input type=\\"file\\" multiple hidden accept><div class=\\"t-overlay t-image-viewer\\" style=\\"z-index:1000;transition-duration:300ms;display:none;\\"><!--[--><!--]--></div></div></div><!--]--></div></div>"`;