Compare commits
13 Commits
436f537166
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b60ac3da7 | ||
|
|
8e64ee0c7a | ||
|
|
769720024f | ||
|
|
c06d1d336d | ||
|
|
3d6a71f8e6 | ||
|
|
2e01f58e67 | ||
|
|
2984e3780a | ||
|
|
d1ee94e5c4 | ||
|
|
f857c40ca2 | ||
|
|
bd4edfdade | ||
|
|
e5420edc64 | ||
|
|
55103d3778 | ||
|
|
742ebb4e74 |
@@ -1,29 +1,3 @@
|
|||||||
@use 'typography' as *;
|
|
||||||
@use 'utils' as *;
|
|
||||||
|
|
||||||
// Typography
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
line-height: 100%;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
@include h1('h1');
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@include h2('h2');
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
@include h3('h3');
|
|
||||||
}
|
|
||||||
|
|
||||||
//скроллбар
|
//скроллбар
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
5
assets/scss/mixins.scss
Normal file
5
assets/scss/mixins.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@mixin mobile {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
@use 'utils' as *;
|
|
||||||
|
|
||||||
// Font Face Declarations
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src:
|
|
||||||
url('/fonts/Inter-Regular.woff2') format('woff2'),
|
|
||||||
url('/fonts/Inter-Regular.woff') format('woff');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src:
|
|
||||||
url('/fonts/Inter-Medium.woff2') format('woff2'),
|
|
||||||
url('/fonts/Inter-Medium.woff') format('woff');
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
src:
|
|
||||||
url('/fonts/Inter-SemiBold.woff2') format('woff2'),
|
|
||||||
url('/fonts/Inter-SemiBold.woff') format('woff');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font Variables
|
|
||||||
$font-family-base:
|
|
||||||
'Inter',
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
sans-serif;
|
|
||||||
|
|
||||||
// Font Weights
|
|
||||||
$font-weight-regular: 400;
|
|
||||||
$font-weight-medium: 500;
|
|
||||||
$font-weight-semibold: 600;
|
|
||||||
|
|
||||||
// Custom type
|
|
||||||
@mixin font($size, $weight, $lineHeight, $namespace: null, $onlyVars: false) {
|
|
||||||
@if ($namespace) {
|
|
||||||
@if ($onlyVars) {
|
|
||||||
--#{$namespace}-font-size: #{$size};
|
|
||||||
--#{$namespace}-font-weight: #{$weight};
|
|
||||||
--#{$namespace}-line-height: #{$lineHeight};
|
|
||||||
} @else {
|
|
||||||
font-size: var(--#{$namespace}-font-size, $size);
|
|
||||||
font-weight: var(--#{$namespace}-font-weight, $weight);
|
|
||||||
line-height: var(--#{$namespace}-line-height, $lineHeight);
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
font-size: $size;
|
|
||||||
font-weight: $weight;
|
|
||||||
line-height: $lineHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headline */
|
|
||||||
@mixin h1($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(32px, $font-weight-semibold, 100%, $namespace, $onlyVars);
|
|
||||||
@include mobile {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@mixin h2($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(24px, $font-weight-semibold, 100%, $namespace, $onlyVars);
|
|
||||||
@include mobile {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@mixin h3($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(16px, $font-weight-semibold, 140%, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text */
|
|
||||||
// 16 medium-(medium/bold)
|
|
||||||
@mixin txt-m($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(16px, $font-weight-regular, 100%, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-m-m($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(16px, $font-weight-medium, 100%, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-m-b($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(16px, $font-weight-semibold, 100%, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 14 regular-(medium/bold)
|
|
||||||
@mixin txt-r($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(14px, $font-weight-regular, 20px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-r-m($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(14px, $font-weight-medium, 20px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-r-b($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(14px, $font-weight-semibold, 20px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12 text small-(medium/bold)
|
|
||||||
@mixin txt-s($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(12px, $font-weight-regular, 18px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-s-m($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(12px, $font-weight-medium, 18px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@mixin txt-s-b($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(12px, $font-weight-semibold, 18px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10 text-tiny
|
|
||||||
@mixin txt-t($namespace: null, $onlyVars: false) {
|
|
||||||
@include font(10px, $font-weight-medium, 15px, $namespace, $onlyVars);
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
@use 'sass:color';
|
|
||||||
|
|
||||||
@mixin mobile {
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$indents: 0 2 4 5 6 8 10 12 15 16 18 20 24 25 28 30 32 36 40 48 50 52 60 64;
|
|
||||||
|
|
||||||
@each $i in $indents {
|
|
||||||
.m#{$i} {
|
|
||||||
margin: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx#{$i} {
|
|
||||||
margin-left: #{$i}px;
|
|
||||||
margin-right: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my#{$i} {
|
|
||||||
margin-top: #{$i}px;
|
|
||||||
margin-bottom: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt#{$i} {
|
|
||||||
margin-top: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb#{$i} {
|
|
||||||
margin-bottom: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml#{$i} {
|
|
||||||
margin-left: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mr#{$i} {
|
|
||||||
margin-right: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p#{$i} {
|
|
||||||
padding: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.px#{$i} {
|
|
||||||
padding-left: #{$i}px;
|
|
||||||
padding-right: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.py#{$i} {
|
|
||||||
padding-top: #{$i}px;
|
|
||||||
padding-bottom: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pt#{$i} {
|
|
||||||
padding-top: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pb#{$i} {
|
|
||||||
padding-bottom: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl#{$i} {
|
|
||||||
padding-left: #{$i}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pr#{$i} {
|
|
||||||
padding-right: #{$i}px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mla {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mra {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-auto {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@each $align in ('left', 'right', 'center') {
|
|
||||||
.text-align-#{$align} {
|
|
||||||
text-align: #{$align};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-25 { width: 25% !important; }
|
|
||||||
.w-50 { width: 50% !important; }
|
|
||||||
.w-75 { width: 75% !important; }
|
|
||||||
.w-100 { width: 100% !important; }
|
|
||||||
|
|
||||||
.d-none { display: none !important; }
|
|
||||||
.d-inline { display: inline !important; }
|
|
||||||
.d-inline-block { display: inline-block !important; }
|
|
||||||
.d-block { display: block !important; }
|
|
||||||
.d-table { display: table !important; }
|
|
||||||
.d-table-row { display: table-row !important; }
|
|
||||||
.d-table-cell { display: table-cell !important; }
|
|
||||||
.d-flex { display: flex !important; }
|
|
||||||
.d-inline-flex { display: inline-flex !important; }
|
|
||||||
|
|
||||||
.flex-row { flex-direction: row !important; }
|
|
||||||
.flex-column { flex-direction: column !important; }
|
|
||||||
.flex-row-reverse { flex-direction: row-reverse !important; }
|
|
||||||
.flex-column-reverse { flex-direction: column-reverse !important; }
|
|
||||||
|
|
||||||
.flex-wrap { flex-wrap: wrap !important; }
|
|
||||||
.flex-nowrap { flex-wrap: nowrap !important; }
|
|
||||||
.flex-wrap-reverse { flex-wrap: wrap-reverse !important; }
|
|
||||||
|
|
||||||
.justify-content-start { justify-content: flex-start !important; }
|
|
||||||
.justify-content-end { justify-content: flex-end !important; }
|
|
||||||
.justify-content-center { justify-content: center !important; }
|
|
||||||
.justify-content-between { justify-content: space-between !important; }
|
|
||||||
.justify-content-around { justify-content: space-around !important; }
|
|
||||||
|
|
||||||
.align-items-start { align-items: flex-start !important; }
|
|
||||||
.align-items-end { align-items: flex-end !important; }
|
|
||||||
.align-items-center { align-items: center !important; }
|
|
||||||
.align-items-baseline { align-items: baseline !important; }
|
|
||||||
.align-items-stretch { align-items: stretch !important; }
|
|
||||||
|
|
||||||
.align-content-start { align-content: flex-start !important; }
|
|
||||||
.align-content-end { align-content: flex-end !important; }
|
|
||||||
.align-content-center { align-content: center !important; }
|
|
||||||
.align-content-between { align-content: space-between !important; }
|
|
||||||
.align-content-around { align-content: space-around !important; }
|
|
||||||
.align-content-stretch { align-content: stretch !important; }
|
|
||||||
|
|
||||||
.align-self-auto { align-self: auto !important; }
|
|
||||||
.align-self-start { align-self: flex-start !important; }
|
|
||||||
.align-self-end { align-self: flex-end !important; }
|
|
||||||
.align-self-center { align-self: center !important; }
|
|
||||||
.align-self-baseline { align-self: baseline !important; }
|
|
||||||
.align-self-stretch { align-self: stretch !important; }
|
|
||||||
|
|
||||||
.text-align-center { text-align: center !important; }
|
|
||||||
.text-align-left { text-align: left !important; }
|
|
||||||
.text-align-right { text-align: right !important; }
|
|
||||||
|
|
||||||
.cursor-pointer { cursor: pointer !important; }
|
|
||||||
.cursor-not-allowed { cursor: not-allowed !important; }
|
|
||||||
.cursor-progress { cursor: progress !important; }
|
|
||||||
@@ -11,8 +11,13 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="delivery-info__item">
|
<li class="delivery-info__item">
|
||||||
<Icon class="delivery-info__icon" name="lucide:shirt" size="xl" />
|
<Icon
|
||||||
<span class="delivery-info__text">{{ checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'с примеркой' : 'без примерки' }}</span>
|
class="delivery-info__icon"
|
||||||
|
:name="checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'lucide:shirt' : 'i-lucide-ban'" size="xl"
|
||||||
|
/>
|
||||||
|
<span class="delivery-info__text">
|
||||||
|
{{ checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'с примеркой' : 'без примерки' }}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="delivery-info__item">
|
<li class="delivery-info__item">
|
||||||
|
|||||||
96
components/DeliverySearch.vue
Normal file
96
components/DeliverySearch.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="delivery__search">
|
||||||
|
<UInput
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Выберите пункт выдачи"
|
||||||
|
:ui="{ trailing: 'pe-1' }"
|
||||||
|
>
|
||||||
|
<template v-if="searchTerm?.length" #trailing>
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
icon="i-lucide-circle-x"
|
||||||
|
aria-label="Clear input"
|
||||||
|
@click="searchTerm = ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
|
||||||
|
<div class="pickup-point-card__items">
|
||||||
|
<div
|
||||||
|
v-for="pickupPoint in filteredPoints"
|
||||||
|
:key="pickupPoint.id"
|
||||||
|
class="pickup-point-card"
|
||||||
|
@click="onSelectPoint(pickupPoint)"
|
||||||
|
>
|
||||||
|
<div class="pickup-point-card__content">
|
||||||
|
<h3>Yandex</h3>
|
||||||
|
<p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p>
|
||||||
|
<h3>Как определить стоимость ?</h3>
|
||||||
|
</div>
|
||||||
|
<Icon class="pickup-point-card__action" name="lucide:chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PickupPoint } from '#shared/yandex_pvz'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { defineEmits } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{ filteredPoints: PickupPoint[] }>()
|
||||||
|
const emit = defineEmits(['update:checkout-pickup-point'])
|
||||||
|
|
||||||
|
const checkoutPickupPoint = defineModel('checkoutPickupPoint', {
|
||||||
|
type: Object as PropType<PickupPoint>,
|
||||||
|
default: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchTerm = defineModel('searchTerm', { type: String, default: '' })
|
||||||
|
|
||||||
|
const onSelectPoint = (pickupPoint: PickupPoint) => {
|
||||||
|
checkoutPickupPoint.value = pickupPoint
|
||||||
|
emit('update:checkout-pickup-point', pickupPoint)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.pickup-point-card {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&__items {
|
||||||
|
height: calc(100dvh - 54px - 40px - 24px);
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
height: calc(100dvh - 72px - 40px - 32px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
components/MapControlFitting.vue
Normal file
30
components/MapControlFitting.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<UTabs v-model="fitting" :content="false" :items="tabs" size="sm" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TabsItem } from '@nuxt/ui'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { IPvzMapFittingTabs } from '#shared/types'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const fitting = defineModel('fitting', { type: Object as PropType<IPvzMapFittingTabs>, default: () => IPvzMapFittingTabs.ALL })
|
||||||
|
|
||||||
|
const tabs = computed<TabsItem[]>(() => [
|
||||||
|
{
|
||||||
|
value: IPvzMapFittingTabs.ALL,
|
||||||
|
label: fitting.value === IPvzMapFittingTabs.ALL ? 'все' : '',
|
||||||
|
icon: 'i-lucide-globe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IPvzMapFittingTabs.ALLOW,
|
||||||
|
label: fitting.value === IPvzMapFittingTabs.ALLOW ? 'с примеркой' : '',
|
||||||
|
icon: 'i-lucide-shirt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IPvzMapFittingTabs.FORBID,
|
||||||
|
label: fitting.value === IPvzMapFittingTabs.FORBID ? 'без примерки' : '',
|
||||||
|
icon: 'i-lucide-ban',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
27
components/MapControlTabs.vue
Normal file
27
components/MapControlTabs.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<UTabs v-model="activeTab" :content="false" :items="tabs" size="sm" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TabsItem } from '@nuxt/ui'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { IPvzMapTabs } from '#shared/types'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const activeTab = defineModel('activeTab', { type: Object as PropType<IPvzMapTabs>, default: () => IPvzMapTabs.MAP })
|
||||||
|
|
||||||
|
const tabs = computed<TabsItem[]>(() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: IPvzMapTabs.MAP,
|
||||||
|
label: activeTab.value === IPvzMapTabs.MAP ? 'Карта' : '',
|
||||||
|
icon: 'lucide:map-pin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IPvzMapTabs.LIST,
|
||||||
|
label: activeTab.value === IPvzMapTabs.LIST ? 'Список' : '',
|
||||||
|
icon: 'i-lucide-list',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<UPageCard>
|
<teleport
|
||||||
|
v-if="isMobile && isSummary" to="footer"
|
||||||
|
>
|
||||||
|
<UPageCard
|
||||||
|
class="pay-block--mobile"
|
||||||
|
variant="ghost"
|
||||||
|
:ui="{
|
||||||
|
body: 'w-full',
|
||||||
|
footer: 'hidden',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div>
|
||||||
|
<div v-if="cartSum">
|
||||||
|
<ProductPrice :is-headline="false" :price="cartSum" />
|
||||||
|
</div>
|
||||||
|
<div>{{ `${cart?.line_items?.length} шт` }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
class="justify-center text-center w-full ml10"
|
||||||
|
size="xl"
|
||||||
|
:label="pay ? 'оформить заказ' : 'перейти к оформлению'"
|
||||||
|
@click="pay ? createOrder() : router.push(`/checkout/delivery`)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPageCard>
|
||||||
|
</teleport>
|
||||||
|
|
||||||
|
<UPageCard v-else>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div>товары {{ `(${cart?.line_items?.length} шт)` }}</div>
|
<div>товары {{ `(${cart?.line_items?.length} шт)` }}</div>
|
||||||
<div v-if="cartSum">
|
<div v-if="cartSum">
|
||||||
@@ -21,27 +52,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IBspb } from '~/server/shared/types/bspb'
|
import type { BsbpCreateResponse } from '#shared/bsbp_create'
|
||||||
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
pay: {
|
pay: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isSummary: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)', {
|
||||||
|
ssrWidth: 768,
|
||||||
|
})
|
||||||
const { cart, cartSum } = useCart()
|
const { cart, cartSum } = useCart()
|
||||||
|
|
||||||
const createOrder = async () => {
|
const createOrder = async () => {
|
||||||
const { data } = await useFetch<IBspb>('/api/bspb', {
|
const { data } = await useFetch<BsbpCreateResponse>('/api/bsbp_create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
order: {
|
order: {
|
||||||
typeRid: 'Purchase',
|
typeRid: 'Purchase',
|
||||||
amount: cartSum,
|
amount: cartSum,
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
hppRedirectUrl: `${import.meta.env.VITE_BASE_URL}/cart`,
|
hppRedirectUrl: `${process.env.VITE_BASE_URL}/cart`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -50,3 +89,15 @@ const createOrder = async () => {
|
|||||||
window.open(redirectUrl, '_blank')
|
window.open(redirectUrl, '_blank')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.pay-block {
|
||||||
|
&--mobile {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ function addToCartBtn() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.product-description {
|
.product-description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 30px 30px 0;
|
padding: 30px 30px 0;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
:key="pickupPoint.id"
|
:key="pickupPoint.id"
|
||||||
position="top-center left-center"
|
position="top-center left-center"
|
||||||
:settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }"
|
:settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }"
|
||||||
@click="$emit('update:modelValue', pickupPoint)"
|
@click="onSelectPoint(pickupPoint)"
|
||||||
>
|
>
|
||||||
<div class="marker">
|
<div class="marker">
|
||||||
<Icon name="i-lucide-map-pin" class="marker__icon" />
|
<Icon name="i-lucide-map-pin" class="marker__icon" />
|
||||||
@@ -37,41 +37,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</YandexMapClusterer>
|
</YandexMapClusterer>
|
||||||
|
|
||||||
|
<YandexMapControls :settings="{ position: isMobile ? 'bottom left' : 'top left', orientation: 'vertical' }">
|
||||||
|
<YandexMapControl>
|
||||||
|
<MapControlFitting v-model:fitting="fitting" />
|
||||||
|
</YandexMapControl>
|
||||||
|
</YandexMapControls>
|
||||||
|
|
||||||
|
<YandexMapControls v-if="isMobile" :settings="{ position: 'bottom right', orientation: 'vertical' }">
|
||||||
|
<YandexMapControl>
|
||||||
|
<MapControlTabs v-model:active-tab="activeTab" />
|
||||||
|
</YandexMapControl>
|
||||||
|
</YandexMapControls>
|
||||||
</YandexMap>
|
</YandexMap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { PickupPoint } from '#shared/yandex_pvz'
|
||||||
import type { LngLatBounds, YMap } from '@yandex/ymaps3-types'
|
import type { LngLatBounds, YMap } from '@yandex/ymaps3-types'
|
||||||
import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap'
|
import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap'
|
||||||
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer'
|
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer'
|
||||||
import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
|
import type { PropType } from 'vue'
|
||||||
import { useGeolocation } from '@vueuse/core'
|
import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
|
||||||
import { computed, shallowRef } from 'vue'
|
import { useGeolocation, useMediaQuery } from '@vueuse/core'
|
||||||
|
import { computed, defineEmits, shallowRef } from 'vue'
|
||||||
import {
|
import {
|
||||||
YandexMap,
|
YandexMap,
|
||||||
YandexMapClusterer,
|
YandexMapClusterer,
|
||||||
|
YandexMapControl,
|
||||||
|
YandexMapControls,
|
||||||
YandexMapDefaultFeaturesLayer,
|
YandexMapDefaultFeaturesLayer,
|
||||||
YandexMapDefaultSchemeLayer,
|
YandexMapDefaultSchemeLayer,
|
||||||
YandexMapMarker,
|
YandexMapMarker,
|
||||||
} from 'vue-yandex-maps'
|
} from 'vue-yandex-maps'
|
||||||
|
import MapControlFitting from '~/components/MapControlFitting.vue'
|
||||||
|
import MapControlTabs from '~/components/MapControlTabs.vue'
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: PickupPoint, pickupPoints: PickupPoint[] }>()
|
defineProps<{ pickupPoints: PickupPoint[] }>()
|
||||||
|
const emit = defineEmits(['update:checkout-pickup-point'])
|
||||||
|
|
||||||
defineEmits<{
|
const checkoutPickupPoint = defineModel('checkoutPickupPoint', { type: Object as PropType<PickupPoint>, default: () => undefined })
|
||||||
(e: 'update:modelValue', value: PickupPoint | undefined): void
|
const activeTab = defineModel('activeTab', { type: Object as PropType<IPvzMapTabs>, default: () => IPvzMapTabs.MAP })
|
||||||
}>()
|
const fitting = defineModel('fitting', { type: Object as PropType<IPvzMapFittingTabs>, default: () => IPvzMapFittingTabs.ALL })
|
||||||
|
|
||||||
const { coords } = useGeolocation()
|
const { coords } = useGeolocation()
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)')
|
||||||
const clusterer = shallowRef<YMapClusterer | null>(null)
|
const clusterer = shallowRef<YMapClusterer | null>(null)
|
||||||
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]])
|
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]])
|
||||||
const map = shallowRef<null | YMap>(null)
|
const map = shallowRef<null | YMap>(null)
|
||||||
|
|
||||||
const hasCoords = computed(() => coords.value?.latitude !== Infinity && coords.value?.longitude !== Infinity)
|
const hasCoords = computed(() => coords.value.latitude !== Infinity && coords.value.longitude !== Infinity)
|
||||||
|
|
||||||
const location = ref<YMapLocationRequest>({
|
const location = ref<YMapLocationRequest>({
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onSelectPoint = (pickupPoint: PickupPoint) => {
|
||||||
|
checkoutPickupPoint.value = pickupPoint
|
||||||
|
emit('update:checkout-pickup-point', pickupPoint)
|
||||||
|
}
|
||||||
|
|
||||||
watch(coords, (newCoords) => {
|
watch(coords, (newCoords) => {
|
||||||
if (newCoords && hasCoords.value) {
|
if (newCoords && hasCoords.value) {
|
||||||
location.value = {
|
location.value = {
|
||||||
@@ -82,7 +107,10 @@ watch(coords, (newCoords) => {
|
|||||||
}
|
}
|
||||||
}, { once: true })
|
}, { once: true })
|
||||||
|
|
||||||
watch(() => props.modelValue, (newPickupPoint) => {
|
watch(() => checkoutPickupPoint.value, (newPickupPoint) => {
|
||||||
|
if (!newPickupPoint)
|
||||||
|
return
|
||||||
|
|
||||||
location.value = {
|
location.value = {
|
||||||
center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude],
|
center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude],
|
||||||
zoom: 18,
|
zoom: 18,
|
||||||
@@ -132,6 +160,11 @@ watch(() => props.modelValue, (newPickupPoint) => {
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover &__icon {
|
||||||
|
color: var(--ui-primary);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cluster {
|
.cluster {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export const useCart = () => {
|
|||||||
cartItem?.variation_id === item?.variation_id), 1)
|
cartItem?.variation_id === item?.variation_id), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cartRemoveAllItems = () => {
|
||||||
|
cart.value = { line_items: [] }
|
||||||
|
}
|
||||||
|
|
||||||
const cartProducts = computed(() => cart?.value?.line_items?.map((line_item) => {
|
const cartProducts = computed(() => cart?.value?.line_items?.map((line_item) => {
|
||||||
const { productsData } = useProduct(line_item?.variation_id.toString())
|
const { productsData } = useProduct(line_item?.variation_id.toString())
|
||||||
return productsData
|
return productsData
|
||||||
@@ -37,6 +41,7 @@ export const useCart = () => {
|
|||||||
cart,
|
cart,
|
||||||
cartAddItem,
|
cartAddItem,
|
||||||
cartRemoveItem,
|
cartRemoveItem,
|
||||||
|
cartRemoveAllItems,
|
||||||
|
|
||||||
cartProducts,
|
cartProducts,
|
||||||
cartSum,
|
cartSum,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
|
import type { PickupPoint } from '#shared/yandex_pvz'
|
||||||
import { createSharedComposable, useStorage } from '@vueuse/core'
|
import { createSharedComposable, useMediaQuery, useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
export const useCheckout = createSharedComposable(() => {
|
export const useCheckout = createSharedComposable(() => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)')
|
||||||
|
|
||||||
const checkoutPickupPoint = useStorage<PickupPoint | undefined>(
|
const checkoutPickupPoint = useStorage<PickupPoint | undefined>(
|
||||||
'checkout-pickupPoint',
|
'checkout-pickupPoint',
|
||||||
@@ -32,23 +33,26 @@ export const useCheckout = createSharedComposable(() => {
|
|||||||
const checkoutSteps = [
|
const checkoutSteps = [
|
||||||
{
|
{
|
||||||
step: 1,
|
step: 1,
|
||||||
title: 'delivery',
|
title: isMobile.value ? 'mobileDelivery' : 'delivery',
|
||||||
|
route: 'delivery',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 2,
|
step: 2,
|
||||||
title: 'contacts',
|
title: isMobile.value ? 'mobileContacts' : 'contacts',
|
||||||
|
route: 'contacts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 3,
|
step: 3,
|
||||||
title: 'summary',
|
title: isMobile.value ? 'mobileSummary' : 'summary',
|
||||||
|
route: 'summary',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const currentCheckoutStep
|
const currentCheckoutStep
|
||||||
= ref(checkoutSteps.find(value => value.title === route.path.split('/').pop()) || checkoutSteps[0])
|
= ref(checkoutSteps.find(value => value.route === route.path.split('/').pop()) || checkoutSteps[0])
|
||||||
|
|
||||||
function previewStep() {
|
function previewStep() {
|
||||||
if (isPickupPointSelected.value) {
|
if (isPickupPointSelected.value && !isMobile.value) {
|
||||||
setCheckoutPickupPoint(undefined)
|
setCheckoutPickupPoint(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,7 @@ export const useCheckout = createSharedComposable(() => {
|
|||||||
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
||||||
if (findIndex !== 0) {
|
if (findIndex !== 0) {
|
||||||
currentCheckoutStep.value = checkoutSteps[findIndex - 1]
|
currentCheckoutStep.value = checkoutSteps[findIndex - 1]
|
||||||
router.push(`/checkout/${currentCheckoutStep?.value.title}`)
|
router.push(`/checkout/${currentCheckoutStep?.value.route}`)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
router.push(`/cart`)
|
router.push(`/cart`)
|
||||||
@@ -67,12 +71,12 @@ export const useCheckout = createSharedComposable(() => {
|
|||||||
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
||||||
if (findIndex + 1 !== checkoutSteps.length) {
|
if (findIndex + 1 !== checkoutSteps.length) {
|
||||||
currentCheckoutStep.value = checkoutSteps[findIndex + 1]
|
currentCheckoutStep.value = checkoutSteps[findIndex + 1]
|
||||||
router.push(`/checkout/${currentCheckoutStep?.value.title}`)
|
router.push(`/checkout/${currentCheckoutStep?.value.route}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStep(pathName: string) {
|
function setStep(pathName: string) {
|
||||||
currentCheckoutStep.value = checkoutSteps.find(value => value.title === pathName) || checkoutSteps[0]
|
currentCheckoutStep.value = checkoutSteps.find(value => value.route === pathName) || checkoutSteps[0]
|
||||||
router.push(`/checkout/${pathName}`)
|
router.push(`/checkout/${pathName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
},
|
},
|
||||||
"checkoutSteps": {
|
"checkoutSteps": {
|
||||||
"delivery": "Выберите адрес получения заказа",
|
"delivery": "Выберите адрес получения заказа",
|
||||||
|
"mobileDelivery": "доставка",
|
||||||
"contacts": "Введите данные получателя",
|
"contacts": "Введите данные получателя",
|
||||||
"summary": "Подтвердите заказ"
|
"mobileContacts": "контакты",
|
||||||
|
"summary": "Подтвердите заказ",
|
||||||
|
"mobileSummary": "заказ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,19 +12,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main" :style="showFooter ? 'margin-bottom: 54px' : 'margin-bottom: 0'">
|
||||||
<UContainer class="container">
|
<UContainer class="container">
|
||||||
<slot />
|
<slot />
|
||||||
</UContainer>
|
</UContainer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer" :class="{ 'footer--hidden': !showFooter }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCheckout } from '../composables/useCheckout'
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
|
import { useCheckout } from '~/composables/useCheckout'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)', {
|
||||||
|
ssrWidth: 768,
|
||||||
|
})
|
||||||
|
const showFooter = computed(() => route.path !== '/checkout/delivery' && isMobile.value)
|
||||||
|
|
||||||
const { previewStep, currentCheckoutStep, checkoutSteps } = useCheckout()
|
const { previewStep, currentCheckoutStep, checkoutSteps } = useCheckout()
|
||||||
const { t } = useI18n()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -63,8 +72,27 @@ const { t } = useI18n()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 70px;
|
||||||
|
background-color: #0f172b;
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: 54px;
|
margin-top: 54px;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
|
||||||
|
&__hide-footer {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,15 +32,20 @@
|
|||||||
</UContainer>
|
</UContainer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- <footer class="footer"> -->
|
<footer class="footer" :class="{ 'footer--hidden': !(route.path === '/cart' && isMobile) }" />
|
||||||
<!-- Footer -->
|
|
||||||
<!-- </footer> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { cart } = useCart()
|
const { cart } = useCart()
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)', {
|
||||||
|
ssrWidth: 768,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -106,12 +111,22 @@ const { cart } = useCart()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.footer {
|
||||||
flex: 1;
|
position: fixed;
|
||||||
margin-top: 54px;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 70px;
|
||||||
|
background-color: #0f172b;
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.main {
|
||||||
margin-top: auto;
|
flex: 1;
|
||||||
|
margin-block: 54px 64px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
import { createResolver } from '@nuxt/kit'
|
||||||
|
|
||||||
|
const { resolve } = createResolver(import.meta.url)
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: true,
|
ssr: true,
|
||||||
compatibilityDate: '2025-05-15',
|
compatibilityDate: '2025-05-15',
|
||||||
@@ -18,7 +22,7 @@ export default defineNuxtConfig({
|
|||||||
'vue-yandex-maps/nuxt',
|
'vue-yandex-maps/nuxt',
|
||||||
],
|
],
|
||||||
yandexMaps: {
|
yandexMaps: {
|
||||||
apikey: process.env.VITE_YANDEX_MAPS_TOKEN,
|
apikey: '13f4c06b-cb7e-4eeb-81f1-af52f12587b2', // неправильно, через .env ошибка в проде РАЗОБРАТСЯ
|
||||||
},
|
},
|
||||||
css: ['~/assets/css/main.css', '~/assets/scss/main.scss'],
|
css: ['~/assets/css/main.css', '~/assets/scss/main.scss'],
|
||||||
i18n: {
|
i18n: {
|
||||||
@@ -40,4 +44,16 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
vite: {
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `@use "${resolve('./assets/scss/mixins')}" as *;`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
provider: 'google',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cart">
|
<div class="cart">
|
||||||
<div class="cart__items">
|
<div v-if="cart.line_items.length > 0" class="cart__items">
|
||||||
<div
|
<div
|
||||||
v-for="cartItem in cart?.line_items"
|
v-for="cartItem in cart?.line_items"
|
||||||
:key="cartItem.variation_id"
|
:key="cartItem.variation_id"
|
||||||
@@ -9,23 +9,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PayBlock />
|
<div v-else>
|
||||||
|
Корзина пока что пуста
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<PayBlock :is-summary="true" />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { WooOrderCreateResponse } from '#shared/woo_orders_create'
|
||||||
import { useCart } from '~/composables'
|
import { useCart } from '~/composables'
|
||||||
import PayBlock from '../components/PayBlock.vue'
|
import PayBlock from '../components/PayBlock.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { cart } = useCart()
|
const { cart, cartRemoveAllItems } = useCart()
|
||||||
const { checkoutContacts, checkoutPickupPoint } = useCheckout()
|
const { checkoutContacts, checkoutPickupPoint } = useCheckout()
|
||||||
|
|
||||||
|
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
// Зарпос должен быть в админке bsbp, ждём доступов. Текущее решение просто создает order в WooCommerce
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!route?.query?.ID)
|
if (!route?.query?.ID || cart.value.line_items.length === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
await $fetch('/api/create', {
|
await wait(5000)
|
||||||
|
await useFetch<WooOrderCreateResponse>('/api/woo_orders_create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
payment_method: 'bacs',
|
payment_method: 'bacs',
|
||||||
@@ -46,12 +57,12 @@ onMounted(async () => {
|
|||||||
status: 'processing',
|
status: 'processing',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cartRemoveAllItems()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.cart {
|
.cart {
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
@@ -63,6 +74,7 @@ onMounted(async () => {
|
|||||||
@include mobile {
|
@include mobile {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items {
|
&__items {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="contacts" @submit.prevent="onSubmit">
|
<form v-if="checkoutContacts" class="contacts" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<UInput
|
<UInput
|
||||||
id="name"
|
id="name"
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useCheckout } from '../../composables/useCheckout'
|
import { useCheckout } from '~/composables/useCheckout'
|
||||||
|
|
||||||
const { checkoutContacts, setCheckoutContacts, nextStep } = useCheckout()
|
const { checkoutContacts, setCheckoutContacts, nextStep } = useCheckout()
|
||||||
|
|
||||||
@@ -205,5 +205,9 @@ definePageMeta({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,66 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="coords" class="delivery">
|
<div v-if="coords" class="delivery">
|
||||||
<div class="delivery__sidebar">
|
<!-- Desktop -->
|
||||||
|
<div v-if="!isMobile" class="delivery__sidebar">
|
||||||
<DeliveryInfo v-if="isPickupPointSelected" />
|
<DeliveryInfo v-if="isPickupPointSelected" />
|
||||||
|
|
||||||
<div v-else class="delivery__search">
|
<DeliverySearch
|
||||||
<UInput
|
v-else
|
||||||
v-model="searchTerm"
|
v-model:checkout-pickup-point="checkoutPickupPoint"
|
||||||
size="xl"
|
v-model:search-term="searchTerm"
|
||||||
class="w-full"
|
:filtered-points="filteredPoints"
|
||||||
placeholder="Выберите пункт выдачи"
|
@update:checkout-pickup-point="updatePoint()"
|
||||||
:ui="{ trailing: 'pe-1' }"
|
/>
|
||||||
>
|
|
||||||
<template v-if="searchTerm?.length" #trailing>
|
|
||||||
<UButton
|
|
||||||
color="neutral"
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
icon="i-lucide-circle-x"
|
|
||||||
aria-label="Clear input"
|
|
||||||
@click="searchTerm = ''"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</UInput>
|
|
||||||
|
|
||||||
<div class="delivery__items">
|
|
||||||
<div
|
|
||||||
v-for="pickupPoint in filteredPoints"
|
|
||||||
:key="pickupPoint.id"
|
|
||||||
class="pickup-point-card"
|
|
||||||
@click="checkoutPickupPoint = pickupPoint"
|
|
||||||
>
|
|
||||||
<div class="pickup-point-card__content">
|
|
||||||
<h3>Yandex</h3>
|
|
||||||
<p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p>
|
|
||||||
<h3>с day month бесплатно</h3>
|
|
||||||
</div>
|
|
||||||
<Icon class="pickup-point-card__action" name="lucide:chevron-right" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile -->
|
||||||
|
<UDrawer
|
||||||
|
v-if="isMobile"
|
||||||
|
v-model:open="open"
|
||||||
|
fixed
|
||||||
|
:ui="{
|
||||||
|
content: 'fixed bg-default ring ring-default flex focus:outline-none overflow-hidden',
|
||||||
|
container: 'w-full flex flex-col gap-4 p-4 overflow-hidden',
|
||||||
|
body: 'flex-1 overflow-y-auto',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="px-4 pb-4">
|
||||||
|
<DeliveryInfo v-if="isPickupPointSelected" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
|
||||||
|
<UModal
|
||||||
|
v-model:open="openDeliverySearch"
|
||||||
|
fullscreen
|
||||||
|
:dismissible="false"
|
||||||
|
:ui="{
|
||||||
|
body: 'overflow-hidden',
|
||||||
|
overlay: 'bg-black/50',
|
||||||
|
header: 'hidden',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<DeliverySearch
|
||||||
|
v-if="activeTab === IPvzMapTabs.LIST"
|
||||||
|
v-model:checkout-pickup-point="checkoutPickupPoint"
|
||||||
|
v-model:search-term="searchTerm"
|
||||||
|
:filtered-points="filteredPoints"
|
||||||
|
@update:checkout-pickup-point="updatePoint()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UDrawer
|
||||||
|
v-if="isMobile"
|
||||||
|
v-model:open="open"
|
||||||
|
fixed
|
||||||
|
:ui="{
|
||||||
|
content: 'fixed bg-default ring ring-default flex focus:outline-none overflow-hidden',
|
||||||
|
container: 'w-full flex flex-col gap-4 p-4 overflow-hidden',
|
||||||
|
body: 'flex-1 overflow-y-auto',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="px-4 pb-4">
|
||||||
|
<DeliveryInfo v-if="isPickupPointSelected" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="d-flex flex-row w-full justify-between">
|
||||||
|
<MapControlFitting v-model:fitting="fitting" />
|
||||||
|
|
||||||
|
<MapControlTabs v-model:active-tab="activeTab" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
<!-- Desktop Mobile -->
|
||||||
<PvzMap
|
<PvzMap
|
||||||
v-model="checkoutPickupPoint"
|
v-model:checkout-pickup-point="checkoutPickupPoint"
|
||||||
|
v-model:active-tab="activeTab"
|
||||||
|
v-model:fitting="fitting"
|
||||||
:pickup-points="filteredPoints"
|
:pickup-points="filteredPoints"
|
||||||
@update:model-value="setCheckoutPickupPoint(checkoutPickupPoint)"
|
@update:checkout-pickup-point="updatePoint()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PickupPoint, YandexPvzResponse } from '~/server/shared/types/yandex_pvz'
|
import type { YandexLocationDetectResponse } from '#shared/yandex_location_detect'
|
||||||
import { useGeolocation } from '@vueuse/core'
|
import type { YandexPvzPoint, YandexPvzResponse } from '#shared/yandex_pvz'
|
||||||
|
import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
|
||||||
|
import { useGeolocation, useMediaQuery } from '@vueuse/core'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import DeliveryInfo from '~/components/DeliveryInfo.vue'
|
import DeliveryInfo from '~/components/DeliveryInfo.vue'
|
||||||
|
import DeliverySearch from '~/components/DeliverySearch.vue'
|
||||||
import PvzMap from '~/components/PvzMap.vue'
|
import PvzMap from '~/components/PvzMap.vue'
|
||||||
import { useCheckout } from '~/composables/useCheckout'
|
import { useCheckout } from '~/composables/useCheckout'
|
||||||
|
|
||||||
const { setCheckoutPickupPoint, isPickupPointSelected, checkoutPickupPoint } = useCheckout()
|
const { setCheckoutPickupPoint, isPickupPointSelected, checkoutPickupPoint } = useCheckout()
|
||||||
const { coords } = useGeolocation()
|
const { coords } = useGeolocation()
|
||||||
|
const open = ref(false)
|
||||||
|
const isMobile = useMediaQuery('(max-width: 1280px)')
|
||||||
const yandexPvz = ref<YandexPvzResponse>()
|
const yandexPvz = ref<YandexPvzResponse>()
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
|
const activeTab = ref()
|
||||||
|
const fitting = ref(IPvzMapFittingTabs.ALL)
|
||||||
|
const openDeliverySearch = ref()
|
||||||
|
|
||||||
const waitForCoords = () =>
|
const waitForCoords = () =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
@@ -82,7 +129,7 @@ onMounted(async () => {
|
|||||||
const openstreetmap = await response.json()
|
const openstreetmap = await response.json()
|
||||||
|
|
||||||
// получение geo_id из названию города
|
// получение geo_id из названию города
|
||||||
const { data: yandexLocation } = await useFetch('/api/yandex_location', {
|
const { data: yandexLocation } = await useFetch<YandexLocationDetectResponse>('/api/yandex_location_detect', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
location: openstreetmap?.address?.city,
|
location: openstreetmap?.address?.city,
|
||||||
@@ -100,15 +147,30 @@ onMounted(async () => {
|
|||||||
yandexPvz.value = yandexPvzApi.value
|
yandexPvz.value = yandexPvzApi.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO сделать отдельный компонент UISearch
|
const filteredPoints = computed<YandexPvzPoint[]>(() => {
|
||||||
const filteredPoints = computed<PickupPoint[]>(() => {
|
const points = yandexPvz.value?.points || []
|
||||||
if (!searchTerm.value || searchTerm.value === '') {
|
const term = searchTerm.value?.toLowerCase() || ''
|
||||||
return yandexPvz.value?.points || []
|
|
||||||
}
|
|
||||||
|
|
||||||
return yandexPvz.value?.points?.filter(point =>
|
return points.filter((point) => {
|
||||||
point?.address?.full_address?.toLowerCase().includes(searchTerm.value.toLowerCase()),
|
const matchesSearch = !term || point.address.full_address.toLowerCase().includes(term)
|
||||||
) || []
|
|
||||||
|
const isAllowed = point.pickup_services.is_fitting_allowed
|
||||||
|
const matchesFitting
|
||||||
|
= fitting.value === IPvzMapFittingTabs.ALL
|
||||||
|
|| (fitting.value === IPvzMapFittingTabs.ALLOW && isAllowed)
|
||||||
|
|| (fitting.value === IPvzMapFittingTabs.FORBID && !isAllowed)
|
||||||
|
|
||||||
|
return matchesSearch && matchesFitting
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function updatePoint() {
|
||||||
|
setCheckoutPickupPoint(checkoutPickupPoint.value)
|
||||||
|
open.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => activeTab.value, () => {
|
||||||
|
openDeliverySearch.value = activeTab.value === IPvzMapTabs.LIST
|
||||||
})
|
})
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -117,8 +179,6 @@ definePageMeta({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.delivery {
|
.delivery {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -130,36 +190,5 @@ definePageMeta({
|
|||||||
width: 410px;
|
width: 410px;
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items {
|
|
||||||
height: calc(100dvh - 54px - 40px - 24px);
|
|
||||||
overflow-y: auto;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-point-card {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__action {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -58,7 +58,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PayBlock pay />
|
<ClientOnly>
|
||||||
|
<PayBlock pay :is-summary="true" />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -75,8 +77,6 @@ definePageMeta({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
@@ -85,10 +85,18 @@ definePageMeta({
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items {
|
&__items {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="index">
|
<div class="index">
|
||||||
<div class="cards-list">
|
<div v-if="productCardData.length" class="cards-list">
|
||||||
<div
|
<div
|
||||||
v-for="product in productCardData"
|
v-for="product in pizda"
|
||||||
:key="product.id"
|
:key="product.id"
|
||||||
class="card"
|
class="card"
|
||||||
@click="router.push(`/product/${product.id}`)"
|
@click="router.push(`/product/${product.id}`)"
|
||||||
@@ -30,12 +30,15 @@
|
|||||||
import { useProductsList } from '~/composables'
|
import { useProductsList } from '~/composables'
|
||||||
|
|
||||||
const { productCardData } = useProductsList()
|
const { productCardData } = useProductsList()
|
||||||
|
|
||||||
|
// Карточка вариативного товара с разными цветами в процессе разработки, поэтому отображаем только карточку товара с одним цветов
|
||||||
|
const pizda = computed(() => {
|
||||||
|
return [productCardData.value.find(xueta => xueta.name === 'Пальто мужское'), productCardData.value.find(xueta => xueta.name === 'Пальто мужское'), productCardData.value.find(xueta => xueta.name === 'Пальто мужское'), productCardData.value.find(xueta => xueta.name === 'Пальто мужское')]
|
||||||
|
})
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.cards-list {
|
.cards-list {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '~/assets/scss/utils' as *;
|
|
||||||
|
|
||||||
.product {
|
.product {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
38
server/api/bsbp_create.ts
Normal file
38
server/api/bsbp_create.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { BsbpCreateRequest, BsbpCreateResponse } from '#shared/bsbp_create'
|
||||||
|
import https from 'node:https'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { defineEventHandler, readBody } from 'h3'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event): Promise<BsbpCreateResponse | { error: string }> => {
|
||||||
|
const merchantId = process.env.BSPB_MERCHANT_ID!
|
||||||
|
const merchantPassword = process.env.BSPB_MERCHANT_PASSWORD!
|
||||||
|
const apiUrl = process.env.BSPB_API_URL!
|
||||||
|
|
||||||
|
const assetsStorage = useStorage('assets:server')
|
||||||
|
const bspbKey = await assetsStorage.getItem<string>('pgtest_key.key')
|
||||||
|
const bspbCert = await assetsStorage.getItem<string>('pgtest_cer_2025.pem')
|
||||||
|
|
||||||
|
const agent = new https.Agent({
|
||||||
|
key: bspbKey!,
|
||||||
|
cert: bspbCert!,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderData = await readBody<BsbpCreateRequest>(event)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post<BsbpCreateResponse>(`${apiUrl}/order`, orderData, {
|
||||||
|
httpsAgent: agent,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Basic ${Buffer.from(`TerminalSys/${merchantId}:${merchantPassword}`).toString('base64')}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
return { error: e?.message ?? 'Unknown BSPB error' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import https from 'node:https'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const merchantId = process.env.BSPB_MERCHANT_ID!
|
|
||||||
const merchantPassword = process.env.BSPB_MERCHANT_PASSWORD!
|
|
||||||
const apiUrl = process.env.BSPB_API_URL!
|
|
||||||
|
|
||||||
const assetsStorage = useStorage('assets:server')
|
|
||||||
|
|
||||||
const bspbKey = await assetsStorage.getItem<string>('pgtest_key.key')
|
|
||||||
const bspbCert = await assetsStorage.getItem<string>('pgtest_cer_2025.pem')
|
|
||||||
|
|
||||||
const agent = new https.Agent({
|
|
||||||
key: bspbKey!,
|
|
||||||
cert: bspbCert!,
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const orderData = await readBody(event)
|
|
||||||
|
|
||||||
const response = await axios.post(`${apiUrl}/order`, orderData, {
|
|
||||||
httpsAgent: agent,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Basic ${Buffer.from(`TerminalSys/${merchantId}:${merchantPassword}`).toString('base64')}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return response?.data || []
|
|
||||||
})
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { WooOrderCreateRequest, WooOrderCreateResponse } from '#shared/woo_orders_create'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
import { defineEventHandler, readBody } from 'h3'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event): Promise<WooOrderCreateResponse | { error: string }> => {
|
||||||
try {
|
try {
|
||||||
const orderData = await readBody(event)
|
const orderData = await readBody<WooOrderCreateRequest>(event)
|
||||||
|
|
||||||
const requestUrl = 'https://wp.koptilnya.xyz/wp-json/wc/v3/orders'
|
const requestUrl = 'https://wp.koptilnya.xyz/wp-json/wc/v3/orders'
|
||||||
const consumerKey = 'ck_8b5477a1573ce6038ef1367f25d95cede1de4559'
|
const consumerKey = 'ck_8b5477a1573ce6038ef1367f25d95cede1de4559'
|
||||||
@@ -11,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const encodedAuth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64')
|
const encodedAuth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64')
|
||||||
|
|
||||||
const response = await axios.post(requestUrl, orderData, {
|
const response = await axios.post<WooOrderCreateResponse>(requestUrl, orderData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Basic ${encodedAuth}`,
|
'Authorization': `Basic ${encodedAuth}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import type { YandexLocationDetectRequest, YandexLocationDetectResponse } from '#shared/yandex_location_detect'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
import { defineEventHandler, readBody } from 'h3'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event): Promise<YandexLocationDetectResponse | { error: string }> => {
|
||||||
try {
|
try {
|
||||||
const data = await readBody(event)
|
const data = await readBody<YandexLocationDetectRequest>(event)
|
||||||
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
||||||
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post<YandexLocationDetectResponse>(
|
||||||
`${apiUrl}/location/detect`,
|
`${apiUrl}/location/detect`,
|
||||||
{
|
{
|
||||||
location: data?.location,
|
location: data?.location,
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { YandexPvzResponse } from '~/server/shared/types/yandex_pvz'
|
import type { YandexPvzRequest, YandexPvzResponse } from '#shared/yandex_pvz'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
import { defineEventHandler, readBody } from 'h3'
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<YandexPvzResponse | { error: string }> => {
|
export default defineEventHandler(async (event): Promise<YandexPvzResponse | { error: string }> => {
|
||||||
try {
|
try {
|
||||||
const data = await readBody(event)
|
const data = await readBody<YandexPvzRequest>(event)
|
||||||
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
||||||
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface IBspb {
|
|
||||||
order: {
|
|
||||||
id: number
|
|
||||||
hppUrl: string
|
|
||||||
password: string
|
|
||||||
accessToken: string
|
|
||||||
status: string
|
|
||||||
cvv2AuthStatus: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
export interface GeoPosition {
|
|
||||||
latitude: number
|
|
||||||
longitude: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PvzAddress {
|
|
||||||
apartment: string
|
|
||||||
building: string
|
|
||||||
comment: string
|
|
||||||
country: string
|
|
||||||
full_address: string
|
|
||||||
geoId: number
|
|
||||||
house: string
|
|
||||||
housing: string
|
|
||||||
locality: string
|
|
||||||
postal_code: string
|
|
||||||
region: string
|
|
||||||
street: string
|
|
||||||
subRegion: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Contact {
|
|
||||||
phone: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeObject {
|
|
||||||
hours: number
|
|
||||||
minutes: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduleRestriction {
|
|
||||||
days: number[]
|
|
||||||
time_from: TimeObject
|
|
||||||
time_to: TimeObject
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Schedule {
|
|
||||||
time_zone: number
|
|
||||||
restrictions: ScheduleRestriction[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PickupServices {
|
|
||||||
is_fitting_allowed: boolean
|
|
||||||
is_partial_refuse_allowed: boolean
|
|
||||||
is_paperless_pickup_allowed: boolean
|
|
||||||
is_unboxing_allowed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PickupPoint {
|
|
||||||
id: string
|
|
||||||
operator_station_id: string
|
|
||||||
operator_id: string
|
|
||||||
name: string
|
|
||||||
type: 'pickup_point'
|
|
||||||
address: PvzAddress
|
|
||||||
position: GeoPosition
|
|
||||||
instruction: string
|
|
||||||
available_for_dropoff: boolean
|
|
||||||
available_for_c2c_dropoff: boolean
|
|
||||||
contact: Contact
|
|
||||||
schedule: Schedule
|
|
||||||
pickup_services: PickupServices
|
|
||||||
payment_methods: string[]
|
|
||||||
is_dark_store: boolean
|
|
||||||
is_market_partner: boolean
|
|
||||||
is_post_office: boolean
|
|
||||||
is_yandex_branded: boolean
|
|
||||||
dayoffs: any[]
|
|
||||||
deactivation_date: string | null
|
|
||||||
deactivation_date_predicted_debt: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YandexPvzResponse extends PickupPoint {
|
|
||||||
points: PickupPoint[]
|
|
||||||
}
|
|
||||||
23
shared/bsbp_create.ts
Normal file
23
shared/bsbp_create.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface BsbpOrder {
|
||||||
|
typeRid: string
|
||||||
|
amount: number
|
||||||
|
currency: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
hppRedirectUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BsbpCreateRequest {
|
||||||
|
order: BsbpOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BsbpCreateResponse {
|
||||||
|
order: {
|
||||||
|
id: number
|
||||||
|
hppUrl: string
|
||||||
|
password: string
|
||||||
|
accessToken: string
|
||||||
|
status: string
|
||||||
|
cvv2AuthStatus: string
|
||||||
|
}
|
||||||
|
}
|
||||||
9
shared/types.ts
Normal file
9
shared/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export enum IPvzMapTabs {
|
||||||
|
MAP = 'map',
|
||||||
|
LIST = 'list',
|
||||||
|
}
|
||||||
|
export enum IPvzMapFittingTabs {
|
||||||
|
ALL = 'all',
|
||||||
|
ALLOW = 'allow',
|
||||||
|
FORBID = 'Forbid',
|
||||||
|
}
|
||||||
160
shared/woo_orders_create.ts
Normal file
160
shared/woo_orders_create.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// ------------------------------------
|
||||||
|
// REQUEST: Create WooCommerce Order
|
||||||
|
// ------------------------------------
|
||||||
|
export interface WooOrderCreateBilling {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
address_1: string
|
||||||
|
address_2: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
postcode: string
|
||||||
|
country: string
|
||||||
|
email: string
|
||||||
|
phone: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderCreateShipping {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
address_1: string
|
||||||
|
address_2: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
postcode: string
|
||||||
|
country: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderCreateLineItem {
|
||||||
|
product_id: number
|
||||||
|
variation_id?: number
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderCreateShippingLine {
|
||||||
|
method_id: string
|
||||||
|
method_title: string
|
||||||
|
total: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderCreateRequest {
|
||||||
|
payment_method: string
|
||||||
|
payment_method_title: string
|
||||||
|
set_paid: boolean
|
||||||
|
billing: WooOrderCreateBilling
|
||||||
|
shipping: WooOrderCreateShipping
|
||||||
|
line_items: WooOrderCreateLineItem[]
|
||||||
|
shipping_lines: WooOrderCreateShippingLine[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// RESPONSE: WooCommerce Order
|
||||||
|
// ------------------------------------
|
||||||
|
export interface WooMetaData {
|
||||||
|
id: number
|
||||||
|
key: string
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooTaxItem {
|
||||||
|
id: number
|
||||||
|
total: string
|
||||||
|
subtotal: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooLineItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
product_id: number
|
||||||
|
variation_id: number
|
||||||
|
quantity: number
|
||||||
|
tax_class: string
|
||||||
|
subtotal: string
|
||||||
|
subtotal_tax: string
|
||||||
|
total: string
|
||||||
|
total_tax: string
|
||||||
|
taxes: WooTaxItem[]
|
||||||
|
meta_data: WooMetaData[]
|
||||||
|
sku: string
|
||||||
|
price: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooTaxLine {
|
||||||
|
id: number
|
||||||
|
rate_code: string
|
||||||
|
rate_id: number
|
||||||
|
label: string
|
||||||
|
compound: boolean
|
||||||
|
tax_total: string
|
||||||
|
shipping_tax_total: string
|
||||||
|
meta_data: WooMetaData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooShippingLine {
|
||||||
|
id: number
|
||||||
|
method_title: string
|
||||||
|
method_id: string
|
||||||
|
total: string
|
||||||
|
total_tax: string
|
||||||
|
taxes: WooTaxItem[]
|
||||||
|
meta_data: WooMetaData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooLink {
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderLinks {
|
||||||
|
self: WooLink[]
|
||||||
|
collection: WooLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WooOrderCreateResponse {
|
||||||
|
id: number
|
||||||
|
parent_id: number
|
||||||
|
number: string
|
||||||
|
order_key: string
|
||||||
|
created_via: string
|
||||||
|
version: string
|
||||||
|
status: string
|
||||||
|
currency: string
|
||||||
|
date_created: string
|
||||||
|
date_created_gmt: string
|
||||||
|
date_modified: string
|
||||||
|
date_modified_gmt: string
|
||||||
|
discount_total: string
|
||||||
|
discount_tax: string
|
||||||
|
shipping_total: string
|
||||||
|
shipping_tax: string
|
||||||
|
cart_tax: string
|
||||||
|
total: string
|
||||||
|
total_tax: string
|
||||||
|
prices_include_tax: boolean
|
||||||
|
customer_id: number
|
||||||
|
customer_ip_address: string
|
||||||
|
customer_user_agent: string
|
||||||
|
customer_note: string
|
||||||
|
|
||||||
|
billing: WooOrderCreateBilling
|
||||||
|
shipping: WooOrderCreateShipping
|
||||||
|
|
||||||
|
payment_method: string
|
||||||
|
payment_method_title: string
|
||||||
|
transaction_id: string
|
||||||
|
date_paid: string | null
|
||||||
|
date_paid_gmt: string | null
|
||||||
|
date_completed: string | null
|
||||||
|
date_completed_gmt: string | null
|
||||||
|
cart_hash: string
|
||||||
|
|
||||||
|
meta_data: WooMetaData[]
|
||||||
|
|
||||||
|
line_items: WooLineItem[]
|
||||||
|
tax_lines: WooTaxLine[]
|
||||||
|
shipping_lines: WooShippingLine[]
|
||||||
|
fee_lines: any[]
|
||||||
|
coupon_lines: any[]
|
||||||
|
refunds: any[]
|
||||||
|
|
||||||
|
_links: WooOrderLinks
|
||||||
|
}
|
||||||
12
shared/yandex_location_detect.ts
Normal file
12
shared/yandex_location_detect.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface YandexLocationVariantDetect {
|
||||||
|
geo_id: number
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexLocationDetectResponse {
|
||||||
|
variants: YandexLocationVariantDetect[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexLocationDetectRequest {
|
||||||
|
location: string
|
||||||
|
}
|
||||||
127
shared/yandex_pvz.ts
Normal file
127
shared/yandex_pvz.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// -----------------------------
|
||||||
|
// REQUEST
|
||||||
|
// -----------------------------
|
||||||
|
export type YandexPvzPaymentMethod = 'already_paid'
|
||||||
|
|
||||||
|
export interface YandexPvzCoordinateRange {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzPickupServicesRequest {
|
||||||
|
is_fitting_allowed: boolean
|
||||||
|
is_partial_refuse_allowed: boolean
|
||||||
|
is_paperless_pickup_allowed: boolean
|
||||||
|
is_unboxing_allowed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzRequest {
|
||||||
|
pickup_point_ids?: string[]
|
||||||
|
geo_id?: number
|
||||||
|
longitude?: YandexPvzCoordinateRange
|
||||||
|
latitude?: YandexPvzCoordinateRange
|
||||||
|
type?: 'pickup_point'
|
||||||
|
payment_method?: YandexPvzPaymentMethod
|
||||||
|
available_for_dropoff?: boolean
|
||||||
|
is_yandex_branded?: boolean
|
||||||
|
is_not_branded_partner_station?: boolean
|
||||||
|
is_post_office?: boolean
|
||||||
|
payment_methods?: YandexPvzPaymentMethod[]
|
||||||
|
pickup_services?: YandexPvzPickupServicesRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// RESPONSE
|
||||||
|
// -----------------------------
|
||||||
|
export interface YandexPvzPosition {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzAddress {
|
||||||
|
geoId: string
|
||||||
|
country: string
|
||||||
|
region: string
|
||||||
|
subRegion: string
|
||||||
|
locality: string
|
||||||
|
street: string
|
||||||
|
house: string
|
||||||
|
housing: string
|
||||||
|
apartment: string
|
||||||
|
building: string
|
||||||
|
comment: string
|
||||||
|
full_address: string
|
||||||
|
postal_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzContact {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
patronymic: string
|
||||||
|
phone: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzTimeObject {
|
||||||
|
hours: number
|
||||||
|
minutes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzScheduleRestriction {
|
||||||
|
days: number[]
|
||||||
|
time_from: YandexPvzTimeObject
|
||||||
|
time_to: YandexPvzTimeObject
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzSchedule {
|
||||||
|
time_zone: number
|
||||||
|
restrictions: YandexPvzScheduleRestriction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzPickupServices {
|
||||||
|
is_fitting_allowed: boolean
|
||||||
|
is_partial_refuse_allowed: boolean
|
||||||
|
is_paperless_pickup_allowed: boolean
|
||||||
|
is_unboxing_allowed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzDayoff {
|
||||||
|
date: string
|
||||||
|
date_utc: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzDeactivation {
|
||||||
|
deactivation_date_predicted_debt: boolean
|
||||||
|
available_for_dropoff: boolean
|
||||||
|
available_for_c2c_dropoff: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzPoint {
|
||||||
|
ID: string
|
||||||
|
operator_station_id: string
|
||||||
|
name: string
|
||||||
|
type: 'pickup_point'
|
||||||
|
|
||||||
|
position: YandexPvzPosition
|
||||||
|
address: YandexPvzAddress
|
||||||
|
instruction: string
|
||||||
|
|
||||||
|
payment_methods: YandexPvzPaymentMethod[]
|
||||||
|
contact: YandexPvzContact
|
||||||
|
schedule: YandexPvzSchedule
|
||||||
|
|
||||||
|
is_yandex_branded: boolean
|
||||||
|
is_market_partner: boolean
|
||||||
|
is_dark_store: boolean
|
||||||
|
is_post_office: boolean
|
||||||
|
|
||||||
|
pickup_services: YandexPvzPickupServices
|
||||||
|
|
||||||
|
deactivation_date: YandexPvzDeactivation
|
||||||
|
|
||||||
|
dayoffs: YandexPvzDayoff[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YandexPvzResponse {
|
||||||
|
points: YandexPvzPoint[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user