抽取成通用的 FormPreviewDialog 组件

pull/711/head
Lemon 2025-02-25 16:06:21 +08:00
parent bc77178b37
commit 52d38572a1
3 changed files with 224 additions and 141 deletions

View File

@ -0,0 +1,218 @@
<template>
<el-dialog
:title="title"
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
@open="handleDialogOpen"
:fullscreen="false"
width="800"
class="form-preview-dialog"
destroy-on-close
draggable
:close-on-click-modal="false"
>
<div class="form-preview-wrapper">
<div class="form-preview-container" ref="containerRef">
<form-create :rule="rule" :option="option" />
</div>
<!-- 滚动按钮 -->
<div class="scroll-buttons" v-if="isDialogOpen">
<div
v-show="showTopBtn"
class="scroll-btn"
@click="scrollToTop"
>
<el-icon :size="20">
<ArrowUpBold />
</el-icon>
</div>
<div
v-show="showBottomBtn"
class="scroll-btn"
@click="scrollToBottom"
>
<el-icon :size="20">
<ArrowDownBold />
</el-icon>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
import { useDebounceFn } from '@vueuse/core'
defineOptions({ name: 'FormPreviewDialog' })
const props = defineProps({
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
default: '表单详情'
},
rule: {
type: Array,
required: true
},
option: {
type: Object,
required: true
},
scrollThreshold: {
type: Number,
default: 100
}
})
defineEmits(['update:modelValue'])
const containerRef = ref<HTMLElement | null>(null)
const showTopBtn = ref(false)
const showBottomBtn = ref(false)
const isDialogOpen = ref(false)
// 使
const handleScroll = useDebounceFn(() => {
const container = containerRef.value
if (container) {
const { scrollTop, scrollHeight, clientHeight } = container
showTopBtn.value = scrollTop > props.scrollThreshold
showBottomBtn.value = scrollTop + clientHeight < scrollHeight - props.scrollThreshold
}
}, 100)
const scrollToTop = () => {
const container = containerRef.value
if (container) {
container.scrollTo({
top: 0,
behavior: 'smooth'
})
}
}
const scrollToBottom = () => {
const container = containerRef.value
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
})
}
}
const initScrollButtons = async () => {
await nextTick()
//
setTimeout(() => {
handleScroll()
}, 300)
}
const handleDialogOpen = () => {
isDialogOpen.value = true
//
containerRef.value?.addEventListener('scroll', handleScroll)
//
initScrollButtons()
}
onUnmounted(() => {
containerRef.value?.removeEventListener('scroll', handleScroll)
})
// modelValue
watch(() => props.modelValue, (newVal) => {
if (!newVal) {
isDialogOpen.value = false
showTopBtn.value = false
showBottomBtn.value = false
}
})
</script>
<style lang="scss">
.form-preview-dialog.el-dialog {
margin: 5vh auto !important;
.el-dialog__body {
position: relative;
padding: 0;
height: 70vh;
max-height: 800px;
}
.el-dialog__header {
cursor: move;
margin: 0;
padding: 15px 20px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
}
</style>
<style lang="scss" scoped>
.form-preview-wrapper {
position: relative;
height: 100%;
width: 100%;
}
.form-preview-container {
height: 100%;
width: 100%;
overflow-y: auto;
padding: 20px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--el-color-primary-light-5);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background-color: #f5f7fa;
}
}
.scroll-buttons {
position: absolute;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 2001;
.scroll-btn {
background-color: var(--el-color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
cursor: pointer;
transition: all 0.3s;
opacity: 0.8;
&:hover {
background-color: var(--el-color-primary);
opacity: 1;
transform: scale(1.1);
}
}
}
</style>

View File

@ -1,119 +0,0 @@
<template>
<div class="scroll-buttons-overlay">
<div
v-show="showTopBtn"
class="scroll-btn"
@click="scrollToTop"
>
<el-icon :size="20">
<ArrowUpBold />
</el-icon>
</div>
<div
v-show="showBottomBtn"
class="scroll-btn"
@click="scrollToBottom"
>
<el-icon :size="20">
<ArrowDownBold />
</el-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
const props = defineProps({
containerSelector: {
type: String,
required: true
},
//
scrollThreshold: {
type: Number,
default: 100
}
})
const showTopBtn = ref(false)
const showBottomBtn = ref(false)
const handleScroll = () => {
const container = document.querySelector(props.containerSelector)
if (container) {
const { scrollTop, scrollHeight, clientHeight } = container as Element
showTopBtn.value = scrollTop > props.scrollThreshold
showBottomBtn.value = scrollTop + clientHeight < scrollHeight - 10
}
}
const scrollToTop = () => {
const container = document.querySelector(props.containerSelector)
if (container) {
container.scrollTo({
top: 0,
behavior: 'smooth'
})
}
}
const scrollToBottom = () => {
const container = document.querySelector(props.containerSelector)
if (container) {
container.scrollTo({
top: (container as Element).scrollHeight,
behavior: 'smooth'
})
}
}
onMounted(() => {
const container = document.querySelector(props.containerSelector)
if (container) {
container.addEventListener('scroll', handleScroll)
}
})
onUnmounted(() => {
const container = document.querySelector(props.containerSelector)
if (container) {
container.removeEventListener('scroll', handleScroll)
}
})
</script>
<style lang="scss" scoped>
.scroll-buttons-overlay {
position: fixed;
right: calc(50% - 400px - 60px);
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 2001;
.scroll-btn {
background-color: var(--el-color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
cursor: pointer;
transition: all 0.3s;
opacity: 0.8;
&:hover {
background-color: var(--el-color-primary);
opacity: 1;
transform: scale(1.1);
}
}
}
</style>

View File

@ -254,14 +254,11 @@
</Dialog>
<!-- 弹窗表单详情 -->
<Dialog title="表单详情" :fullscreen="false" v-model="formDetailVisible" width="800" class="form-detail-dialog">
<div class="form-detail-container relative" @scroll="handleScroll">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</div>
<!-- 使用新组件替换原来的滚动按钮 -->
<ScrollButtons container-selector=".form-detail-container" />
</Dialog>
<FormPreviewDialog
v-model="formDetailVisible"
:rule="formDetailPreview.rule"
:option="formDetailPreview.option"
/>
</template>
<script lang="ts" setup>
@ -279,9 +276,7 @@ import { cloneDeep, isEqual } from 'lodash-es'
import { useTagsView } from '@/hooks/web/useTagsView'
import { useDebounceFn } from '@vueuse/core'
import { subString } from '@/utils/index'
import { ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
import { ElBacktop } from 'element-plus'
import ScrollButtons from '@/components/ScrollButtons/index.vue'
import FormPreviewDialog from '@/components/FormPreviewDialog/index.vue'
defineOptions({ name: 'BpmModel' })
@ -620,17 +615,6 @@ watchEffect(() => {
</script>
<style lang="scss">
.form-detail-dialog.el-dialog {
margin: 5vh auto !important;
.el-dialog__body {
position: relative;
padding: 0;
height: 70vh;
max-height: 800px;
}
}
.rename-dialog.el-dialog {
padding: 0 !important;