Merge commit 'ead85a5652f2c935ebbbd6b24486b57e9839dffd' into HEAD

This commit is contained in:
zong 2025-10-02 13:12:20 +08:00
commit b3f89ab5fe
4 changed files with 311 additions and 240 deletions

View File

@ -1,12 +1,6 @@
<script setup>
import { twMerge } from "tailwind-merge";
import { computed, defineProps, onMounted, ref, watch } from "vue";
/* ----------------------------------------------------------------
id名.showModal(): 開啟 modal
id名.close(): 關閉 modal
詳細請參考 daisyUI
------------------------------------------------------------------- */
import { defineProps } from "vue";
const props = defineProps({
id: String,
@ -14,52 +8,40 @@ const props = defineProps({
onCancel: Function,
modalClass: String,
width: Number || String,
draggable: {
type: Boolean,
default: false,
},
backdrop: {
type: Boolean,
default: true,
},
modalStyle: {
type: Object,
default: {},
},
draggable: { type: Boolean, default: false },
backdrop: { type: Boolean, default: true },
modalStyle: { type: Object, default: () => ({}) },
});
const dom = ref(null)
onMounted(() => {
document.querySelector(`#${props.id}[open]`)?.addEventListener("load",()=>{
console.log("loading")
})
document.querySelector(`#${props.id}`).addEventListener("load",()=>{
console.log("loading")
})
})
</script>
<template>
<!-- Open the modal using ID.showModal() method -->
<!-- :class="twMerge('modal', open && innerOpen ? 'modal-open' : 'modal-close')" -->
<dialog ref="dom" :id="id" :class="twMerge(
'modal',
backdrop
? ''
: 'focus-visible:outline-none backdrop:bg-transparent',
)" :style="modalStyle" v-draggable="draggable">
<div :class="twMerge(
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-scroll bg-normal',
modalClass
)
" :style="{ maxWidth: isNaN(width) ? width : `${width}px` }">
<div class="text-2xl font-bold">
<slot name="modalTitle">
{{ title }}
</slot>
<dialog
:id="id"
:class="
twMerge(
'modal',
backdrop ? '' : 'focus-visible:outline-none backdrop:bg-transparent'
)
"
:style="modalStyle"
v-draggable="
draggable ? { handle: '[data-drag-handle]', target: '.modal-box' } : false
"
>
<div
:class="
twMerge(
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-auto bg-normal',
modalClass
)
"
:style="{ maxWidth: isNaN(width) ? width : `${width}px` }"
>
<!-- 把手只在這裡按下才可拖動 -->
<div class="text-2xl font-bold select-none" data-drag-handle>
<slot name="modalTitle">{{ title }}</slot>
</div>
<div class="min-h-[200px]">
<slot name="modalContent"></slot>
</div>
@ -68,25 +50,17 @@ onMounted(() => {
<slot name="modalAction"></slot>
</div>
</div>
<form v-if="backdrop" method="dialog" class="modal-backdrop">
<button @click="() => {
onCancel ? onCancel() : cancel();
}
">
<button
@click="
() => {
onCancel && onCancel();
}
"
>
close
</button>
</form>
</dialog>
</template>
<style lang="css" scoped>
.modal-box::before {
@apply fixed top-1 right-1 h-5 w-5 rotate-90 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background01.svg')] bg-center;
content: "";
}
.modal-action::after {
@apply absolute -bottom-3 -left-4 h-5 w-5 rotate-90 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background05.svg')] bg-center;
content: "";
}
</style>

View File

@ -1,65 +1,97 @@
const moveModal = (elmnt) => {
console.log(elmnt);
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
elmnt.addEventListener("mousedown", dragMouseDown, {
passive: false,
});
// src/directives/draggable.js
// 用法:在 Modal 外層加 v-draggable="{ handle: '[data-drag-handle]', target: '.modal-box' }"
// - handle只在這個節點按下才可拖動
// - target實際被拖動的 DOM預設用 el 本身)
function dragMouseDown(e) {
console.log("dragMouseDown", e);
e = e || window.event;
function createDraggable(el, options = {}) {
const cancelSelector =
'input, textarea, select, button, [contenteditable="true"], [data-drag-cancel]';
const target =
(options.target && el.querySelector(options.target)) ||
el.querySelector('.modal-box') ||
el;
const handle =
(options.handle && el.querySelector(options.handle)) || target;
let startX = 0,
startY = 0,
originLeft = 0,
originTop = 0,
dragging = false;
// 只在「左鍵」且「不是表單控制項」時啟動拖曳
function onMouseDown(e) {
if (e.button !== 0) return;
// 點到可互動元件就不要拖(也不要 preventDefault讓它能聚焦
if (e.target.matches(cancelSelector) || e.target.closest(cancelSelector)) {
return;
}
// 這裡才開始準備拖曳
dragging = true;
const rect = target.getBoundingClientRect();
originLeft = rect.left;
originTop = rect.top;
startX = e.clientX;
startY = e.clientY;
// 只有在真的要拖時才阻止預設,避免選字/圖片拖移
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.body.addEventListener("mouseup", closeDragElement, {
passive: false,
});
// call a function whenever the cursor moves:
document.body.addEventListener("mousemove", elementDrag, {
passive: false,
});
// 以視窗為座標系比較直觀
target.style.position = 'fixed';
target.style.left = '0px';
target.style.top = '0px';
target.style.transform = `translate(${originLeft}px, ${originTop}px)`;
window.addEventListener('mousemove', onMouseMove, { passive: false });
window.addEventListener('mouseup', onMouseUp, { passive: true });
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
function onMouseMove(e) {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
target.style.transform = `translate(${originLeft + dx}px, ${originTop + dy}px)`;
}
function closeDragElement() {
// stop moving when mouse button is released:
document.body.removeEventListener("mouseup", closeDragElement);
document.body.removeEventListener("mousemove", elementDrag);
function onMouseUp() {
dragging = false;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}
};
handle.style.cursor = 'move';
handle.addEventListener('mousedown', onMouseDown, { passive: false });
// 提供清理函式給 unmounted 用
return () => {
handle.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
}
export const draggable = {
install(app) {
app.directive("draggable", {
mounted: (el, binding, vnode, prevVnode) => {
console.log("draggable", $(`#${el.id}`).draggable);
if (binding.value) {
if ($(`#${el.id}`).draggable) {
$(`#${el.id}`).draggable({
cursor: "move",
scroll: true,
container: ".app-container",
});
} else {
moveModal(el);
}
app.directive('draggable', {
mounted(el, binding) {
// 允許 v-draggable 或 v-draggable="true" 或 v-draggable="{ handle, target }"
const enabled =
binding.value === '' || binding.value === true || typeof binding.value === 'object';
if (!enabled) return;
// 以前用 jQuery UI 的判斷會在沒有 $ 或 .draggable 時噴錯,直接移除
const options = typeof binding.value === 'object' ? binding.value : {};
el.__dragCleanup__ = createDraggable(el, options);
},
unmounted(el) {
if (el.__dragCleanup__) {
el.__dragCleanup__();
delete el.__dragCleanup__;
}
},
});

View File

@ -1,38 +1,22 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import VendorModal from "./VendorModal.vue";
import { getOperationCompanyList,deleteOperationCompany } from "@/apis/operation";
import {
getOperationCompanyList,
deleteOperationCompany,
} from "@/apis/operation";
import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const columns = computed(() => [
{
title: t("operation.vendor"),
key: "name",
},
{
title: t("operation.contact_person"),
key: "contact_person",
},
{
title: t("operation.phone"),
key: "phone",
},
{
title: t("operation.email"),
key: "email",
},
{
title: t("operation.created_at"),
key: "created_at",
},
{
title: t("operation.operation"),
key: "operation",
width: 200,
},
{ title: t("operation.vendor"), key: "name" },
{ title: t("operation.contact_person"), key: "contact_person" },
{ title: t("operation.phone"), key: "phone" },
{ title: t("operation.email"), key: "email" },
{ title: t("operation.created_at"), key: "created_at" },
{ title: t("operation.operation"), key: "operation", width: 200 },
]);
const dataSource = ref([]);
@ -49,7 +33,9 @@ onMounted(() => {
getDataSource();
});
const formState = ref({
// ====== Modal ======
const MODAL_ID = "company_modal";
const emptyForm = () => ({
contact_person: "",
email: "",
name: "",
@ -60,22 +46,36 @@ const formState = ref({
address: "",
});
const openModal = (record) => {
if (record.id) {
formState.value = { ...record };
} else {
formState.value = {
contact_person: "",
email: "",
name: "",
phone: "",
remark: "",
tax_id_number: "",
city: "",
address: "",
};
const formState = ref(emptyForm());
// (/)
const openModal = (payload) => {
const el = document.getElementById(MODAL_ID);
//
if (payload === false) {
try {
el?.close?.();
} catch {}
return;
}
// true
if (payload === true || payload == null) {
formState.value = emptyForm();
try {
el?.showModal?.();
} catch {}
return;
}
// record
if (payload && typeof payload === "object") {
formState.value = { ...emptyForm(), ...payload }; //
try {
el?.showModal?.();
} catch {}
}
company_modal.showModal();
};
const remove = async (id) => {
@ -95,15 +95,19 @@ const remove = async (id) => {
<template>
<div class="flex justify-start items-center mt-10 mb-5">
<h3 class="text-xl mr-5">{{ $t("assetManagement.company") }}</h3>
<!-- 子層會渲染新增按鈕照你的原樣 -->
<VendorModal
:formState="formState"
:getData="getDataSource"
:openModal="openModal"
/>
</div>
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-else-if="column.key === 'operation'">
<button
class="btn btn-sm btn-success text-white mr-2"
@ -118,6 +122,7 @@ const remove = async (id) => {
{{ $t("button.delete") }}
</button>
</template>
<template v-else>
{{ record[column.key] }}
</template>

View File

@ -1,23 +1,21 @@
<script setup>
import { ref, onMounted, defineProps, inject, watch } from "vue";
import { ref, inject } from "vue";
import * as yup from "yup";
import "yup-phone-lite";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import {
postOperationCompany,
updateOperationCompany,
} from "@/apis/operation";
import { postOperationCompany, updateOperationCompany } from "@/apis/operation";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const props = defineProps({
formState: Object,
getData: Function,
openModal: Function
openModal: Function, // / modal
});
const deptScheme = yup.object({
const schema = yup.object({
name: yup.string().required(t("button.required")),
contact_person: yup.string().nullable(true),
email: yup.string().email().nullable(true),
@ -28,34 +26,104 @@ const deptScheme = yup.object({
remark: yup.string().nullable(true),
});
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
useFormErrorMessage(deptScheme);
const { formErrorMsg, handleSubmit, handleErrorReset } =
useFormErrorMessage(schema);
const loading = ref(false);
const MODAL_ID = "company_modal";
/** ====== 關閉 Modal多重保險 ====== */
const closeModal = () => {
// 1) boolean
if (typeof props.openModal === "function") {
try {
if (props.openModal.length >= 1) {
// openModal(false)
props.openModal(false);
} else {
// toggler false
try {
props.openModal(false);
} catch {
props.openModal();
}
}
return;
} catch (_) {}
}
// 2) <dialog> close() Modal
const el = document.getElementById(MODAL_ID);
if (el?.close) {
try {
el.close();
return;
} catch (_) {}
}
// 3) Modal
try {
el?.dispatchEvent?.(new CustomEvent("close", { bubbles: true }));
} catch (_) {}
};
/** ====== 開啟 Modal呼叫父層 ====== */
const openModal = () => {
if (typeof props.openModal === "function") {
try {
if (props.openModal.length >= 1) props.openModal(true);
else props.openModal();
} catch {
//
}
}
};
const onCancel = () => {
handleErrorReset();
company_modal.close();
//
//
closeModal();
};
const onOk = async () => {
const value = await handleSubmit(deptScheme, props.formState);
if (props.formState?.id) {
res = await updateOperationCompany(value);
} else {
res = await postOperationCompany(value);
}
if (res.isSuccess) {
props.getData();
onCancel();
} else {
openToast("error", res.msg, "#company_modal");
try {
loading.value = true;
const value = await handleSubmit(schema, props.formState);
let res;
if (props.formState?.id) {
// API id updateOperationCompany(props.formState.id, value)
res = await updateOperationCompany(value);
} else {
res = await postOperationCompany(value);
}
if (res?.isSuccess) {
await props.getData?.();
props.openModal(false);
//
closeModal();
//
handleErrorReset();
props.openModal(false);
openToast?.("success", t("common.success"), `#${MODAL_ID}`);
} else {
openToast?.("error", res?.msg ?? t("common.failed"), `#${MODAL_ID}`);
}
} catch (err) {
openToast?.("error", err?.message ?? t("common.failed"), `#${MODAL_ID}`);
} finally {
loading.value = false;
}
};
</script>
<template>
<button class="btn btn-sm btn-add " @click.stop.prevent="props.openModal">
<button class="btn btn-sm btn-add" @click.stop.prevent="openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal
id="company_modal"
:title="props.formState?.id ? t('button.edit') : t('button.add')"
@ -63,87 +131,79 @@ const onOk = async () => {
width="710"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
<Input :value="formState" class="my-2" name="name">
<form class="mt-5 w-full flex flex-wrap justify-between">
<Input :value="props.formState" class="my-2" name="name">
<template #topLeft>{{ $t("operation.name") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.name }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="contact_person">
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.name }}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="contact_person">
<template #topLeft>{{ $t("operation.contact_person") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.contact_person }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="phone">
<template #bottomLeft>
<span class="text-error text-base">{{
formErrorMsg.contact_person
}}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="phone">
<template #topLeft>{{ $t("operation.phone") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.phone }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="email">
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.phone }}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="email">
<template #topLeft>{{ $t("operation.email") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.email }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="city">
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.email }}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="city">
<template #topLeft>{{ $t("operation.city") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.city }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="address">
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.city }}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="address">
<template #topLeft>{{ $t("operation.address") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.address }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="tax_id_number">
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.address }}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="tax_id_number">
<template #topLeft>{{ $t("operation.tax_id_number") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.tax_id_number }}
</span></template
></Input
>
<Input :value="formState" class="my-2" name="remark">
<template #bottomLeft>
<span class="text-error text-base">{{
formErrorMsg.tax_id_number
}}</span>
</template>
</Input>
<Input :value="props.formState" class="my-2" name="remark">
<template #topLeft>{{ $t("operation.remark") }}</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.remark }}
</span></template
></Input
>
<template #bottomLeft>
<span class="text-error text-base">{{ formErrorMsg.remark }}</span>
</template>
</Input>
</form>
</template>
<template #modalAction>
<button
type="reset"
type="button"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
:disabled="loading"
>
{{ $t("button.cancel") }}
</button>
<button
type="submit"
type="button"
class="btn btn-outline-success"
@click.stop.prevent="onOk"
:disabled="loading"
>
<span v-if="loading" class="loading loading-spinner loading-xs mr-2" />
{{ $t("button.submit") }}
</button>
</template>