mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 13:28:38 +08:00
新增 FaImagePreview 组件,并移除 ImagePreview 组件
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ImagePreview',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
src: string
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
}>(),
|
||||
{
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
)
|
||||
|
||||
const realWidth = computed(() => {
|
||||
return typeof props.width === 'string' ? props.width : `${props.width}px`
|
||||
})
|
||||
|
||||
const realHeight = computed(() => {
|
||||
return typeof props.height === 'string' ? props.height : `${props.height}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElImage :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<FaIcon name="i-ph:image-broken-duotone" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-image {
|
||||
background-color: var(--el-fill-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 30px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -110,6 +110,14 @@ const routes: RouteRecordRaw = {
|
||||
title: '悬浮卡片',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'imagepreview',
|
||||
name: 'componentExampleBuiltInImagePreview',
|
||||
component: () => import('@/views/component_built_in_example/imagepreview.vue'),
|
||||
meta: {
|
||||
title: '图片预览',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'input',
|
||||
name: 'componentExampleBuiltInInput',
|
||||
@@ -271,14 +279,6 @@ const routes: RouteRecordRaw = {
|
||||
title: '上传',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'imagepreview',
|
||||
name: 'componentExampleExtendImagePreview',
|
||||
component: () => import('@/views/component_extend_example/image.preview.vue'),
|
||||
meta: {
|
||||
title: '图片预览',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pcas',
|
||||
name: 'componentExampleExtendPcas',
|
||||
|
||||
Vendored
+1
-1
@@ -24,6 +24,7 @@ declare module 'vue' {
|
||||
FaFixedActionBar: typeof import('./../ui/components/FaFixedActionBar/index.vue')['default']
|
||||
FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
|
||||
FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
|
||||
FaImagePreview: typeof import('./../ui/components/FaImagePreview/index.vue')['default']
|
||||
FaInput: typeof import('./../ui/components/FaInput/index.vue')['default']
|
||||
FaKbd: typeof import('./../ui/components/FaKbd/index.vue')['default']
|
||||
FaMaskScrollContainer: typeof import('./../ui/components/FaMaskScrollContainer/index.vue')['default']
|
||||
@@ -47,7 +48,6 @@ declare module 'vue' {
|
||||
FaToast: typeof import('./../ui/components/FaToast/index.vue')['default']
|
||||
FaTooltip: typeof import('./../ui/components/FaTooltip/index.vue')['default']
|
||||
FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
|
||||
ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
|
||||
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
|
||||
ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits & { animationEnd: [] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-2000 data-[state=closed]:animate-out data-[state=open]:animate-in bg-black/80 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0"
|
||||
/>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed z-2000 inset-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
props.class,
|
||||
)"
|
||||
@animationend="emits('animationEnd')"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ring"
|
||||
>
|
||||
<X class="h-8 w-8" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 grid data-[state=closed]:animate-out data-[state=open]:animate-in place-items-center overflow-y-auto bg-black/80 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="forwarded"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute right-3 top-3 rounded-md p-0.5 transition-colors hover:bg-secondary"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Dialog } from './Dialog.vue'
|
||||
export { default as DialogClose } from './DialogClose.vue'
|
||||
export { default as DialogContent } from './DialogContent.vue'
|
||||
export { default as DialogDescription } from './DialogDescription.vue'
|
||||
export { default as DialogFooter } from './DialogFooter.vue'
|
||||
export { default as DialogHeader } from './DialogHeader.vue'
|
||||
export { default as DialogScrollContent } from './DialogScrollContent.vue'
|
||||
export { default as DialogTitle } from './DialogTitle.vue'
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue'
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils'
|
||||
import { Dialog, DialogContent } from './dialog'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaImagePreview',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
src: string
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
load: []
|
||||
error: []
|
||||
}>()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const isError = ref(false)
|
||||
const isOpen = ref(false)
|
||||
|
||||
// 图片操作相关状态
|
||||
const scale = ref(1)
|
||||
const rotate = ref(0)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
|
||||
function handleLoad() {
|
||||
isLoading.value = false
|
||||
emits('load')
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
isError.value = true
|
||||
isLoading.value = false
|
||||
emits('error')
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (!isError.value) {
|
||||
isOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 图片操作函数
|
||||
function handleZoomIn() {
|
||||
scale.value = Math.min(scale.value + 0.25, 3)
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
scale.value = Math.max(scale.value - 0.25, 0.5)
|
||||
}
|
||||
|
||||
function handleOriginalSize() {
|
||||
scale.value = 1
|
||||
}
|
||||
|
||||
function handleRotateLeft() {
|
||||
rotate.value = rotate.value - 90
|
||||
}
|
||||
|
||||
function handleRotateRight() {
|
||||
rotate.value = rotate.value + 90
|
||||
}
|
||||
|
||||
// 处理滚轮缩放
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault()
|
||||
if (e.deltaY < 0) {
|
||||
handleZoomIn()
|
||||
}
|
||||
else {
|
||||
handleZoomOut()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标按下事件
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
isDragging.value = true
|
||||
dragStart.value = {
|
||||
x: e.clientX - position.value.x,
|
||||
y: e.clientY - position.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标移动事件
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!isDragging.value) {
|
||||
return
|
||||
}
|
||||
position.value = {
|
||||
x: e.clientX - dragStart.value.x,
|
||||
y: e.clientY - dragStart.value.y,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鼠标松开事件
|
||||
function handleMouseUp() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 重置图片状态
|
||||
function resetImageState() {
|
||||
scale.value = 1
|
||||
rotate.value = 0
|
||||
position.value = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
// 监听鼠标事件
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
function handleAnimationEnd() {
|
||||
if (!isOpen.value) {
|
||||
resetImageState()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="group/image-preview relative inline-block overflow-hidden border rounded-lg">
|
||||
<img
|
||||
:src="src"
|
||||
:class="cn('size-50 object-contain cursor-pointer bg-background transition-all duration-300 group-hover/image-preview:scale-110', props.class, {
|
||||
invisible: isError || isLoading,
|
||||
})"
|
||||
@error="handleError"
|
||||
@load="handleLoad"
|
||||
@click="!isLoading && !isError && handleClick()"
|
||||
>
|
||||
<div v-if="isLoading" class="absolute left-0 top-0 h-full w-full flex-center">
|
||||
<slot name="loading">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-8 text-secondary-foreground/50" />
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="isError" class="absolute left-0 top-0 h-full w-full flex-center">
|
||||
<slot name="error">
|
||||
<FaIcon name="i-ph:image-broken-duotone" class="size-8 text-secondary-foreground/50" />
|
||||
</slot>
|
||||
</div>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="size-full" @animation-end="handleAnimationEnd">
|
||||
<div class="relative size-full" @wheel="handleWheel">
|
||||
<img
|
||||
:src="src"
|
||||
class="mx-auto max-h-full max-w-full object-contain"
|
||||
:class="{
|
||||
'transition-all duration-300': !isDragging,
|
||||
}"
|
||||
:style="{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale}) rotate(${rotate}deg)`,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
}"
|
||||
@mousedown.prevent="handleMouseDown"
|
||||
>
|
||||
<FaButtonGroup class="absolute bottom-4 left-1/2 scale-125 -translate-x-1/2">
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomIn">
|
||||
<FaIcon name="i-carbon:zoom-in" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleZoomOut">
|
||||
<FaIcon name="i-carbon:zoom-out" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleOriginalSize">
|
||||
<FaIcon name="i-lucide:maximize" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateLeft">
|
||||
<FaIcon name="i-carbon:rotate" class="size-5" />
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="icon" class="border-none bg-muted/50" @click="handleRotateRight">
|
||||
<FaIcon name="i-carbon:rotate-180" class="size-5" />
|
||||
</FaButton>
|
||||
</FaButtonGroup>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
enabled: false
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FaPageHeader title="图片预览" description="FaImagePreview" />
|
||||
<FaPageMain>
|
||||
<div class="space-x-4">
|
||||
<FaImagePreview src="https://fantastic-admin.hurui.me/logo.svg" />
|
||||
<FaImagePreview src="http://www.baidu.com" class="size-25" />
|
||||
<FaImagePreview src="http://www.baidu.com">
|
||||
<template #error>
|
||||
<div class="flex-col-center text-sm text-secondary-foreground/50">
|
||||
<div>
|
||||
自定义错误信息
|
||||
</div>
|
||||
<div>
|
||||
图片加载失败
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FaImagePreview>
|
||||
</div>
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user