Files
web/app/components/media/ImageLayout.vue
2026-05-05 11:06:49 +07:00

401 lines
11 KiB
Vue

<template>
<div class="columns mx-0 mb-0 pb-0">
<div class="column is-6">
<div class="tags are-medium">
<a
v-for="v in hashtag"
@click="refreshShow(v)"
>
<span :class="`tag ${v.file__hashtag === current ? 'is-primary' : ''}`">
{{ v.file__hashtag }}
{{ `(${v.count})` }}
</span>
</a>
</div>
</div>
<div class="column is-6">
<div class="is-flex is-align-items-center is-justify-content-flex-end">
<div class="buttons has-addons are-small my-0">
<button
v-for="layout in Object.values(layoutModes)"
:key="layout"
:class="['button px-5', { 'is-primary': layout === layoutMode }]"
@click="layoutMode = layout"
>
<SvgIcon
v-bind="{
name: `${layout}.svg`,
type: layout === layoutMode ? 'white' : 'primary',
size: 20,
}"
/>
</button>
</div>
<div
v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })"
class="ml-6 is-flex is-align-items-center"
>
<div class="mr-5">
<label class="checkbox mr-2">
<input
type="checkbox"
v-model="convertToWebp"
/>
<span class="ml-2">Convert to WebP</span>
</label>
<input
v-if="convertToWebp"
class="input is-small"
type="number"
v-model.number="webpQuality"
min="1"
max="100"
style="width: 60px"
placeholder="80"
/>
</div>
<FileUpload
position="right"
type="image"
:convert="convertToWebp"
:quality="webpQuality"
@files="onUploaded"
/>
</div>
</div>
</div>
</div>
<component
:is="layoutMode === layoutModes.GRID ? 'div' : 'table'"
:class="layoutMode === layoutModes.GRID ? 'columns is-multiline px-2 mt-0' : 'table is-fullwidth is-bordered'"
>
<template v-if="layoutMode === layoutModes.LIST">
<thead>
<tr>
<th>File</th>
<th>Mô tả</th>
<th>Hashtag</th>
<th>Người tải lên</th>
<th>
<span class="icon-text">
<span class="mr-5">Chức năng</span>
<a
class="mr-3"
@click="excel()"
>
<SvgIcon v-bind="{ name: 'excel.png', type: 'black', size: 20 }"></SvgIcon>
</a>
<a
v-if="$getEditRights()"
@click="importData()"
>
<SvgIcon v-bind="{ name: 'upload.svg', type: 'black', size: 20 }"></SvgIcon>
</a>
</span>
</th>
</tr>
</thead>
<tbody>
<ImageCard
v-for="(image, index) in imgshow"
:key="image.id"
v-bind="{
image,
index,
layoutMode,
loadImages,
deleteImage,
viewImage,
}"
/>
</tbody>
</template>
<ImageCard
v-else
v-for="(image, index) in imgshow"
:key="image.id"
v-bind="{ image, index, layoutMode, loadImages, deleteImage, viewImage }"
/>
</component>
<!-- Modal xem ảnh lớn -->
<div
v-if="typeof activeImageIndex === 'number'"
class="modal is-active"
>
<div
class="modal-background"
@click="closeViewImage"
></div>
<div
class="modal-content"
style="width: 90vw; height: 90vh; border-radius: 8px; overflow: hidden"
>
<div
class="is-flex"
style="height: 100%; transition: all 0.3s ease-out"
:style="`transform: translateX(${-100 * activeImageIndex}%)`"
>
<div
v-for="image in imgshow"
:key="image.id"
class="has-background-black"
style="flex: 0 0 100%; display: flex; justify-content: center; align-items: center"
>
<nuxt-img
loading="lazy"
:src="`${$getpath()}static/files/${image.file__file}`"
:alt="image.file__name"
style="object-fit: contain; height: 100%"
/>
</div>
</div>
<div class="gallery-arrows">
<button
@click="showPrevImage()"
class="button is-rounded"
style="left: 24px"
>
<SvgIcon v-bind="{ name: 'left1.svg', type: 'black', size: 20 }" />
</button>
<button
@click="showNextImage()"
class="button is-rounded"
style="right: 24px"
>
<SvgIcon v-bind="{ name: 'right.svg', type: 'black', size: 20 }" />
</button>
</div>
<div class="gallery-count">
{{ activeImageIndex + 1 }}
/
{{ imgshow.length }}
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="closeViewImage"
></button>
</div>
<Modal
@dataevent="dataevent"
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
/>
</template>
<script setup>
import { ref } from "vue";
import { debounce } from "es-toolkit";
import { useNuxtApp } from "#app";
import FileUpload from "@/components/media/FileUpload.vue";
import ImageCard from "@/components/media/ImageCard.vue";
// pass only projectId for project images, pass both for product images
const props = defineProps({
productId: String,
projectId: String,
});
const { dealer } = useStore();
const { $snackbar, $getdata, $insertapi, $deleteapi, $findapi, $getapi, $dayjs, $unique } = useNuxtApp();
const layoutModes = { GRID: "grid", LIST: "list" };
const layoutMode = ref(layoutModes.GRID);
const convertToWebp = ref(false);
const webpQuality = ref(80);
const project = await $getdata("project", { id: props.projectId }, undefined, true);
const images = ref([]);
const isForProduct = computed(() => props.productId !== undefined);
const product = isForProduct.value ? await $getdata("product", { id: props.productId }, undefined, true) : undefined;
const showmodal = ref();
const imgshow = ref([]);
const hashtag = ref();
const current = ref();
async function loadImages() {
const values =
"id,file__hashtag,file__code,file,create_time,file__code,file__type,file__doc_type,file__name,file__file,file__size,file__caption,file__user__fullname,";
const projectImages = await $getdata("projectfile", undefined, {
filter: { project: project.id, file__type: 2 },
values: values + "project",
sort: "-create_time",
});
if (isForProduct.value) {
const productImages = await $getdata("productfile", undefined, {
filter: { product: props.productId, file__type: 2 },
values: values + "product",
sort: "-create_time",
});
const sameTemplateImages = projectImages.filter((img) => img.file__name.startsWith(product.template_name));
images.value = [...productImages, ...sameTemplateImages];
} else {
images.value = projectImages;
}
imgshow.value = [...images.value];
hashtag.value = $unique(
images.value.filter((v) => v.file__hashtag),
["file__hashtag"],
);
hashtag.value.map((v) => (v.count = images.value.filter((x) => x.file__hashtag === v.file__hashtag).length));
}
const handleKeyDown = debounce((e) => {
if (e.key === "Escape") closeViewImage();
if (e.key === "ArrowLeft") showPrevImage();
if (e.key === "ArrowRight") showNextImage();
}, 100);
onMounted(() => {
loadImages();
window.addEventListener("keydown", handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener("keydown", handleKeyDown);
});
async function attachFiles(files) {
if (!project) return 0;
const payload = files.map((file) => ({
file: file.id,
project: isForProduct.value ? undefined : project.id,
product: isForProduct.value ? product.id : undefined,
}));
const result = await $insertapi(isForProduct.value ? "productfile" : "projectfile", payload);
if (result === "error") {
throw new Error("Không thể liên kết tệp");
}
return payload.length;
}
async function onUploaded(uploadedFiles) {
if (!Array.isArray(uploadedFiles) || uploadedFiles.length === 0) return;
try {
const attachedCount = await attachFiles(uploadedFiles);
if (attachedCount > 0) {
loadImages();
$snackbar(`Đã thêm ${attachedCount} ảnh thành công`, "Thành công", "Success");
}
} catch (error) {
console.error("Error attaching images:", error);
$snackbar("Không thể thêm ảnh, vui lòng thử lại", "Lỗi", "Error");
}
}
async function deleteImage(image) {
try {
await $deleteapi(isForProduct.value ? "productfile" : "projectfile", image.id);
await $deleteapi("file", image.file);
loadImages();
$snackbar("Đã xóa ảnh thành công", "Thành công", "Success");
} catch (error) {
console.error("Error deleting image:", error);
$snackbar("Xóa ảnh không thành công", "Lỗi", "Error");
}
}
async function excel() {
const found = $findapi("exportcsv");
found.params = {};
let fields = [
{ name: "code", label: "code" },
{ name: "name", label: "name" },
{ name: "caption", label: "caption" },
{ name: "hashtag", label: "hashtag" },
];
found.params.fields = JSON.stringify(fields);
found.url = "exportcsv/File/";
found.params.filter = { code__in: images.value.map((v) => v.file__code) };
const rs = await $getapi([found]);
if (rs === "error") return $snackbar("Đã xảy ra lỗi. Vui lòng thử lại.");
const url = window.URL.createObjectURL(new Blob([rs[0].data]));
const link = document.createElement("a");
const fileName = `${$dayjs(new Date()).format("YYYYMMDDhhmmss")}-data.csv`;
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
function importData() {
showmodal.value = {
title: "Nhập bút toán theo lô",
vbind: { code: "file-import" },
width: "90%",
height: "500px",
component: "parameter/ImportData",
};
}
function refreshShow(tag) {
if (current.value === tag.file__hashtag) {
imgshow.value = [...images.value];
current.value = null;
return;
}
imgshow.value = images.value.filter((v) => v.file__hashtag === tag.file__hashtag);
current.value = tag.file__hashtag;
}
// State cho xem ảnh lớns
const activeImageIndex = ref(null);
function viewImage(index) {
activeImageIndex.value = index;
}
function showNextImage() {
activeImageIndex.value = (activeImageIndex.value + 1) % imgshow.value.length;
}
function showPrevImage() {
activeImageIndex.value = (activeImageIndex.value - 1 + imgshow.value.length) % imgshow.value.length;
}
function closeViewImage() {
activeImageIndex.value = null;
}
</script>
<style scoped>
.gallery-arrows {
> * {
position: absolute;
top: 50%;
background-color: rgba(255, 255, 255, 0.6);
padding: 0.5rem;
border: none;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
&:hover {
background-color: rgba(255, 255, 255, 0.9);
}
}
}
.gallery-count {
position: absolute;
bottom: 24px;
left: 0;
right: 0;
width: fit-content;
margin-inline: auto;
padding: 4px 12px;
border-radius: 9999px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
font-size: 0.85rem;
}
</style>