add image blur detection and background management pages

This commit is contained in:
2025-03-12 16:12:19 +08:00
parent a20ea6c76b
commit 9e6dd55087
60 changed files with 6647 additions and 650 deletions

View File

@@ -45,7 +45,7 @@
<div class="upload-container">
<AUploadDragger
v-model:fileList="upload.fileList"
:beforeUpload="upload.beforeUpload"
:beforeUpload="upload.beforeUploadWithWebWorker"
:customRequest="customUploadRequest"
:directory="false"
:maxCount="10"

View File

@@ -113,6 +113,34 @@
</template>
</ASwitch>
</div>
<div class="upload-setting-item">
<AFlex :vertical="false" align="center" justify="flex-start" gap="middle">
<AAvatar size="default" shape="square" :src="lock"/>
<span class="upload-setting-item-name">图像加密</span>
</AFlex>
<ASwitch v-model:checked="uploadStore.uploadSetting.encrypt">
<template #checkedChildren>
<check-outlined/>
</template>
<template #unCheckedChildren>
<close-outlined/>
</template>
</ASwitch>
</div>
<div class="upload-setting-item">
<AFlex :vertical="false" align="center" justify="flex-start" gap="middle">
<AAvatar size="default" shape="square" :src="blur"/>
<span class="upload-setting-item-name">模糊检测</span>
</AFlex>
<ASwitch v-model:checked="uploadStore.uploadSetting.blur_detection">
<template #checkedChildren>
<check-outlined/>
</template>
<template #unCheckedChildren>
<close-outlined/>
</template>
</ASwitch>
</div>
</div>
</template>
<script setup lang="ts">
@@ -125,7 +153,8 @@ import gps from "@/assets/svgs/gps.svg";
import target from "@/assets/svgs/target.svg";
import qr from "@/assets/svgs/qr.svg";
import face_detection from "@/assets/svgs/face_detection.svg";
import lock from "@/assets/svgs/lock.svg";
import blur from "@/assets/svgs/blur.svg";
const uploadStore = useStore().upload;

View File

