可移动对话框

master
lzq 2026-01-20 19:34:14 +08:00
parent f22cd3fa7e
commit 20dc0f4048
14 changed files with 370 additions and 42 deletions

View File

@ -30,6 +30,7 @@
margin: 0;
padding: 0;
border: 0;
user-select: none;
}
</style>
<title></title>

View File

@ -82,5 +82,34 @@
}
.el-modal-dialog {
height 100% !important
box-sizing border-box !important
.el-overlay-dialog {
height 100% !important
overflow: hidden !important
display: flex !important
justify-content: center !important
align-items: center !important
box-sizing border-box !important
}
.el-dialog {
height 90% !important
margin 0 !important
box-sizing border-box !important
}
.draggable-dialog > .el-dialog__header > .el-dialog__title {
cursor move !important
display block
width 100%
}
.el-dialog__body {
height calc(100% - 88px) !important
box-sizing border-box !important
}
}

View File

@ -19,7 +19,7 @@ export const useAppPageStore = defineStore('AppPage', () => {
}))/* as Reactive<AppTypes.PageContext> */
const keepAliveInclude = computed(() => {
return pages.filter(it => it.keepAlive).map(it => it.routeName)
return pages.filter(it => it.keepAlive).map(it => Strings.pascalCase(it.routeName))
})
function open(ctx?: AppTypes.PageContext) {

View File

@ -1,5 +1,6 @@
import { SpecialPage } from '@/common/router/constants.ts'
import type { RouteRecordRaw } from 'vue-router'
import Strings from '@/common/utils/strings.ts'
// 导入 src/pages 目录下的所有 page.ts 文件
// K文件路径VPageConfig 对象
@ -13,6 +14,7 @@ const routeConfigs = Object.entries(configPath_routeConfig_map).map(([ configPat
path = routePath,
name = routePath.substring(routePath.lastIndexOf('/') + 1),
title = name,
componentName = Strings.pascalCase(name),
keepAlive = false,
icon = '',
component,
@ -25,6 +27,7 @@ const routeConfigs = Object.entries(configPath_routeConfig_map).map(([ configPat
parent,
path,
name,
componentName,
title,
icon,
keepAlive,

View File

@ -46,6 +46,10 @@ declare global {
*
*/
title?: string
/**
*
*/
componentName?: string
/**
*
*/

View File

@ -0,0 +1,184 @@
export class MoveHandle {
public id: string
public container: {
el?: HTMLElement;
width: number;
height: number;
x: number;
y: number;
}
public widget: {
el?: HTMLElement;
width: number;
height: number;
x: number;
y: number;
}
public triggerArea?: HTMLElement
public startPoint: {
x: number;
y: number;
}
public offset: {
x: number;
y: number;
}
public dragging: boolean
constructor(id: string, container?: HTMLElement, widget?: HTMLElement, triggerArea?: HTMLElement) {
this.id = id
this.container = {width: 0, height: 0, x: 0, y: 0}
this.widget = {width: 0, height: 0, x: 0, y: 0}
this.startPoint = {x: 0, y: 0}
this.offset = {x: 0, y: 0}
this.dragging = false
this.setContainer(container)
.setWidget(widget)
.setTriggerArea(triggerArea)
MoveHandles.set(this.id, this)
}
setContainer(el?: HTMLElement) {
if (MoveHandleIns === this) return this
this.container.el = el
if (el != null) {
const containerRect = el.getBoundingClientRect()
this.container.width = containerRect.width
this.container.height = containerRect.height
this.container.x = containerRect.x
this.container.y = containerRect.y
}
return this
}
setWidget(el?: HTMLElement) {
if (MoveHandleIns === this) return this
this.widget.el = el
if (el != null) {
const widgetRect = el.getBoundingClientRect()
this.widget.width = widgetRect.width
this.widget.height = widgetRect.height
this.widget.x = widgetRect.x
this.widget.y = widgetRect.y
}
return this
}
setTriggerArea(el?: HTMLElement) {
if (MoveHandleIns === this) return this
this.triggerArea?.removeEventListener('mousedown', handlerMousedown)
el?.addEventListener('mousedown', handlerMousedown)
this.triggerArea = el
return this
}
destroy() {
MoveHandles.delete(this.id)
this.disable()
this.setContainer()
.setWidget()
.setTriggerArea()
}
enable() {
if (this.container.el == null
|| this.widget.el == null
|| this.triggerArea == null) {
return
}
MoveHandleIns = this
}
disable() {
if (MoveHandleIns === this) MoveHandleIns = null
return this
}
}
let MoveHandleIns: MoveHandle | null = null
const MoveHandles: Map<string, MoveHandle> = new Map()
function handlerMousedown(e: Event) {
if (MoveHandleIns == null
|| MoveHandleIns.container.el == null
|| MoveHandleIns.widget.el == null
) return
const mouseEvent = e as MouseEvent
mouseEvent.preventDefault()
mouseEvent.stopPropagation()
MoveHandleIns.dragging = true
MoveHandleIns.startPoint.x = mouseEvent.clientX
MoveHandleIns.startPoint.y = mouseEvent.clientY
const transform = window.getComputedStyle(MoveHandleIns.widget.el).transform
let translateX = 0
let translateY = 0
if (transform !== 'none') {
const matrix = new DOMMatrix(transform)
translateX = matrix.e
translateY = matrix.f
} else {
MoveHandleIns.widget.el.style.transform = 'translate(0, 0)'
}
MoveHandleIns.offset.x = translateX
MoveHandleIns.offset.y = translateY
const containerRect = MoveHandleIns.container.el.getBoundingClientRect()
const widgetRect = MoveHandleIns.widget.el.getBoundingClientRect()
MoveHandleIns.container.width = containerRect.width
MoveHandleIns.container.height = containerRect.height
MoveHandleIns.container.x = containerRect.x
MoveHandleIns.container.y = containerRect.y
MoveHandleIns.widget.width = widgetRect.width
MoveHandleIns.widget.height = widgetRect.height
MoveHandleIns.widget.x = widgetRect.x
MoveHandleIns.widget.y = widgetRect.y
document.addEventListener('mouseup', handlerMouseup)
document.addEventListener('mousemove', handlerMove)
}
function handlerMove(e: Event) {
if (MoveHandleIns == null
|| !MoveHandleIns.dragging
|| MoveHandleIns.widget.el == null
) return
const mouseEvent = e as MouseEvent
let dx = mouseEvent.clientX - MoveHandleIns.startPoint.x
let dy = mouseEvent.clientY - MoveHandleIns.startPoint.y
// 左右边界
if (dx < 0) {
const min_dx = MoveHandleIns.container.x - MoveHandleIns.widget.x
dx = Math.max(dx, min_dx)
} else {
const max_dx = (MoveHandleIns.container.x + MoveHandleIns.container.width) - (MoveHandleIns.widget.x + MoveHandleIns.widget.width)
dx = Math.min(dx, max_dx)
}
// 上下边界
if (dy < 0) {
const min_dy = MoveHandleIns.container.y - MoveHandleIns.widget.y
dy = Math.max(dy, min_dy)
} else {
const max_dy = (MoveHandleIns.container.y + MoveHandleIns.container.height) - (MoveHandleIns.widget.y + MoveHandleIns.widget.height)
dy = Math.min(dy, max_dy)
}
let newOffsetX = MoveHandleIns.offset.x + dx
let newOffsetY = MoveHandleIns.offset.y + dy
MoveHandleIns.widget.el.style.transform = `translate(${newOffsetX}px, ${newOffsetY}px)`
}
function handlerMouseup() {
if (MoveHandleIns == null) return
MoveHandleIns.dragging = false
document.removeEventListener('mousemove', handlerMove)
document.removeEventListener('mouseup', handlerMouseup)
}
export default function (id: string, container?: HTMLElement, widget?: HTMLElement, triggerArea?: HTMLElement) {
return MoveHandles.get(id) ?? new MoveHandle(id, container, widget, triggerArea)
}

View File

@ -0,0 +1,101 @@
<script lang="ts" setup>
import { nanoid } from 'nanoid'
import useMove from '@/common/utils/use-move.ts'
const props = withDefaults(defineProps<{
show: boolean
title: string
closeOnSuccess?: boolean
maxHeight?: string
closed?: () => void
submitHandler: () => Promise<any>
}>(), {
maxHeight: '90%',
closeOnSuccess: true,
closed() {
},
})
const emits = defineEmits([ 'update:show' ])
function submit() {
submiting.value = true
props.submitHandler()
.then(() => {
if (props.closeOnSuccess) {
showDialog.value = false
}
})
.finally(() => {
submiting.value = false
})
}
const id = nanoid()
const modalClass = 'draggable-dialog-modal-' + id
const showDialog = computed({
get: () => props.show,
set: (val) => {
emits('update:show', val)
},
})
const submiting = ref(false)
let moveHandle = useMove(id)
function openedHandler() {
const modal = document.querySelector('.' + modalClass)
let container = modal?.querySelector('.el-overlay-dialog') as HTMLElement
let dialog = container?.querySelector('.draggable-dialog') as HTMLElement
let header = dialog?.querySelector('.el-dialog__header .el-dialog__title') as HTMLElement
moveHandle
.disable()
.setContainer(container)
.setWidget(dialog)
.setTriggerArea(header)
.enable()
}
function closedHandler() {
moveHandle
.disable()
.setContainer()
.setWidget()
.setTriggerArea()
}
onActivated(() => {
moveHandle.enable()
})
onUnmounted(() => {
moveHandle.destroy()
})
</script>
<template>
<ElDialog
:close-on-click-modal="false"
:modal="false"
:modal-class="modalClass"
:model-value="showDialog"
:title="title"
class="draggable-dialog"
destroy-on-close
width="fit-content" @close="closed"
@closed="closedHandler"
@opened="openedHandler"
@update:model-value="showDialog = $event"
>
<ElScrollbar>
<slot/>
</ElScrollbar>
<template #footer>
<ElButton @click="showDialog = false">关闭</ElButton>
<ElButton :loading="submiting" type="primary" @click="submit"></ElButton>
</template>
</ElDialog>
</template>
<style lang="stylus" scoped>
</style>

View File

@ -544,6 +544,7 @@ onMounted(doSearch)
}
}
}
.pagination {
justify-content center

View File

@ -17,8 +17,6 @@
contain: layout paint;
transform: translateZ(0);
box-sizing border-box
//box-shadow: inset rgba(30, 35, 43, 0.16) 0px 0 10px 1px;
//background-color: white;
display flex
flex-direction column
gap 10px

View File

@ -16,7 +16,6 @@ declare module 'vue' {
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
@ -75,7 +74,6 @@ declare global {
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElCard: typeof import('element-plus/es')['ElCard']
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']

View File

@ -31,7 +31,7 @@ onUnmounted(() => {
<div>
<ElButton :icon="isCollapse?elIcons.Fold:elIcons.Expand" text @click="isCollapse = !isCollapse"/>
<ElBreadcrumb :separator-icon="elIcons.ArrowRight">
<ElBreadcrumbItem v-for="(item, i) in appPageStore?.ctx?.breadcrumb??[]" :key="'a-frame-header-breadcrumb'+i">{{ item }}</ElBreadcrumbItem>
<ElBreadcrumbItem v-for="(item, i) in appPageStore?.currentPage?.breadcrumb??[]" :key="'a-frame-header-breadcrumb'+i">{{ item }}</ElBreadcrumbItem>
</ElBreadcrumb>
</div>
<div>
@ -51,7 +51,9 @@ onUnmounted(() => {
<ElMain class="a-frame-main">
<RouterView #="{ Component }">
<Transition name="slide-fade">
<KeepAlive :include="appPageStore?.keepAliveInclude">
<component :is="Component"/>
</KeepAlive>
</Transition>
</RouterView>
</ElMain>

View File

@ -1,5 +1,10 @@
<template>
<ElDialog v-model="showDialog" :close-on-click-modal="false" destroy-on-close width="fit-content" @close="dialogCloseHandler">
<ADialog
v-model:show="showDialog"
:closed="dialogCloseHandler"
:submit-handler="submitHandler"
title="产品分类"
>
<ElForm :model="formData" :rules="rules" ref="goodsCategoryForm" class="form-panel" label-width="auto">
<ElFormItem label-width="90" label="业务类型" prop="bizType">
<!-- <ElInput /> -->
@ -31,7 +36,7 @@
<ElButton @click="showDialog = false">{{ status === 'view' ? '关闭' : '取消' }}</ElButton>
<ElButton v-if="status !== 'view'" :loading="submiting" type="primary" @click="submitHandler"></ElButton>
</template>
</ElDialog>
</ADialog>
</template>
<script lang="ts" setup>
@ -46,6 +51,8 @@ import {
type FormInstance,
type FormRules,
} from 'element-plus'
import ADialog from '@/components/a-dialog/ADialog.vue'
const emits = defineEmits([ 'editSucc' ])
const showDialog = ref(false)
@ -88,10 +95,10 @@ function dialogCloseHandler() {
}
function submitHandler() {
if (status.value === 'view') return
if (status.value === 'view') return Promise.reject()
submiting.value = true
if (formData.id != null) {
FormUtil.submit(goodsCategoryFormIns, () => GoodsCategoryApi.modify(formData))
return FormUtil.submit(goodsCategoryFormIns, () => GoodsCategoryApi.modify(formData))
.then(() => {
ElMessage.success('修改成功')
emits('editSucc')
@ -101,7 +108,7 @@ function submitHandler() {
submiting.value = false
})
} else {
FormUtil.submit(goodsCategoryFormIns, () => GoodsCategoryApi.add(formData))
return FormUtil.submit(goodsCategoryFormIns, () => GoodsCategoryApi.add(formData))
.then(() => {
ElMessage.success('添加成功')
emits('editSucc')

View File

@ -39,7 +39,7 @@
<ElTableColumn label="是否可用" prop="canuse">
<template #default="{row}">
<ElSwitch v-model="row.canuse" @change="enableGoodsHandler($event,row.id)"/>
<ElSwitch v-model="row.canuse" @change="enableGoodsHandler($event as boolean,row.id)"/>
</template>
</ElTableColumn>
<ElTableColumn label="备注" prop="memo"/>

View File

@ -1,5 +1,10 @@
<template>
<ElDialog v-model="showDialog" :close-on-click-modal="false" destroy-on-close width="fit-content" @close="dialogCloseHandler">
<ADialog
v-model:show="showDialog"
:closed="dialogCloseHandler"
:submit-handler="submitHandler"
title="产品信息"
>
<ElForm :model="formData" :rules="rules" ref="goodsForm" class="form-panel" label-width="auto">
<ElFormItem label-width="90" label="产品类型" prop="goodsCategoryId">
<!-- <ElInput v-model="formData.goodsCategoryId" :disabled="status === 'view'" placeholder="产品类型 Id" /> -->
@ -37,12 +42,25 @@
<ElFormItem label-width="90" label="是否可用" prop="canuse">
<el-switch v-model="formData.canuse" :disabled="status === 'view'"/>
</ElFormItem>
<ElFormItem label="商品编码" label-width="90" prop="sn">
<ElInput v-model="formData.sn" :disabled="status === 'view'" placeholder="商品编码"/>
</ElFormItem>
<ElFormItem label="商品编码" label-width="90" prop="sn">
<ElInput v-model="formData.sn" :disabled="status === 'view'" placeholder="商品编码"/>
</ElFormItem>
<ElFormItem label="商品编码" label-width="90" prop="sn">
<ElInput v-model="formData.sn" :disabled="status === 'view'" placeholder="商品编码"/>
</ElFormItem>
<ElFormItem label="商品编码" label-width="90" prop="sn">
<ElInput v-model="formData.sn" :disabled="status === 'view'" placeholder="商品编码"/>
</ElFormItem>
<ElFormItem label="商品编码" label-width="90" prop="sn">
<ElInput v-model="formData.sn" :disabled="status === 'view'" placeholder="商品编码"/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showDialog = false">{{ status === 'view' ? '关闭' : '取消' }}</ElButton>
<ElButton v-if="status !== 'view'" :loading="submiting" type="primary" @click="submitHandler"></ElButton>
</template>
</ElDialog>
</ADialog>
</template>
<script lang="ts" setup>
@ -59,18 +77,17 @@ import Uploader from '@/components/uploader/Uploader.vue'
import GoodsCategoryApi from '@/pages/gds/goods-category/goods-category-api.ts'
import DictApi from '@/pages/sys/dict/dict-api.ts'
import ADict from '@/components/a-dict/ADict.vue'
import ADialog from '@/components/a-dialog/ADialog.vue'
const emits = defineEmits([ 'editSucc' ])
const showDialog = ref(false)
const submiting = ref(false)
const status = ref<'add' | 'view' | 'modify'>('add')
const goodsFormIns = useTemplateRef<FormInstance>('goodsForm')
const uploaderIns = useTemplateRef<InstanceType<typeof Uploader>>('uploader')
const formData = Utils.resetAble(reactive<GoodsTypes.SearchGoodsResult>({canuse: true}))
const rules = reactive<FormRules<GoodsTypes.SearchGoodsResult>>({
id: [ {required: true, message: '请填写Id', trigger: 'blur'} ],
goodsCategoryId: [ {required: true, message: '请填写产品类型', trigger: 'blur'} ],
sn: [ {required: true, message: '请填写商品编码', trigger: 'blur'} ],
goodsName: [ {required: true, message: '请填写产品名称', trigger: 'blur'} ],
@ -85,27 +102,18 @@ function dialogCloseHandler() {
}
function submitHandler() {
if (status.value === 'view') return
submiting.value = true
if (status.value === 'view') return Promise.reject()
if (formData.id != null) {
FormUtil.submit(goodsFormIns, () => GoodsApi.modify(formData))
return FormUtil.submit(goodsFormIns, () => GoodsApi.modify(formData))
.then(() => {
ElMessage.success('修改成功')
emits('editSucc')
showDialog.value = false
})
.finally(() => {
submiting.value = false
})
} else {
FormUtil.submit(goodsFormIns, () => GoodsApi.add(formData))
return FormUtil.submit(goodsFormIns, () => GoodsApi.add(formData))
.then(() => {
ElMessage.success('添加成功')
emits('editSucc')
showDialog.value = false
})
.finally(() => {
submiting.value = false
})
}
}
@ -113,18 +121,11 @@ function submitHandler() {
const category = ref<GoodsCategoryTypes.SearchGoodsCategoryResult[]>([])
const loading = ref(false)
const remoteMethod = (query: string) => {
console.log(query, 'query')
loading.value = true
GoodsCategoryApi.paging({size: 50, categoryName: query || undefined}).then((res) => {
category.value = res.data.records
loading.value = false
// category.value = list.value.filter((item) => {
// return item.label.toLowerCase().includes(query.toLowerCase())
// })
})
// } else {
// category.value = [];
// }
}
const unitList = ref<DictItemTypes.SearchDictItemResult[]>([])
const getOpt = () => {
@ -132,7 +133,6 @@ const getOpt = () => {
dictKey: 'unit',
}
DictApi.obtainDictData(searchForm).then((res) => {
// console.log(res.data);
unitList.value = res.data
})
}