Files
web/app/components/media/CropImage.vue
2026-06-07 11:17:10 +07:00

178 lines
5.2 KiB
Vue

<template>
<div
v-if="image"
class="fixed-grid has-12-cols"
>
<div class="grid is-gap-2">
<div class="cell is-col-span-8">
<Cropper
ref="cropper"
:src="image"
:canvas="false"
@change="onChange"
:stencil-props="getRatio"
/>
</div>
<div class="cell is-col-span-4">
<div
v-if="avatar"
class="is-italic has-text-grey"
>
* Di chuyển khung để chọn hình ảnh phù hợp
</div>
<div v-else>
<p class="label">Chọn tỷ lệ:</p>
<div class="fixed-grid has-12-cols">
<div class="grid is-gap-0.5">
<div
class="cell is-col-span-3"
v-for="ratio in ratios"
:key="ratio"
>
<button
:class="['button is-fullwidth is-small', curRatio === ratio ? 'is-primary' : 'is-light']"
@click="curRatio = ratio"
>
{{ ratio }}
</button>
</div>
</div>
</div>
<hr />
<div
v-if="coordinates"
class="fixed-grid has-3-cols is-family-monospace"
>
<div class="grid">
<div class="cell p-2 rounded has-background-white-ter has-text-centered">
<p class="fs-11 is-uppercase has-text-grey">width</p>
<p class="font-semibold">{{ coordinates.width }}</p>
</div>
<div class="cell p-2 rounded has-background-white-ter has-text-centered">
<p class="fs-11 is-uppercase has-text-grey">height</p>
<p class="font-semibold">{{ coordinates.height }}</p>
</div>
<div class="cell p-2 rounded has-background-white-ter has-text-centered">
<p class="fs-11 is-uppercase has-text-grey">ratio</p>
<p class="font-semibold">{{ coordinates.ratio }}</p>
</div>
</div>
</div>
<hr />
<div class="block is-flex is-gap-1 is-flex-wrap-wrap">
<button
v-for="v in [
{ name: 'Ghi đè', value: 'replace' },
{ name: 'Tạo file mới', value: 'new' },
]"
:key="v.value"
@click="radio = v.value"
:class="['button fs-14', radio === v.value ? 'is-primary is-light' : 'is-white']"
>
<span class="icon">
<Icon
:name="
radio === v.value
? 'material-symbols:radio-button-checked-outline'
: 'material-symbols:radio-button-unchecked'
"
:size="18"
/>
</span>
<span>{{ v.name }}</span>
</button>
</div>
<div class="block">
<button
class="button is-primary"
:class="loading && 'is-loading'"
@click="updateImage"
>
Lưu lại
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { CircleStencil, Cropper } from "vue-advanced-cropper";
import "vue-advanced-cropper/dist/style.css";
export default {
components: {
Cropper,
CircleStencil,
},
props: ["selected", "image", "avatar"],
data() {
return {
coordinates: undefined,
ratios: [
"1/1",
"5/4",
"4/3",
"3/2",
"5/3",
"16/9",
"2/1",
"3/1",
"4/5",
"3/4",
"2/3",
"3/5",
"9/16",
"1/2",
"1/3",
],
curRatio: "1/1",
radio: this.avatar ? "new" : "replace",
loading: false,
};
},
computed: {
getRatio() {
return { aspectRatio: this.$calc(this.curRatio) };
},
},
methods: {
onChange({ coordinates }) {
this.coordinates = coordinates;
this.coordinates.ratio = (this.coordinates.width / this.coordinates.height).toFixed(2);
},
updateImage() {
const { canvas } = this.$refs.cropper.getResult();
if (canvas) canvas.toBlob((blob) => this.saveAs(blob));
},
async saveAs(blob) {
try {
this.loading = true;
const form = new FormData();
const name =
this.selected.file.indexOf("-") > 0
? this.selected.file.substring(15, this.selected.file.length)
: this.selected.file;
this.fileName = this.$dayjs(new Date()).format("YYYYMMDDhhmmss") + "-" + name;
if (this.radio === "replace") this.fileName = this.selected.file;
form.append("filename", this.fileName);
form.append("name", name);
form.append("file", blob);
form.append("type", "file");
form.append("size", this.selected.size);
form.append("user", this.$store.login.id);
const result = await this.$insertapi("upload", { data: form });
if (result === "error") return;
this.$emit("modalevent", { name: "image", data: result.rows[0] });
} catch (error) {
console.error(error);
} finally {
this.loading = false;
}
},
},
};
</script>