@@ -1,446 +0,0 @@
<script setup lang="ts">
// utilities
import type {CSSProperties} from 'vue';
import {computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue';
// prop types
export interface Props {
aspectRatio?: 'taller' | 'wider'
handle?: string | number | boolean
handleSize?: number
hover?: boolean
slideOnClick?: boolean
keyboard?: boolean
keyboardStep?: number
leftImage: string
leftImageAlt?: string
leftImageCss?: object
leftImageLabel?: string
onSliderPositionChange?: (position: number) => void
rightImage: string
rightImageAlt?: string
rightImageCss?: object
rightImageLabel?: string
skeleton?: string | number | boolean
sliderLineColor?: string
sliderLineWidth?: number
sliderPositionPercentage?: number
vertical?: boolean
}
// props
const props = withDefaults(defineProps<Props>(), {
keyboard: false,
keyboardStep: 0.01,
hover: false,
slideOnClick: true,
handleSize: 40,
sliderLineWidth: 2,
sliderPositionPercentage: 0.5,
vertical: false,
onSliderPositionChange: () => {
},
sliderLineColor: '#ffffff',
aspectRatio: 'wider',
});
// emits
const emit = defineEmits<{
(e: 'slideStart', pos: number): void
(e: 'slideEnd', pos: number): void
(e: 'isSliding', state: boolean): void
}>();
// variables
const {
aspectRatio,
leftImage,
leftImageAlt,
leftImageLabel,
leftImageCss,
rightImage,
rightImageAlt,
rightImageLabel,
rightImageCss,
hover,
handle,
handleSize,
sliderLineWidth,
sliderPositionPercentage,
skeleton,
sliderLineColor,
vertical,
onSliderPositionChange,
slideOnClick,
keyboard,
keyboardStep
} = toRefs(props);
const componentId = Math.random().toString(36).substring(2, 9);
const horizontal = !vertical.value;
const containerRef = ref();
const rightImageRef = ref<HTMLImageElement | null>(null);
const leftImageRef = ref<HTMLImageElement | null>(null);
const sliderPosition = ref(sliderPositionPercentage.value);
const containerWidth = ref(0);
const containerHeight = ref(0);
const leftImgLoaded = ref(false);
const rightImgLoaded = ref(false);
const isSliding = ref(false);
// computed refs
const allImagesLoaded = computed(() => leftImgLoaded.value && rightImgLoaded.value);
// Introduce refs(rightImageClip|leftImageClip) to correct bug caused when shifting from deprecated
// css property 'clip' to 'clipPath'. clip-path:inset works as paddings or margin
// so when right image clip reduces, left image clip has to increase for the comparison
// effect to work
const rightImageClip = computed(() => sliderPosition.value);
const leftImageClip = computed(() => 1 - sliderPosition.value);
// computed styles
const containerStyle = computed((): CSSProperties => {
return {
display: allImagesLoaded.value ? 'flex' : 'none',
height: `${containerHeight.value}px`,
};
});
const rightImageStyle = computed((): CSSProperties => {
return {
clipPath: horizontal ? `inset(0px 0px 0px ${containerWidth.value * rightImageClip.value}px)` : `inset(${containerHeight.value * rightImageClip.value}px 0px 0px 0px)`,
...rightImageCss,
};
});
const leftImageStyle = computed((): CSSProperties => {
return {
clipPath: horizontal ? `inset(0px ${containerWidth.value * leftImageClip.value}px 0px 0px)` : `inset(0px 0px ${containerHeight.value * leftImageClip.value}px 0px)`,
...leftImageCss,
};
});
const sliderStyle = computed((): CSSProperties => {
return {
cursor: !hover.value && horizontal ? 'ew-resize !important' : !hover.value && !horizontal ? 'ns-resize !important' : undefined,
flexDirection: horizontal ? 'column' : 'row',
height: horizontal ? '100%' : `${handleSize.value}px`,
left: horizontal ? `${containerWidth.value * sliderPosition.value - handleSize.value / 2}px` : '0',
top: horizontal ? '0' : `${containerHeight.value * sliderPosition.value - handleSize.value / 2}px`,
width: horizontal ? `${handleSize.value}px` : '100%',
};
});
const lineStyle = computed((): CSSProperties => {
return {
background: sliderLineColor.value,
height: horizontal ? '100%' : `${sliderLineWidth.value}px`,
width: horizontal ? `${sliderLineWidth.value}px` : '100%',
};
});
const handleDefaultStyle = computed((): CSSProperties => {
return {
border: `${sliderLineWidth.value}px solid ${sliderLineColor.value}`,
height: `${handleSize.value}px`,
width: `${handleSize.value}px`,
transform: horizontal ? 'none' : 'rotate(90deg)',
};
});
const leftArrowStyle = computed((): CSSProperties => {
return {
border: `inset ${handleSize.value * 0.15}px rgba(0,0,0,0)`,
borderRight: `${handleSize.value * 0.15}px solid ${sliderLineColor.value}`,
marginLeft: `-${handleSize.value * 0.25}px`, // for IE11
marginRight: `${handleSize.value * 0.25}px`,
};
});
const rightArrowStyle = computed((): CSSProperties => {
return {
border: `inset ${handleSize.value * 0.15}px rgba(0,0,0,0)`,
borderLeft: `${handleSize.value * 0.15}px solid ${sliderLineColor.value}`,
marginRight: `-${handleSize.value * 0.25}px`, // for IE11
};
});
const leftLabelStyle = computed((): CSSProperties => {
return {
left: horizontal ? '5%' : '50%',
opacity: isSliding.value ? 0 : 1,
top: horizontal ? '50%' : '3%',
transform: horizontal ? 'translate(0,-50%)' : 'translate(-50%, 0)',
borderRadius: '10px',
};
});
const rightLabelStyle = computed((): CSSProperties => {
return {
opacity: isSliding.value ? 0 : 1,
left: horizontal ? 'unset' : '50%',
right: horizontal ? '5%' : 'unset',
top: horizontal ? '50%' : 'unset',
bottom: horizontal ? 'unset' : '3%',
transform: horizontal ? 'translate(0,-50%)' : 'translate(-50%, 0)',
borderRadius: '10px',
};
});
const leftLabelContainerStyle = computed((): CSSProperties => {
return {
clip: horizontal ? `rect(auto, ${containerWidth.value * sliderPosition.value}px, auto, auto)` : `rect(auto, auto, ${containerHeight.value * sliderPosition.value}px, auto)`,
};
});
const rightLabelContainerStyle = computed((): CSSProperties => {
return {
clipPath: horizontal ? `inset(0px 0px 0px ${containerWidth.value * rightImageClip.value}px)` : `inset(${containerHeight.value * rightImageClip.value}px 0px 0px 0px)`,
};
});
function handleSliding(event: MouseEvent | TouchEvent | KeyboardEvent) {
const e = event as TouchEvent;
// Calc cursor position from the:
// - left edge of the viewport (for horizontal)
// - top edge of the viewport (for vertical)
// @ts-expect-error it is necessary
const cursorXfromViewport = e.touches ? e.touches[0].pageX : e.pageX;
// @ts-expect-error it is necessary
const cursorYfromViewport = e.touches ? e.touches[0].pageY : e.pageY;
// Calc Cursor Position from the:
// - left edge of the window (for horizontal)
// - top edge of the window (for vertical)
// to consider any page scrolling
const cursorXfromWindow = cursorXfromViewport - window.scrollX;
const cursorYfromWindow = cursorYfromViewport - window.scrollY;
// Calc Cursor Position from the left edge of the image
const imagePosition = rightImageRef.value!.getBoundingClientRect();
let pos = horizontal ? cursorXfromWindow - imagePosition.left : cursorYfromWindow - imagePosition.top;
// Set minimum and maximum value-to-prevent the slider from overflowing
const minPos = sliderLineWidth.value / 2;
const maxPos = horizontal ? containerWidth.value - sliderLineWidth.value / 2 : containerHeight.value - sliderLineWidth.value / 2;
if (pos < minPos)
pos = minPos;
if (pos > maxPos)
pos = maxPos;
sliderPosition.value = horizontal ? pos / containerWidth.value : pos / containerHeight.value;
if (onSliderPositionChange.value)
onSliderPositionChange.value(horizontal ? pos / containerWidth.value : pos / containerHeight.value);
}
function startSliding(e: MouseEvent | TouchEvent | KeyboardEvent) {
isSliding.value = true;
emit('slideStart', sliderPosition.value);
emit('isSliding', isSliding.value);
if (!horizontal)
e.preventDefault(); // prevent all default + mobile scrolling if vertical
else if (!('touches' in e))
e.preventDefault(); // prevent default except from mobile scrolling
// Slide the image even if you just click or tap (not drag)
if (slideOnClick.value)
handleSliding(e);
window.addEventListener('mousemove', handleSliding);
window.addEventListener('touchmove', handleSliding);
window.addEventListener('mouseup', finishSliding);
window.addEventListener('touchend', finishSliding);
}
function finishSliding() {
isSliding.value = false;
emit('slideEnd', sliderPosition.value);
emit('isSliding', isSliding.value);
window.removeEventListener('mousemove', handleSliding);
window.removeEventListener('touchmove', handleSliding);
window.removeEventListener('mouseup', finishSliding);
window.removeEventListener('touchend', finishSliding);
}
function handleFocusIn() {
if (keyboard.value)
window.addEventListener('keydown', handleKeyDown);
}
function handleFocusOut() {
if (keyboard.value)
window.removeEventListener('keydown', handleKeyDown);
}
function handleOnClick() {
if (keyboard.value)
window.addEventListener('keydown', handleKeyDown);
}
function handleOnClickOutside(event: KeyboardEvent | MouseEvent) {
if (containerRef.value && !containerRef.value.contains(event.target)) {
// The click is outside the container, remove the event listener
containerRef.value.blur();
window.removeEventListener('keydown', handleKeyDown);
}
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'ArrowDown' && !horizontal) {
e.preventDefault();
if ((sliderPosition.value + keyboardStep.value) > 1)
sliderPosition.value = 1;
else
sliderPosition.value += keyboardStep.value;
} else if (e.key === 'ArrowUp' && !horizontal) {
e.preventDefault();
if ((sliderPosition.value - keyboardStep.value) < 0)
sliderPosition.value = 0;
else
sliderPosition.value -= keyboardStep.value;
} else if (e.key === 'ArrowLeft' && horizontal) {
e.preventDefault();
if ((sliderPosition.value - keyboardStep.value) < 0)
sliderPosition.value = 0;
else
sliderPosition.value -= keyboardStep.value;
} else if (e.key === 'ArrowRight' && horizontal) {
e.preventDefault();
if ((sliderPosition.value + keyboardStep.value) > 1)
sliderPosition.value = 1;
else
sliderPosition.value += keyboardStep.value;
} else {
// do something
}
}
function forceRenderHover(): void {
const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate();
const containerElement = containerRef.value;
if (props.hover) {
containerElement?.addEventListener('mousemove', startSliding);
containerElement?.addEventListener('mouseleave', finishSliding);
} else {
containerElement?.removeEventListener('mousemove', startSliding);
containerElement?.removeEventListener('mouseleave', finishSliding);
containerElement?.addEventListener('mouseup', finishSliding);
containerElement?.addEventListener('touchend', finishSliding);
// containerElement?.addEventListener('mouseleave', finishSliding)
}
}
// Make the component responsive
onMounted(() => {
const containerElement = containerRef.value;
const resizeObserver = new ResizeObserver(([entry]) => {
containerWidth.value = entry.target.getBoundingClientRect().width;
});
resizeObserver.observe(containerElement);
return () => resizeObserver.disconnect();
});
onMounted(() => {
const containerElement = containerRef.value;
// had to include this here, binding it with the container with the if hover prop doesn't work for some reason
if (props.hover) {
containerElement?.addEventListener('mousemove', startSliding); // 03
containerElement?.addEventListener('mouseleave', finishSliding); // 04
}
window.addEventListener('click', handleOnClickOutside);
// containerElement?.addEventListener('mouseleave', finishSliding)
});
onBeforeUnmount(() => {
const containerElement = containerRef.value;
containerElement?.removeEventListener('mousemove', startSliding);
containerElement?.removeEventListener('mouseleave', finishSliding);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('click', handleOnClickOutside);
window.removeEventListener('mousemove', handleSliding);
window.removeEventListener('touchmove', handleSliding);
window.removeEventListener('mouseup', finishSliding);
window.removeEventListener('touchend', finishSliding);
});
// Watch for changes in leftImage
watch(leftImageRef, () => {
leftImgLoaded.value = !!leftImageRef.value?.complete;
});
// Watch for changes in rightImage
watch(rightImageRef, () => {
rightImgLoaded.value = !!rightImageRef.value?.complete;
});
// since hover is the only listener set on mount, we need to rerender component if the value changes
watch(hover, () => {
forceRenderHover();
});
// Calculate container height
watch(
[() => containerWidth.value, () => leftImgLoaded.value, () => rightImgLoaded.value],
() => {
const leftImageWidthHeightRatio = leftImageRef.value!.naturalHeight / leftImageRef.value!.naturalWidth;
const rightImageWidthHeightRatio = rightImageRef.value!.naturalHeight / rightImageRef.value!.naturalWidth;
const idealWidthHeightRatio
= aspectRatio.value === 'taller' ? Math.max(leftImageWidthHeightRatio, rightImageWidthHeightRatio) : Math.min(leftImageWidthHeightRatio, rightImageWidthHeightRatio);
containerHeight.value = containerWidth.value * idealWidthHeightRatio;
},
);
</script>
<template>
<div v-if="skeleton && !allImagesLoaded" data-testid="skeleton" :style="containerStyle" v-html="skeleton"/>
<div
v-else :id="componentId" ref="containerRef" class="vci--container" tabindex="0" data-testid="vci-container"
:style="containerStyle" @click="handleOnClick" @touchstart="startSliding" @touchend="finishSliding"
@focusin="handleFocusIn" @focusout="handleFocusOut" @mousedown="startSliding" @mouseup="finishSliding"
>
<img
ref="rightImageRef" class="vci--right-image" :alt="rightImageAlt" data-testid="right-image" :src="rightImage"
:style="rightImageStyle" @load="rightImgLoaded = true"
>
<img
ref="leftImageRef" class="vci--left-image" :alt="leftImageAlt" data-testid="left-image" :src="leftImage"
:style="leftImageStyle" @load="leftImgLoaded = true"
>
<div class="vci--slider" :style="sliderStyle">
<div class="vci--slider-line" :style="lineStyle"/>
<div v-if="handle" class="vci--custom-handle" v-html="handle"/>
<div v-else class="vci--default-handle" :style="handleDefaultStyle">
<div class="vci--left-arrow" :style="leftArrowStyle"/>
<div class="vci--right-arrow" :style="rightArrowStyle"/>
</div>
<div class="vci--slider-line" :style="lineStyle"/>
</div>
<div v-if="leftImageLabel" class="vci--left-label-container" :style="leftLabelContainerStyle">
<div class="vci--left-label" data-testid="left-image-label" :style="leftLabelStyle">
{{ leftImageLabel }}
</div>
</div>
<div v-if="rightImageLabel" class="vci--right-label-container" :style="rightLabelContainerStyle">
<div class="vci--right-label" data-testid="right-image-label" :style="rightLabelStyle">
{{ rightImageLabel }}
</div>
</div>
</div>
</template>
<style scoped lang="scss" src="./index.scss">
</style>

