Compare commits
No commits in common. "master" and "product-card" have entirely different histories.
master
...
product-ca
@ -6,35 +6,22 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Keyscan
|
- name: LS
|
||||||
run: |
|
run: cd / && cat .dockerenv
|
||||||
ssh-keyscan git.koptilnya.xyz >> ~/.ssh/known_hosts
|
- name: Test SSH access
|
||||||
|
run: ssh -Tv git@koptilnya.xyz
|
||||||
- name: Checkout
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
ssh-key: '${{ secrets.SSH_PRIVATE_KEY }}'
|
||||||
ssh-strict: false
|
ssh-strict: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build
|
- name: Install dependencies
|
||||||
run: docker build -t paxton:latest .
|
|
||||||
|
|
||||||
- name: Stop old container
|
|
||||||
run: docker rm -f paxton || true
|
|
||||||
|
|
||||||
- name: Run
|
|
||||||
run: |
|
run: |
|
||||||
docker run -d \
|
yarn install
|
||||||
--name paxton \
|
yarn build
|
||||||
--network traefik \
|
|
||||||
--label "traefik.enable=true" \
|
|
||||||
--label "traefik.http.routers.paxton.rule=Host(\`paxton.koptilnya.xyz\`)" \
|
|
||||||
--label "traefik.http.routers.paxton.entrypoints=websecure" \
|
|
||||||
--label "traefik.http.routers.paxton.tls.certresolver=myresolver" \
|
|
||||||
--label "traefik.http.services.paxton.loadbalancer.server.port=80" \
|
|
||||||
paxton:latest
|
|
||||||
|
|||||||
28
Dockerfile
28
Dockerfile
@ -1,28 +0,0 @@
|
|||||||
FROM node:22-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
RUN corepack enable
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
RUN yarn install
|
|
||||||
COPY . .
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM node:22-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/.output/ ./
|
|
||||||
|
|
||||||
ENV PORT=80
|
|
||||||
|
|
||||||
ENV BSPB_MERCHANT_ID=TT00001
|
|
||||||
ENV BSPB_MERCHANT_PASSWORD=ztTGre1OBZg3
|
|
||||||
ENV BSPB_API_URL=https://pgtest.bspb.ru:5443
|
|
||||||
|
|
||||||
ENV VITE_YANDEX_MAPS_TOKEN=13f4c06b-cb7e-4eeb-81f1-af52f12587b2
|
|
||||||
|
|
||||||
ENV VITE_YANDEX_B2B_BASE_URL=https://b2b-authproxy.taxi.yandex.net/api/b2b/platform
|
|
||||||
ENV VITE_YANDEX_B2B_TOKEN=y0__xCqxdamBhjVmRUgrIqL1BRb-Au03gwwIZjInV31ftl35o5nNA
|
|
||||||
|
|
||||||
ENV VITE_BASE_URL=http://localhost:3000
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["node", "/app/server/index.mjs"]
|
|
||||||
@ -5283,7 +5283,7 @@ export class Api<
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
secure: true,
|
secure: true,
|
||||||
type: ContentType.Json,
|
type: ContentType.UrlEncoded,
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
export * from './wp/index'
|
export * from './getProductAttributesDetail'
|
||||||
|
export * from './getProductsDetail'
|
||||||
|
export * from './getProductsList'
|
||||||
|
export * from './getProductsVariationsList'
|
||||||
|
|||||||
1
api/endpoints/orders/index.ts
Normal file
1
api/endpoints/orders/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './postOrdersCreate'
|
||||||
4
api/endpoints/orders/postOrdersCreate.ts
Normal file
4
api/endpoints/orders/postOrdersCreate.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import api from '~/api/instance'
|
||||||
|
|
||||||
|
export const postOrdersCreate = async (parent_id: number | undefined) =>
|
||||||
|
await api.wc.v3OrdersCreate({ parent_id })
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './getProductAttributesDetail'
|
|
||||||
export * from './getProductsDetail'
|
|
||||||
export * from './getProductsList'
|
|
||||||
export * from './getProductsVariationsList'
|
|
||||||
1
api/mutations/index.ts
Normal file
1
api/mutations/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './usePostOrdersCreate'
|
||||||
19
api/mutations/usePostOrdersCreate.ts
Normal file
19
api/mutations/usePostOrdersCreate.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { MaybeRef } from 'vue'
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import { unref } from 'vue'
|
||||||
|
import { postOrdersCreate } from '~/api/endpoints/orders'
|
||||||
|
|
||||||
|
export const usePostOrdersCreate = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: { parent_id: MaybeRef<number | undefined> }) =>
|
||||||
|
postOrdersCreate(unref(params.parent_id)),
|
||||||
|
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['post-orders-create', unref(variables.parent_id)],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1 +1,4 @@
|
|||||||
export * from './wp/index'
|
export * from './useGetProductAttributesDetail'
|
||||||
|
export * from './useGetProductsDetail'
|
||||||
|
export * from './useGetProductsList'
|
||||||
|
export * from './useGetProductsVariationsList'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { unref } from 'vue'
|
import { unref } from 'vue'
|
||||||
import { getProductAttributesDetail } from '~/api/endpoints/wp'
|
import { getProductAttributesDetail } from '~/api/endpoints'
|
||||||
|
|
||||||
export const useGetProductAttributesDetail = (productId: MaybeRef<number>) => {
|
export const useGetProductAttributesDetail = (productId: MaybeRef<number>) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { unref } from 'vue'
|
import { unref } from 'vue'
|
||||||
import { getProductsDetail } from '~/api/endpoints/wp'
|
import { getProductsDetail } from '~/api/endpoints'
|
||||||
|
|
||||||
export const useGetProductsDetail = (productId: MaybeRef<number>) => {
|
export const useGetProductsDetail = (productId: MaybeRef<number>) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['get-products-detail', productId],
|
queryKey: ['get-products-detail', productId],
|
||||||
queryFn: () => getProductsDetail(unref(productId)),
|
queryFn: () => getProductsDetail(unref(productId)),
|
||||||
enabled: !!unref(productId),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { getProductsList } from '~/api/endpoints/wp/getProductsList'
|
import { getProductsList } from '~/api/endpoints/getProductsList'
|
||||||
|
|
||||||
export const useGetProductsList = () => {
|
export const useGetProductsList = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { unref } from 'vue'
|
import { unref } from 'vue'
|
||||||
import { getProductsVariationsList } from '~/api/endpoints/wp'
|
import { getProductsVariationsList } from '~/api/endpoints'
|
||||||
|
|
||||||
export const useGetProductsVariationsList = (productId: MaybeRef<number>) => {
|
export const useGetProductsVariationsList = (productId: MaybeRef<number>) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['get-products-variations-list', productId],
|
queryKey: ['get-products-variations-list', productId],
|
||||||
queryFn: () => getProductsVariationsList(unref(productId)),
|
queryFn: () => getProductsVariationsList(unref(productId)),
|
||||||
enabled: !!unref(productId),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './useGetProductAttributesDetail'
|
|
||||||
export * from './useGetProductsDetail'
|
|
||||||
export * from './useGetProductsList'
|
|
||||||
export * from './useGetProductsVariationsList'
|
|
||||||
@ -1,4 +1,29 @@
|
|||||||
//скроллбар
|
@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;
|
||||||
}
|
}
|
||||||
@ -28,33 +53,3 @@
|
|||||||
body {
|
body {
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
//swiper
|
|
||||||
.swiper {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100dvh - 54px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
background: #444;
|
|
||||||
|
|
||||||
/* Center slide text vertically */
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100dvh - 54px);
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 200px !important;
|
|
||||||
--swiper-pagination-bullet-size: 4px
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
@mixin mobile {
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
121
assets/scss/typography.scss
Normal file
121
assets/scss/typography.scss
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
@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);
|
||||||
|
}
|
||||||
148
assets/scss/utils.scss
Normal file
148
assets/scss/utils.scss
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
@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; }
|
||||||
@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul class="delivery-info">
|
|
||||||
<li v-if="showFullContent" class="delivery-info__item">
|
|
||||||
<Icon class="delivery-info__icon" name="lucide:truck" />
|
|
||||||
<span class="delivery-info__text">Yandex</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="delivery-info__item">
|
|
||||||
<Icon class="delivery-info__icon" name="lucide:map-pin" />
|
|
||||||
<span class="delivery-info__text">{{ checkoutPickupPoint?.address?.full_address }}</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="delivery-info__item">
|
|
||||||
<Icon
|
|
||||||
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 class="delivery-info__item">
|
|
||||||
<Icon class="delivery-info__icon" name="lucide:package" />
|
|
||||||
<span class="delivery-info__text">срок хранения — 7 дней</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="delivery-info__item">
|
|
||||||
<UAccordion
|
|
||||||
:items="availableDays"
|
|
||||||
:ui="{
|
|
||||||
body: 'py-0',
|
|
||||||
panel: 'p-0',
|
|
||||||
wrapper: 'space-y-0',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #schedule="{ item }">
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-for="(scheduleItem, index) in item.content"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<Icon class="delivery-info__icon mr-2" name="lucide:clock" />
|
|
||||||
<span class="mr-2">{{ scheduleItem.dayName }}</span>
|
|
||||||
<span>{{ scheduleItem.timeRange }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UAccordion>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<UAccordion :items="details" />
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
v-if="showFullContent"
|
|
||||||
class="justify-content-center"
|
|
||||||
label="Привезти сюда"
|
|
||||||
size="xl"
|
|
||||||
@click="nextStep"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { AccordionItem } from '@nuxt/ui'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useCheckout } from '~/composables/useCheckout'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
showFullContent?: boolean
|
|
||||||
}>(), {
|
|
||||||
showFullContent: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { checkoutPickupPoint, nextStep } = useCheckout()
|
|
||||||
|
|
||||||
const restrictionDays = {
|
|
||||||
1: 'пн',
|
|
||||||
2: 'вт',
|
|
||||||
3: 'ср',
|
|
||||||
4: 'чт',
|
|
||||||
5: 'пт',
|
|
||||||
6: 'сб',
|
|
||||||
7: 'вс',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для форматирования времени с ведущим нулем
|
|
||||||
const formatTime = (time: { hours: number | string, minutes: number | string }): string => {
|
|
||||||
const hours = String(time.hours).padStart(2, '0')
|
|
||||||
const minutes = String(time.minutes).padStart(2, '0')
|
|
||||||
return `${hours}:${minutes}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для форматирования временного диапазона
|
|
||||||
const formatTimeRange = (timeFrom: { hours: number | string, minutes: number | string }, timeTo: { hours: number | string, minutes: number | string }): string => {
|
|
||||||
return `${formatTime(timeFrom)} - ${formatTime(timeTo)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Аккордеон с расписанием - используем слот вместо HTML строки
|
|
||||||
const availableDays = computed(() => {
|
|
||||||
const scheduleItems = checkoutPickupPoint?.value?.schedule?.restrictions?.map((restriction) => {
|
|
||||||
const dayName = restrictionDays[restriction.days[0]]
|
|
||||||
const timeRange = formatTimeRange(restriction.time_from, restriction.time_to)
|
|
||||||
return { dayName, timeRange }
|
|
||||||
}) || []
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'График работы',
|
|
||||||
icon: 'i-lucide:calendar',
|
|
||||||
slot: 'schedule',
|
|
||||||
content: scheduleItems,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const details = ref<AccordionItem[]>([
|
|
||||||
{
|
|
||||||
label: 'Подробнее',
|
|
||||||
icon: 'i-lucide:info',
|
|
||||||
content: checkoutPickupPoint?.value?.address?.comment || 'Дополнительная информация о пункте выдачи',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.delivery-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
flex: 1;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<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,103 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
<div>товары {{ `(${cart?.line_items?.length} шт)` }}</div>
|
|
||||||
<div v-if="cartSum">
|
|
||||||
<ProductPrice :is-headline="false" text="итого" :price="cartSum" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<UButton
|
|
||||||
class="justify-center text-center w-2xs"
|
|
||||||
size="xl"
|
|
||||||
:label="pay ? 'оформить заказ' : 'перейти к оформлению'"
|
|
||||||
@click="pay ? createOrder() : router.push(`/checkout/delivery`)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</UPageCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { BsbpCreateResponse } from '#shared/bsbp_create'
|
|
||||||
import { useMediaQuery } from '@vueuse/core'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
pay: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isSummary: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)', {
|
|
||||||
ssrWidth: 768,
|
|
||||||
})
|
|
||||||
const { cart, cartSum } = useCart()
|
|
||||||
|
|
||||||
const createOrder = async () => {
|
|
||||||
const { data } = await useFetch<BsbpCreateResponse>('/api/bsbp_create', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
order: {
|
|
||||||
typeRid: 'Purchase',
|
|
||||||
amount: cartSum,
|
|
||||||
currency: 'RUB',
|
|
||||||
hppRedirectUrl: `${process.env.VITE_BASE_URL}/cart`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const redirectUrl = `${data?.value?.order?.hppUrl}?orderId=${data?.value?.order?.id}&password=${data.value?.order?.password}`
|
|
||||||
window.open(redirectUrl, '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.pay-block {
|
|
||||||
&--mobile {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UPopover
|
|
||||||
:mode="isMobile ? 'click' : 'hover'"
|
|
||||||
:ui="{ content: '--reka-popper-transform-origin: 50% -20px;' }"
|
|
||||||
>
|
|
||||||
<UButton
|
|
||||||
icon="i-mdi-human-handsdown"
|
|
||||||
label="фотомодель"
|
|
||||||
class="model-btn"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<div class="model-info">
|
|
||||||
<div
|
|
||||||
v-for="parameter in photoModel"
|
|
||||||
:key="parameter?.split(': ')[0]"
|
|
||||||
>
|
|
||||||
{{ parameter?.split(': ')[0] }} <strong>{{ parameter?.split(': ')[1] }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useMediaQuery } from '@vueuse/core'
|
|
||||||
|
|
||||||
const { photoModel } = useProduct()
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.model-btn {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-info {
|
|
||||||
width: 140px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-info strong {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="product-description">
|
<div class="product-description">
|
||||||
<h1>{{ productsData?.name }}</h1>
|
<h1>{{ productsData?.name }}</h1>
|
||||||
|
|
||||||
<ProductPrice :price="currentVariant?.options[0]?.price" />
|
<h2>{{ currentVariant?.options[0]?.price }} <Icon name="ph:currency-rub" /></h2>
|
||||||
|
|
||||||
<ProductVariations v-if="colors?.length > 1" />
|
<ProductVariations v-if="colors?.length > 1" />
|
||||||
|
|
||||||
@ -37,9 +37,9 @@
|
|||||||
<UButton
|
<UButton
|
||||||
:disabled="!currentSize"
|
:disabled="!currentSize"
|
||||||
class="justify-content-center"
|
class="justify-content-center"
|
||||||
:label="currentSize ? `Добавить в корзину` : `Выберите размер`"
|
label="Добавить в корзину"
|
||||||
size="xl"
|
size="xl"
|
||||||
@click="addToCartBtn()"
|
@click="addToCartBtn"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UAccordion :items="items" />
|
<UAccordion :items="items" />
|
||||||
@ -47,12 +47,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProductPrice from '~/components/ProductPrice.vue'
|
|
||||||
import ProductVariations from '~/components/ProductVariations.vue'
|
import ProductVariations from '~/components/ProductVariations.vue'
|
||||||
import { useCurrentProduct, useProduct } from '~/composables'
|
import { useCurrentProduct, useProduct } from '~/composables'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { cartAddItem } = useCart()
|
const { addToCart } = useCart()
|
||||||
|
|
||||||
const { productsData, colors, getAttribute } = useProduct()
|
const { productsData, colors, getAttribute } = useProduct()
|
||||||
const { currentVariant, currentColor, currentMaterial } = useCurrentProduct()
|
const { currentVariant, currentColor, currentMaterial } = useCurrentProduct()
|
||||||
@ -88,29 +87,28 @@ const items = computed(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
function addToCartBtn() {
|
function addToCartBtn() {
|
||||||
cartAddItem({ variation_id: currentSize?.value?.id })
|
addToCart(currentSize?.value?.id)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.product-description {
|
.product-description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 30px 30px 0;
|
max-width: 335px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
align-self: self-start;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
max-height: calc(100vh - 40px);
|
|
||||||
max-width: 350px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
padding-top: 30px;
|
||||||
padding: 20px 30px;
|
padding-inline: 30px;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
&__sizes {
|
&__sizes {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -121,5 +119,13 @@ function addToCartBtn() {
|
|||||||
&__size {
|
&__size {
|
||||||
width: 65px;
|
width: 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,43 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!isMobile" class="product-images">
|
<div class="product-images">
|
||||||
<div
|
<div
|
||||||
v-for="image in currentVariantImages?.slice(0, 5)"
|
v-for="image in currentVariantImages?.slice(0, 5)"
|
||||||
:key="image?.id"
|
:key="image?.id"
|
||||||
>
|
>
|
||||||
<img width="100%" :src="image?.src" :alt="image?.src">
|
<img width="100%" :src="image?.src" :alt="image?.src">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-images__model">
|
|
||||||
<PhotoModel />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Swiper
|
|
||||||
v-if="isMobile"
|
|
||||||
:pagination="true"
|
|
||||||
:loop="true"
|
|
||||||
:modules="modules"
|
|
||||||
class="mySwiper"
|
|
||||||
>
|
|
||||||
<SwiperSlide
|
|
||||||
v-for="image in currentVariantImages?.slice(0, 5)"
|
|
||||||
:key="image?.src"
|
|
||||||
>
|
|
||||||
<img width="100%" :src="image?.src" :alt="image?.src">
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMediaQuery } from '@vueuse/core'
|
|
||||||
import { Pagination } from 'swiper/modules'
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
|
||||||
import { useCurrentProduct } from '~/composables'
|
import { useCurrentProduct } from '~/composables'
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/pagination'
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
const modules = [Pagination]
|
|
||||||
const { currentVariantImages } = useCurrentProduct()
|
const { currentVariantImages } = useCurrentProduct()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -45,14 +19,7 @@ const { currentVariantImages } = useCurrentProduct()
|
|||||||
.product-images {
|
.product-images {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
flex-direction: column;
|
||||||
|
|
||||||
&__model {
|
|
||||||
position: fixed;
|
|
||||||
top: calc(10px + 54px);
|
|
||||||
right: calc(10px + 350px);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="product-price">
|
|
||||||
<h2 v-if="isHeadline" class="product-price__text">
|
|
||||||
{{ text }} {{ price }} <Icon name="ph:currency-rub" />
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div v-else class="product-price__text">
|
|
||||||
{{ text }} {{ price }} <Icon name="ph:currency-rub" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps({
|
|
||||||
price: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isHeadline: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.product-price {
|
|
||||||
&__text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="!hasCoords" class="p-4 text-center">
|
|
||||||
⏳ Определяем ваше местоположение...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<YandexMap
|
|
||||||
v-else
|
|
||||||
v-model="map"
|
|
||||||
class="w-full"
|
|
||||||
:settings="{ location }"
|
|
||||||
style="height: calc(100dvh - 54px)"
|
|
||||||
>
|
|
||||||
<YandexMapDefaultSchemeLayer />
|
|
||||||
<YandexMapDefaultFeaturesLayer />
|
|
||||||
|
|
||||||
<YandexMapClusterer
|
|
||||||
v-model="clusterer"
|
|
||||||
:grid-size="64"
|
|
||||||
zoom-on-cluster-click
|
|
||||||
@true-bounds="trueBounds = $event"
|
|
||||||
>
|
|
||||||
<YandexMapMarker
|
|
||||||
v-for="pickupPoint in pickupPoints"
|
|
||||||
:key="pickupPoint.id"
|
|
||||||
position="top-center left-center"
|
|
||||||
:settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }"
|
|
||||||
@click="onSelectPoint(pickupPoint)"
|
|
||||||
>
|
|
||||||
<div class="marker">
|
|
||||||
<Icon name="i-lucide-map-pin" class="marker__icon" />
|
|
||||||
</div>
|
|
||||||
</YandexMapMarker>
|
|
||||||
|
|
||||||
<template #cluster="{ length }">
|
|
||||||
<div class="cluster fade-in">
|
|
||||||
{{ length }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { PickupPoint } from '#shared/yandex_pvz'
|
|
||||||
import type { LngLatBounds, YMap } from '@yandex/ymaps3-types'
|
|
||||||
import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap'
|
|
||||||
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer'
|
|
||||||
import type { PropType } from 'vue'
|
|
||||||
import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
|
|
||||||
import { useGeolocation, useMediaQuery } from '@vueuse/core'
|
|
||||||
import { computed, defineEmits, shallowRef } from 'vue'
|
|
||||||
import {
|
|
||||||
YandexMap,
|
|
||||||
YandexMapClusterer,
|
|
||||||
YandexMapControl,
|
|
||||||
YandexMapControls,
|
|
||||||
YandexMapDefaultFeaturesLayer,
|
|
||||||
YandexMapDefaultSchemeLayer,
|
|
||||||
YandexMapMarker,
|
|
||||||
} from 'vue-yandex-maps'
|
|
||||||
import MapControlFitting from '~/components/MapControlFitting.vue'
|
|
||||||
import MapControlTabs from '~/components/MapControlTabs.vue'
|
|
||||||
|
|
||||||
defineProps<{ pickupPoints: PickupPoint[] }>()
|
|
||||||
const emit = defineEmits(['update:checkout-pickup-point'])
|
|
||||||
|
|
||||||
const checkoutPickupPoint = defineModel('checkoutPickupPoint', { type: Object as PropType<PickupPoint>, default: () => undefined })
|
|
||||||
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 isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
const clusterer = shallowRef<YMapClusterer | null>(null)
|
|
||||||
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]])
|
|
||||||
const map = shallowRef<null | YMap>(null)
|
|
||||||
|
|
||||||
const hasCoords = computed(() => coords.value.latitude !== Infinity && coords.value.longitude !== Infinity)
|
|
||||||
|
|
||||||
const location = ref<YMapLocationRequest>({
|
|
||||||
zoom: 2,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSelectPoint = (pickupPoint: PickupPoint) => {
|
|
||||||
checkoutPickupPoint.value = pickupPoint
|
|
||||||
emit('update:checkout-pickup-point', pickupPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(coords, (newCoords) => {
|
|
||||||
if (newCoords && hasCoords.value) {
|
|
||||||
location.value = {
|
|
||||||
center: [newCoords.longitude, newCoords.latitude],
|
|
||||||
zoom: 9,
|
|
||||||
duration: 2500,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { once: true })
|
|
||||||
|
|
||||||
watch(() => checkoutPickupPoint.value, (newPickupPoint) => {
|
|
||||||
if (!newPickupPoint)
|
|
||||||
return
|
|
||||||
|
|
||||||
location.value = {
|
|
||||||
center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude],
|
|
||||||
zoom: 18,
|
|
||||||
duration: 500,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.marker {
|
|
||||||
position: relative;
|
|
||||||
background-color: #0f172b;
|
|
||||||
border: 1px solid #0f172b;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 6px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
||||||
transition:
|
|
||||||
transform 0.2s ease,
|
|
||||||
background-color 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
background-color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -6px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-top: 6px solid #0f172b;
|
|
||||||
filter: drop-shadow(0 2px 1px rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
color: white;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover &__icon {
|
|
||||||
color: var(--ui-primary);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cluster {
|
|
||||||
background-color: #0f172b;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
|
||||||
border: 2px solid #fff;
|
|
||||||
transition:
|
|
||||||
transform 0.2s ease,
|
|
||||||
background-color 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
background-color: #1e293b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="cart-item">
|
|
||||||
<NuxtLink :to="`/product/${productsData?.parent_id}`">
|
|
||||||
<img
|
|
||||||
class="cart-item__image"
|
|
||||||
:src="productsData?.images[0]?.src"
|
|
||||||
alt="cart?.image"
|
|
||||||
>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{{ productsData?.name }}
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
{{ productsData?.price }}
|
|
||||||
<Icon name="ph:currency-rub" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cart-item__attributes">
|
|
||||||
<div>{{ t(`colors.${color}`) }} {{ t(`materials.${material}`) }}</div>
|
|
||||||
<div>{{ `размер ${size}` }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
@click="deleteItem(cartItem)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
cartItem: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const { cartRemoveItem } = useCart()
|
|
||||||
const { productsData, color, material, size } = useProduct(props?.cartItem?.variation_id)
|
|
||||||
|
|
||||||
const deleteItem = (item) => {
|
|
||||||
cartRemoveItem({ variation_id: item?.variation_id })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.cart-item {
|
|
||||||
max-width: 600px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 120px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="summary-cart-item">
|
|
||||||
<img
|
|
||||||
class="summary-cart-item__image"
|
|
||||||
:src="productsData?.images[0]?.src"
|
|
||||||
alt="cart?.image"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="summary-cart-item__attributes">
|
|
||||||
{{ size }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
summaryCartItem: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { productsData, size } = useProduct(props?.summaryCartItem?.variation_id)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.summary-cart-item {
|
|
||||||
max-width: 600px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,49 +1,19 @@
|
|||||||
import { useStorage } from '@vueuse/core'
|
|
||||||
import { useProduct } from './useProduct'
|
|
||||||
|
|
||||||
export interface ICartItem {
|
|
||||||
variation_id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICart {
|
|
||||||
line_items: ICartItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCart = () => {
|
export const useCart = () => {
|
||||||
const cart = useStorage<ICart>('cart', { line_items: [] })
|
const addToCart = (item: any) => {
|
||||||
|
if (process.client) {
|
||||||
const cartAddItem = (item: ICartItem) => {
|
localStorage.setItem('cart', JSON.stringify(item))
|
||||||
cart.value.line_items.push(item)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cartRemoveItem = (item: ICartItem) => {
|
const getCart = () => {
|
||||||
cart?.value?.line_items
|
if (process.client) {
|
||||||
?.splice(cart?.value?.line_items
|
return localStorage.getItem('cart')
|
||||||
?.findIndex((cartItem: ICartItem) =>
|
|
||||||
cartItem?.variation_id === item?.variation_id), 1)
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
const cartRemoveAllItems = () => {
|
|
||||||
cart.value = { line_items: [] }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cartProducts = computed(() => cart?.value?.line_items?.map((line_item) => {
|
|
||||||
const { productsData } = useProduct(line_item?.variation_id.toString())
|
|
||||||
return productsData
|
|
||||||
}))
|
|
||||||
|
|
||||||
const cartSum = computed(() => cartProducts?.value?.reduce((acc, curr) => {
|
|
||||||
acc += Number(curr?.value?.price)
|
|
||||||
return acc
|
|
||||||
}, 0))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cart,
|
addToCart,
|
||||||
cartAddItem,
|
getCart,
|
||||||
cartRemoveItem,
|
|
||||||
cartRemoveAllItems,
|
|
||||||
|
|
||||||
cartProducts,
|
|
||||||
cartSum,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
import type { PickupPoint } from '#shared/yandex_pvz'
|
|
||||||
import { createSharedComposable, useMediaQuery, useStorage } from '@vueuse/core'
|
|
||||||
|
|
||||||
export const useCheckout = createSharedComposable(() => {
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
|
|
||||||
const checkoutPickupPoint = useStorage<PickupPoint | undefined>(
|
|
||||||
'checkout-pickupPoint',
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
serializer: {
|
|
||||||
read: (v: string) => v ? JSON.parse(v) : undefined,
|
|
||||||
write: (v: PickupPoint | undefined) => JSON.stringify(v),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const setCheckoutPickupPoint = (point: PickupPoint | undefined) => {
|
|
||||||
checkoutPickupPoint.value = point
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPickupPointSelected = computed(() => !!checkoutPickupPoint.value)
|
|
||||||
|
|
||||||
const checkoutContacts = useStorage('checkout-contacts', { name: '', surname: '', phone: '', email: '' })
|
|
||||||
|
|
||||||
const setCheckoutContacts = (data: { name: string, surname: string, phone: string, email: string }) => {
|
|
||||||
checkoutContacts.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkoutSteps = [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
title: isMobile.value ? 'mobileDelivery' : 'delivery',
|
|
||||||
route: 'delivery',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
title: isMobile.value ? 'mobileContacts' : 'contacts',
|
|
||||||
route: 'contacts',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
title: isMobile.value ? 'mobileSummary' : 'summary',
|
|
||||||
route: 'summary',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const currentCheckoutStep
|
|
||||||
= ref(checkoutSteps.find(value => value.route === route.path.split('/').pop()) || checkoutSteps[0])
|
|
||||||
|
|
||||||
function previewStep() {
|
|
||||||
if (isPickupPointSelected.value && !isMobile.value) {
|
|
||||||
setCheckoutPickupPoint(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
|
||||||
if (findIndex !== 0) {
|
|
||||||
currentCheckoutStep.value = checkoutSteps[findIndex - 1]
|
|
||||||
router.push(`/checkout/${currentCheckoutStep?.value.route}`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
router.push(`/cart`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextStep() {
|
|
||||||
const findIndex = checkoutSteps.findIndex(value => value.step === currentCheckoutStep.value.step)
|
|
||||||
if (findIndex + 1 !== checkoutSteps.length) {
|
|
||||||
currentCheckoutStep.value = checkoutSteps[findIndex + 1]
|
|
||||||
router.push(`/checkout/${currentCheckoutStep?.value.route}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStep(pathName: string) {
|
|
||||||
currentCheckoutStep.value = checkoutSteps.find(value => value.route === pathName) || checkoutSteps[0]
|
|
||||||
router.push(`/checkout/${pathName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isPickupPointSelected,
|
|
||||||
checkoutPickupPoint,
|
|
||||||
setCheckoutPickupPoint,
|
|
||||||
|
|
||||||
checkoutContacts,
|
|
||||||
setCheckoutContacts,
|
|
||||||
|
|
||||||
checkoutSteps,
|
|
||||||
currentCheckoutStep,
|
|
||||||
|
|
||||||
previewStep,
|
|
||||||
nextStep,
|
|
||||||
setStep,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useGetProductsDetail, useGetProductsVariationsList } from '~/api/queries/wp'
|
import { useGetProductsDetail, useGetProductsVariationsList } from '~/api/queries'
|
||||||
|
|
||||||
export const useProduct = (variantId?: string) => {
|
export const useProduct = () => {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const currentId = ref<number>(route.params.id ?? variantId)
|
const currentId = ref<number>(route.params.id)
|
||||||
|
|
||||||
const { data: productsData } = useGetProductsDetail(currentId)
|
const { data: productsData } = useGetProductsDetail(currentId)
|
||||||
const { data: productsVariationsData } = useGetProductsVariationsList(currentId)
|
const { data: productsVariationsData } = useGetProductsVariationsList(currentId)
|
||||||
@ -24,12 +24,6 @@ export const useProduct = (variantId?: string) => {
|
|||||||
const materials = computed(() => getAttribute(productsData?.value?.attributes, 'material')?.options)
|
const materials = computed(() => getAttribute(productsData?.value?.attributes, 'material')?.options)
|
||||||
const sizes = computed(() => getAttribute(productsData?.value?.attributes, 'size')?.options)
|
const sizes = computed(() => getAttribute(productsData?.value?.attributes, 'size')?.options)
|
||||||
|
|
||||||
const color = computed(() => getAttribute(productsData?.value?.attributes, 'color')?.option)
|
|
||||||
const material = computed(() => getAttribute(productsData?.value?.attributes, 'material')?.option)
|
|
||||||
const size = computed(() => getAttribute(productsData?.value?.attributes, 'size')?.option)
|
|
||||||
|
|
||||||
const photoModel = computed(() => productsData?.value?.meta_data?.find(meta => meta?.key === 'photoModel').value?.split(','))
|
|
||||||
|
|
||||||
function getIdentifier(productVariant) {
|
function getIdentifier(productVariant) {
|
||||||
const color = getAttribute(productVariant?.attributes, 'color')?.option
|
const color = getAttribute(productVariant?.attributes, 'color')?.option
|
||||||
const material = getAttribute(productVariant?.attributes, 'material')?.option
|
const material = getAttribute(productVariant?.attributes, 'material')?.option
|
||||||
@ -66,12 +60,6 @@ export const useProduct = (variantId?: string) => {
|
|||||||
materials,
|
materials,
|
||||||
sizes,
|
sizes,
|
||||||
|
|
||||||
color,
|
|
||||||
material,
|
|
||||||
size,
|
|
||||||
|
|
||||||
photoModel,
|
|
||||||
|
|
||||||
variations,
|
variations,
|
||||||
|
|
||||||
getAttribute,
|
getAttribute,
|
||||||
|
|||||||
@ -1,24 +1,17 @@
|
|||||||
import { computed } from 'vue'
|
import { useGetProductsList } from '~/api/queries'
|
||||||
import { useGetProductsList } from '~/api/queries/wp'
|
|
||||||
import { useProduct } from '~/composables'
|
|
||||||
|
|
||||||
export const useProductsList = () => {
|
export const useProductsList = () => {
|
||||||
const { getAttribute } = useProduct()
|
const { getAttribute } = useProduct()
|
||||||
|
|
||||||
// TODO перенести запрос на сервер, на сервере получать id вариантов и делать запросы у useProduct(id),
|
|
||||||
// получать варианты и вместе со всеми вариантами ренедрить список товаров
|
|
||||||
const { data: productData } = useGetProductsList()
|
const { data: productData } = useGetProductsList()
|
||||||
|
|
||||||
const productCardData = computed(() =>
|
const productCardData = computed(() => productData?.value?.map(product => ({
|
||||||
productData?.value?.map(product => ({
|
|
||||||
id: product?.id,
|
id: product?.id,
|
||||||
name: product?.name,
|
name: product?.name,
|
||||||
price: product?.price,
|
price: product?.price,
|
||||||
variations: product?.variations,
|
variations: product?.variations,
|
||||||
images: product?.images?.slice(0, 5),
|
images: product?.images?.slice(0, 5),
|
||||||
colors: getAttribute(product?.attributes, 'color')?.options,
|
colors: getAttribute(product?.attributes, 'color')?.options,
|
||||||
})) ?? [],
|
})) ?? [])
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
productCardData,
|
productCardData,
|
||||||
|
|||||||
@ -8,13 +8,5 @@
|
|||||||
"materials": {
|
"materials": {
|
||||||
"cotton": "хлопок",
|
"cotton": "хлопок",
|
||||||
"cotton-polyester": "хлопок-полиэстер"
|
"cotton-polyester": "хлопок-полиэстер"
|
||||||
},
|
|
||||||
"checkoutSteps": {
|
|
||||||
"delivery": "Выберите адрес получения заказа",
|
|
||||||
"mobileDelivery": "доставка",
|
|
||||||
"contacts": "Введите данные получателя",
|
|
||||||
"mobileContacts": "контакты",
|
|
||||||
"summary": "Подтвердите заказ",
|
|
||||||
"mobileSummary": "заказ"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="layout">
|
|
||||||
<header class="header">
|
|
||||||
<div class="header__container">
|
|
||||||
<Icon class="cursor-pointer w-6 h-6" name="lucide:arrow-left" @click="previewStep" />
|
|
||||||
|
|
||||||
<h3>
|
|
||||||
Шаг {{ currentCheckoutStep?.step }} из {{ checkoutSteps?.length }}
|
|
||||||
•
|
|
||||||
{{ t(`checkoutSteps.${currentCheckoutStep?.title}`) }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main" :style="showFooter ? 'margin-bottom: 54px' : 'margin-bottom: 0'">
|
|
||||||
<UContainer class="container">
|
|
||||||
<slot />
|
|
||||||
</UContainer>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer" :class="{ 'footer--hidden': !showFooter }" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
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()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
--ui-container: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
height: 54px;
|
|
||||||
|
|
||||||
&__container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
flex: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
height: 70px;
|
|
||||||
background-color: #0f172b;
|
|
||||||
|
|
||||||
&--hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
margin-top: 54px;
|
|
||||||
margin-bottom: 64px;
|
|
||||||
|
|
||||||
&__hide-footer {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -11,18 +11,11 @@
|
|||||||
PAXTON
|
PAXTON
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div
|
|
||||||
class="header__cart"
|
|
||||||
@click="router.push(`/cart`)"
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="lucide:shopping-cart"
|
name="lucide:shopping-cart"
|
||||||
class="cursor-pointer w-6 h-6 text-gray-700"
|
class="cursor-pointer mr20 w-6 h-6 text-gray-700"
|
||||||
|
@click="router.push(`/cart`)"
|
||||||
/>
|
/>
|
||||||
<div v-if="cart?.line_items?.length" class="header__cart-count">
|
|
||||||
{{ cart.line_items.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -32,20 +25,14 @@
|
|||||||
</UContainer>
|
</UContainer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer" :class="{ 'footer--hidden': !(route.path === '/cart' && isMobile) }" />
|
<footer class="footer">
|
||||||
|
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 isMobile = useMediaQuery('(max-width: 1280px)', {
|
|
||||||
ssrWidth: 768,
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -58,6 +45,7 @@ const isMobile = useMediaQuery('(max-width: 1280px)', {
|
|||||||
.container {
|
.container {
|
||||||
--ui-container: 100%;
|
--ui-container: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -79,54 +67,24 @@ const isMobile = useMediaQuery('(max-width: 1280px)', {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__headline {
|
&__headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: black;
|
color: black;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cart {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cart-count {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
right: -6px;
|
|
||||||
background: red;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-block: 54px 64px;
|
margin-top: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,29 +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,
|
|
||||||
compatibilityDate: '2025-05-15',
|
compatibilityDate: '2025-05-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: [
|
modules: ['@nuxt/ui', '@nuxt/image', '@nuxt/icon', '@nuxt/fonts', '@nuxtjs/i18n'],
|
||||||
'@nuxt/ui',
|
|
||||||
'@nuxt/image',
|
|
||||||
'@nuxt/icon',
|
|
||||||
'@nuxt/fonts',
|
|
||||||
'@nuxtjs/i18n',
|
|
||||||
[
|
|
||||||
'@vee-validate/nuxt',
|
|
||||||
{
|
|
||||||
autoImports: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'vue-yandex-maps/nuxt',
|
|
||||||
],
|
|
||||||
yandexMaps: {
|
|
||||||
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: {
|
||||||
locales: [
|
locales: [
|
||||||
@ -34,26 +13,4 @@ export default defineNuxtConfig({
|
|||||||
strategy: 'prefix_except_default',
|
strategy: 'prefix_except_default',
|
||||||
detectBrowserLanguage: false,
|
detectBrowserLanguage: false,
|
||||||
},
|
},
|
||||||
app: {
|
|
||||||
head: {
|
|
||||||
meta: [
|
|
||||||
{
|
|
||||||
name: 'viewport',
|
|
||||||
content: 'width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, user-scalable=no',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vite: {
|
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
scss: {
|
|
||||||
additionalData: `@use "${resolve('./assets/scss/mixins')}" as *;`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fonts: {
|
|
||||||
provider: 'google',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
16419
package-lock.json
generated
16419
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -13,26 +13,20 @@
|
|||||||
"typegen": "node .typegen"
|
"typegen": "node .typegen"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^3.7.1",
|
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "1.15.0",
|
"@nuxt/icon": "1.15.0",
|
||||||
"@nuxt/image": "1.10.0",
|
"@nuxt/image": "1.10.0",
|
||||||
"@nuxt/ui": "^4.0.1",
|
"@nuxt/ui": "3.2.0",
|
||||||
"@nuxtjs/i18n": "^10.0.4",
|
"@nuxtjs/i18n": "^10.0.4",
|
||||||
"@tanstack/vue-query": "^5.75.5",
|
"@tanstack/vue-query": "^5.75.5",
|
||||||
"@tanstack/vue-query-devtools": "^5.87.1",
|
"@tanstack/vue-query-devtools": "^5.87.1",
|
||||||
"@vee-validate/nuxt": "^4.15.1",
|
|
||||||
"@vueuse/core": "^13.1.0",
|
"@vueuse/core": "^13.1.0",
|
||||||
"axios": "^1.12.2",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"decimal.js": "^10.5.0",
|
"decimal.js": "^10.5.0",
|
||||||
"maska": "^3.2.0",
|
"nuxt": "^3.17.6",
|
||||||
"nuxt": "^4.1.3",
|
|
||||||
"swiper": "^12.0.2",
|
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1"
|
||||||
"vue-yandex-maps": "^2.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^4.13.2",
|
"@antfu/eslint-config": "^4.13.2",
|
||||||
|
|||||||
@ -1,86 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cart">
|
<div class="cart">
|
||||||
<div v-if="cart.line_items.length > 0" class="cart__items">
|
<div v-if="cartProducts">
|
||||||
<div
|
{{ cartProducts }}
|
||||||
v-for="cartItem in cart?.line_items"
|
|
||||||
:key="cartItem.variation_id"
|
|
||||||
>
|
|
||||||
<CartItem :cart-item="cartItem" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<UButton @click="createOrder">
|
||||||
Корзина пока что пуста
|
Оформить заказ
|
||||||
</div>
|
</UButton>
|
||||||
|
|
||||||
<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 { usePostOrdersCreate } from '~/api/mutations'
|
||||||
import { useCart } from '~/composables'
|
|
||||||
import PayBlock from '../components/PayBlock.vue'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const { getCart } = useCart()
|
||||||
const { cart, cartRemoveAllItems } = useCart()
|
const cartProducts = getCart()
|
||||||
const { checkoutContacts, checkoutPickupPoint } = useCheckout()
|
const { mutateAsync } = usePostOrdersCreate()
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
const createOrder = () => {
|
||||||
|
mutateAsync(cartProducts)
|
||||||
// Зарпос должен быть в админке bsbp, ждём доступов. Текущее решение просто создает order в WooCommerce
|
}
|
||||||
onMounted(async () => {
|
|
||||||
if (!route?.query?.ID || cart.value.line_items.length === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
await wait(5000)
|
|
||||||
await useFetch<WooOrderCreateResponse>('/api/woo_orders_create', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
payment_method: 'bacs',
|
|
||||||
payment_method_title: 'Оплата по реквизитам',
|
|
||||||
set_paid: true,
|
|
||||||
billing: {
|
|
||||||
first_name: checkoutContacts?.value?.name,
|
|
||||||
last_name: checkoutContacts?.value?.surname,
|
|
||||||
phone: checkoutContacts?.value?.phone,
|
|
||||||
email: checkoutContacts?.value?.email,
|
|
||||||
address_1: checkoutPickupPoint.value?.address?.full_address,
|
|
||||||
postcode: checkoutPickupPoint.value?.address?.postal_code,
|
|
||||||
city: checkoutPickupPoint?.value?.address?.locality,
|
|
||||||
country: 'RU',
|
|
||||||
},
|
|
||||||
transaction_id: route?.query?.ID,
|
|
||||||
line_items: cart.value.line_items,
|
|
||||||
status: 'processing',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cartRemoveAllItems()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.cart {
|
|
||||||
margin-top: 120px;
|
|
||||||
margin-inline: auto;
|
|
||||||
max-width: 1200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin-top: 20px;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-inline: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form v-if="checkoutContacts" class="contacts" @submit.prevent="onSubmit">
|
|
||||||
<div>
|
|
||||||
<UInput
|
|
||||||
id="name"
|
|
||||||
v-model="name"
|
|
||||||
v-bind="nameAttrs"
|
|
||||||
name="name"
|
|
||||||
size="xl"
|
|
||||||
placeholder=" "
|
|
||||||
:ui="{ base: 'peer' }"
|
|
||||||
:invalid="Boolean(errors.name)"
|
|
||||||
:help="errors.name"
|
|
||||||
class="relative w-100"
|
|
||||||
:color="errors.name ? 'error' : 'neutral'"
|
|
||||||
highlight
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="pointer-events-none absolute left-3 text-base transition-all peer-focus:-top-3 peer-focus:text-highlighted peer-focus:text-sm peer-focus:font-medium" :class="[
|
|
||||||
name
|
|
||||||
? '-top-3 text-sm text-highlighted font-medium'
|
|
||||||
: 'top-3 text-dimmed peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-dimmed',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span class="inline-flex bg-default px-1">Имя</span>
|
|
||||||
</label>
|
|
||||||
</UInput>
|
|
||||||
|
|
||||||
<div v-if="errors?.name" style="color: #ff6467">
|
|
||||||
{{ errors?.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<UInput
|
|
||||||
id="surname"
|
|
||||||
v-model="surname"
|
|
||||||
v-bind="surnameAttrs"
|
|
||||||
name="surname"
|
|
||||||
size="xl"
|
|
||||||
placeholder=" "
|
|
||||||
:ui="{ base: 'peer' }"
|
|
||||||
:invalid="Boolean(errors.surname)"
|
|
||||||
:help="errors.surname"
|
|
||||||
class="relative w-100"
|
|
||||||
:color="errors.surname ? 'error' : 'neutral'"
|
|
||||||
highlight
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="pointer-events-none absolute left-3 text-base transition-all peer-focus:-top-3 peer-focus:text-highlighted peer-focus:text-sm peer-focus:font-medium" :class="[
|
|
||||||
surname
|
|
||||||
? '-top-3 text-sm text-highlighted font-medium'
|
|
||||||
: 'top-3 text-dimmed peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-dimmed',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span class="inline-flex bg-default px-1">Фамилия</span>
|
|
||||||
</label>
|
|
||||||
</UInput>
|
|
||||||
|
|
||||||
<div v-if="errors?.surname" style="color: #ff6467">
|
|
||||||
{{ errors?.surname }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<UInput
|
|
||||||
id="phone"
|
|
||||||
v-model="phone"
|
|
||||||
v-bind="phoneAttrs"
|
|
||||||
v-maska="'+7 (###) ###-##-##'"
|
|
||||||
name="phone"
|
|
||||||
size="xl"
|
|
||||||
placeholder=" "
|
|
||||||
:ui="{ base: 'peer' }"
|
|
||||||
:invalid="Boolean(errors.phone)"
|
|
||||||
:help="errors.phone"
|
|
||||||
class="relative w-100"
|
|
||||||
:color="errors.phone ? 'error' : 'neutral'"
|
|
||||||
highlight
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="pointer-events-none absolute left-3 text-base transition-all peer-focus:-top-3 peer-focus:text-highlighted peer-focus:text-sm peer-focus:font-medium" :class="[
|
|
||||||
phone
|
|
||||||
? '-top-3 text-sm text-highlighted font-medium'
|
|
||||||
: 'top-3 text-dimmed peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-dimmed',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span class="inline-flex bg-default px-1">телефон</span>
|
|
||||||
</label>
|
|
||||||
</UInput>
|
|
||||||
|
|
||||||
<div v-if="errors?.phone" style="color: #ff6467">
|
|
||||||
{{ errors?.phone }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<UInput
|
|
||||||
id="email"
|
|
||||||
v-model="email"
|
|
||||||
v-bind="emailAttrs"
|
|
||||||
name="email"
|
|
||||||
size="xl"
|
|
||||||
placeholder=" "
|
|
||||||
:ui="{ base: 'peer' }"
|
|
||||||
class="relative w-100"
|
|
||||||
trailing-icon="i-lucide-at-sign"
|
|
||||||
:invalid="Boolean(errors.email)"
|
|
||||||
:help="errors.email"
|
|
||||||
:color="errors.email ? 'error' : 'neutral'"
|
|
||||||
highlight
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="pointer-events-none absolute left-3 text-base transition-all peer-focus:-top-3 peer-focus:text-highlighted peer-focus:text-sm peer-focus:font-medium" :class="[
|
|
||||||
email
|
|
||||||
? '-top-3 text-sm text-highlighted font-medium'
|
|
||||||
: 'top-3 text-dimmed peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-dimmed',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span class="inline-flex bg-default px-1">email</span>
|
|
||||||
</label>
|
|
||||||
</UInput>
|
|
||||||
|
|
||||||
<div v-if="errors?.email" style="color: #ff6467">
|
|
||||||
{{ errors?.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
size="xl"
|
|
||||||
label="продолжить"
|
|
||||||
class="justify-center text-center"
|
|
||||||
type="submit"
|
|
||||||
:disabled="errors && Object.keys(errors).length > 0"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useCheckout } from '~/composables/useCheckout'
|
|
||||||
|
|
||||||
const { checkoutContacts, setCheckoutContacts, nextStep } = useCheckout()
|
|
||||||
|
|
||||||
const { errors, handleSubmit, defineField } = useForm({
|
|
||||||
initialValues: {
|
|
||||||
name: checkoutContacts.value.name,
|
|
||||||
surname: checkoutContacts.value.surname,
|
|
||||||
phone: checkoutContacts.value.phone,
|
|
||||||
email: checkoutContacts.value.email,
|
|
||||||
},
|
|
||||||
validationSchema: {
|
|
||||||
name(value: string) {
|
|
||||||
if (!value)
|
|
||||||
return 'Укажите имя'
|
|
||||||
if (value.trim().length < 2)
|
|
||||||
return 'Минимум 2 символа'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
surname(value: string) {
|
|
||||||
if (!value)
|
|
||||||
return 'Укажите фамилию'
|
|
||||||
if (value.trim().length < 2)
|
|
||||||
return 'Минимум 2 символа'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
phone(value: string) {
|
|
||||||
if (!value)
|
|
||||||
return 'Укажите телефон'
|
|
||||||
const digits = (value || '').replace(/\D/g, '')
|
|
||||||
if (digits.length < 11)
|
|
||||||
return 'Введите корректный телефон'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
email(value: string) {
|
|
||||||
if (!value)
|
|
||||||
return 'Укажите email'
|
|
||||||
const re = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
|
|
||||||
if (!re.test(value))
|
|
||||||
return 'Введите корректный email'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const [name, nameAttrs] = defineField('name')
|
|
||||||
const [surname, surnameAttrs] = defineField('surname')
|
|
||||||
const [phone, phoneAttrs] = defineField('phone')
|
|
||||||
const [email, emailAttrs] = defineField('email')
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit((values) => {
|
|
||||||
setCheckoutContacts(values)
|
|
||||||
nextStep()
|
|
||||||
})
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'checkout',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.contacts {
|
|
||||||
margin-inline: auto;
|
|
||||||
max-width: 400px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
padding-inline: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="coords" class="delivery">
|
|
||||||
<!-- Desktop -->
|
|
||||||
<div v-if="!isMobile" class="delivery__sidebar">
|
|
||||||
<DeliveryInfo v-if="isPickupPointSelected" />
|
|
||||||
|
|
||||||
<DeliverySearch
|
|
||||||
v-else
|
|
||||||
v-model:checkout-pickup-point="checkoutPickupPoint"
|
|
||||||
v-model:search-term="searchTerm"
|
|
||||||
:filtered-points="filteredPoints"
|
|
||||||
@update:checkout-pickup-point="updatePoint()"
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
v-model:checkout-pickup-point="checkoutPickupPoint"
|
|
||||||
v-model:active-tab="activeTab"
|
|
||||||
v-model:fitting="fitting"
|
|
||||||
:pickup-points="filteredPoints"
|
|
||||||
@update:checkout-pickup-point="updatePoint()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { YandexLocationDetectResponse } from '#shared/yandex_location_detect'
|
|
||||||
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 DeliveryInfo from '~/components/DeliveryInfo.vue'
|
|
||||||
import DeliverySearch from '~/components/DeliverySearch.vue'
|
|
||||||
import PvzMap from '~/components/PvzMap.vue'
|
|
||||||
import { useCheckout } from '~/composables/useCheckout'
|
|
||||||
|
|
||||||
const { setCheckoutPickupPoint, isPickupPointSelected, checkoutPickupPoint } = useCheckout()
|
|
||||||
const { coords } = useGeolocation()
|
|
||||||
const open = ref(false)
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
const yandexPvz = ref<YandexPvzResponse>()
|
|
||||||
const searchTerm = ref('')
|
|
||||||
const activeTab = ref()
|
|
||||||
const fitting = ref(IPvzMapFittingTabs.ALL)
|
|
||||||
const openDeliverySearch = ref()
|
|
||||||
|
|
||||||
const waitForCoords = () =>
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
const stop = watch(coords, (newCoords) => {
|
|
||||||
if (newCoords.latitude && newCoords.longitude) {
|
|
||||||
stop()
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await waitForCoords()
|
|
||||||
|
|
||||||
// обратное геокодирование (т.е. получаение города из координат)
|
|
||||||
const response = await fetch(
|
|
||||||
`https://nominatim.openstreetmap.org/reverse?lat=${coords.value.latitude}&lon=${coords.value.longitude}&format=json&accept-language=ru`,
|
|
||||||
)
|
|
||||||
const openstreetmap = await response.json()
|
|
||||||
|
|
||||||
// получение geo_id из названию города
|
|
||||||
const { data: yandexLocation } = await useFetch<YandexLocationDetectResponse>('/api/yandex_location_detect', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
location: openstreetmap?.address?.city,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// получения пунктов выдачи города из geo_id
|
|
||||||
const { data: yandexPvzApi } = await useFetch<YandexPvzResponse>('/api/yandex_pvz', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
geo_id: yandexLocation?.value?.variants[0]?.geo_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
yandexPvz.value = yandexPvzApi.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredPoints = computed<YandexPvzPoint[]>(() => {
|
|
||||||
const points = yandexPvz.value?.points || []
|
|
||||||
const term = searchTerm.value?.toLowerCase() || ''
|
|
||||||
|
|
||||||
return points.filter((point) => {
|
|
||||||
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({
|
|
||||||
layout: 'checkout',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.delivery {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
&__sidebar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-height: calc(100vh - 40px);
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 410px;
|
|
||||||
padding-inline: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="summary">
|
|
||||||
<div class="summary__info">
|
|
||||||
<h3>в заказе • {{ cart?.line_items?.length }} шт</h3>
|
|
||||||
|
|
||||||
<div class="summary__items">
|
|
||||||
<div
|
|
||||||
v-for="cartItem in cart?.line_items"
|
|
||||||
:key="cartItem.variation_id"
|
|
||||||
>
|
|
||||||
<SummaryCartItem :summary-cart-item="cartItem" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<h3 class="d-flex gap-1 text-center align-items-center">
|
|
||||||
Yandex
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
size="sm"
|
|
||||||
class="text-muted-foreground mla cursor-pointer"
|
|
||||||
variant="ghost"
|
|
||||||
icon="i-ph-pencil-simple-line"
|
|
||||||
@click="setStep('delivery')"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<DeliveryInfo :show-full-content="false" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<h3 class="d-flex gap-1 text-center align-items-center">
|
|
||||||
Получатель
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
size="sm"
|
|
||||||
class="text-muted-foreground mla cursor-pointer"
|
|
||||||
variant="ghost"
|
|
||||||
icon="i-ph-pencil-simple-line"
|
|
||||||
@click="setStep('contacts')"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-ph-user" class="text-muted-foreground" />
|
|
||||||
<span>{{ checkoutContacts?.name }} {{ checkoutContacts?.surname }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-ph-envelope-simple" class="text-muted-foreground" />
|
|
||||||
<span>{{ checkoutContacts?.email }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UIcon name="i-ph-phone" class="text-muted-foreground" />
|
|
||||||
<span>{{ checkoutContacts?.phone }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<PayBlock pay :is-summary="true" />
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import SummaryCartItem from '../../components/cart/SummaryCartItem.vue'
|
|
||||||
import PayBlock from '../../components/PayBlock.vue'
|
|
||||||
|
|
||||||
const { cart } = useCart()
|
|
||||||
const { checkoutContacts, setStep } = useCheckout()
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'checkout',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.summary {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin-inline: auto;
|
|
||||||
padding-inline: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin-bottom: 26px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="index">
|
<div class="index">
|
||||||
<div v-if="productCardData.length" class="cards-list">
|
|
||||||
<div
|
<div
|
||||||
v-for="product in pizda"
|
class="cards-list"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="product in productCardData"
|
||||||
:key="product.id"
|
:key="product.id"
|
||||||
class="card"
|
class="card"
|
||||||
@click="router.push(`/product/${product.id}`)"
|
@click="router.push(`/product/${product.id}`)"
|
||||||
>
|
>
|
||||||
<img class="card__image" :src="product?.images?.[0]?.src" alt="card?.image">
|
<img class="card__image" :src="product?.images[0]?.src" alt="card?.image">
|
||||||
|
|
||||||
<div class="card__description">
|
<div class="card__description">
|
||||||
<div>{{ product?.name }}</div>
|
<div>{{ product?.name }}</div>
|
||||||
@ -30,11 +32,6 @@
|
|||||||
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>
|
||||||
|
|
||||||
@ -43,37 +40,22 @@ const router = useRouter()
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
gap: 30px 4px;
|
gap: 4px;
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
min-width: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
padding: 10px 5px;
|
padding-inline: 10px;
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
padding: 8px 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,109 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="target" class="product">
|
<div class="product">
|
||||||
<ProductImages v-if="isMobile" />
|
<ProductImages />
|
||||||
|
|
||||||
<div v-if="isMobile" class="product-info">
|
|
||||||
<div class="product-info__title">
|
|
||||||
<ProductPrice :price="currentVariant?.options[0]?.price || 0" />
|
|
||||||
|
|
||||||
<PhotoModel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>
|
|
||||||
{{ productsData?.name }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="mt20">
|
|
||||||
<UButton
|
|
||||||
icon="lucide:shopping-cart"
|
|
||||||
class="justify-content-center w-100 d-flex"
|
|
||||||
label="Добавить"
|
|
||||||
size="xl"
|
|
||||||
@click="open = !open"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UDrawer
|
|
||||||
v-if="isMobile"
|
|
||||||
v-model:open="open"
|
|
||||||
class="product__drawer"
|
|
||||||
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 ref="targetDrawer" style="overflow-y: auto;">
|
|
||||||
<ProductDescription />
|
<ProductDescription />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
|
||||||
|
|
||||||
<ProductImages v-if="!isMobile" />
|
|
||||||
<ProductDescription v-if="!isMobile" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UseSwipeDirection } from '@vueuse/core'
|
|
||||||
import { useMediaQuery, useScroll, useSwipe } from '@vueuse/core'
|
|
||||||
import { computed, shallowRef } from 'vue'
|
|
||||||
import ProductPrice from '~/components/ProductPrice.vue'
|
|
||||||
import { useCurrentProduct, useProduct } from '~/composables'
|
|
||||||
|
|
||||||
const { productsData } = useProduct()
|
|
||||||
const { currentVariant } = useCurrentProduct()
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1280px)')
|
|
||||||
const open = ref(false)
|
|
||||||
const target = shallowRef<HTMLElement | null>(null)
|
|
||||||
const targetHeight = computed(() => target.value?.offsetHeight)
|
|
||||||
const targetDrawer = shallowRef<HTMLElement | null>(null)
|
|
||||||
const targetDrawerHeight = computed(() => targetDrawer.value?.offsetHeight)
|
|
||||||
const { y } = useScroll(targetDrawer)
|
|
||||||
|
|
||||||
const { lengthY } = useSwipe(
|
|
||||||
target,
|
|
||||||
{
|
|
||||||
passive: false,
|
|
||||||
onSwipeEnd(e: TouchEvent, direction: UseSwipeDirection) {
|
|
||||||
if (lengthY.value > 0 && targetHeight.value && (Math.abs(lengthY.value) / targetHeight.value) >= 0.1) {
|
|
||||||
open.value = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const { lengthY: drawerLengthY } = useSwipe(
|
|
||||||
targetDrawer,
|
|
||||||
{
|
|
||||||
passive: false,
|
|
||||||
onSwipeEnd(e: TouchEvent, direction: UseSwipeDirection) {
|
|
||||||
if (drawerLengthY.value < 0 && y.value === 0 && targetDrawerHeight.value && (Math.abs(drawerLengthY.value) / targetDrawerHeight.value) >= 0.2) {
|
|
||||||
open.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (isMobile.value) {
|
|
||||||
document.body.style.overflow = 'hidden'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (isMobile.value) {
|
|
||||||
document.body.style.overflow = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.product {
|
.product {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -113,22 +19,4 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
|
||||||
padding-inline: 15px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 40px;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { vMaska } from 'maska/vue'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.directive('maska', vMaska)
|
|
||||||
})
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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,33 +0,0 @@
|
|||||||
import type { WooOrderCreateRequest, WooOrderCreateResponse } from '#shared/woo_orders_create'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<WooOrderCreateResponse | { error: string }> => {
|
|
||||||
try {
|
|
||||||
const orderData = await readBody<WooOrderCreateRequest>(event)
|
|
||||||
|
|
||||||
const requestUrl = 'https://wp.koptilnya.xyz/wp-json/wc/v3/orders'
|
|
||||||
const consumerKey = 'ck_8b5477a1573ce6038ef1367f25d95cede1de4559'
|
|
||||||
const consumerSecret = 'cs_d0ccaa93e8efe4f76eef0b70c9828a58fc53459f'
|
|
||||||
|
|
||||||
const encodedAuth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64')
|
|
||||||
|
|
||||||
const response = await axios.post<WooOrderCreateResponse>(requestUrl, orderData, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Basic ${encodedAuth}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
console.error('Ошибка при создании заказа WooCommerce:', error)
|
|
||||||
|
|
||||||
return {
|
|
||||||
error: true,
|
|
||||||
message: error.response?.data?.message || 'Ошибка при создании заказа',
|
|
||||||
details: error.response?.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import type { YandexLocationDetectRequest, YandexLocationDetectResponse } from '#shared/yandex_location_detect'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<YandexLocationDetectResponse | { error: string }> => {
|
|
||||||
try {
|
|
||||||
const data = await readBody<YandexLocationDetectRequest>(event)
|
|
||||||
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
|
||||||
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
|
||||||
|
|
||||||
const response = await axios.post<YandexLocationDetectResponse>(
|
|
||||||
`${apiUrl}/location/detect`,
|
|
||||||
{
|
|
||||||
location: data?.location,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept-Language': 'ru-RU',
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Ошибка при запросе к Яндекс API:', error)
|
|
||||||
return { error: `Не удалось получить координаты: ${error}` }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import type { YandexPvzRequest, YandexPvzResponse } from '#shared/yandex_pvz'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { defineEventHandler, readBody } from 'h3'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<YandexPvzResponse | { error: string }> => {
|
|
||||||
try {
|
|
||||||
const data = await readBody<YandexPvzRequest>(event)
|
|
||||||
const apiUrl = process.env.VITE_YANDEX_B2B_BASE_URL!
|
|
||||||
const token = process.env.VITE_YANDEX_B2B_TOKEN!
|
|
||||||
|
|
||||||
const response = await axios.post<YandexPvzResponse>(
|
|
||||||
`${apiUrl}/pickup-points/list`,
|
|
||||||
{
|
|
||||||
geo_id: data?.geo_id,
|
|
||||||
type: 'pickup_point',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept-Language': 'ru-RU',
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Ошибка при запросе к Яндекс API:', error)
|
|
||||||
return { error: `Не удалось получить точки ПВЗ: ${error}` }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDRTCCAi0CATMwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCUlUxGTAXBgNV
|
|
||||||
BAcMEFNhaW50LVBldGVyc2J1cmcxDTALBgNVBAoMBEJTUEIxDDAKBgNVBAsMA0RQ
|
|
||||||
UzEXMBUGA1UEAwwOTUVSQ0hBTlRTX0JTUEIwHhcNMjUwMzA3MDY0MzM3WhcNMjYw
|
|
||||||
MzA3MDY0MzM3WjBzMQswCQYDVQQGEwJSVTEMMAoGA1UECBMDU1BiMQwwCgYDVQQH
|
|
||||||
EwNTUGIxDTALBgNVBAoTBEJTUEIxDTALBgNVBAsTBEJTUEIxDzANBgNVBAMTBnBn
|
|
||||||
dGVzdDEZMBcGCSqGSIb3DQEJARYKcGdAYnNwYi5ydTCCASIwDQYJKoZIhvcNAQEB
|
|
||||||
BQADggEPADCCAQoCggEBAMSPemQhfttoPicfiel1cW0Hx2HBb0xy/Gn3d1luIVj3
|
|
||||||
wOdh0eF5mVhbFbgo66eS3o7Arj4PkdV/+Od1KkCCJd96fJj1anPRTSeu2Z+X/l+0
|
|
||||||
8FqAECzVuYfHN/VDwvkV7d5R3+0+gkGGpodBnsudreTWG9bBVFb4lyCJvZeg+uWl
|
|
||||||
NyT79gWWkq51xwe+qAtNqKV3wPUUt1A4PXAJ5oJZF6IcXQs2wNRsJ25kiHPO9Wdz
|
|
||||||
Zibx6vJwnV5HP89hpM0ZdGEJKQT2c60ZfCNw8wDswmZSOp/mQmzkm1uiHOrp2Hdz
|
|
||||||
A9bqZaXYwhwTcmZBtOcCw+2eiuFiwVfV9nH/R84A2w0CAwEAATANBgkqhkiG9w0B
|
|
||||||
AQsFAAOCAQEAV7UMCKTCAH+gZsT53kzCXPV3iWgNK2LTle/GVAKZX1Jto4B1Yn4b
|
|
||||||
0KXmy0/PB2lRuwKogF40MwYN1lvn9qQ7Vohyi5qyd8kK4Z2CI5dYHjfwWjdW2fEz
|
|
||||||
A0qaV4VCCBZch76zbqRczI8ulUsH+o8cXsNgjw6QmrKJj74Otl6x2hQ34TP9W7yV
|
|
||||||
J/mOoxDavzulMRj01fcg+DhBqebMypU1okGWeqPMBtYWnpqObYWniKEdqNZ//Cfr
|
|
||||||
HTVQarDq92T67oCZ7CRu0+Ty7UL/l2SZu4EPlJhlNpg+ZYVqPoGqprjuYJ670l5c
|
|
||||||
uDxHwAWppDJWeQlwrZRcjuznoj1gtw6ojQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEowIBAAKCAQEAxI96ZCF+22g+Jx+J6XVxbQfHYcFvTHL8afd3WW4hWPfA52HR
|
|
||||||
4XmZWFsVuCjrp5LejsCuPg+R1X/453UqQIIl33p8mPVqc9FNJ67Zn5f+X7TwWoAQ
|
|
||||||
LNW5h8c39UPC+RXt3lHf7T6CQYamh0Gey52t5NYb1sFUVviXIIm9l6D65aU3JPv2
|
|
||||||
BZaSrnXHB76oC02opXfA9RS3UDg9cAnmglkXohxdCzbA1GwnbmSIc871Z3NmJvHq
|
|
||||||
8nCdXkc/z2GkzRl0YQkpBPZzrRl8I3DzAOzCZlI6n+ZCbOSbW6Ic6unYd3MD1upl
|
|
||||||
pdjCHBNyZkG05wLD7Z6K4WLBV9X2cf9HzgDbDQIDAQABAoIBAFpAtyX661C7UK6O
|
|
||||||
ILj9oBM8GySbuQsVUSkm47pAgzdiq0SS+dfaCbs0N4jT4UCUg3RwrJD6fS/XDubY
|
|
||||||
OYpdOB6hE0z4guSjGhY6htps4/P2FNa5LrQnfoUFyH/pmUDd/Na0KWm55f1IYnaA
|
|
||||||
fvVndU05spatNpiolFvwYwmYdRu0LtBkDc80aBVsX2WHuC2TD99qjIFjhNjgVI/v
|
|
||||||
E8jN3aqXsSFd0tKvBQivsAn/iwx8SyFJmrn2jddy2jewZdF6inB70TjvjY8fWNGb
|
|
||||||
TV2CIJj0ALmkIMS2/30LNWJZOOpCkQvcnO1seU5WACUDOQhG/iW2lXttRjL6C2PM
|
|
||||||
P0swDoECgYEA5I+CxZqYE2qa4/mdRZMvX+tlqrVGn3ix2mmVPJaageE6jW/vO2/j
|
|
||||||
synSpzTml0bpDapsY8Wj6Q30duuoZmQiSDi/uW2+aao81qxr7GWczwPRFdxmZX2i
|
|
||||||
jJrIe8f2gBkGaCxL2kzXvKyc4hackfI3SHDlXi38G7A0j0u+tU5JvpkCgYEA3Ch8
|
|
||||||
+04VrUprMmUDj4vt/BKPNk4Y1fVbvHwy9Mkfen4JoJHLUE2dqBizSkdia7RVd8eV
|
|
||||||
UT4kvS2kVofSt/rG0WkPD8rvtNBa7aqSj6JtvgBKrtbazU7hptzMiqoF/9Rw74b6
|
|
||||||
E1gbePlSB46ao2LGu1HOe80Rw9Ycq3bElBGrzJUCgYEAirVhmsTIeDghSiupu5io
|
|
||||||
jqDQcXpUIuHfpfqfvEZ1/E6Q91cwK7UqzgeatSkQrEw3kbiU0TQX1o9GholcCM/K
|
|
||||||
UmRGTqWAgqXzCCFZ2fyM3sGlOYwphHxrksM42o4vVexaDAyd+BzcbL+g8kDgwl3q
|
|
||||||
GQeS28Yykycrrq88TNH3RTkCgYAWoa7fYpKF4t2MK6gnDHplbD7+lR6md/d7M8VF
|
|
||||||
NpueyvAQaoxc7+2iBw//NcFfUwVqL8EgveOm8tcu8f1uXkAr7MHYnMLxcm22es9g
|
|
||||||
JpFjc8I5oOqTKmW18oKwSnQdbWhCpzxz2p3QXMja8ATjgNbvEKTKQzVtTUhbM/VX
|
|
||||||
R03C1QKBgGzat0GeWjYoQ8rCG9EuN2qGyvKTpD6VteygIXIVpL9sr1mXHAnfbnZC
|
|
||||||
OelMdoA/7nfRVIZ4yDbZclx8I0UOGx+rkqiyBgZ/+5BG24HT4sB9uIy5hE/odobR
|
|
||||||
aC58VQBAHijbEvvhNSXt96asV0tPVcLtG3V4zFDqF3NtR78e0OPg
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
export enum IPvzMapTabs {
|
|
||||||
MAP = 'map',
|
|
||||||
LIST = 'list',
|
|
||||||
}
|
|
||||||
export enum IPvzMapFittingTabs {
|
|
||||||
ALL = 'all',
|
|
||||||
ALLOW = 'allow',
|
|
||||||
FORBID = 'Forbid',
|
|
||||||
}
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
// ------------------------------------
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export interface YandexLocationVariantDetect {
|
|
||||||
geo_id: number
|
|
||||||
address: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YandexLocationDetectResponse {
|
|
||||||
variants: YandexLocationVariantDetect[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YandexLocationDetectRequest {
|
|
||||||
location: string
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
// -----------------------------
|
|
||||||
// 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[]
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user