This commit is contained in:
Viet An
2026-06-07 09:22:53 +07:00
parent d17bc0583c
commit ee914cc382
8 changed files with 653 additions and 385 deletions

View File

@@ -10,144 +10,147 @@
<div class="control">
<div class="file is-primary">
<label class="file-label">
<button :class="['button is-primary is-small', loading && 'is-loading']">
<span class="icon">
<Icon
name="material-symbols:upload-rounded"
:size="18"
/>
</span>
<span>Tải lên từ máy tính</span>
</button>
<input
class="file-input"
type="file"
:id="docid"
@change="doChange"
multiple
name="resume"
@change="doChange"
class="file-input is-clickable"
:id="docid"
/>
<a
class="button is-primary is-rounded is-small"
:class="loading ? 'is-loading' : null"
>
<SvgIcon v-bind="{ name: 'add5.svg', type: 'white', size: 18 }"></SvgIcon>
<span class="fs-14 ml-1">Tải lên từ máy tính</span>
</a>
</label>
</div>
</div>
<div :class="`control ${showUrl ? '' : 'is-expanded'}`">
<a
class="button is-dark is-rounded is-small"
@click="displayInput()"
<div :class="['control', !showUrl && 'is-expanded']">
<button
class="button is-small"
@click="displayInput"
>
<SvgIcon v-bind="{ name: 'add5.svg', type: 'white', size: 18 }"></SvgIcon>
<span class="fs-14 ml-1">Tải lên từ đường dẫn</span>
</a>
<span class="icon">
<Icon
name="material-symbols:link-rounded"
:size="18"
/>
</span>
<span>Tải lên từ đường dẫn</span>
</button>
</div>
<div
class="control is-expanded"
v-if="showUrl"
class="field has-addons is-flex-grow-1 mb-0"
>
<input
class="input is-small is-rounded"
id="url"
v-model="url"
style="width: 250px"
type="text"
placeholder="Nhập đường dẫn vào đây"
/>
<a
class="button is-primary is-small px-4 ml-3"
@click="checkUrl()"
>
<SvgIcon v-bind="{ name: 'upload.svg', type: 'white', size: 22 }"></SvgIcon>
</a>
<a
class="ml-4"
@click="showUrl = false"
>
<SvgIcon v-bind="{ name: 'close.svg', type: 'dark', size: 22 }"></SvgIcon>
</a>
<div class="control">
<input
class="input is-small w-60"
id="url"
v-model.trim="url"
type="text"
placeholder="Nhập đường dẫn"
/>
</div>
<div class="control">
<button
class="button is-light is-small rounded-full"
@click="showUrl = false"
>
<span class="icon">
<Icon
name="material-symbols:close-rounded"
:size="18"
/>
</span>
</button>
</div>
<div class="control">
<button
class="button is-primary is-small"
:disabled="!url"
@click="checkUrl"
>
<span class="icon">
<Icon
name="material-symbols:arrow-forward-rounded"
:size="20"
/>
</span>
</button>
</div>
</div>
<div class="control">
<div class="control has-icons-left">
<input
class="input is-small is-rounded"
v-model="search"
style="width: 250px"
type="text"
placeholder="Tìm kiếm"
@keyup="beginSearch"
class="input is-small is-rounded w-2xs"
placeholder="Tìm kiếm"
/>
</div>
<div class="control">
<span
class="is-clickable"
@click="mode = 'image'"
class="icon is-small is-left"
style="left: 2px"
>
<SvgIcon v-bind="{ name: 'image3.svg', type: 'dark', size: 25 }"></SvgIcon>
<Icon
name="material-symbols:search"
:size="18"
/>
</span>
</div>
<div class="control">
<span
class="is-clickable"
@click="mode = 'list'"
>
<SvgIcon v-bind="{ name: 'list.png', type: 'dark', size: 25 }"></SvgIcon>
</span>
<div class="tabs is-toggle m-0">
<ul class="is-flex-grow-0 ml-auto">
<li
v-for="viewMode in viewModes"
:key="viewMode.name"
:class="[viewMode.name === mode && 'is-active']"
@click="mode = viewMode.name"
>
<a class="px-3 py-1">
<span class="icon m-0">
<Icon
:name="viewMode.icon"
:size="18"
/>
</span>
</a>
</li>
</ul>
</div>
</div>
<DataView
class="mt-3"
v-bind="vbind"
v-if="mode === 'list'"
></DataView>
<div
class="tile is-ancestor mx-0 px-0 pt-3"
v-else
>
<div class="tile is-vertical">
<div
class="tile is-parent"
v-for="(v, i) in group"
:key="i"
>
<article
class="tile is-child"
v-bind="vbind"
class="mt-3"
/>
<div v-else>
<div
class="fixed-grid has-5-cols"
v-for="(_, i) in group"
:key="i"
>
<div class="grid">
<div
v-for="(k, j) in getData(i)"
:key="j"
@mouseover="focus = k"
@mouseleave="focus = undefined"
class="cell relative rounded-md is-clipped"
>
<div
class="image px-2 pb-2"
<figure
v-if="k.file && type === 'image'"
class="image is-square mx-auto is-flex is-align-items-center"
>
<NuxtImg
:src="`${path}download?name=${k.file}`"
:id="'commentImage' + k.id"
/>
<div
class="text-image"
v-if="focus === k"
>
<a
class="button is-primary is-small"
@click="selectMedia(k)"
>
<SvgIcon v-bind="{ name: 'checked.svg', type: 'white', size: 22 }"></SvgIcon>
</a>
<a
class="button is-primary is-small ml-2"
@click="editImage(k)"
>
<SvgIcon v-bind="{ name: 'crop.svg', type: 'white', size: 22 }"></SvgIcon>
</a>
<a
class="button is-primary is-small ml-2"
@click="copyMedia(k, 'image')"
>
<SvgIcon v-bind="{ name: 'copy.svg', type: 'white', size: 22 }"></SvgIcon>
</a>
<a
class="button is-danger is-small ml-2"
@click="deleteMedia(k, 'file')"
>
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 22 }"></SvgIcon>
</a>
</div>
</div>
</figure>
<div
class="ml-2 mr-2"
v-else-if="k.file && type === 'video'"
@@ -164,12 +167,17 @@
class="mt-2"
v-if="focus === k"
>
<a
<button
class="button is-primary"
@click="selectMedia(k)"
>
<SvgIcon v-bind="{ name: 'check3.svg', type: 'primary', size: 22 }"></SvgIcon>
</a>
<span class="icon">
<Icon
name="material-symbols:check-rounded"
:size="18"
/>
</span>
</button>
</div>
</vue-plyr>
</div>
@@ -182,28 +190,88 @@
class="mt-2"
v-if="focus === k"
>
<a
<button
class="button is-primary"
@click="selectMedia(k)"
>
<SvgIcon v-bind="{ name: 'check3.svg', type: 'primary', size: 22 }"></SvgIcon>
</a>
<span class="icon">
<Icon
name="material-symbols:check-rounded"
:size="18"
/>
</span>
</button>
</div>
</div>
</article>
<div
v-if="focus === k"
class="absolute top-0 size-full"
>
<div class="size-full has-background-black opacity-15"></div>
<div class="buttons has-addons absolute left-0 right-0 bottom-2 mx-auto w-fit">
<button
class="button is-small is-white"
@click="selectMedia(k)"
>
<span class="icon">
<Icon
name="material-symbols:check-rounded"
:size="18"
/>
</span>
</button>
<button
class="button is-small is-white"
@click="editImage(k)"
>
<span class="icon">
<Icon
name="material-symbols:crop-free-rounded"
:size="18"
/>
</span>
</button>
<button
class="button is-small is-white"
@click="copyMedia(k)"
>
<span class="icon">
<Icon
name="material-symbols:content-copy-outline-rounded"
:size="18"
/>
</span>
</button>
<button
class="button is-small is-white"
@click="deleteMedia(k, 'file')"
>
<span class="icon">
<Icon
name="material-symbols:delete-outline-rounded"
:size="18"
class="has-text-danger"
/>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<Modal
@close="showmodal = undefined"
v-if="showmodal"
v-bind="showmodal"
@close="showmodal = undefined"
@image="updateImage"
@files="getFiles"
v-if="showmodal"
/>
</div>
</template>
<script>
import DataView from "~/components/datatable/DataView.vue";
export default {
props: ["source"],
data() {
@@ -222,6 +290,10 @@ export default {
url: undefined,
search: undefined,
mode: "image",
viewModes: [
{ name: "list", icon: "material-symbols:format-list-bulleted-rounded" },
{ name: "image", icon: "material-symbols:grid-view-outline-rounded" },
],
timer: undefined,
vbind: { api: "file", setting: "image-fields" },
dataFiles: [],
@@ -230,13 +302,15 @@ export default {
};
},
async created() {
let found = this.$findapi("file");
found.params = {
filter: { user: this.login.id },
const fileApi = this.$findapi("file");
fileApi.params = {
sort: "-create_time",
type__code: this.type,
filter: {
user: this.login.id,
type__code: this.type,
},
};
let result = await this.$getapi([found]);
const result = await this.$getapi([fileApi]);
this.data = result[0].data.rows;
},
watch: {
@@ -298,7 +372,7 @@ export default {
this.timer = setTimeout(() => this.startSearch(e.target.value), 150);
},
async startSearch(value) {
const filter = { user: this.login.id };
const filter = { user: this.login.id, type__code: this.type };
if (!this.$empty(value)) filter.name__icontains = value.toLowerCase();
this.data = await this.$getdata("file", { filter });
},
@@ -307,29 +381,25 @@ export default {
this.url = undefined;
setTimeout(() => document.getElementById("url").focus(), 100);
},
checkUrl() {
async checkUrl() {
if (this.$empty(this.url)) return this.$snackbar("Đường dẫn không hợp lệ", "Error");
let self = this;
this.loading = true;
this.$axios
.get(this.url, { responseType: "blob" })
.then(function (response) {
var reader = new window.FileReader();
reader.onload = (e) => {
self.image = e.target.result;
setTimeout(() => self.doUpload(e.target.result), 100);
};
reader.readAsDataURL(response.data);
self.loading = false;
})
.catch((e) => {
self.$buefy.toast.open({
duration: 3000,
message: `Đường dẫn không hợp lệ`,
type: "is-danger",
});
self.loading = false;
});
try {
const self = this;
this.loading = true;
const res = await $fetch(this.url);
const reader = new window.FileReader();
reader.onload = (e) => {
self.image = e.target.result;
setTimeout(() => self.doUpload(e.target.result), 100);
};
reader.readAsDataURL(res);
} catch (error) {
this.$snackbar("Đường dẫn không hợp lệ", "Error");
console.error(error);
} finally {
this.loading = false;
}
},
doUpload() {
const image = document.getElementById("image");
@@ -340,102 +410,103 @@ export default {
ctx.drawImage(image, 0, 0);
if (canvas) canvas.toBlob((blod) => this.saveAs(blod));
},
async saveAs(blod) {
var form = new FormData();
async saveAs(blob) {
const form = new FormData();
const fileName = this.$dayjs(new Date()).format("YYYYMMDDhhmmss") + "-" + this.$id() + ".png";
form.append("filename", fileName);
form.append("name", `${this.$id()}.png`);
form.append("file", blod);
form.append("file", blob);
form.append("type", "image");
form.append("size", 100);
form.append("user", this.$store.login.id);
let result = await this.$insertapi("upload", { data: form });
const result = await this.$insertapi("upload", { data: form });
if (result === "error") return;
this.updateImage(result.rows[0]);
this.showUrl = false;
},
async uploadImage(file) {
this.loading = true;
let result = await this.$insertapi("upload", { data: file.form });
const result = await this.$insertapi("upload", { data: file.form });
this.data.splice(0, 0, result.rows[0]);
this.loading = false;
},
getData(i) {
if (this.type === "video") {
var list = this.data.slice(i * 5, i * 5 + 5);
for (let index = 0; index < 5; index++) {
if (list.length < index + 1) list.push({ file: undefined });
}
} else {
list = this.data.slice(i * 6, i * 6 + 6);
for (let index = 0; index < 6; index++) {
if (list.length < index + 1) list.push({ file: undefined });
}
const list = this.data.slice(i * 5, i * 5 + 5);
for (let index = 0; index < 5; index++) {
if (list.length < index + 1) list.push({ file: undefined });
}
return list;
},
selectMedia(v) {
let copy = this.media ? this.$copy(this.media) : {};
async selectMedia(v) {
const copy = this.media ? this.$copy(this.media) : {};
copy.type = "image";
copy.open = false;
copy.select = v;
this.media = copy;
let row = this.$copy(v);
const row = this.$copy(v);
if (this.source) {
let params = { name: v.file, type: "file" };
this.$axios
.get(`${this.path}download/`, {
params: params,
responseType: "blob",
})
.then(function (response) {
var reader = new window.FileReader();
reader.onload = (e) => {
fetch(e.target.result)
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], v.name, { type: "image/png" });
row.source = { file: file };
});
};
reader.readAsDataURL(response.data);
});
const res = await $fetch(`${this.path}download/`, {
params: {
name: v.file,
type: "file",
},
});
const reader = new window.FileReader();
reader.onload = (e) => {
fetch(e.target.result)
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], v.name, { type: "image/png" });
row.source = { file };
});
};
reader.readAsDataURL(res);
}
this.$emit("modalevent", { name: "selectimage", data: row });
},
editImage(v) {
async editImage(v) {
this.loading = true;
this.selected = v;
let self = this;
let params = { name: v.file, type: "file" };
this.$axios.get(`${this.path}download/`, { params: params, responseType: "blob" }).then(function (response) {
var reader = new window.FileReader();
reader.onload = (e) => {
self.image = e.target.result;
self.showmodal = {
component: "media/CropImage",
width: "65%",
title: "Cắt hình ảnh",
vbind: { selected: self.selected, image: self.image },
};
};
reader.readAsDataURL(response.data);
self.loading = false;
const self = this;
const res = await $fetch(`${this.path}download/`, {
params: {
name: v.file,
type: "file",
},
});
const reader = new window.FileReader();
reader.onload = (e) => {
self.image = e.target.result;
self.showmodal = {
component: "media/CropImage",
title: "Cắt hình ảnh",
width: "90%",
height: "auto",
vbind: { selected: self.selected, image: self.image },
};
};
reader.readAsDataURL(res);
self.loading = false;
},
copyMedia(v) {
this.$copyToClipboard(`${this.path}download/?name=${v.file}`);
},
deleteMedia(v, name) {
let self = this;
var remove = async function () {
let result = await self.$deleteapi(name, v.id);
let idx = self.data.findIndex((x) => x.id === v.id);
const remove = async function () {
await self.$deleteapi(name, v.id);
const idx = self.data.findIndex((x) => x.id === v.id);
self.$delete(self.data, idx);
};
this.$buefy.dialog.confirm({
message: "Bạn muốn xóa file: " + v.file,
onConfirm: () => remove(),
onConfirm: remove,
});
},
updateImage(v) {
@@ -445,3 +516,10 @@ export default {
},
};
</script>
<style scoped>
.tabs {
--bulma-tabs-toggle-link-active-background-color: var(--bulma-link-90);
--bulma-tabs-toggle-link-active-border-color: var(--bulma-link-90);
--bulma-tabs-toggle-link-active-color: var(--bulma-link-40);
}
</style>