View File

@@ -1,93 +0,0 @@
.vci--container {
box-sizing: border-box;
position: relative;
display: flex;
overflow: hidden;
width: 100%;
}
.vci--right-image {
display: flex;
position: absolute;
object-fit: cover;
height: 100%;
width: 100%;
}
.vci--left-image {
display: flex;
position: absolute;
object-fit: cover;
height: 100%;
width: 100%;
}
.vci--slider {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
.vci--slider-line {
flex: 0 1 auto;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
}
.vci--custom-handle {
box-sizing: border-box;
display: flex;
flex: 1 0 auto;
justify-content: center;
align-items: center;
height: auto;
width: auto;
}
.vci--default-handle {
box-sizing: border-box;
display: flex;
flex: 1 0 auto;
justify-content: center;
align-items: center;
border-radius: 100%;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
}
.vci--left-arrow {
height: 0px;
width: 0px;
}
.vci--right-arrow {
height: 0px;
width: 0px;
}
.vci--left-label {
position: absolute;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.5);
color: white;
transition: opacity 0.1s ease-out;
}
.vci--right-label {
position: absolute;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.5);
color: white;
transition: opacity 0.1s ease-out;
}
.vci--right-label-container {
position: absolute;
height: 100%;
width: 100%;
}
.vci--left-label-container {
position: absolute;
height: 100%;
width: 100%;
}

View File

@@ -1,3 +0,0 @@
import VueCompareImage from './VueCompareImage.vue';
export {VueCompareImage};