first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
:: BASE_DOC ::
## API
### ImageViewer Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
background-color | String | - | \- | N
close-btn | String / Boolean / Object | false | \- | N
delete-btn | String / Boolean / Object | false | \- | N
image-props | Object | - | Typescript: `ImageProps`[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/image-viewer/type.ts) | N
images | Array | [] | Typescript: `Array<string>` | N
initial-index | Number | 0 | Typescript: `Number` | N
lazy | Boolean | true | \- | N
show-index | Boolean | false | \- | N
using-custom-navbar | Boolean | false | \- | N
visible | Boolean | false | hide or show image viewer。`v-model:visible` is supported | N
default-visible | Boolean | false | hide or show image viewer。uncontrolled property | N
### ImageViewer Events
name | params | description
-- | -- | --
change | `(context: { index: number })` | \-
close | `(context: { trigger: 'overlay' \| 'button', visible: Boolean, index: Number } )` | \-
delete | `(context: { index: number } )` | \-
### ImageViewer Slots
name | Description
-- | --
close-btn | \-
delete-btn | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-image-viewer-close-margin-left | @spacer-1 | -
--td-image-viewer-delete-margin-right | @spacer-1 | -
--td-image-viewer-mask-bg-color | @mask-active | -
--td-image-viewer-nav-bg-color | #000 | -
--td-image-viewer-nav-color | @text-color-anti | -
--td-image-viewer-nav-height | 96rpx | -
--td-image-viewer-nav-index-font-size | @font-size-base | -
--td-image-viewer-top | @position-fixed-top | -

View File

@@ -0,0 +1,78 @@
---
title: ImageViewer 图片预览
description: 图片全屏放大预览效果,包含全屏背景色、页码位置样式、增加操作等规范。
spline: data
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TImageViewer from '@tdesign/uniapp/image-viewer/image-viewer.vue';
```
### 类型
#### 基础图片预览
{{ base }}
#### 带操作图片预览
顶部区域可以配置关闭按钮、页码信息、删除按钮。
{{ delete }}
> 当使用自定义导航栏的时候,顶部的操作按钮会被遮挡,此时需要开启 `using-custom-navbar` 来解决
## API
### ImageViewer Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
background-color | String | - | 遮罩的背景颜色 | N
close-btn | String / Boolean / Object | false | 是否显示关闭操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `close`,值为 `Object` 类型,表示透传至 `icon` ,不传表示不显示图标 | N
delete-btn | String / Boolean / Object | false | 是否显示删除操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `delete`,值为 `Object` 类型,表示透传至 `icon`,不传表示不显示图标 | N
image-props | Object | - | 透传至 Image 组件。TS 类型:`ImageProps`[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/image-viewer/type.ts) | N
images | Array | [] | 图片数组。TS 类型:`Array<string>` | N
initial-index | Number | 0 | 初始化页码。TS 类型:`Number` | N
lazy | Boolean | true | 是否开启图片懒加载。开启后会预加载当前图片、相邻图片 | N
show-index | Boolean | false | 是否显示页码 | N
using-custom-navbar | Boolean | false | 是否使用了自定义导航栏 | N
visible | Boolean | false | 隐藏/显示预览。支持语法糖 `v-model:visible` | N
default-visible | Boolean | false | 隐藏/显示预览。非受控属性 | N
### ImageViewer Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { index: number })` | 翻页时回调
close | `(context: { trigger: 'overlay' \| 'button', visible: Boolean, index: Number } )` | 点击操作按钮button或者overlay时触发
delete | `(context: { index: number } )` | 点击删除操作按钮时触发
### ImageViewer Slots
名称 | 描述
-- | --
close-btn | 关闭操作
delete-btn | 删除操作
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-image-viewer-close-margin-left | @spacer-1 | -
--td-image-viewer-delete-margin-right | @spacer-1 | -
--td-image-viewer-mask-bg-color | @mask-active | -
--td-image-viewer-nav-bg-color | #000 | -
--td-image-viewer-nav-color | @text-color-anti | -
--td-image-viewer-nav-height | 96rpx | -
--td-image-viewer-nav-index-font-size | @font-size-base | -
--td-image-viewer-top | @position-fixed-top | -

View File

@@ -0,0 +1,4 @@
export function shouldLoadImage(index, currentIndex, loadedIndexes) {
return Math.abs(index - currentIndex) <= 1 || (loadedIndexes && loadedIndexes.indexOf(index) !== -1);
}

