WIP: cart
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Adapters\OcModelCatalogProductAdapter;
|
use App\Adapters\OcModelCatalogProductAdapter;
|
||||||
use App\ApplicationFactory;
|
use App\ApplicationFactory;
|
||||||
|
use Cart\Cart;
|
||||||
use Cart\Currency;
|
use Cart\Currency;
|
||||||
use Cart\Tax;
|
use Cart\Tax;
|
||||||
use Openguru\OpenCartFramework\ImageTool\ImageTool;
|
use Openguru\OpenCartFramework\ImageTool\ImageTool;
|
||||||
@@ -60,6 +61,14 @@ class Controllerextensiontgshophandle extends Controller
|
|||||||
return new ImageTool(DIR_IMAGE, HTTPS_SERVER);
|
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');
|
$this->load->model('checkout/order');
|
||||||
|
|
||||||
$app->bind('model_checkout_order', function () {
|
$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
|
<?php
|
||||||
|
|
||||||
use App\Handlers\CategoriesHandler;
|
use App\Handlers\CategoriesHandler;
|
||||||
|
use App\Handlers\CheckoutHandler;
|
||||||
use App\Handlers\HelloWorldHandler;
|
use App\Handlers\HelloWorldHandler;
|
||||||
use App\Handlers\OrderCreateHandler;
|
use App\Handlers\OrderCreateHandler;
|
||||||
use App\Handlers\ProductsHandler;
|
use App\Handlers\ProductsHandler;
|
||||||
@@ -11,4 +12,7 @@ return [
|
|||||||
'order_create' => [OrderCreateHandler::class, 'handle'],
|
'order_create' => [OrderCreateHandler::class, 'handle'],
|
||||||
|
|
||||||
'categoriesList' => [CategoriesHandler::class, 'index'],
|
'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": {
|
"dependencies": {
|
||||||
"@heroicons/vue": "^2.2.0",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"ofetch": "^1.4.1",
|
"ofetch": "^1.4.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
@@ -1364,6 +1365,12 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/vue": "^2.2.0",
|
"@heroicons/vue": "^2.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"ofetch": "^1.4.1",
|
"ofetch": "^1.4.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
|
|||||||
@@ -1,43 +1,69 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
|
import md5 from 'crypto-js/md5';
|
||||||
|
import ftch from "@/utils/ftch.js";
|
||||||
|
|
||||||
export const useCartStore = defineStore('cart', {
|
export const useCartStore = defineStore('cart', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
items: [],
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
getProduct(productId) {
|
getItem(rowId) {
|
||||||
return this.items.find(item => parseInt(item.productId) === parseInt(productId)) ?? null;
|
return this.items.find(item => item.rowId === rowId) ?? null;
|
||||||
},
|
},
|
||||||
|
|
||||||
hasProduct(productId) {
|
hasItem(rowId) {
|
||||||
return this.getProduct(productId) !== null;
|
return this.getItem(rowId) !== null;
|
||||||
},
|
},
|
||||||
|
|
||||||
addProduct(productId, productName, price, quantity = 1, options = []) {
|
async addProduct(productId, productName, price, quantity = 1, options = []) {
|
||||||
this.items.push({
|
const rowId = this.generateRowId(productId, options);
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
rowId: rowId,
|
||||||
productId: productId,
|
productId: productId,
|
||||||
productName: productName,
|
productName: productName,
|
||||||
price: price,
|
price: price,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
options: options,
|
options: JSON.parse(JSON.stringify(options)), // ← 💥 глубокая копия!
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.items.push(item);
|
||||||
|
|
||||||
|
return rowId;
|
||||||
},
|
},
|
||||||
|
|
||||||
removeProduct(productId) {
|
removeItem(rowId) {
|
||||||
this.items.splice(this.items.indexOf(productId), 1);
|
this.items.splice(this.items.indexOf(rowId), 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
getQuantity(productId) {
|
getQuantity(rowId) {
|
||||||
if (this.hasProduct(productId)) {
|
if (this.hasItem(rowId)) {
|
||||||
return this.getProduct(productId).quantity;
|
return this.getItem(rowId).quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
setQuantity(productId, quantity) {
|
setQuantity(rowId, quantity) {
|
||||||
this.getProduct(productId).quantity = 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 = '/';
|
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}`, {
|
return await $fetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=${action}`, {
|
||||||
|
method: json ? 'POST' : 'GET',
|
||||||
query: query,
|
query: query,
|
||||||
|
body: json,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-3xl mx-auto p-4 space-y-6">
|
<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-if="cart.items.length > 0">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="item in cart.items"
|
v-for="item in cart.items"
|
||||||
:key="item.productId"
|
:key="item.rowId"
|
||||||
class="card w-96 bg-base-100 card-sm shadow-sm"
|
class="card w-96 bg-base-100 card-sm shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -31,8 +39,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-between">
|
<div class="card-actions justify-between">
|
||||||
<Quantity v-model="item.quantity" @update:modelValue="onQuantityUpdate(item, $event)"/>
|
<Quantity v-model="item.quantity" @update:modelValue="onQuantityUpdate(item.rowId, $event)"/>
|
||||||
<button class="btn btn-error" @click="remove(item)">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -57,14 +65,9 @@ import Quantity from "@/components/Quantity.vue";
|
|||||||
|
|
||||||
const cart = useCartStore()
|
const cart = useCartStore()
|
||||||
|
|
||||||
function onQuantityUpdate(item, newQuantity) {
|
function onQuantityUpdate(rowId, newQuantity) {
|
||||||
if (newQuantity === 0) {
|
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>
|
</script>
|
||||||
@@ -77,8 +77,10 @@ const productId = computed(() => route.params.id);
|
|||||||
const product = ref({});
|
const product = ref({});
|
||||||
const cart = useCartStore();
|
const cart = useCartStore();
|
||||||
|
|
||||||
|
const rowId = computed(() => cart.generateRowId(productId.value, product.value.options));
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
const item = cart.items.find(i => i.productId === productId.value);
|
const item = cart.getItem(rowId.value);
|
||||||
return item && item.quantity > 0
|
return item && item.quantity > 0
|
||||||
? `В корзине`
|
? `В корзине`
|
||||||
: 'Добавить в корзину'
|
: 'Добавить в корзину'
|
||||||
@@ -95,21 +97,19 @@ const canAddToCart = computed(() => {
|
|||||||
&& !item.value;
|
&& !item.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(required);
|
|
||||||
|
|
||||||
return required.length === 0;
|
return required.length === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isInCartNow = computed(() => {
|
const isInCartNow = computed(() => {
|
||||||
return cart.hasProduct(productId.value);
|
return cart.hasItem(rowId.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const quantity = computed(() => {
|
const quantity = computed(() => {
|
||||||
return cart.getQuantity(productId.value);
|
return cart.getQuantity(rowId.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
function actionBtnClick() {
|
function actionBtnClick() {
|
||||||
if (cart.hasProduct(productId.value)) {
|
if (cart.hasItem(rowId.value)) {
|
||||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||||
router.push({name: 'cart.show'});
|
router.push({name: 'cart.show'});
|
||||||
} else {
|
} else {
|
||||||
@@ -120,10 +120,10 @@ function actionBtnClick() {
|
|||||||
|
|
||||||
function setQuantity(newQuantity) {
|
function setQuantity(newQuantity) {
|
||||||
if (newQuantity === 0) {
|
if (newQuantity === 0) {
|
||||||
cart.removeProduct(productId.value);
|
cart.removeItem(rowId.value);
|
||||||
window.Telegram.WebApp.HapticFeedback.notificationOccurred('warning');
|
window.Telegram.WebApp.HapticFeedback.notificationOccurred('warning');
|
||||||
} else {
|
} else {
|
||||||
cart.setQuantity(productId.value, newQuantity);
|
cart.setQuantity(rowId.value, newQuantity);
|
||||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user