This commit is contained in:
Viet An
2026-05-20 14:31:37 +07:00
parent 5768a8a9fb
commit 629a530b5f
17 changed files with 738 additions and 316 deletions

View File

@@ -143,10 +143,6 @@ onUnmounted(() => {
overflow: visible;
}
.modal-card-body {
overflow: visible;
}
.modal-card:not(:has(.modal-card-head)) .modal-card-body {
border-start-start-radius: var(--bulma-radius-large);
border-start-end-radius: var(--bulma-radius-large);

View File

@@ -434,13 +434,13 @@ var props = defineProps({
});
const emit = defineEmits(["modalevent", "changepos", "close"]);
var colorchoice = store.colorchoice;
var errors = [];
var currentTab = ref("value");
const errors = ref([]);
const currentTab = ref("value");
var currentField = $copy(props.field);
var pagedata = store[props.pagename];
var fields = [];
var label = currentField.label;
var showmodal = ref();
const showmodal = ref();
const checkFilter = function () {};
const getMenu = function () {
let field = currentField;
@@ -455,8 +455,8 @@ var datatype = store.datatype;
var current = 1;
var value1 = undefined;
var value2 = undefined;
var moneyunit = store.moneyunit;
var radioType = store.datatype.find((v) => v.code === currentField.format);
const moneyunit = store.moneyunit;
const radioType = store.datatype.find((v) => v.code === currentField.format);
var selectUnit = currentField.format === "number" ? moneyunit.find((v) => v.detail === currentField.unit) : undefined;
var bgcolor = undefined;
var radioBGcolor = colorchoice.find((v) => v.code === "none");

View File

@@ -247,6 +247,7 @@
Tạo cột
</button>
<Modal
v-if="showmodal"
v-bind="showmodal"
@label="changeLabel"
@close="close"

View File

@@ -0,0 +1,32 @@
<script setup>
import ProductForm from "@/components/imports/ProductForm.vue";
import ProductVariantFormNew from "@/components/imports/ProductVariantFormNew.vue";
const menus = [
{
id: "product",
name: "Tạo sản phẩm",
},
{
id: "product-variant",
name: "Thêm phiên bản",
},
];
const activeMenu = ref(menus[0]);
</script>
<template>
<div class="tabs">
<ul>
<li
v-for="menu in menus"
:key="menu.id"
:class="[{ 'is-active': activeMenu.id === menu.id }]"
>
<a @click="activeMenu = menu">{{ menu.name }}</a>
</li>
</ul>
</div>
<ProductForm v-if="activeMenu.id === 'product'" />
<ProductVariantFormNew v-if="activeMenu.id === 'product-variant'" />
</template>

View File

@@ -1,6 +1,6 @@
<script setup>
import DataView from "@/components/datatable/DataView.vue";
import AddProductVariantForm from "@/components/imports/AddProductVariantForm.vue";
import ProductVariantForm from "@/components/imports/ProductVariantForm.vue";
const product = ref();
const key = ref(0);
watch(product, () => {
@@ -54,7 +54,7 @@ watch(product, () => {
</div>
</div>
</div>
<AddProductVariantForm
<ProductVariantForm
v-if="product"
:productId="product.id"
@created="key++"

View File

@@ -1,238 +0,0 @@
<script setup>
import InputNumber from "@/components/common/InputNumber.vue";
import FileUpload from "@/components/media/FileUpload.vue";
import { omitBy } from "es-toolkit";
const props = defineProps({
productId: Number,
});
const emit = defineEmits(["created"]);
const { $buildFileUrl, $empty, $insertapi } = useNuxtApp();
const isPending = ref(false);
const body = ref({
product: props.productId,
price: null,
internal_storage: null,
ram: null,
color: null,
image: null,
note: "",
});
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;
}
async function createProductVariant() {
isPending.value = true;
// 1. insert row to Product_Image, get back id
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);
isPending.value = false;
if (res !== "error") {
emit("created");
}
}
</script>
<template>
<div>
<h1 class="subtitle">Thêm phiên bản</h1>
<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,
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,
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,
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-50)"
>
<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
class="w-36 h-36 rounded-md is-flex is-align-items-center is-justify-content-center"
style="border: 1px solid var(--bulma-grey-80)"
>
<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="Object.values(body).every($empty)"
@click.prevent="createProductVariant"
>
<span class="icon">
<Icon
name="material-symbols:add-rounded"
:size="18"
/>
</span>
<span>Thêm phiên bản</span>
</button>
</div>
</div>
</form>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import ProductForm from "@/components/imports/ProductForm.vue";
import ProductVariantForm from "@/components/imports/ProductVariantForm.vue";
const props = defineProps({
variant: Object,
});
</script>
<template>
<h1 class="subtitle">Sản phẩm</h1>
<ProductForm :variantId="variant.id" />
<h1 class="subtitle">Phiên bản</h1>
<ProductVariantForm :variantId="variant.id" />
</template>

View File

@@ -0,0 +1,32 @@
<script setup>
const props = defineProps({
variant: Object,
});
const showModal = ref(null);
</script>
<template>
<a
@click="
showModal = {
component: 'imports/EditProduct',
title: 'Cập nhật sản phẩm',
width: '90%',
height: '400px',
vbind: { variant: props.variant },
}
"
>
<span class="icon">
<Icon
name="material-symbols:edit-outline-rounded"
:size="18"
/>
</span>
</a>
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = null"
/>
</template>

View File

@@ -1,56 +1,12 @@
<script setup>
import AddProductVariant from "@/components/imports/AddProductVariant.vue";
import Products from "@/components/imports/Products.vue";
const menus = [
{
id: "product",
name: "Sản phẩm",
},
{
id: "product-variant",
name: "Phiên bản",
},
];
const activeMenu = ref(menus[0]);
</script>
<template>
<div class="fixed-grid has-12-cols">
<div class="grid is-gap-4">
<div class="cell is-col-span-2">
<aside class="menu">
<ul class="menu-list">
<li
v-for="menu in menus"
:key="menu.id"
>
<a
@click="activeMenu = menu"
:class="[{ 'is-active': activeMenu.id === menu.id }]"
>{{ menu.name }}</a
>
</li>
</ul>
</aside>
</div>
<div class="cell is-col-span-10">
<Products v-if="activeMenu.id === 'product'" />
<AddProductVariant v-if="activeMenu.id === 'product-variant'" />
<div class="grid">
<div class="cell is-col-span-12">
<Products />
</div>
</div>
</div>
</template>
<style scoped>
.menu-list a {
font-size: 0.95em;
--bulma-menu-list-link-padding: 0.75em 1.25em;
&:not(.is-active) {
--bulma-menu-item-background-l: 95%;
}
&.is-active {
--bulma-menu-item-h: var(--bulma-menu-item-selected-h);
--bulma-menu-item-s: var(--bulma-menu-item-selected-s);
--bulma-menu-item-l: var(--bulma-menu-item-selected-l);
}
}
</style>

View File

@@ -1,8 +1,14 @@
<script setup>
const { $insertapi } = useNuxtApp();
import { isEqual } from "es-toolkit";
const props = defineProps({
variantId: Number,
});
const { $copy, $getdata, $insertapi, $patchapi } = useNuxtApp();
const isPending = ref(false);
const body = ref({
const defaultBody = ref({
name: null,
manufacturer: null,
os: null,
@@ -19,15 +25,61 @@ const body = ref({
design: null,
});
const body = ref($copy(defaultBody.value));
const productVariant = ref();
onMounted(async () => {
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 = {
name: variant.product__name,
manufacturer: variant.product__manufacturer,
os: variant.product__os,
battery: variant.product__battery,
screen: variant.product__screen,
cpu: variant.product__cpu,
gpu: variant.product__gpu,
camera_system: variant.product__camera_system,
sim: variant.product__sim,
network_technology: variant.product__network_technology,
charging_technology: variant.product__charging_technology,
external_storage: variant.product__external_storage,
ip_rating: variant.product__ip_rating,
design: variant.product__design,
};
body.value = $copy(defaultBody.value);
}
});
const isDirty = computed(() => !isEqual(body.value, defaultBody.value));
function selected(field, data) {
if (data === null) body.value[field] = data;
else body.value[field] = data.id;
}
const refreshData = inject("refreshData");
async function createProduct() {
async function submit() {
isPending.value = true;
const res = await $insertapi("product", body.value);
const res = props.variantId
? await $patchapi("product", {
id: productVariant.value.product,
...body.value,
})
: await $insertapi("product", body.value);
if (res !== "error") {
if (refreshData) refreshData();
}
@@ -61,6 +113,7 @@ async function createProduct() {
column: ['name'],
first: true,
clearable: true,
optionid: body.os,
placeholder: 'OS',
addon: {
component: 'imports/addons/AddOS',
@@ -83,6 +136,7 @@ async function createProduct() {
column: ['name'],
first: true,
clearable: true,
optionid: body.manufacturer,
placeholder: 'Hãng',
addon: {
component: 'imports/addons/AddManufacturer',
@@ -105,6 +159,7 @@ async function createProduct() {
column: ['code'],
first: true,
clearable: true,
optionid: body.battery,
placeholder: 'Pin',
addon: {
component: 'imports/addons/AddBattery',
@@ -127,6 +182,7 @@ async function createProduct() {
column: ['resolution', 'standard', 'technology'],
first: true,
clearable: true,
optionid: body.screen,
placeholder: 'Màn hình',
addon: {
component: 'imports/addons/AddScreen',
@@ -150,6 +206,7 @@ async function createProduct() {
first: true,
clearable: true,
placeholder: 'CPU',
optionid: body.cpu,
addon: {
component: 'imports/addons/AddCPU',
width: '70%',
@@ -171,6 +228,7 @@ async function createProduct() {
column: ['name'],
first: true,
clearable: true,
optionid: body.gpu,
placeholder: 'GPU',
addon: {
component: 'imports/addons/AddGPU',
@@ -193,6 +251,7 @@ async function createProduct() {
column: ['code'],
first: true,
clearable: true,
optionid: body.camera_system,
placeholder: 'Camera',
addon: {
component: 'imports/addons/AddCamera',
@@ -215,6 +274,7 @@ async function createProduct() {
column: ['code'],
first: true,
clearable: true,
optionid: body.sim,
placeholder: 'SIM',
addon: {
component: 'imports/addons/AddSIM',
@@ -237,6 +297,7 @@ async function createProduct() {
column: ['name'],
first: true,
clearable: true,
optionid: body.network_technology,
placeholder: 'Kết nối',
addon: {
component: 'imports/addons/AddNetworkTechnology',
@@ -259,6 +320,7 @@ async function createProduct() {
column: ['code'],
first: true,
clearable: true,
optionid: body.charging_technology,
placeholder: 'Công nghệ sạc',
addon: {
component: 'imports/addons/AddChargingTechnology',
@@ -281,6 +343,7 @@ async function createProduct() {
column: ['code'],
first: true,
clearable: true,
optionid: body.external_storage,
placeholder: 'Bộ nhớ ngoài',
position: 'is-top-right',
addon: {
@@ -304,6 +367,7 @@ async function createProduct() {
column: ['name'],
first: true,
clearable: true,
optionid: body.ip_rating,
placeholder: 'Chỉ số IP',
position: 'is-top-right',
addon: {
@@ -327,6 +391,7 @@ async function createProduct() {
column: ['frame_material', 'back_material'],
first: true,
clearable: true,
optionid: body.design,
placeholder: 'Chất liệu',
position: 'is-top-right',
addon: {
@@ -343,13 +408,13 @@ async function createProduct() {
<div class="cell is-col-span-12">
<button
:class="['button is-primary', { 'is-loading': isPending }]"
:disabled="Object.values(body).every((v) => v === null)"
@click.prevent="createProduct"
:disabled="!isDirty"
@click.prevent="submit"
>
<span class="icon">
<Icon name="material-symbols:add-rounded" />
<Icon :name="variantId ? 'material-symbols:edit-outline-rounded' : 'material-symbols:add-rounded'" />
</span>
<span>Tạo sản phẩm</span>
<span>{{ variantId ? "Cập nhật" : "Tạo" }} sản phẩm</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,289 @@
<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-50)"
>
<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-80)"
>
<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>

View File

@@ -0,0 +1,266 @@
<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-50)"
>
<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-80)"
>
<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>

View File

@@ -5,20 +5,20 @@ import DataView from "@/components/datatable/DataView.vue";
<template>
<DataView
v-bind="{
api: 'product',
setting: 'products',
pagename: 'products',
api: 'Product_Variant',
setting: 'product-variants',
pagename: 'product-variants',
params: {
values:
'id,code,name,manufacturer,manufacturer__name,os,os__name,battery,battery__code,screen,cpu,cpu__name,gpu,gpu__name,camera_system,camera_system__code,sim,sim__code,network_technology,network_technology__name,charging_technology,charging_technology__code,external_storage,external_storage__max_capacity,ip_rating,ip_rating__code,design,create_time,update_time',
'id,code,product,product__name,product__os__name,product__external_storage__max_capacity,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',
sort: 'id',
},
timeopt: { time: 36000 },
modal: {
component: 'imports/AddProductForm',
component: 'imports/AddProduct',
title: 'Tạo sản phẩm',
width: '75%',
height: 'auto',
width: '90%',
height: '450px',
},
}"
/>

View File

@@ -2,7 +2,7 @@
<div class="is-flex is-align-items-center is-gap-1">
<Icon
name="material-symbols:cancel-rounded"
:size="22"
:size="18"
class="has-text-danger-80"
/>
<p

View File

@@ -2,7 +2,7 @@
<div class="is-flex is-align-items-center is-gap-1">
<Icon
name="material-symbols:check-circle-rounded"
:size="22"
:size="18"
class="has-text-success-70"
/>
<p