WIP: cart

This commit is contained in:
Nikita Kiselev
2025-07-22 00:27:40 +03:00
parent ce2c9e374d
commit 626ee6ecb0
10 changed files with 165 additions and 37 deletions

View File

@@ -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 () {

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Adapters;
class OcCartAdapter
{
}

View File

@@ -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,
]);
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}
},
},
});

View File

@@ -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,
});
};

View File

@@ -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>

View File

@@ -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();
}
}