View File

@@ -0,0 +1,75 @@
.t-image-viewer {
position: fixed;
top: var(--td-image-viewer-top, var(--td-position-fixed-top, 0));
left: 0;
bottom: 0;
right: 0;
z-index: 1001;
transform: translateZ(0);
overflow: hidden;
}
.t-image-viewer__mask {
position: absolute;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--td-image-viewer-mask-bg-color, var(--td-mask-active, rgba(0, 0, 0, 0.6)));
}
.t-image-viewer__content {
width: 100vw;
display: inline-block;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 1005;
}
.t-image-viewer .swiper {
outline: 0;
}
.t-image-viewer__image {
width: 100%;
display: inline-block;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.t-image-viewer :deep(.t-image--external) {
width: inherit;
height: inherit;
display: block;
}
.t-image-viewer__nav {
width: 100%;
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--td-image-viewer-nav-height, 96rpx);
background-color: var(--td-image-viewer-nav-bg-color, #000);
left: 0;
color: var(--td-image-viewer-nav-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
z-index: 1005;
}
.t-image-viewer__nav-close {
margin-left: var(--td-image-viewer-close-margin-left, var(--td-spacer-1, 24rpx));
}
.t-image-viewer__nav-close:empty {
display: none;
}
.t-image-viewer__nav-delete {
margin-right: var(--td-image-viewer-delete-margin-right, var(--td-spacer-1, 24rpx));
}
.t-image-viewer__nav-delete:empty {
display: none;
}
.t-image-viewer__nav-close,
.t-image-viewer__nav-delete {
font-size: 48rpx;
}
.t-image-viewer__nav-index {
flex: 1;
font-size: var(--td-image-viewer-nav-index-font-size, var(--td-font-size-base, 28rpx));
text-align: center;
}

View File

@@ -0,0 +1,317 @@
<template>
<view
v-if="dataVisible"
:id="classPrefix"
:class="classPrefix + ' ' + tClass"
:style="tools._style([customStyle, '--td-image-viewer-top: ' + distanceTop + 'px'])"
:aria-modal="true"
aria-role="dialog"
aria-label="图片查看器"
@touchmove.stop.prevent="true"
>
<view
:class="classPrefix + '__mask'"
data-source="overlay"
:style="tools._style([backgroundColor && '--td-image-viewer-mask-bg-color: ' + backgroundColor])"
aria-role="button"
aria-label="关闭"
@click="(e) => onClose(e, 'overlay')"
/>
<block v-if="images && images.length">
<view :class="classPrefix + '__content'">
<swiper
class="swiper"
:style="swiperStyle[currentSwiperIndex] && swiperStyle[currentSwiperIndex].style"
:autoplay="false"
:current="currentSwiperIndex"
tabindex="0"
@change="onSwiperChange"
@click="(e) => onClose(e, '')"
>
<swiper-item
v-for="(item, index) in images"
:key="index"
:class="classPrefix + '__preview-image'"
>
<t-image
v-if="!lazy || shouldLoadImage(index, currentSwiperIndex, loadedImageIndexes)"
:t-class="prefix + '-image--external'"
:class="classPrefix + '__image'"
:custom-style="(imagesStyle[index] && imagesStyle[index].style) || ''"
:data-index="index"
:src="item"
:mode="(imageProps && imageProps.mode) || 'aspectFit'"
:lazy="(imageProps && imageProps.lazy) || false"
:loading="(imageProps && imageProps.loading) || 'default'"
:shape="(imageProps && imageProps.shape) || 'square'"
:webp="(imageProps && imageProps.webp) || false"
:show-menu-by-longpress="(imageProps && imageProps.showMenuByLongpress) || false"
@load="onImageLoadSuccess($event, { index })"
/>
</swiper-item>
</swiper>
</view>
<view
:class="classPrefix + '__nav'"
>
<view
:class="classPrefix + '__nav-close'"
aria-role="button"
aria-label="关闭"
@click.stop.prevent="(e) => onClose(e, '')"
>
<slot name="close-btn" />
<block
v-if="_closeBtn"
name="icon"
>
<t-icon
:custom-style="_closeBtn.style || ''"
:t-class="_closeBtn.tClass"
:prefix="_closeBtn.prefix"
:name="_closeBtn.name"
:size="_closeBtn.size"
:color="_closeBtn.color"
:aria-hidden="!!_closeBtn.ariaHidden"
:aria-label="_closeBtn.ariaLabel"
:aria-role="_closeBtn.ariaRole"
@click="_closeBtn.click || ''"
/>
</block>
</view>
<view
v-if="showIndex"
:class="classPrefix + '__nav-index'"
>
{{ currentSwiperIndex + 1 }}/{{ images.length }}
</view>
<view
:class="classPrefix + '__nav-delete'"
aria-role="button"
aria-label="删除"
@click="onDelete"
>
<slot name="delete-btn" />
<t-icon
v-if="_deleteBtn"
:custom-style="_deleteBtn.style || ''"
:t-class="_deleteBtn.tClass"
:prefix="_deleteBtn.prefix"
:name="_deleteBtn.name"
:size="_deleteBtn.size"
:color="_deleteBtn.color"
:aria-hidden="!!_deleteBtn.ariaHidden"
:aria-label="_deleteBtn.ariaLabel"
:aria-role="_deleteBtn.ariaRole"
@click="_deleteBtn.click || ''"
/>
</view>
</view>
</block>
</view>
</template>
<script>
import TImage from '../image/image';
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { styles, calcIcon, systemInfo } from '../common/utils';
import { prefix } from '../common/config';
import props from './props';
import tools from '../common/utils.wxs';
import { shouldLoadImage } from './computed.js';
import useCustomNavbar from '../mixins/using-custom-navbar';
const name = `${prefix}-image-viewer`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'visible',
event: 'close',
},
],
externalClasses: [`${prefix}-class`],
mixins: [useCustomNavbar],
components: {
TImage,
TIcon,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
currentSwiperIndex: 0,
loadedImageIndexes: [],
windowHeight: 0,
windowWidth: 0,
swiperStyle: {},
imagesStyle: {},
maskTop: 0,
tools,
_deleteBtn: null,
_closeBtn: null,
dataVisible: this.visible,
};
},
watch: {
visible: {
handler(v) {
this.dataVisible = v;
this.init();
},
},
initialIndex: 'init',
images: 'init',
closeBtn: {
handler(v) {
this._closeBtn = calcIcon(v, 'close');
},
immediate: true,
},
deleteBtn: {
handler(v) {
this._deleteBtn = calcIcon(v, 'delete');
},
immediate: true,
},
},
created() {
this.saveScreenSize();
// this.calcMaskTop();
},
mounted() {
this.init();
},
methods: {
shouldLoadImage,
init() {
const { visible: dataVisible, images, initialIndex } = this;
if (dataVisible && images?.length) {
this.loadedImageIndexes = [];
this.currentSwiperIndex = initialIndex >= images.length ? images.length - 1 : initialIndex;
}
},
calcMaskTop() {
if (this.usingCustomNavbar) {
const rect = wx?.getMenuButtonBoundingClientRect() || null;
const { statusBarHeight } = systemInfo;
if (rect && statusBarHeight) {
this.maskTop = rect.top - statusBarHeight + rect.bottom;
}
}
},
saveScreenSize() {
const { windowHeight, windowWidth } = systemInfo;
this.windowHeight = windowHeight;
this.windowWidth = windowWidth;
},
calcImageDisplayStyle(imageWidth, imageHeight) {
const { windowWidth, windowHeight } = this;
const ratio = imageWidth / imageHeight;
// 图片宽高都小于屏幕宽高
if (imageWidth < windowWidth && imageHeight < windowHeight) {
return {
styleObj: {
width: `${imageWidth}px`,
height: `${imageHeight}px`,
left: '50%',
transform: 'translate(-50%, -50%)',
},
};
}
// 图片宽高至少存在一个大于屏幕宽高,此时判断图片宽高比,按长边显示
if (ratio >= 1) {
return {
styleObj: {
width: '100vw',
height: `${(windowWidth / ratio)}px`,
},
};
}
// 图片的高大于宽纵向图设定高度为100vh宽度自适应且确保宽度不超过屏幕宽度
const scaledHeight = ratio * windowHeight * 2;
if (scaledHeight < windowWidth) {
return {
styleObj: {
width: `${scaledHeight}rpx`,
height: '100vh',
left: '50%',
transform: 'translate(-50%, -50%)',
},
};
}
// 当通过高度计算的图片宽度超过屏幕宽度时, 以屏幕宽度为基准, 重新计算高度
return {
styleObj: {
width: '100vw',
height: `${(windowWidth / imageWidth) * imageHeight}px`,
},
};
},
onImageLoadSuccess({ e }, { index }) {
const {
detail: { width, height },
} = e;
const { mode, styleObj } = this.calcImageDisplayStyle(width, height);
const originImagesStyle = this.imagesStyle;
const originSwiperStyle = this.swiperStyle;
if (!this.loadedImageIndexes.includes(index)) {
this.loadedImageIndexes = [...this.loadedImageIndexes, index];
}
this.swiperStyle = {
...originSwiperStyle,
[index]: {
style: `height: ${styleObj.height}`,
},
};
this.imagesStyle = {
...originImagesStyle,
[index]: {
mode,
style: styles({ ...styleObj }),
},
};
},
onSwiperChange(e) {
const {
detail: { current },
} = e;
this.currentSwiperIndex = current;
this._trigger('change', { index: current });
},
onClose(e, source) {
this._trigger('close', { visible: false, trigger: source || 'button', index: this.currentSwiperIndex });
},
onDelete() {
this._trigger('delete', { index: this.currentSwiperIndex });
},
},
});
</script>
<style scoped>
@import './image-viewer.css';
</style>

