WIP: cart
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Adapters\OcModelCatalogProductAdapter;
|
||||
use App\ApplicationFactory;
|
||||
use Cart\Cart;
|
||||
use Cart\Currency;
|
||||
use Cart\Tax;
|
||||
use Openguru\OpenCartFramework\ImageTool\ImageTool;
|
||||
@@ -60,6 +61,14 @@ class Controllerextensiontgshophandle extends Controller
|
||||
return new ImageTool(DIR_IMAGE, HTTPS_SERVER);
|
||||
});
|
||||
|
||||
$app->bind(\Cart\Cart::class, function () {
|
||||
return $this->cart;
|
||||
});
|
||||
|
||||
$app->bind(DB::class, function () {
|
||||
return $this->db;
|
||||
});
|
||||
|
||||
$this->load->model('checkout/order');
|
||||
|
||||
$app->bind('model_checkout_order', function () {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Adapters;
|
||||
|
||||
class OcCartAdapter
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Cart\Cart;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
|
||||
class CheckoutHandler
|
||||
{
|
||||
private Cart $cart;
|
||||
private \DB $connection;
|
||||
|
||||
public function __construct(Cart $cart, \DB $database)
|
||||
{
|
||||
$this->cart = $cart;
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function addToCart(Request $request): JsonResponse
|
||||
{
|
||||
$item = $request->json();
|
||||
|
||||
$options = [];
|
||||
|
||||
foreach ($item['options'] as $option) {
|
||||
if (! empty($option['value']) && ! empty($option['value']['product_option_value_id'])) {
|
||||
$options[$option['product_option_id']] = $option['value']['product_option_value_id'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->cart->add(
|
||||
$item['productId'],
|
||||
$item['quantity'],
|
||||
$options,
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $this->cart->getProducts(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkout(Request $request): JsonResponse
|
||||
{
|
||||
$items = $request->json();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$options = [];
|
||||
|
||||
foreach ($item['options'] as $option) {
|
||||
if (! empty($option['value']) && ! empty($option['value']['product_option_value_id'])) {
|
||||
$options[$option['product_option_id']] = $option['value']['product_option_value_id'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->cart->add(
|
||||
$item['productId'],
|
||||
$item['quantity'],
|
||||
$options,
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Handlers\CategoriesHandler;
|
||||
use App\Handlers\CheckoutHandler;
|
||||
use App\Handlers\HelloWorldHandler;
|
||||
use App\Handlers\OrderCreateHandler;
|
||||
use App\Handlers\ProductsHandler;
|
||||
@@ -11,4 +12,7 @@ return [
|
||||
'order_create' => [OrderCreateHandler::class, 'handle'],
|
||||
|
||||
'categoriesList' => [CategoriesHandler::class, 'index'],
|
||||
|
||||
'checkout' => [CheckoutHandler::class, 'checkout'],
|
||||
'addToCart' => [CheckoutHandler::class, 'addToCart'],
|
||||
];
|
||||
|
||||
7
spa/package-lock.json
generated
7
spa/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"swiper": "^11.2.10",
|
||||
@@ -1364,6 +1365,12 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"swiper": "^11.2.10",
|
||||
|
||||
@@ -1,43 +1,69 @@
|
||||
import {defineStore} from "pinia";
|
||||
import md5 from 'crypto-js/md5';
|
||||
import ftch from "@/utils/ftch.js";
|
||||
|
||||
export const useCartStore = defineStore('cart', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
getProduct(productId) {
|
||||
return this.items.find(item => parseInt(item.productId) === parseInt(productId)) ?? null;
|
||||
getItem(rowId) {
|
||||
return this.items.find(item => item.rowId === rowId) ?? null;
|
||||
},
|
||||
|
||||
hasProduct(productId) {
|
||||
return this.getProduct(productId) !== null;
|
||||
hasItem(rowId) {
|
||||
return this.getItem(rowId) !== null;
|
||||
},
|
||||
|
||||
addProduct(productId, productName, price, quantity = 1, options = []) {
|
||||
this.items.push({
|
||||
async addProduct(productId, productName, price, quantity = 1, options = []) {
|
||||
const rowId = this.generateRowId(productId, options);
|
||||
|
||||
const item = {
|
||||
rowId: rowId,
|
||||
productId: productId,
|
||||
productName: productName,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
options: options,
|
||||
});
|
||||
options: JSON.parse(JSON.stringify(options)), // ← 💥 глубокая копия!
|
||||
};
|
||||
|
||||
this.items.push(item);
|
||||
|
||||
return rowId;
|
||||
},
|
||||
|
||||
removeProduct(productId) {
|
||||
this.items.splice(this.items.indexOf(productId), 1);
|
||||
removeItem(rowId) {
|
||||
this.items.splice(this.items.indexOf(rowId), 1);
|
||||
},
|
||||
|
||||
getQuantity(productId) {
|
||||
if (this.hasProduct(productId)) {
|
||||
return this.getProduct(productId).quantity;
|
||||
getQuantity(rowId) {
|
||||
if (this.hasItem(rowId)) {
|
||||
return this.getItem(rowId).quantity;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
setQuantity(productId, quantity) {
|
||||
this.getProduct(productId).quantity = quantity;
|
||||
}
|
||||
setQuantity(rowId, quantity) {
|
||||
this.getItem(rowId).quantity = quantity;
|
||||
},
|
||||
|
||||
generateRowId(productId, options) {
|
||||
return md5(productId + JSON.stringify(options)).toString();
|
||||
},
|
||||
|
||||
async checkout() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const {data} = await ftch('checkout', null, this.items);
|
||||
this.items = data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -2,8 +2,10 @@ import {$fetch} from "ofetch";
|
||||
|
||||
const BASE_URL = '/';
|
||||
|
||||
export default async function (action, query) {
|
||||
export default async function (action, query = null, json = null) {
|
||||
return await $fetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=${action}`, {
|
||||
method: json ? 'POST' : 'GET',
|
||||
query: query,
|
||||
body: json,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<div class="max-w-3xl mx-auto p-4 space-y-6">
|
||||
<h2 class="text-2xl">Корзина</h2>
|
||||
<h2 class="text-2xl">
|
||||
Корзина
|
||||
<span v-if="cart.isLoading" class="loading loading-spinner loading-md"></span>
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<button class="btn" @click="cart.checkout()" :disabled="cart.isLoading">
|
||||
Checkout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="cart.items.length > 0">
|
||||
|
||||
<div
|
||||
v-for="item in cart.items"
|
||||
:key="item.productId"
|
||||
:key="item.rowId"
|
||||
class="card w-96 bg-base-100 card-sm shadow-sm"
|
||||
>
|
||||
<div class="card-body">
|
||||
@@ -31,8 +39,8 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions justify-between">
|
||||
<Quantity v-model="item.quantity" @update:modelValue="onQuantityUpdate(item, $event)"/>
|
||||
<button class="btn btn-error" @click="remove(item)">
|
||||
<Quantity v-model="item.quantity" @update:modelValue="onQuantityUpdate(item.rowId, $event)"/>
|
||||
<button class="btn btn-error" @click="cart.removeItem(item.rowId)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
@@ -57,14 +65,9 @@ import Quantity from "@/components/Quantity.vue";
|
||||
|
||||
const cart = useCartStore()
|
||||
|
||||
function onQuantityUpdate(item, newQuantity) {
|
||||
function onQuantityUpdate(rowId, newQuantity) {
|
||||
if (newQuantity === 0) {
|
||||
remove(item)
|
||||
cart.removeItem(rowId)
|
||||
}
|
||||
}
|
||||
|
||||
function remove(item) {
|
||||
const index = cart.items.findIndex(i => i.productId === item.productId)
|
||||
if (index !== -1) cart.items.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
@@ -77,8 +77,10 @@ const productId = computed(() => route.params.id);
|
||||
const product = ref({});
|
||||
const cart = useCartStore();
|
||||
|
||||
const rowId = computed(() => cart.generateRowId(productId.value, product.value.options));
|
||||
|
||||
const buttonText = computed(() => {
|
||||
const item = cart.items.find(i => i.productId === productId.value);
|
||||
const item = cart.getItem(rowId.value);
|
||||
return item && item.quantity > 0
|
||||
? `В корзине`
|
||||
: 'Добавить в корзину'
|
||||
@@ -95,21 +97,19 @@ const canAddToCart = computed(() => {
|
||||
&& !item.value;
|
||||
});
|
||||
|
||||
console.log(required);
|
||||
|
||||
return required.length === 0;
|
||||
});
|
||||
|
||||
const isInCartNow = computed(() => {
|
||||
return cart.hasProduct(productId.value);
|
||||
return cart.hasItem(rowId.value);
|
||||
});
|
||||
|
||||
const quantity = computed(() => {
|
||||
return cart.getQuantity(productId.value);
|
||||
return cart.getQuantity(rowId.value);
|
||||
});
|
||||
|
||||
function actionBtnClick() {
|
||||
if (cart.hasProduct(productId.value)) {
|
||||
if (cart.hasItem(rowId.value)) {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
router.push({name: 'cart.show'});
|
||||
} else {
|
||||
@@ -120,10 +120,10 @@ function actionBtnClick() {
|
||||
|
||||
function setQuantity(newQuantity) {
|
||||
if (newQuantity === 0) {
|
||||
cart.removeProduct(productId.value);
|
||||
cart.removeItem(rowId.value);
|
||||
window.Telegram.WebApp.HapticFeedback.notificationOccurred('warning');
|
||||
} else {
|
||||
cart.setQuantity(productId.value, newQuantity);
|
||||
cart.setQuantity(rowId.value, newQuantity);
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user