<script setup lang="ts">
import { useDraggable, useMouseInElement } from '@vueuse/core';
import type { CSSProperties } from 'vue';
interface Props {
isMask?: boolean
dialogStyle?: Record<string, any>
title?: string
footer?: boolean
isClose?: boolean
// 最外層自定義類
className?: string
// 默認插入到body下
appendTo?: HTMLElement | string
type?: number | string
submitText: string
}
defineOptions({
name: 'ProDialog',
});
withDefaults(defineProps<Props>(), {
isMask: false,
title: '標題',
type: 0,
dialogStyle: () => {
return {
width: '300px',
left: '40%',
top: '35%',
};
},
footer: true,
isClose: true,
appendTo: 'body',
submitText: '保存',
});
const emit = defineEmits(['submit', 'close', 'cancel', 'resize']);
const containerEleRef = ref<HTMLElement>();
const modalTitleRef = ref<HTMLElement | null>(null);
const { x, y, isDragging } = useDraggable(modalTitleRef);
const resizeTransformX = ref(0); // 彈窗大小變化導致的水平方向上的位置偏移量
const startX = ref<number>(0);
const startY = ref<number>(0);
const startedDrag = ref(false);
const transformX = ref(0);
const transformY = ref(0);
const preTransformX = ref(0);
const preTransformY = ref(0);
const dragRect = ref({ left: 0, right: 0, top: 0, bottom: 0 });
watch([x, y], () => {
if (!startedDrag.value) {
startX.value = x.value;
startY.value = y.value;
const bodyRect = document.body.getBoundingClientRect();
const titleRect = modalTitleRef.value!.getBoundingClientRect();
dragRect.value.right = bodyRect.width - titleRect.width;
dragRect.value.bottom = bodyRect.height - titleRect.height;
preTransformX.value = transformX.value;
preTransformY.value = transformY.value;
}
startedDrag.value = true;
});
watch(isDragging, () => {
if (!isDragging.value) {
startedDrag.value = false;
}
});
watchEffect(() => {
if (startedDrag.value) {
transformX.value
= preTransformX.value
+ Math.min(Math.max(dragRect.value.left, x.value), dragRect.value.right)
- startX.value;
transformY.value
= preTransformY.value
+ Math.min(Math.max(dragRect.value.top, y.value), dragRect.value.bottom)
- startY.value;
}
});
const transformStyle = computed<CSSProperties>(() => {
// console.log(transformX.value, resizeTransformX.value);
return {
transform: `translate(${transformX.value + resizeTransformX.value}px, ${transformY.value}px)`,
};
});
// ----------拖動縮放-----------
const dragType = ref('none'); // left:左側拖動 right: 右側拖動 down: 下方拖動 leftDown: 左下角拖動 rightDown: 右下角拖動 none: 不拖動
// 光標樣式
const cursor = computed(() => {
if (dragType.value === 'left' || dragType.value === 'right') {
return 'cursor-ew-resize';
}
if (dragType.value === 'down') {
return 'cursor-ns-resize';
}
if (dragType.value === 'leftDown') {
return 'cursor-nesw-resize';
}
if (dragType.value === 'rightDown') {
return 'cursor-nwse-resize';
}
return 'cursor-auto';
});
const modalBodyRef = ref<HTMLElement | null>(null);
const { isDragging: modalBodyIsDragging } = useDraggable(modalBodyRef);
const { x: mouseX, y: mouseY, elementPositionX, elementPositionY, elementHeight, elementWidth } = useMouseInElement(modalBodyRef);
const bodyWidth = ref(0);
const bodyHeight = ref(0);
let startBodyWidth = 0; // 拖動開始時modal-body的寬度
let startBodyHeight = 0;// 拖動開始時modal-body的高度
let startMouseX = 0; // 拖動開始時鼠標的x值
let startMouseY = 0;// 拖動開始時鼠標的y值
let isStartDrag = false; // 拖動開始標識
let preResizeTransformX = 0; // 上一次拖動的x軸偏移量
watch(modalBodyIsDragging, () => {
// 拖動結束
if (!modalBodyIsDragging.value) {
isStartDrag = false;
}
});
watch([mouseX, mouseY], () => {
const bodyW = elementWidth.value;
const bodyH = elementHeight.value;
const bodyX = elementPositionX.value;
const bodyY = elementPositionY.value;
const bodyMaxX = bodyX + bodyW;
const bodyMaxY = bodyY + bodyH;
// 計算拖動類型
if (!modalBodyIsDragging.value) {
if (mouseX.value >= bodyX && mouseX.value <= bodyX + 5 && mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
dragType.value = 'leftDown';
}
else if (mouseX.value <= bodyMaxX && mouseX.value >= bodyMaxX - 5 && mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
dragType.value = 'rightDown';
}
else if (mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
dragType.value = 'down';
}
else if ((mouseX.value >= bodyX && mouseX.value <= bodyX + 5)) {
dragType.value = 'left';
}
else if (mouseX.value <= bodyMaxX && mouseX.value >= bodyMaxX - 5) {
dragType.value = 'right';
}
else {
dragType.value = 'none';
}
}
if (modalBodyIsDragging.value && !isStartDrag) {
// 開始拖動
isStartDrag = true;
bodyWidth.value = bodyW;
bodyHeight.value = bodyH;
startBodyWidth = bodyWidth.value;
startBodyHeight = bodyHeight.value;
startMouseX = mouseX.value;
startMouseY = mouseY.value;
preResizeTransformX = resizeTransformX.value;
}
else if (modalBodyIsDragging.value && isStartDrag) {
const diffX = mouseX.value - startMouseX;
const diffY = mouseY.value - startMouseY;
if (dragType.value === 'left') {
bodyWidth.value = startBodyWidth - diffX;
resizeTransformX.value = preResizeTransformX + diffX;
}
else if (dragType.value === 'leftDown') {
bodyWidth.value = startBodyWidth - diffX;
bodyHeight.value = startBodyHeight + diffY;
resizeTransformX.value = preResizeTransformX + diffX;
}
else if (dragType.value === 'right') {
bodyWidth.value = startBodyWidth + diffX;
}
else if (dragType.value === 'rightDown') {
bodyWidth.value = startBodyWidth + diffX;
bodyHeight.value = startBodyHeight + diffY;
}
else if (dragType.value === 'down') {
bodyHeight.value = startBodyHeight + diffY;
}
emit('resize', { w: bodyWidth.value, h: bodyHeight.value });
}
});
const show = ref<boolean>(false);
function open() {
show.value = true;
}
function close() {
show.value = false;
emit('close');
}
function submit() {
emit('submit');
}
function reset() {
transformX.value = 0;
transformY.value = 0;
}
function cancel() {
show.value = false;
emit('cancel');
}
defineExpose({
open,
close,
cancel,
show,
reset,
});
</script>
<template>
<Teleport :to="appendTo">
<Transition name="modal">
<div v-if="show" class="w-full h-auto" :class="className">
<div v-if="isMask" :class="appendTo === 'body' ? 'modal-mask' : ''" />
<div ref="containerEleRef"
:style="appendTo === 'body' ? Object.assign(dialogStyle, transformStyle) : {}"
:class="appendTo === 'body' ? 'modal-wrap' : ''">
<div class="modal-container" :class="[`modal_box_${type}`]">
<div v-if="isClose" class="close" @click="close">
<PubSvgIcon name="close_blue" :size="30" class="m-auto" />
</div>
<div ref="modalTitleRef" class="modal-header bg-$vp-c-bg select-none cursor-move">
<slot name="header">
{{ title }}
</slot>
</div>
<div ref="modalBodyRef" class="modal-body" :class="[cursor]" :style="{
width: bodyWidth > 0 ? `${bodyWidth}px` : 'auto',
height: bodyHeight > 0 ? `${bodyHeight}px` : 'auto'
}">
<slot />
</div>
<div class="modal-footer">
<slot name="footer">
<div v-if="footer" class="flex justify-center">
<a-space :size="8">
<a-button @click="cancel">
取消
</a-button>
<a-button type="primary" @click="submit">
{{ submitText }}
</a-button>
</a-space>
</div>
</slot>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style lang="less" scoped>
.title_pre(@height, @pl, @fz, @color, @url, @t, @r, @b, @l, @t-2, @r-2, @b-2, @l-2) {
position: relative;
z-index: 5;
height:~"@{height}px";
padding-left: @pl;
font-family: "hxbzt";
font-size: @fz;
line-height:~"@{height}px";
color: @color;
span {
position: relative;
z-index: 9;
}
span.sub-title {
font-family: "微軟雅黑";
font-size: 12px;
color: #f7f7f7;
}
&:before {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
content: "";
border-width:~"@{t}px"~"@{r}px"~"@{b}px"~"@{l}px";
border-image: url(@url) @t @r @b @l fill;
border-image-width:~"@{t-2}px"~"@{r-2}px"~"@{b-2}px"~"@{l-2}px";
}
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
// z-index: 9998;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s ease;
}
.modal-wrap {
position: fixed;
// z-index: 9999;
}
.modal-container {
&.modal_box_0 {
padding: 20px;
color: #fff;
background: rgba(0, 20, 42, 0.7);
backdrop-filter: blur(5px);
border: 1px solid #747b84;
border-radius: 2.67px;
.modal-header {
text-align: center;
h3 {
margin-top: 0;
color: #42b983;
}
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
.close {
position: absolute;
top: -8px;
right: -8px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
transition: all 0.3s ease;
&:hover {
transform: rotate(90deg);
}
}
}
&.modal_box_1 {
.border-box(100, 200, 50, 100, "@/assets/images/common/border_box_5.png");
.modal-header {
padding-left: 28px;
line-height: 40px;
text-align: left;
}
.modal-body {
padding: 32px;
}
.close {
position: absolute;
top: 10px;
right: 22px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
cursor: pointer;
background: #113756;
border-radius: 4px;
:deep(svg) {
font-size: 22px !important;
transition: all 0.5s ease;
&:hover {
transform: rotate(90deg);
}
}
}
}
&.modal_box_3 {
padding: 20px 0;
color: #fff;
background: rgba(0, 20, 42, 0.7);
backdrop-filter: blur(5px);
border-radius: 2.67px;
.modal-header {
text-align: center;
h3 {
margin-top: 0;
color: #42b983;
}
}
.modal-body {
margin: 0;
}
.modal-default-button {
float: right;
}
.close {
position: absolute;
top: -8px;
right: -8px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
transition: all 0.3s ease;
&:hover {
transform: rotate(90deg);
}
}
}
&.modal_box_7 {
padding-top: 15px;
padding-bottom: 30px;
padding-left: 16px;
padding-right: 16px;
border-width: 100px 100px;
border-image: url("@/assets/images/common/box_bg_1.png") 100 80 fill;
border-image-width: 50px 40px;
&::before {
content: "";
position: absolute;
top: -45px;
left: -51px;
z-index: 3;
width: 289px;
height: 193px;
pointer-events: none;
background: url("@/assets/images/common/height_light.png");
background-size: 100% 100%;
}
.modal-header {
.title_pre(36, 42px, 18px, "#fff", "@/assets/images/common/border-box0.png", 2, 120, 2, 120, 1, 60, 1, 60);
font-family: youshe;
margin-bottom: 15px;
}
.close {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: rotate(90deg);
}
}
}
}
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
.border-box(@bt, @lr, @bt-2, @lr-2, @url) {
position: relative;
z-index: 5;
&::after {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
content: "";
border-width:~"@{bt}px"~"@{lr}px";
border-image: url(@url) @bt @lr fill;
border-image-width:~"@{bt-2}px"~"@{lr-2}px";
}
}
</style>
<script setup lang="ts">
import { message } from 'ant-design-vue';
import { certTypeOptions, typeList, levelList, districtCodeList } from './enums.ts'
import mapBox from "./mapBox.vue";
const props = withDefaults(defineProps<{
obj: object // 傳遞對象
appendTo: string
title: string
showbtm: boolean
}>(), {
showbtm: () => false,
obj: () => {
return {
top: '14%',
left: '30%',
close: true,
};
},
appendTo: 'body',
title: '應急救援警情接收',
});
const emit = defineEmits(['saveupdata']);
const styleObj = ref(props.obj) as any;
const handdialogRef = ref();
//表單
const formRef = ref()
const eventform = ref({
});
// 打開
function show() {
handdialogRef.value.open();
}
// 關閉
function close() {
handdialogRef.value.close();
}
//提交保存
function confirm() {
formRef.value?.validate().then(() => {
postEventSave(eventform.value).then((res) => {
if (res.success) {
message.success('提交成功');
emit('saveupdata') //刷新列表
handdialogRef.value.close();
} else {
message.error(res.msg);
}
}).catch(() => { });
});
}
defineExpose({
show,
close,
});
</script>
<template>
<Dialog2 ref="handdialogRef" :footer="showbtm" :title="props.title" type="7" :is-close="styleObj.close"
:dialog-style="{ width: '48.43rem', height: '50.36rem', zIndex: 1049, top: styleObj.top, left: styleObj.left }"
:append-to="props.appendTo" submitText="提交" @close="close" :isMask="true">
<div class="content">
<a-form ref="formRef" label-align="right" name="dynamic_rule" :model="eventform"
:label-col="{ style: { width: '6rem' } }">
<ContentBox title="用戶信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="姓名" :colon="false" name="reportName" :rules="[{ required: true, message: '請輸入' }]">
<a-input v-model:value="eventform.reportName" placeholder="請輸入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="手機號碼" :colon="false" name="reportPhone"
:rules="[{ required: true, message: '請輸入' },{type: 'string', pattern: /^1[3|4|5|6|7|8|9][0-9]{9}$/, message: '請輸入正確的手機號', trigger: 'blur'}]">
<a-input v-model:value="eventform.reportPhone" placeholder="請輸入" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="電子郵箱" :colon="false" name="reportEmail"
:rules="[{ required: true, message: '請輸入' },{type: 'string', pattern: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, message: '請輸入正確的郵箱', trigger: 'blur'}]">
<a-input v-model:value="eventform.reportEmail" placeholder="請輸入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="證件類型" :colon="false" name="reportCertificateType"
:rules="[{ required: true, message: '請輸入' }]">
<a-select v-model:value="eventform.reportCertificateType" :options="certTypeOptions" :allow-clear="true"
placeholder="請選擇" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="證件號碼" :colon="false" name="reportCertificateNum"
:rules="[{ required: true, message: '請輸入' },{type: 'string', pattern: /^(\d{18}|\d{17}x|\d{17}X)$/, message: '請輸入正確的證件號碼', trigger: 'blur'}]">
<a-input v-model:value="eventform.reportCertificateNum" placeholder="請輸入" />
</a-form-item>
</a-col>
</a-row>
</ContentBox>
<ContentBox title="設備信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="設備機型" :colon="false" name="equipModel" :rules="[{ required: true, message: '請輸入' }]">
<a-input v-model:value="eventform.equipModel" placeholder="請輸入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="設備ID/SN" :colon="false" name="equipSn">
<a-input v-model:value="eventform.equipSn" placeholder="請輸入" />
</a-form-item>
</a-col>
</a-row>
</ContentBox>
<ContentBox title="事故信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="事故類型" :colon="false" name="type" :rules="[{ required: true, message: '請輸入' }]">
<a-select v-model:value="eventform.type" :options="typeList" :allow-clear="true" placeholder="請選擇" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="風險等級" :colon="false" name="level" :rules="[{ required: true, message: '請輸入' }]">
<a-select v-model:value="eventform.level" :options="levelList" :allow-clear="true" placeholder="請選擇" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item :colon="false" label="事故時間" name="accidentTime"
:rules="[{ required: true, message: '請選擇時間' }]">
<a-date-picker v-model:value="eventform.accidentTime" show-time value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%;" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="事故地點" :colon="false" name="districtCode"
:rules="[{ required: true, message: '請輸入' }]">
<a-select v-model:value="eventform.districtCode" :options="districtCodeList" :allow-clear="true"
placeholder="請選擇" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="" :colon="false" name="place" :rules="[{ required: true, message: '請輸入' }]">
<a-input v-model:value="eventform.place" placeholder="請輸入詳細地址" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12" class="pr-5">
<a-form-item label="事故經度" :colon="false" name="longitude">
<a-input v-model:value="eventform.longitude" placeholder="請輸入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="事故緯度" :colon="false" name="latitude">
<a-input v-model:value="eventform.latitude" placeholder="請輸入" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="24">
<!-- <a-form-item label="事故原因" :colon="false" name="cause" :rules="[{ required: true, message: '請輸入' }]">
<a-textarea v-model:value="eventform.cause" placeholder="請輸入(最多輸入300字)" :row="6" :maxlength="300" />
</a-form-item> -->
<div class="map-box">
<mapBox />
</div>
</a-col>
</a-row>
<a-row class="mt-4">
<a-col :span="24">
<a-form-item label="事故原因" :colon="false" name="cause" :rules="[{ required: true, message: '請輸入' }]">
<a-textarea v-model:value="eventform.cause" placeholder="請輸入(最多輸入300字)" :row="6" :maxlength="300" />
</a-form-item>
</a-col>
</a-row>
</ContentBox>
</a-form>
<div class="text-center">
<a-button class="mr-4 newbtn" @click="close">
取消
</a-button>
<a-button class="newbtn" @click="confirm">
確定
</a-button>
</div>
</div>
</Dialog2>
</template>
<style lang="less" scoped>
.content {
height: 680px;
overflow-y: auto;
}
.text-center {
.newbtn {
width: 94px;
height: 34px;
background: url(@/assets/images/common/btn-bg.png)no-repeat center center;
background-size: 100% 100%;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 15px;
color: #FFFFFF;
text-align: center;
&:hover {
color: #fff;
}
}
}
.map-box {
width: 100%;
height: 300px;
}
</style>