View File

@@ -0,0 +1,69 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdImageViewerProps } from './type';
export default {
/** 遮罩的背景颜色 */
backgroundColor: {
type: String,
default: '',
},
/** 是否显示关闭操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `close`,值为 `Object` 类型,表示透传至 `icon` ,不传表示不显示图标 */
closeBtn: {
type: [String, Boolean, Object],
default: false as TdImageViewerProps['closeBtn'],
},
/** 是否显示删除操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `delete`,值为 `Object` 类型,表示透传至 `icon`,不传表示不显示图标 */
deleteBtn: {
type: [String, Boolean, Object],
default: false as TdImageViewerProps['deleteBtn'],
},
/** 透传至 Image 组件 */
imageProps: {
type: Object,
},
/** 图片数组 */
images: {
type: Array,
default: (): TdImageViewerProps['images'] => [],
},
/** 初始化页码 */
initialIndex: {
type: Number,
default: 0,
},
/** 是否开启图片懒加载。开启后会预加载当前图片、相邻图片 */
lazy: {
type: Boolean,
default: true,
},
/** 是否显示页码 */
showIndex: Boolean,
/** 是否使用了自定义导航栏 */
usingCustomNavbar: Boolean,
/** 隐藏/显示预览 */
visible: {
type: Boolean,
default: undefined,
},
/** 隐藏/显示预览,非受控属性 */
defaultVisible: Boolean,
/** 翻页时回调 */
onChange: {
type: Function,
default: () => ({}),
},
/** 点击操作按钮button或者overlay时触发 */
onClose: {
type: Function,
default: () => ({}),
},
/** 点击删除操作按钮时触发 */
onDelete: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,76 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdImageProps as ImageProps } from '../image/type';
export interface TdImageViewerProps {
/**
* 遮罩的背景颜色
* @default ''
*/
backgroundColor?: string;
/**
* 是否显示关闭操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `close`,值为 `Object` 类型,表示透传至 `icon` ,不传表示不显示图标
* @default false
*/
closeBtn?: string | boolean | object;
/**
* 是否显示删除操作,前提需要开启页码。值为字符串表示图标名称,值为 `true` 表示使用默认图标 `delete`,值为 `Object` 类型,表示透传至 `icon`,不传表示不显示图标
* @default false
*/
deleteBtn?: string | boolean | object;
/**
* 透传至 Image 组件
*/
imageProps?: ImageProps;
/**
* 图片数组
* @default []
*/
images?: Array<string>;
/**
* 初始化页码
* @default 0
*/
initialIndex?: Number;
/**
* 是否开启图片懒加载。开启后会预加载当前图片、相邻图片
* @default true
*/
lazy?: boolean;
/**
* 是否显示页码
* @default false
*/
showIndex?: boolean;
/**
* 是否使用了自定义导航栏
* @default false
*/
usingCustomNavbar?: boolean;
/**
* 隐藏/显示预览
* @default false
*/
visible?: boolean;
/**
* 隐藏/显示预览,非受控属性
* @default false
*/
defaultVisible?: boolean;
/**
* 翻页时回调
*/
onChange?: (context: { index: number }) => void;
/**
* 点击操作按钮button或者overlay时触发
*/
onClose?: (context: { trigger: 'overlay' | 'button'; visible: Boolean; index: Number }) => void;
/**
* 点击删除操作按钮时触发
*/
onDelete?: (context: { index: number }) => void;
}