Files
web/app/components/imports/ProductVariantFormNew.vue
2026-05-22 09:57:12 +07:00

267 lines
7.8 KiB
Vue

<script setup>
import InputNumber from "@/components/common/InputNumber.vue";
import FileUpload from "@/components/media/FileUpload.vue";
import { isEqual, omitBy } from "es-toolkit";
const emit = defineEmits(["created"]);
const { $buildFileUrl, $copy, $empty, $getdata, $insertapi, $patchapi } = useNuxtApp();
const isPending = ref(false);
const defaultBody = ref({
product: null,
price: null,
internal_storage: null,
ram: null,
color: null,
image: null,
note: "",
});
const body = ref($copy(defaultBody.value));
const productVariant = ref();
const isDirty = computed(() => {
if (!isEqual(body.value, defaultBody.value)) return true;
if (uploadedImage.value) return true;
});
const uploadedImage = ref();
function selected(field, data) {
if (data === null || field === "price") {
body.value[field] = data;
} else {
body.value[field] = data.id;
}
}
function onFiles(files) {
const file = files[0];
uploadedImage.value = file;
}
const refreshData = inject("refreshData");
async function submit() {
isPending.value = true;
// 1. insert row to Product_Image, get back id
if (uploadedImage.value) {
const productImage = await $insertapi("Product_Image", {
name: uploadedImage.value.name,
path: $buildFileUrl(uploadedImage.value.file),
});
body.value.image = productImage.id;
}
// 2. upload to Product_Variant table
const trimmedBody = omitBy(body.value, $empty);
const res = await $insertapi("Product_Variant", trimmedBody);
if (res !== "error") {
if (refreshData) refreshData();
}
isPending.value = false;
}
</script>
<template>
<form class="fixed-grid has-12-cols">
<div class="grid">
<div class="cell is-col-span-12">
<div class="field">
<label class="label">Sản phẩm</label>
<SearchBox
v-bind="{
api: 'product',
field: 'name',
column: ['name'],
first: true,
clearable: true,
placeholder: 'Sản phẩm',
addon: {
component: 'imports/ProductForm',
width: '80%',
height: 'auto',
title: 'Tạo sản phẩm',
},
}"
@option="selected('product', $event)"
/>
</div>
</div>
<div class="cell is-col-span-3">
<div class="field">
<label class="label">Đơn giá</label>
<div class="control">
<InputNumber
v-bind="{
record: body,
attr: 'price',
placeholder: 'Đơn giá',
unit: 'VND',
}"
@number="selected('price', $event)"
/>
</div>
</div>
</div>
<div class="cell is-col-span-3">
<div class="field">
<label class="label">Màu sắc</label>
<SearchBox
v-bind="{
api: 'Color',
field: 'name',
column: ['name'],
first: true,
clearable: true,
optionid: body.color,
placeholder: 'Màu sắc',
addon: {
component: 'imports/addons/AddColor',
width: '60%',
height: 'auto',
title: 'Thêm màu sắc',
},
}"
@option="selected('color', $event)"
/>
</div>
</div>
<div class="cell is-col-span-3">
<div class="field">
<label class="label">RAM</label>
<SearchBox
v-bind="{
api: 'RAM',
field: 'code',
column: ['code'],
first: true,
clearable: true,
optionid: body.ram,
placeholder: 'RAM',
addon: {
component: 'imports/addons/AddRAM',
width: '60%',
height: 'auto',
title: 'Thêm RAM',
},
}"
@option="selected('ram', $event)"
/>
</div>
</div>
<div class="cell is-col-span-3">
<div class="field">
<label class="label">Bộ nhớ trong</label>
<SearchBox
v-bind="{
api: 'Internal_Storage',
field: 'code',
column: ['code'],
first: true,
clearable: true,
optionid: body.internal_storage,
placeholder: 'Bộ nhớ trong',
addon: {
component: 'imports/addons/AddInternalStorage',
width: '60%',
height: 'auto',
title: 'Thêm bộ nhớ trong',
},
}"
@option="selected('internal_storage', $event)"
/>
</div>
</div>
<div class="cell is-col-span-6">
<div class="field">
<label class="label">Ghi chú</label>
<textarea
v-model.trim="body.note"
class="textarea"
name="note"
placeholder="Ghi chú"
rows="5"
></textarea>
</div>
</div>
<div class="cell is-col-span-6">
<div class="field">
<label class="label">Hình ảnh</label>
<div class="control is-flex is-gap-1">
<div>
<div
v-if="uploadedImage"
class="relative"
style="border: 1px solid var(--bulma-grey)"
>
<figure class="image is-128x128">
<img :src="$buildFileUrl(uploadedImage.file)" />
</figure>
<button
type="button"
class="button is-small is-danger is-light absolute"
:style="{ top: '5px', right: '5px' }"
@click="uploadedImage = undefined"
>
<span class="icon">
<Icon
name="material-symbols:delete-outline-rounded"
:size="20"
/>
</span>
</button>
</div>
<div v-else-if="productVariant?.image__path">
<figure class="image is-128x128">
<img :src="productVariant.image__path" />
</figure>
</div>
<div
v-else
class="w-36 h-36 rounded-md is-flex is-align-items-center is-justify-content-center"
style="border: 1px solid var(--bulma-grey-lighter)"
>
<Icon
name="material-symbols:add-photo-alternate-outline-rounded"
:size="40"
class="has-text-grey-light"
/>
</div>
</div>
<FileUpload
:type="['image']"
:multiple="false"
@files="onFiles"
>
<template #icon>
<Icon
name="material-symbols:add-photo-alternate-outline-rounded"
:size="20"
/>
</template>
<span class="font-medium">Chọn</span>
</FileUpload>
</div>
</div>
</div>
<div class="cell is-col-span-12">
<button
:class="['button is-primary', { 'is-loading': isPending }]"
:disabled="!body.product || !isDirty"
@click.prevent="submit"
>
<span class="icon">
<Icon
:name="variantId ? 'material-symbols:edit-outline-rounded' : 'material-symbols:add-rounded'"
:size="18"
/>
</span>
<span>{{ variantId ? "Cập nhật" : "Thêm" }} phiên bản</span>
</button>
</div>
</div>
</form>
</template>