changes
This commit is contained in:
@@ -14,6 +14,22 @@
|
|||||||
.input {
|
.input {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
--bulma-delete-background-alpha: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
border-radius: var(--bulma-modal-card-head-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card-head {
|
||||||
|
--bulma-shadow: 0 0.125em 0 0 hsla(var(--bulma-shadow-h), var(--bulma-shadow-s), var(--bulma-shadow-l), 0.1);
|
||||||
|
}
|
||||||
// might break lots of stuff
|
// might break lots of stuff
|
||||||
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
|
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
|
||||||
// margin-bottom: inherit;
|
// margin-bottom: inherit;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="#__nuxt > div">
|
<Teleport
|
||||||
|
v-if="Object.values(props).some((x) => isNotNil(x))"
|
||||||
|
to="#__nuxt > div"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="modal is-active"
|
class="modal is-active has-text-text-20"
|
||||||
@click="doClick"
|
@click="doClick"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -11,17 +14,19 @@
|
|||||||
<div
|
<div
|
||||||
class="modal-card"
|
class="modal-card"
|
||||||
:id="docid"
|
:id="docid"
|
||||||
:style="`width:${vWidth}; border-radius:16px;`"
|
:style="{
|
||||||
|
width: $store.viewport <= 2 ? 'calc(100% - 2rem)' : width || '60%',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="modal-card-head my-0 py-2"
|
|
||||||
v-if="title"
|
v-if="title"
|
||||||
|
class="modal-card-head px-4 py-3"
|
||||||
>
|
>
|
||||||
<div style="width: 100%">
|
<div class="w-full">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped is-align-items-center">
|
||||||
<div class="control is-expanded has-text-left">
|
<div class="control is-expanded has-text-left">
|
||||||
<p
|
<p
|
||||||
class="fsb-18 has-text-primary"
|
class="fs-17 font-semibold has-text-primary"
|
||||||
v-html="title"
|
v-html="title"
|
||||||
></p>
|
></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,18 +40,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<section
|
<section
|
||||||
class="modal-card-body px-4 py-4"
|
class="modal-card-body p-4"
|
||||||
:style="`min-height:${
|
:style="{
|
||||||
height ? height : '750px'
|
minHeight: height || '750px',
|
||||||
};border-bottom-left-radius:16px; border-bottom-right-radius:16px;`"
|
}"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="resolvedComponent"
|
:is="resolvedComponent"
|
||||||
v-bind="props.vbind"
|
v-bind="vbind"
|
||||||
@modalevent="modalEvent"
|
@modalevent="modalEvent"
|
||||||
@close="closeModal"
|
@close="closeModal"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
<footer class="modal-card-foot pt-0 px-4 pb-4"></footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -54,11 +60,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, defineAsyncComponent, shallowRef, watchEffect } from "vue";
|
import { onMounted, defineAsyncComponent, shallowRef, watchEffect } from "vue";
|
||||||
import { useStore } from "@/stores/index";
|
import { isNotNil } from "es-toolkit";
|
||||||
|
|
||||||
const emit = defineEmits(["close", "remove", "select", "dataevent", "update"]);
|
const emit = defineEmits(["close", "remove", "select", "dataevent", "update"]);
|
||||||
const store = useStore();
|
const { $id, $store } = useNuxtApp();
|
||||||
const { $id } = useNuxtApp();
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: String,
|
component: String,
|
||||||
@@ -95,12 +100,9 @@ watchEffect(() => {
|
|||||||
loadDynamicComponent();
|
loadDynamicComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
const viewport = store.viewport;
|
|
||||||
const docid = $id();
|
const docid = $id();
|
||||||
const title = props.title;
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const lock = false;
|
const lock = false;
|
||||||
const vWidth = viewport <= 2 ? "100%" : props.width || "60%";
|
|
||||||
|
|
||||||
const closeModal = function () {
|
const closeModal = function () {
|
||||||
if (!lock) emit("close");
|
if (!lock) emit("close");
|
||||||
@@ -140,3 +142,8 @@ onUnmounted(() => {
|
|||||||
if (count === 0) document.documentElement.classList.remove("is-clipped");
|
if (count === 0) document.documentElement.classList.remove("is-clipped");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
footer:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
97
app/components/Modal2.vue
Normal file
97
app/components/Modal2.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
component: String,
|
||||||
|
title: String,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
vbind: Object,
|
||||||
|
onClose: Function,
|
||||||
|
onEvent: Function,
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentFiles = import.meta.glob("@/components/**/*.vue");
|
||||||
|
const resolvedComponent = shallowRef(null);
|
||||||
|
|
||||||
|
function loadDynamicComponent() {
|
||||||
|
if (!props.component) {
|
||||||
|
resolvedComponent.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = `/components/${props.component}.vue`;
|
||||||
|
const componentPath = Object.keys(componentFiles).find((path) => path.endsWith(fullPath));
|
||||||
|
|
||||||
|
if (componentPath) {
|
||||||
|
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
|
||||||
|
} else {
|
||||||
|
console.error(`Không tìm thấy component tại: ${fullPath}`);
|
||||||
|
resolvedComponent.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theo dõi sự thay đổi của props.component để load lại nếu cần
|
||||||
|
watchEffect(() => {
|
||||||
|
loadDynamicComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEvent = (id, eventName, data) => props.onEvent(id, eventName, data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
:id="id"
|
||||||
|
class="modal is-active has-text-text-20"
|
||||||
|
@click="
|
||||||
|
(e) => {
|
||||||
|
if (e.target.classList.contains('modal-background')) props.onClose(id);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div
|
||||||
|
class="modal-card"
|
||||||
|
:style="{
|
||||||
|
// width: $store.viewport <= 2 ? 'calc(100% - 2rem)' : width || '60%',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
v-if="title"
|
||||||
|
class="modal-card-head px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="field is-grouped is-align-items-center">
|
||||||
|
<div class="control is-expanded has-text-left">
|
||||||
|
<p
|
||||||
|
class="fs-17 font-semibold has-text-primary"
|
||||||
|
v-html="title"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<div class="control has-text-right">
|
||||||
|
<button
|
||||||
|
class="delete is-medium"
|
||||||
|
@click="onClose(id)"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section
|
||||||
|
class="modal-card-body p-4"
|
||||||
|
:style="{
|
||||||
|
minHeight: height || '750px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="resolvedComponent"
|
||||||
|
v-bind="vbind"
|
||||||
|
@close="onClose(id)"
|
||||||
|
@event="(e) => handleEvent(id, e.name, e.data)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot pt-0 px-4 pb-4"></footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,45 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="confirm">
|
||||||
<p v-html="content"></p>
|
<p v-html="content"></p>
|
||||||
<p class="border-bottom mt-3 mb-5"></p>
|
<Teleport
|
||||||
<div class="field is-grouped">
|
defer
|
||||||
<div class="control is-expanded">
|
to=".modal-card:has(.confirm) .modal-card-foot"
|
||||||
<button
|
>
|
||||||
class="button is-primary has-text-white"
|
<div class="field is-grouped">
|
||||||
@click="confirm()"
|
<div class="control is-expanded">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="confirm()"
|
||||||
|
>
|
||||||
|
Đồng ý
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-white"
|
||||||
|
@click="cancel()"
|
||||||
|
>
|
||||||
|
Hủy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="control"
|
||||||
|
v-if="duration"
|
||||||
>
|
>
|
||||||
Đồng ý
|
<CountDown
|
||||||
</button>
|
:duration="duration"
|
||||||
<button
|
@close="cancel()"
|
||||||
class="button is-dark ml-5"
|
/>
|
||||||
@click="cancel()"
|
</div>
|
||||||
>
|
|
||||||
Hủy bỏ
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
</Teleport>
|
||||||
class="control"
|
|
||||||
v-if="duration"
|
|
||||||
>
|
|
||||||
<CountDown
|
|
||||||
v-bind="{ duration: duration }"
|
|
||||||
@close="cancel()"
|
|
||||||
></CountDown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: ["content", "duration"],
|
content: String,
|
||||||
methods: {
|
duration: Number,
|
||||||
cancel() {
|
});
|
||||||
this.$emit("close");
|
|
||||||
},
|
const emit = defineEmits(["close", "modalevent"]);
|
||||||
confirm() {
|
|
||||||
this.$emit("modalevent", { name: "confirm" });
|
function cancel() {
|
||||||
this.cancel();
|
emit("close");
|
||||||
},
|
}
|
||||||
},
|
|
||||||
};
|
function confirm() {
|
||||||
|
emit("modalevent", { name: "confirm" });
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,66 @@
|
|||||||
<script lang="ts" setup></script>
|
<script setup>
|
||||||
|
import FileUpload from "@/components/media/FileUpload.vue";
|
||||||
|
|
||||||
<template>Import</template>
|
const { $getdata } = useNuxtApp();
|
||||||
|
const files = ref([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const filesFetched = await $getdata("file", {
|
||||||
|
id__gte: 12473,
|
||||||
|
id__lte: 12475,
|
||||||
|
});
|
||||||
|
|
||||||
|
files.value = filesFetched;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onClick(url) {
|
||||||
|
const blob = await $fetch("/api/hello", {
|
||||||
|
query: { url },
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
const urlDownload = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = urlDownload;
|
||||||
|
link.setAttribute("download", "aName");
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildUrl = (f) => `https://api.utopia.com.vn/download?name=${f.file}&type=file`;
|
||||||
|
|
||||||
|
function onFiles(files) {
|
||||||
|
console.log("files", toRaw(files));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FileUpload
|
||||||
|
:type="['file']"
|
||||||
|
class="mb-2"
|
||||||
|
@files="onFiles"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:upload-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span class="font-medium">Import</span>
|
||||||
|
</FileUpload>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
v-for="(url, i) in files.map(buildUrl)"
|
||||||
|
:key="i"
|
||||||
|
@click="onClick(url)"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:download-rounded"
|
||||||
|
:size="18"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>{{ files[i].name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="file is-primary"
|
|
||||||
id="ignore"
|
id="ignore"
|
||||||
v-if="position === 'left'"
|
class="file is-primary"
|
||||||
>
|
>
|
||||||
<label class="file-label">
|
<label
|
||||||
|
class="file-label"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
class="file-input"
|
class="file-input"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -14,117 +16,106 @@
|
|||||||
name="resume"
|
name="resume"
|
||||||
@change="doChange"
|
@change="doChange"
|
||||||
/>
|
/>
|
||||||
<span class="file-cta px-2">
|
<span class="file-cta px-3 py-1.5">
|
||||||
<span class="icon-text is-clickable">
|
<span class="icon-text is-gap-0.5 is-align-items-center">
|
||||||
<SvgIcon v-bind="{ name: 'attach-file.svg', type: 'white', size: 22 }"></SvgIcon>
|
<slot name="icon">
|
||||||
<!--<span class="has-text-white">File</span>-->
|
<Icon
|
||||||
|
name="material-symbols:attach-file-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="field is-grouped is-grouped-right"
|
|
||||||
id="ignore"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<div class="control">
|
|
||||||
<div class="file is-primary">
|
|
||||||
<label class="file-label">
|
|
||||||
<input
|
|
||||||
class="file-input"
|
|
||||||
type="file"
|
|
||||||
:id="docid"
|
|
||||||
multiple
|
|
||||||
name="resume"
|
|
||||||
@change="doChange"
|
|
||||||
/>
|
|
||||||
<span class="file-cta is-primary px-1">
|
|
||||||
<span class="icon-text is-clickable">
|
|
||||||
<SvgIcon v-bind="{ name: 'attach-file.svg', type: 'white', size: 22 }"></SvgIcon>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Modal
|
<Modal
|
||||||
@close="showmodal = undefined"
|
|
||||||
v-bind="showmodal"
|
v-bind="showmodal"
|
||||||
v-if="showmodal"
|
@close="showmodal = undefined"
|
||||||
@files="getFiles"
|
@files="getFiles"
|
||||||
></Modal>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
defineOptions({
|
||||||
props: ["type", "position", "convert", "quality"],
|
inheritAttrs: false,
|
||||||
data() {
|
});
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
files: undefined,
|
|
||||||
dataFiles: [],
|
|
||||||
vtype: this.type || ["image"],
|
|
||||||
showmodal: undefined,
|
|
||||||
docid: this.$id(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getFileExtension(fileName) {
|
|
||||||
if (!fileName || typeof fileName !== "string") return "";
|
|
||||||
const parts = fileName.split(".");
|
|
||||||
return parts.length > 1 ? parts.pop().toLowerCase() : "";
|
|
||||||
},
|
|
||||||
getType(ext) {
|
|
||||||
// copied from 01-common.js
|
|
||||||
const imageFormat = ["png", "jpg", "jpeg", "bmp", "gif", "svg", "webp"];
|
|
||||||
const videoFormat = ["wmv", "avi", "mp4", "flv", "mov", "mpg", "amv", "rm"];
|
|
||||||
|
|
||||||
if (ext === "pdf") return "pdf";
|
const props = defineProps({
|
||||||
if (imageFormat.includes(ext)) return "image";
|
type: Array,
|
||||||
if (videoFormat.includes(ext)) return "video";
|
convert: String,
|
||||||
return "file";
|
quality: Number,
|
||||||
},
|
});
|
||||||
doChange() {
|
|
||||||
this.dataFiles = [];
|
|
||||||
const fileList = document.getElementById(this.docid).files;
|
|
||||||
this.files = Array.from(fileList);
|
|
||||||
if (this.files.length === 0) return;
|
|
||||||
|
|
||||||
// Xác định giá trị convert: "1" nếu convert được bật, "0" nếu không
|
const emit = defineEmits(["files"]);
|
||||||
const convertValue = this.convert ? "1" : "0";
|
const { $id, $snackbar, $upload } = useNuxtApp();
|
||||||
const qualityValue = this.convert && this.quality ? this.quality : null;
|
|
||||||
|
|
||||||
this.files.map((v) => {
|
const vtype = props.type || ["image"];
|
||||||
const ext = this.getFileExtension(v.name);
|
const files = ref();
|
||||||
const type = this.getType(ext);
|
const dataFiles = ref();
|
||||||
|
const showmodal = ref();
|
||||||
|
const docid = $id();
|
||||||
|
|
||||||
if (!this.vtype.includes(type)) {
|
function getFileExtension(fileName) {
|
||||||
this.$snackbar(`Định dạng file "${ext}" không hợp lệ`);
|
if (!fileName || typeof fileName !== "string") return "";
|
||||||
return;
|
const parts = fileName.split(".");
|
||||||
}
|
return parts.length > 1 ? parts.pop().toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
let file = this.$upload(v, type, 1, convertValue, qualityValue);
|
function getType(ext) {
|
||||||
this.dataFiles.push(file);
|
// copied from 01-common.js
|
||||||
});
|
const imageFormat = ["png", "jpg", "jpeg", "bmp", "gif", "svg", "webp"];
|
||||||
|
const videoFormat = ["wmv", "avi", "mp4", "flv", "mov", "mpg", "amv", "rm"];
|
||||||
|
|
||||||
this.showmodal = {
|
if (ext === "pdf") return "pdf";
|
||||||
component: "media/UploadProgress",
|
if (imageFormat.includes(ext)) return "image";
|
||||||
title: "Upload files",
|
if (videoFormat.includes(ext)) return "video";
|
||||||
width: "700px",
|
return "file";
|
||||||
height: "200px",
|
}
|
||||||
vbind: { files: this.dataFiles },
|
|
||||||
};
|
function doChange() {
|
||||||
this.clearFileList();
|
dataFiles.value = [];
|
||||||
},
|
const fileList = document.getElementById(docid).files;
|
||||||
clearFileList() {
|
files.value = Array.from(fileList);
|
||||||
const fileInput = document.getElementById(this.docid);
|
if (files.value.length === 0) return;
|
||||||
const dt = new DataTransfer();
|
|
||||||
fileInput.files = dt.files;
|
// Xác định giá trị convert: "1" nếu convert được bật, "0" nếu không
|
||||||
},
|
const convertValue = props.convert ? "1" : "0";
|
||||||
getFiles(files) {
|
const qualityValue = props.convert && props.quality ? props.quality : null;
|
||||||
this.$emit("files", files);
|
|
||||||
setTimeout(() => (this.showmodal = undefined), 3000);
|
files.value.map((v) => {
|
||||||
},
|
const ext = getFileExtension(v.name);
|
||||||
},
|
const type = getType(ext);
|
||||||
};
|
|
||||||
|
if (!vtype.includes(type)) {
|
||||||
|
$snackbar(`Định dạng file "${ext}" không hợp lệ`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = $upload(v, type, 1, convertValue, qualityValue);
|
||||||
|
dataFiles.value.push(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
showmodal.value = {
|
||||||
|
component: "media/UploadProgress",
|
||||||
|
title: "Upload files",
|
||||||
|
width: "700px",
|
||||||
|
height: "200px",
|
||||||
|
vbind: { files: dataFiles.value },
|
||||||
|
};
|
||||||
|
clearFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFileList() {
|
||||||
|
const fileInput = document.getElementById(docid);
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
fileInput.files = dt.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFiles(files) {
|
||||||
|
emit("files", files);
|
||||||
|
setTimeout(() => {
|
||||||
|
showmodal.value = undefined;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="has-text-left">
|
<div class="fs-14 has-text-left is-flex is-flex-direction-column is-gap-1">
|
||||||
<p
|
<p
|
||||||
class="py-1 border-bottom"
|
v-for="(v, i) in vfiles"
|
||||||
v-for="v in vfiles"
|
:key="i"
|
||||||
>
|
>
|
||||||
<span class="icon-text">
|
<span class="icon-text is-gap-1 is-align-items-center">
|
||||||
<span class="mr-5">{{ v.name }}</span>
|
<span class="icon">
|
||||||
<SvgIcon
|
<Icon
|
||||||
v-bind="{ name: 'check2.svg', type: 'primary', size: 22 }"
|
name="svg-spinners:90-ring"
|
||||||
v-if="v.status === 'success'"
|
:size="22"
|
||||||
></SvgIcon>
|
class="has-text-primary"
|
||||||
<SvgIcon
|
v-if="v.status === 'uploading'"
|
||||||
v-bind="{ name: 'error.svg', type: 'danger', size: 22 }"
|
/>
|
||||||
v-else-if="v.status === 'error'"
|
<Icon
|
||||||
></SvgIcon>
|
name="material-symbols:check-circle-rounded"
|
||||||
|
:size="22"
|
||||||
|
class="has-text-primary"
|
||||||
|
v-else-if="v.status === 'success'"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:cancel-rounded"
|
||||||
|
:size="22"
|
||||||
|
class="has-text-danger"
|
||||||
|
v-else-if="v.status === 'error'"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>{{ v.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="icon-text has-text-danger ml-6"
|
class="icon-text is-gap-1 has-text-danger ml-6"
|
||||||
v-if="v.error"
|
v-if="v.error"
|
||||||
>
|
>
|
||||||
<SvgIcon v-bind="{ name: 'error.svg', type: 'danger', size: 22 }"></SvgIcon>
|
<Icon
|
||||||
<span class="ml-1">{{ v.text }}</span>
|
name="material-symbols:cancel-rounded"
|
||||||
|
:size="22"
|
||||||
|
class="has-text-danger"
|
||||||
|
/>
|
||||||
|
<span>{{ v.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
class="button is-small is-white is-loading px-0 ml-4"
|
|
||||||
v-if="v.status === 'uploading'"
|
|
||||||
>
|
|
||||||
Loading
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
35
app/composables/useModal.js
Normal file
35
app/composables/useModal.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const activeModals = ref([]);
|
||||||
|
|
||||||
|
export default function useModal() {
|
||||||
|
function open(component, options = {}) {
|
||||||
|
const id = Date.now() + Math.random();
|
||||||
|
const modal = {
|
||||||
|
id,
|
||||||
|
component,
|
||||||
|
title: options.title,
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
vbind: options.props || {},
|
||||||
|
onClose: options.onClose,
|
||||||
|
onEvent: options.onEvent,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
activeModals.value.push(modal);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(id) {
|
||||||
|
const index = activeModals.value.findIndex((m) => m.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
const modal = activeModals.value[index];
|
||||||
|
modal.onClose?.();
|
||||||
|
activeModals.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(id, eventName, data) {
|
||||||
|
const modal = activeModals.value.find((m) => m.id === id);
|
||||||
|
modal?.onEvent?.({ name: eventName, data });
|
||||||
|
}
|
||||||
|
return { modals: activeModals, open, close, send };
|
||||||
|
}
|
||||||
@@ -12,10 +12,14 @@
|
|||||||
@close="$store.removeSnackbar()"
|
@close="$store.removeSnackbar()"
|
||||||
/>
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
v-if="showmodal"
|
|
||||||
v-bind="showmodal"
|
v-bind="showmodal"
|
||||||
@close="showmodal = undefined"
|
@close="showmodal = undefined"
|
||||||
/>
|
/>
|
||||||
|
<!-- <Modal2
|
||||||
|
v-for="modal in modals"
|
||||||
|
:key="modal.id"
|
||||||
|
v-bind="modal"
|
||||||
|
/> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ import { onMounted } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import SnackBar from "@/components/snackbar/SnackBar.vue";
|
import SnackBar from "@/components/snackbar/SnackBar.vue";
|
||||||
import Modal from "@/components/Modal.vue";
|
import Modal from "@/components/Modal.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { $getdata, $requestLogin, $store } = useNuxtApp();
|
const { $getdata, $requestLogin, $store } = useNuxtApp();
|
||||||
var authorized = ref(false);
|
var authorized = ref(false);
|
||||||
|
|||||||
@@ -1097,7 +1097,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
? $snackbar("Data has been successfully saved to the system.", "Success", "Success")
|
? $snackbar("Data has been successfully saved to the system.", "Success", "Success")
|
||||||
: $snackbar("Dữ liệu đã được lưu vào hệ thống", "Thành công", "Success");
|
: $snackbar("Dữ liệu đã được lưu vào hệ thống", "Thành công", "Success");
|
||||||
}
|
}
|
||||||
return rs.data;
|
return rs;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return "error";
|
return "error";
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ import MenuAccount from "~/components/menu/MenuAccount.vue";
|
|||||||
import PhaseAdvance from "~/components/application/PhaseAdvance.vue";
|
import PhaseAdvance from "~/components/application/PhaseAdvance.vue";
|
||||||
import ImageLayout from "@/components/media/ImageLayout.vue";
|
import ImageLayout from "@/components/media/ImageLayout.vue";
|
||||||
import ProjectDocuments from "~/components/product/ProjectDocuments.vue";
|
import ProjectDocuments from "~/components/product/ProjectDocuments.vue";
|
||||||
import ProductEdit from "~/components/product/ProductEdit.vue";
|
|
||||||
|
|
||||||
import Cart from "~/components/product/Cart.vue";
|
import Cart from "~/components/product/Cart.vue";
|
||||||
import CountdownTimer from "~/components/common/CountdownTimer.vue";
|
import CountdownTimer from "~/components/common/CountdownTimer.vue";
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"vue3-quill": "^0.3.1"
|
"vue3-quill": "^0.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/material-symbols": "^1.2.69",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
@@ -1289,6 +1290,15 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/material-symbols": {
|
||||||
|
"version": "1.2.69",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.69.tgz",
|
||||||
|
"integrity": "sha512-N5n1p1vXsSJag9RmorZAwN5f5rs0lJfYa5xKs1FGKNV9DerVlMqG4nCYOoCb7XjwWWT1o+1Dc9Uu/cmDtCnb8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@iconify/collections": {
|
"node_modules/@iconify/collections": {
|
||||||
"version": "1.0.670",
|
"version": "1.0.670",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.670.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.670.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"vue": "latest"
|
"vue": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/material-symbols": "^1.2.69",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
|
|||||||
12
server/api/hello.js
Normal file
12
server/api/hello.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const query = getQuery(event);
|
||||||
|
const response = await $fetch.raw(query.url, { responseType: "arrayBuffer" });
|
||||||
|
|
||||||
|
// Copy the content-type from the upstream response
|
||||||
|
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
||||||
|
|
||||||
|
setHeader(event, "Content-Type", contentType);
|
||||||
|
setHeader(event, "Content-Disposition", `attachment; filename="download"`);
|
||||||
|
|
||||||
|
return response._data;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user