From 5ed03d3959bfe6636ffe1a81a0cd6fa488f9a710 Mon Sep 17 00:00:00 2001 From: Dylan-duqingyu <63580483+Dylan-duqingyu@users.noreply.github.com> Date: Fri, 27 Aug 2021 12:37:50 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=A2=84=E8=A7=88=E7=BB=84=E4=BB=B6,=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD;=E5=85=B6?= =?UTF-8?q?=E4=B8=AD=E4=BE=9D=E8=B5=96=E5=85=B6=E4=BB=96=E7=BB=84=E4=BB=B6?= =?UTF-8?q?(d-input-number=EF=BC=8Cd-modal)=E9=9C=80=E8=A6=81=E7=AD=89?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E7=94=B0=E7=BB=84=E5=BC=80=E5=8F=91=E5=AE=8C?= =?UTF-8?q?=E5=86=8D=E7=A7=BB=E5=85=A5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/image-preview/index.ts | 14 ++ .../src/image-preview-directive.ts | 52 ++++++ .../src/image-preview-service.ts | 34 ++++ .../image-preview/src/image-preview-types.ts | 24 +++ devui/image-preview/src/image-preview.scss | 115 ++++++++++++ devui/image-preview/src/image-preview.tsx | 170 ++++++++++++++++++ devui/image-preview/src/transform.ts | 130 ++++++++++++++ sites/components/image-preview/index.md | 137 ++++++++++++++ 8 files changed, 676 insertions(+) create mode 100644 devui/image-preview/index.ts create mode 100644 devui/image-preview/src/image-preview-directive.ts create mode 100644 devui/image-preview/src/image-preview-service.ts create mode 100644 devui/image-preview/src/image-preview-types.ts create mode 100644 devui/image-preview/src/image-preview.scss create mode 100644 devui/image-preview/src/image-preview.tsx create mode 100644 devui/image-preview/src/transform.ts create mode 100644 sites/components/image-preview/index.md diff --git a/devui/image-preview/index.ts b/devui/image-preview/index.ts new file mode 100644 index 00000000..bf970529 --- /dev/null +++ b/devui/image-preview/index.ts @@ -0,0 +1,14 @@ +import type { App } from 'vue' +import ImagePreviewDirective from './src/image-preview-directive' +import ImagePreviewService from './src/image-preview-service' + +export { ImagePreviewDirective, ImagePreviewService } + +export default { + title: 'ImagePreview 图片预览', + category: '数据展示', + install(app: App): void { + app.directive('ImagePreview', ImagePreviewDirective) + app.config.globalProperties.$imagePreviewService = ImagePreviewService + } +} diff --git a/devui/image-preview/src/image-preview-directive.ts b/devui/image-preview/src/image-preview-directive.ts new file mode 100644 index 00000000..5317dff6 --- /dev/null +++ b/devui/image-preview/src/image-preview-directive.ts @@ -0,0 +1,52 @@ +import { BindingTypes } from './image-preview-types' +import ImagePreviewService from './image-preview-service' + +function mountedPreviewImages(url: string, urlList: Array): void { + ImagePreviewService.open({ url, previewUrlList: urlList }) +} +function unmountedPreviewImages() { + ImagePreviewService.close() +} + +function getImgByEl(el: HTMLElement): Array { + const urlList = [...el.querySelectorAll('img')].map((item: HTMLImageElement) => + item.getAttribute('src') + ) + return urlList +} + +function handleImgByEl(el: HTMLElement) { + el.addEventListener('click', (e: MouseEvent) => { + e.stopPropagation() + const target = e.target as HTMLElement + if (target?.nodeName?.toLowerCase() === 'img') { + const urlList = getImgByEl(el) + const url = target.getAttribute('src') + mountedPreviewImages(url, urlList) + } + }) +} + +export default { + mounted(el: HTMLElement, binding: BindingTypes | undefined) { + if (!binding.value) { + return handleImgByEl(el) + } + const { custom, disableDefault } = binding.value + // console.log('指令参数:', custom, disableDefault, zIndex, backDropZIndex) + if (custom instanceof Object) { + custom.open = () => { + const urlList = getImgByEl(el) + mountedPreviewImages(urlList?.[0], urlList) + } + custom.close = () => unmountedPreviewImages() + } + if (disableDefault) { + return + } + handleImgByEl(el) + }, + unmounted() { + unmountedPreviewImages() + } +} diff --git a/devui/image-preview/src/image-preview-service.ts b/devui/image-preview/src/image-preview-service.ts new file mode 100644 index 00000000..fd26b5c9 --- /dev/null +++ b/devui/image-preview/src/image-preview-service.ts @@ -0,0 +1,34 @@ +import { createApp } from 'vue' +import { ImagePreviewProps } from './image-preview-types' +import imagePreview from './image-preview' + +function createComponent(props: ImagePreviewProps) { + return createApp(imagePreview, props) +} + +class ImagePreviewService { + static $body: HTMLElement | null = null + static $div: HTMLDivElement | null = null + // 暂时的禁止滚动穿透,后续应该考虑用modal组件来渲染预览组件 + static $overflow = '' + + static open(props: ImagePreviewProps) { + this.$body = document.body + this.$div = document.createElement('div') + this.$overflow = this.$body.style.overflow + this.$body.appendChild(this.$div) + createComponent(props).mount(this.$div) + + this.$body.style.setProperty('overflow', 'hidden', 'important') + } + static close() { + this.$body?.style.setProperty('overflow', this.$overflow) + this.$overflow = null + + this.$div && this.$body.removeChild(this.$div) + this.$body = null + this.$div = null + } +} + +export default ImagePreviewService diff --git a/devui/image-preview/src/image-preview-types.ts b/devui/image-preview/src/image-preview-types.ts new file mode 100644 index 00000000..1f4053cb --- /dev/null +++ b/devui/image-preview/src/image-preview-types.ts @@ -0,0 +1,24 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export const imagePreviewProps = { + url: { + type: String, + default: '' + }, + previewUrlList: { + type: Array as PropType, + default: () => [] + } +} as const + +export interface BindingTypes { + value: { + custom?: any + disableDefault?: boolean + zIndex?: number + backDropZIndex?: number + } + [key: string]: any +} + +export type ImagePreviewProps = ExtractPropTypes diff --git a/devui/image-preview/src/image-preview.scss b/devui/image-preview/src/image-preview.scss new file mode 100644 index 00000000..28a4f786 --- /dev/null +++ b/devui/image-preview/src/image-preview.scss @@ -0,0 +1,115 @@ +@import '../../style/theme/_z-index'; +@import '../../style/theme/_shadow'; +@import '../../style/theme/_color'; +@import '../../style/theme/_corner'; + +.devui-image-preview { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: calc(#{$devui-z-index-modal} - 1); + background: $devui-shadow; + border-radius: $devui-border-radius; + box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + svg, + polygon, + g, + path { + fill: $devui-icon-text; + } + @mixin fixed-button() { + position: fixed; + z-index: $devui-z-index-modal; + cursor: pointer; + width: 36px; + height: 36px; + border-radius: 50%; + background: $devui-highlight-overlay; + box-shadow: $devui-shadow-length-base $devui-light-shadow; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover { + background: $devui-area; + } + + svg { + width: 38px; + height: 18px; + } + } + + &-main-image { + width: auto; + height: auto; + max-width: 90%; + max-height: 90%; + margin-top: -20px; + cursor: grab; + } + + &-close-btn { + @include fixed-button(); + + top: 15px; + right: 20px; + } + + &-arrow-left { + @include fixed-button(); + + top: 50%; + left: 20px; + transform: translateY(-50%); + } + + &-arrow-right { + @include fixed-button(); + + top: 50%; + right: 20px; + transform: translateY(-50%); + } + + &-toolbar { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background: $devui-highlight-overlay; + box-shadow: $devui-shadow-length-fullscreen-overlay $devui-light-shadow; + + button { + display: inline-flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + color: $devui-text; + } + + .devui-image-preview-index { + display: inline-flex; + width: 100px; + justify-content: center; + align-items: center; + cursor: pointer; + } + + & > :not(:first-child) { + margin-left: 20px; + } + } +} diff --git a/devui/image-preview/src/image-preview.tsx b/devui/image-preview/src/image-preview.tsx new file mode 100644 index 00000000..e389d339 --- /dev/null +++ b/devui/image-preview/src/image-preview.tsx @@ -0,0 +1,170 @@ +import './image-preview.scss' +import { defineComponent, ref, computed, onMounted } from 'vue' +import { imagePreviewProps, ImagePreviewProps } from './image-preview-types' +import ImagePreviewService from './image-preview-service' +import Transform from './transform' + +export default defineComponent({ + name: 'DImagePreview', + props: imagePreviewProps, + emits: [], + setup(props: ImagePreviewProps) { + let transform: Transform = null + const index = ref(0) + const url = computed(() => props.previewUrlList[index.value]) + + function initTransform() { + const imageElement: HTMLImageElement = document.querySelector( + '.devui-image-preview-main-image' + ) + transform = new Transform(imageElement) + } + function initIndex() { + index.value = props.previewUrlList.findIndex(curUrl => curUrl === props.url) + } + function onPrev() { + index.value = index.value <= 0 ? props.previewUrlList.length - 1 : index.value - 1 + } + function onNext() { + index.value = index.value >= props.previewUrlList.length - 1 ? 0 : index.value + 1 + } + function onClose() { + ImagePreviewService.close() + } + function onZoomIn() { + transform.setZoomIn() + } + function onZoomOut() { + transform.setZoomOut() + } + function onRotate() { + transform.setRotate() + } + function onZoomBest() { + transform.setZoomBest() + } + function onZoomOriginal() { + transform.setZoomOriginal() + } + + onMounted(() => { + initIndex() + initTransform() + }) + + return () => { + return ( +
+ {/* 预览图 */} + + {/* 按钮区 */} + + + + {/* 底部固定区 */} +
+ + + + + + {index.value + 1}:{props.previewUrlList.length} + + + + +
+
+ ) + } + } +}) diff --git a/devui/image-preview/src/transform.ts b/devui/image-preview/src/transform.ts new file mode 100644 index 00000000..ff35f90f --- /dev/null +++ b/devui/image-preview/src/transform.ts @@ -0,0 +1,130 @@ +interface Options { + transformX?: number + transformY?: number + zoom?: number + rotate?: number +} +interface HTMLElementPlus extends HTMLElement { + onmousewheel?: (...args: any[]) => void +} + +export default class Transform { + private el: HTMLElementPlus + + private oTransformX = 0 + private oTransformY = 0 + private transformX: number + private transformY: number + private zoom: number + private rotate: number + + private STEP = 0.25 + private MIN_SCALE = 0.2 + private MAX_SCALE = 2.5 + private TRANSFORMX = 0 + private TRANSFORMY = 0 + private ZOOM = 1 + private ROTATE = 0 + + constructor(el: HTMLElementPlus, options: Options = {}) { + this.el = el + this.transformX = options.transformX || this.TRANSFORMX + this.transformY = options.transformY || this.TRANSFORMY + this.zoom = options.zoom || this.ZOOM + this.rotate = options.rotate || this.ROTATE + + this.handleDefaultDraggable() + this.onDraggable() + this.onMouseWheel() + } + handleDefaultDraggable() { + document.body.ondragstart = () => { + window.event.returnValue = false + return false + } + } + onDraggable() { + this.el.onmousedown = (e: MouseEvent) => { + const ox = e.clientX + const oy = e.clientY + document.onmousemove = (e1: MouseEvent) => { + const disX = e1.clientX - ox + const disY = e1.clientY - oy + this.transformX = this.oTransformX + disX + this.transformY = this.oTransformY + disY + this.el.style.cursor = 'grabbing' + this.setPosition() + } + } + document.onmouseup = () => { + document.onmousemove = null + this.oTransformX = this.transformX + this.oTransformY = this.transformY + this.el.style.cursor = 'grab' + } + } + onMouseWheel() { + const handleWheel = this.throttle(this.setMouseWheel, 100) + this.el.onmousewheel = e => { + const value: number = -e.wheelDelta || e.deltaY || e.detail + handleWheel(value) + } + } + throttle(fn: (...args: any[]) => void, t: number) { + let timer = null + return (...args) => { + if (!timer) { + setTimeout(() => { + timer = null + fn.apply(this, args) + }, t) + } + } + } + setMouseWheel(value: number) { + if (value < 0) { + if (this.zoom >= this.MAX_SCALE) { + this.el.style.cursor = 'not-allowed' + return + } + this.el.style.cursor = 'zoom-in' + this.setZoomIn(this.STEP) + } else { + if (this.zoom <= this.MIN_SCALE) { + this.el.style.cursor = 'not-allowed' + return + } + this.el.style.cursor = 'zoom-out' + this.setZoomOut(this.STEP) + } + this.setPosition() + } + setZoomIn(step = this.STEP) { + this.zoom = Math.min(this.MAX_SCALE, this.zoom + step) + this.setPosition() + } + setZoomOut(step = this.STEP) { + this.zoom = Math.max(this.MIN_SCALE, this.zoom - step) + this.setPosition() + } + setZoomBest() { + this.reset() + this.setPosition() + } + setZoomOriginal() { + this.reset() + this.setPosition() + } + setRotate() { + this.rotate += 0.25 + this.setPosition() + } + reset() { + this.transformX = this.TRANSFORMX + this.transformY = this.TRANSFORMY + this.zoom = this.ZOOM + } + setPosition() { + this.el.style.transform = `translate(${this.transformX}px, ${this.transformY}px) scale(${this.zoom}) rotate(${this.rotate}turn)` + } +} diff --git a/sites/components/image-preview/index.md b/sites/components/image-preview/index.md new file mode 100644 index 00000000..4095dc58 --- /dev/null +++ b/sites/components/image-preview/index.md @@ -0,0 +1,137 @@ +# ImagePreview 图片预览 + +预览一张或多张图片的组件。 + +### 何时使用 + +需要根据用户传入进行图片预览展示或对容器内图片进行预览时。 + +### 基本用法 + +使用 image-preview 指令,对容器内图片进行预览。 + +
+ +
+ +```html +
+ +
+ + +``` + +### 自定义开启预览窗口 + +传入 custom 参数,指令会自动注入 open 方法,通过 custom.open 开启预览窗口 + +
+ +
+自定义 + +```html +
+ +
+自定义 + + +``` + +### 设置 zIndex + +通过设置 zIndex 控制弹出效果的层级,设置 backDropZIndex 控制弹出层背景的层级。 +可以看到当设置 zIndex 小于 backDropZIndex 时,imagePreview 会显示在背景下方。 +可以通过 Esc 关闭 imagePreview。 + +``` +// 待嵌入 modal 组件即可 +``` + +### API + +| 参数 | 类型 | 默认 | 说明 | +| :------------: | :-------: | :---: | :------------------------------------------------------------ | +| custom | `Object` | -- | 可选,指令会自动注入 open 方法,通过 custom.open 开启预览窗口 | +| disableDefault | `Boolean` | false | 可选,关闭默认点击触发图片预览方式 | +| zIndex | `Number` | 1050 | 可选,可选,设置预览时图片的 z-index 值 | +| backDropZIndex | `Number` | 1040 | 可选,设置预览时图片背景的 z-index 值 | + + + + -- Gitee From 9b766e4d0ac6476f23a2a468cd2dd2323477e3ed Mon Sep 17 00:00:00 2001 From: Dylan-duqingyu <63580483+Dylan-duqingyu@users.noreply.github.com> Date: Fri, 27 Aug 2021 15:58:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=8C=87=E4=BB=A4=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/image-preview.spec.ts | 87 +++++++++++++++++++ devui/image-preview/index.ts | 2 +- sites/components/image-preview/index.md | 10 +-- 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 devui/image-preview/__tests__/image-preview.spec.ts diff --git a/devui/image-preview/__tests__/image-preview.spec.ts b/devui/image-preview/__tests__/image-preview.spec.ts new file mode 100644 index 00000000..ee81693e --- /dev/null +++ b/devui/image-preview/__tests__/image-preview.spec.ts @@ -0,0 +1,87 @@ +import { mount } from '@vue/test-utils' +import { ImagePreviewDirective } from '../index' +import { ref } from 'vue' + +// 指令图片模板 +const imageTemplate = ` + + +` +// 全局属性 +const global = { + directives: { + dImagePreview: ImagePreviewDirective + } +} + +describe('image-preview', () => { + it('image-preview click', async () => { + const wrapper = mount( + { + template: ` +
+ ${imageTemplate} +
+ ` + }, + { + global + } + ) + const img = wrapper.find('#testImg') + await img.trigger('click') + const ele = document.querySelector('.devui-image-preview-main-image') + expect(ele).toBeTruthy() + const closeBtn = document.querySelector('.devui-image-preview-close-btn') as any + closeBtn.click() + }) + + it('image-preview disableDefault', async () => { + const wrapper = mount( + { + template: ` +
+ ${imageTemplate} +
+ ` + }, + { + global + } + ) + const img = wrapper.find('#testImg') + await img.trigger('click') + const ele = document.querySelector('.devui-image-preview-main-image') + expect(ele).toBeFalsy() + }) + + it('image-preview custom', async () => { + const custom: any = ref({}) + const open = () => custom.value.open() + const wrapper = mount( + { + template: ` +
+ ${imageTemplate} +
+ + `, + setup() { + return { + custom, + open + } + } + }, + { + global + } + ) + const customBtn = wrapper.find('#open') + await customBtn.trigger('click') + const ele = document.querySelector('.devui-image-preview-main-image') + expect(ele).toBeTruthy() + const closeBtn = document.querySelector('.devui-image-preview-close-btn') as any + closeBtn.click() + }) +}) diff --git a/devui/image-preview/index.ts b/devui/image-preview/index.ts index bf970529..4b791a64 100644 --- a/devui/image-preview/index.ts +++ b/devui/image-preview/index.ts @@ -8,7 +8,7 @@ export default { title: 'ImagePreview 图片预览', category: '数据展示', install(app: App): void { - app.directive('ImagePreview', ImagePreviewDirective) + app.directive('dImagePreview', ImagePreviewDirective) app.config.globalProperties.$imagePreviewService = ImagePreviewService } } diff --git a/sites/components/image-preview/index.md b/sites/components/image-preview/index.md index 4095dc58..4b9210db 100644 --- a/sites/components/image-preview/index.md +++ b/sites/components/image-preview/index.md @@ -8,14 +8,14 @@ ### 基本用法 -使用 image-preview 指令,对容器内图片进行预览。 +使用 v-dImagePreview 指令,对容器内图片进行预览。 -
+
```html -
+
@@ -38,14 +38,14 @@ 传入 custom 参数,指令会自动注入 open 方法,通过 custom.open 开启预览窗口 -
+
自定义 ```html
-- Gitee From 0e1fc1e02947b6d589d56bf24aed0931207724e1 Mon Sep 17 00:00:00 2001 From: duqingyu <1065161421@qq.com> Date: Fri, 3 Sep 2021 20:13:53 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=8C=87=E4=BB=A4=E5=91=BD=E5=90=8D=E4=B8=BA?= =?UTF-8?q?=E7=83=A4=E4=B8=B2=E6=96=B9=E5=BC=8F=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/image-preview/__tests__/image-preview.spec.ts | 6 +++--- devui/image-preview/index.ts | 2 +- sites/components/image-preview/index.md | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/devui/image-preview/__tests__/image-preview.spec.ts b/devui/image-preview/__tests__/image-preview.spec.ts index ee81693e..519e4e62 100644 --- a/devui/image-preview/__tests__/image-preview.spec.ts +++ b/devui/image-preview/__tests__/image-preview.spec.ts @@ -19,7 +19,7 @@ describe('image-preview', () => { const wrapper = mount( { template: ` -
+
${imageTemplate}
` @@ -40,7 +40,7 @@ describe('image-preview', () => { const wrapper = mount( { template: ` -
+
${imageTemplate}
` @@ -61,7 +61,7 @@ describe('image-preview', () => { const wrapper = mount( { template: ` -
+
${imageTemplate}
diff --git a/devui/image-preview/index.ts b/devui/image-preview/index.ts index 4b791a64..81251496 100644 --- a/devui/image-preview/index.ts +++ b/devui/image-preview/index.ts @@ -8,7 +8,7 @@ export default { title: 'ImagePreview 图片预览', category: '数据展示', install(app: App): void { - app.directive('dImagePreview', ImagePreviewDirective) + app.directive('d-image-preview', ImagePreviewDirective) app.config.globalProperties.$imagePreviewService = ImagePreviewService } } diff --git a/sites/components/image-preview/index.md b/sites/components/image-preview/index.md index 4b9210db..20867c52 100644 --- a/sites/components/image-preview/index.md +++ b/sites/components/image-preview/index.md @@ -8,14 +8,14 @@ ### 基本用法 -使用 v-dImagePreview 指令,对容器内图片进行预览。 +使用 v-d-image-preview 指令,对容器内图片进行预览。 -
+
```html -
+
@@ -38,14 +38,14 @@ 传入 custom 参数,指令会自动注入 open 方法,通过 custom.open 开启预览窗口 -
+
自定义 ```html
-- Gitee