290 lines
8.5 KiB
Vue
290 lines
8.5 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 props = defineProps({
|
|
productId: Number,
|
|
variantId: Number,
|
|
});
|
|
|
|
const emit = defineEmits(["created"]);
|
|
|
|
const { $buildFileUrl, $copy, $empty, $getdata, $insertapi, $patchapi } = useNuxtApp();
|
|
const isPending = ref(false);
|
|
const defaultBody = ref({
|
|
product: props.productId,
|
|
price: null,
|
|
internal_storage: null,
|
|
ram: null,
|
|
color: null,
|
|
image: null,
|
|
note: "",
|
|
});
|
|
const body = ref($copy(defaultBody.value));
|
|
|
|
const productVariant = ref();
|
|
onMounted(async () => {
|
|
// if props.variant (edit), fetch data to fill in body
|
|
if (props.variantId) {
|
|
const variant = await $getdata(
|
|
"Product_Variant",
|
|
undefined,
|
|
{
|
|
filter: { id: props.variantId },
|
|
values:
|
|
"id,code,product,product__name,product__os,product__manufacturer,product__battery,product__screen,product__cpu,product__gpu,product__camera_system,product__sim,product__network_technology,product__charging_technology,product__external_storage,product__ip_rating,product__design,color,color__code,color__name,color__hex_code,ram,ram__code,ram__capacity,internal_storage,internal_storage__code,internal_storage__capacity,image,image__path,price,note,create_time,update_time",
|
|
},
|
|
true,
|
|
);
|
|
|
|
productVariant.value = variant;
|
|
|
|
defaultBody.value = {
|
|
product: variant.product,
|
|
price: variant.price,
|
|
internal_storage: variant.internal_storage,
|
|
ram: variant.ram,
|
|
color: variant.color,
|
|
image: variant.image,
|
|
note: variant.note,
|
|
};
|
|
|
|
body.value = $copy(defaultBody.value);
|
|
}
|
|
});
|
|
|
|
const isDirty = computed(() => {
|
|
if (!isEqual(body.value, defaultBody.value)) return true;
|
|
if (uploadedImage.value) return true;
|
|
});
|
|
|
|
const uploadedImage = ref();
|
|
|
|
watch(
|
|
() => props.productId,
|
|
(newVal) => {
|
|
body.value.product = newVal;
|
|
},
|
|
);
|
|
|
|
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 = props.variantId
|
|
? await $patchapi("Product_Variant", {
|
|
id: props.variantId,
|
|
...trimmedBody,
|
|
})
|
|
: 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-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>
|