[{"data":1,"prerenderedAt":24},["ShallowReactive",2],{"project:craft-cartel":3},{"html":4,"toc":5},"\u003Cp>Оптовому клиенту мало выбрать товар. После заказа ему нужно следить за исполнением, возвращаться к документам и сверять расчёты с поставщиком. В Craft Cartel мы собрали эти задачи в мобильном приложении: каталог напитков и снеков соседствует с личным кабинетом, историей заказов и запросом актов сверки.\u003C\u002Fp>\n\u003Cp>Мы связали интерфейс на Vue с серверной частью на «1С-Битрикс» и выпустили приложение для iOS и Android. Расскажу, как устроен путь клиента от просмотра ассортимента до возвращения к заказам и какие технические решения помогают пройти его с телефона.\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"\u002Fmedia\u002Fcases\u002Fcraft-cartel\u002Fhero-v1.webp\" alt=\"Каталог, карточка товара и фильтры Craft Cartel\" loading=\"lazy\" decoding=\"async\" \u002F>\u003C\u002Fp>\n\u003Cp>\u003Cem>Каталог, параметры товара и выбор условий фильтрации. Ассортимент и цены на экранах — на момент съёмки.\u003C\u002Fem>\u003C\u002Fp>\n\u003Ch2 id=\"section-1\">Каталог становится частью рабочего кабинета\u003C\u002Fh2>\n\u003Cp>Craft Cartel — оптовый поставщик крафтового пива, сидра, безалкогольных напитков и снеков. Это определяет устройство каталога: категории разделены на розлив и фасовку, рядом с названием товара указаны производитель, тара, объём и характеристики напитка. Клиент сравнивает позиции по параметрам закупки.\u003C\u002Fp>\n\u003Cp>Основой интерфейса служит Vue. Переходами между каталогом, карточкой и корзиной управляет Vue Router. Общие данные экранов хранятся в Vuex: например, состав корзины и состояние её загрузки. Axios выполняет запросы к серверу, а vue-axios подключает его к приложению.\u003C\u002Fp>\n\u003Cp>Так мы разделили ответственность. Мобильная часть показывает товары и передаёт действия клиента на сервер. Сервер рассчитывает корзину, оформляет заказ и возвращает данные, которые приложение показывает клиенту.\u003C\u002Fp>\n\u003Cp>Покажу, как эти части взаимодействуют при загрузке корзины.\u003C\u002Fp>\n\u003Cp>Например, здесь состояние корзины меняется только после получения ответа. Ошибка запроса не оставляет индикатор загрузки включённым:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: js\">\u003Ccode class=\"hljs language-js\">\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { createApp } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { createStore } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vuex'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { createRouter, createWebHistory } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue-router'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> axios \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'axios'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">VueAxios\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue-axios'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">App\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'.\u002FApp.vue'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">CartPage\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'.\u002FCartPage.vue'\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> http = axios.\u003Cspan class=\"hljs-title function_\">create\u003C\u002Fspan>({ \u003Cspan class=\"hljs-attr\">baseURL\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">'\u002Fexample-api\u002F'\u003C\u002Fspan> })\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> store = \u003Cspan class=\"hljs-title function_\">createStore\u003C\u002Fspan>({\n  \u003Cspan class=\"hljs-attr\">state\u003C\u002Fspan>: \u003Cspan class=\"hljs-function\">() =&gt;\u003C\u002Fspan> ({ \u003Cspan class=\"hljs-attr\">cart\u003C\u002Fspan>: [], \u003Cspan class=\"hljs-attr\">loading\u003C\u002Fspan>: \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>, \u003Cspan class=\"hljs-attr\">error\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">''\u003C\u002Fspan> }),\n  \u003Cspan class=\"hljs-attr\">mutations\u003C\u002Fspan>: {\n    \u003Cspan class=\"hljs-title function_\">patch\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">state, values\u003C\u002Fspan>) { \u003Cspan class=\"hljs-title class_\">Object\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">assign\u003C\u002Fspan>(state, values) }\n  },\n  \u003Cspan class=\"hljs-attr\">actions\u003C\u002Fspan>: {\n    \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">loadCart\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">{ commit }\u003C\u002Fspan>) {\n      \u003Cspan class=\"hljs-title function_\">commit\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'patch'\u003C\u002Fspan>, { \u003Cspan class=\"hljs-attr\">loading\u003C\u002Fspan>: \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>, \u003Cspan class=\"hljs-attr\">error\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">''\u003C\u002Fspan> })\n      \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> { data } = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> http.\u003Cspan class=\"hljs-title function_\">get\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'cart'\u003C\u002Fspan>)\n        \u003Cspan class=\"hljs-title function_\">commit\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'patch'\u003C\u002Fspan>, { \u003Cspan class=\"hljs-attr\">cart\u003C\u002Fspan>: data.\u003Cspan class=\"hljs-property\">items\u003C\u002Fspan> })\n      } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-title function_\">commit\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'patch'\u003C\u002Fspan>, { \u003Cspan class=\"hljs-attr\">error\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">'Не удалось загрузить корзину'\u003C\u002Fspan> })\n      } \u003Cspan class=\"hljs-keyword\">finally\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-title function_\">commit\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'patch'\u003C\u002Fspan>, { \u003Cspan class=\"hljs-attr\">loading\u003C\u002Fspan>: \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan> })\n      }\n    }\n  }\n})\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> router = \u003Cspan class=\"hljs-title function_\">createRouter\u003C\u002Fspan>({\n  \u003Cspan class=\"hljs-attr\">history\u003C\u002Fspan>: \u003Cspan class=\"hljs-title function_\">createWebHistory\u003C\u002Fspan>(),\n  \u003Cspan class=\"hljs-attr\">routes\u003C\u002Fspan>: [{ \u003Cspan class=\"hljs-attr\">path\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">'\u002Fcart'\u003C\u002Fspan>, \u003Cspan class=\"hljs-attr\">component\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">CartPage\u003C\u002Fspan> }]\n})\n\u003Cspan class=\"hljs-title function_\">createApp\u003C\u002Fspan>(\u003Cspan class=\"hljs-title class_\">App\u003C\u002Fspan>).\u003Cspan class=\"hljs-title function_\">use\u003C\u002Fspan>(store).\u003Cspan class=\"hljs-title function_\">use\u003C\u002Fspan>(router).\u003Cspan class=\"hljs-title function_\">use\u003C\u002Fspan>(\u003Cspan class=\"hljs-title class_\">VueAxios\u003C\u002Fspan>, http).\u003Cspan class=\"hljs-title function_\">mount\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'#app'\u003C\u002Fspan>)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>App\u003C\u002Fcode> — корневой компонент приложения, а \u003Ccode>CartPage\u003C\u002Fcode> отображает корзину и запускает её загрузку через \u003Ccode>store.dispatch('loadCart')\u003C\u002Fcode>. Сам состав заказа приходит с сервера, а Vuex даёт экрану единое место для чтения данных и состояния запроса.\u003C\u002Fp>\n\u003Ch2 id=\"section-2\">Найти нужное и получить отклик на касание\u003C\u002Fh2>\n\u003Cp>На главной клиент может перейти к новинкам и акциям, в каталоге — выбирать по разделам или брендам. Для горизонтальных подборок мы использовали Swiper с компонентами для Vue. Он работает в баннерах главной и товарных блоках, в том числе в подборке похожих товаров.\u003C\u002Fp>\n\u003Cp>Вместо длинного вертикального списка соседние позиции можно просмотреть свайпом. Минимальный пример такой подборки:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: vue\">\u003Ccode class=\"hljs language-vue\">\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">setup\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-javascript\">\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { \u003Cspan class=\"hljs-title class_\">Swiper\u003C\u002Fspan>, \u003Cspan class=\"hljs-title class_\">SwiperSlide\u003C\u002Fspan> } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'swiper\u002Fvue'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'swiper\u002Fcss'\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-title function_\">defineProps\u003C\u002Fspan>({ \u003Cspan class=\"hljs-attr\">products\u003C\u002Fspan>: { \u003Cspan class=\"hljs-attr\">type\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">Array\u003C\u002Fspan>, \u003Cspan class=\"hljs-attr\">required\u003C\u002Fspan>: \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan> } })\n\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Swiper\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:slides-per-view\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"1.4\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:space-between\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"12\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">SwiperSlide\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">v-for\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"product in products\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:key\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"product.id\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">article\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">h3\u003C\u002Fspan>&gt;\u003C\u002Fspan>{{ product.name }}\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">h3\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">p\u003C\u002Fspan>&gt;\u003C\u002Fspan>{{ product.pack }}\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">p\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">article\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">SwiperSlide\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">Swiper\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Часть следующей карточки остаётся видимой и подсказывает, что подборку можно пролистать. Содержимое карточки при этом остаётся обычной разметкой Vue.\u003C\u002Fp>\n\u003Cp>В списке товаров клиент открывает фильтры и сортировку. На сенсорном экране нет привычного наведения мыши, поэтому для части элементов мы добавили симуляцию hover: при касании появляется состояние нажатия, после отпускания оно снимается с небольшой задержкой. Кнопка отвечает на действие прямо под пальцем.\u003C\u002Fp>\n\u003Cp>Это собственный приём интерфейса. Его можно выразить отдельным Vue-компонентом: обработчики принадлежат кнопке и появляются вместе с ней, даже если сама панель открылась позднее.\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: vue\">\u003Ccode class=\"hljs language-vue\">\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">setup\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-javascript\">\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { ref, onBeforeUnmount } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue'\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> pressed = \u003Cspan class=\"hljs-title function_\">ref\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>)\n\u003Cspan class=\"hljs-keyword\">let\u003C\u002Fspan> timer\n\u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">press\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003C\u002Fspan>) {\n  \u003Cspan class=\"hljs-built_in\">clearTimeout\u003C\u002Fspan>(timer)\n  pressed.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> = \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>\n}\n\u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">release\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003C\u002Fspan>) {\n  timer = \u003Cspan class=\"hljs-built_in\">setTimeout\u003C\u002Fspan>(\u003Cspan class=\"hljs-function\">() =&gt;\u003C\u002Fspan> { pressed.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> = \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan> }, \u003Cspan class=\"hljs-number\">140\u003C\u002Fspan>)\n}\n\u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">cancel\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003C\u002Fspan>) {\n  \u003Cspan class=\"hljs-built_in\">clearTimeout\u003C\u002Fspan>(timer)\n  pressed.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> = \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>\n}\n\u003Cspan class=\"hljs-title function_\">onBeforeUnmount\u003C\u002Fspan>(cancel)\n\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">type\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"button\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:class\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"{ pressed }\"\u003C\u002Fspan>\n    @\u003Cspan class=\"hljs-attr\">touchstart.passive\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"press\"\u003C\u002Fspan> @\u003Cspan class=\"hljs-attr\">touchend\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"release\"\u003C\u002Fspan>\n    @\u003Cspan class=\"hljs-attr\">touchcancel\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"cancel\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    Фильтры\n  \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">style\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">scoped\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-css\">\n\u003Cspan class=\"hljs-selector-tag\">button\u003C\u002Fspan>\u003Cspan class=\"hljs-selector-class\">.pressed\u003C\u002Fspan> { \u003Cspan class=\"hljs-attribute\">background\u003C\u002Fspan>: \u003Cspan class=\"hljs-number\">#dce8ff\u003C\u002Fspan>; }\n\u003Cspan class=\"hljs-selector-tag\">button\u003C\u002Fspan>\u003Cspan class=\"hljs-selector-pseudo\">:focus-visible\u003C\u002Fspan> { \u003Cspan class=\"hljs-attribute\">outline\u003C\u002Fspan>: \u003Cspan class=\"hljs-number\">2px\u003C\u002Fspan> solid \u003Cspan class=\"hljs-number\">#24556c\u003C\u002Fspan>; }\n\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">style\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Задержка сохраняет короткий отклик после отпускания. Отмена жеста сбрасывает подсветку, а удаление компонента очищает таймер. Такая подсветка сообщает о касании; ожидание серверного ответа показывается отдельно.\u003C\u002Fp>\n\u003Ch2 id=\"section-3\">Не собирать одинаковые данные заново\u003C\u002Fh2>\n\u003Cp>За переходом к каталогу стоит работа сервера. Даже список разделов нужно подготовить для приложения. Для повторяющихся запросов мы используем кеширование в контроллерах «1С-Битрикс»: оно применяется, в частности, к разделам, брендам и баннерам главной.\u003C\u002Fp>\n\u003Cp>Контроллер сначала проверяет сохранённый результат. Если тот ещё действителен, ответ можно вернуть сразу; иначе данные собираются и записываются в кеш. Для публичного списка разделов этот порядок выглядит так. \u003Ccode>$loadSections\u003C\u002Fcode> — функция, которая получает актуальный список из источника данных:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: php\">\u003Ccode class=\"hljs language-php\">\u003Cspan class=\"hljs-keyword\">use\u003C\u002Fspan> \u003Cspan class=\"hljs-title\">Bitrix\u003C\u002Fspan>\\\u003Cspan class=\"hljs-title\">Main\u003C\u002Fspan>\\\u003Cspan class=\"hljs-title\">Data\u003C\u002Fspan>\\\u003Cspan class=\"hljs-title\">Cache\u003C\u002Fspan>;\n\n\u003Cspan class=\"hljs-function\">\u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title\">publicSections\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003Cspan class=\"hljs-keyword\">callable\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$loadSections\u003C\u002Fspan>\u003C\u002Fspan>): \u003Cspan class=\"hljs-title\">array\u003C\u002Fspan>\n\u003C\u002Fspan>{\n    \u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan> = \u003Cspan class=\"hljs-title class_\">Cache\u003C\u002Fspan>::\u003Cspan class=\"hljs-title function_\">createInstance\u003C\u002Fspan>();\n    \u003Cspan class=\"hljs-variable\">$ttl\u003C\u002Fspan> = \u003Cspan class=\"hljs-number\">300\u003C\u002Fspan>;\n    \u003Cspan class=\"hljs-variable\">$key\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">'example-public-sections-v1'\u003C\u002Fspan>;\n    \u003Cspan class=\"hljs-variable\">$directory\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">'\u002Fexample-case\u002Fsections'\u003C\u002Fspan>;\n\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (\u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan>-&gt;\u003Cspan class=\"hljs-title function_\">initCache\u003C\u002Fspan>(\u003Cspan class=\"hljs-variable\">$ttl\u003C\u002Fspan>, \u003Cspan class=\"hljs-variable\">$key\u003C\u002Fspan>, \u003Cspan class=\"hljs-variable\">$directory\u003C\u002Fspan>)) {\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan>-&gt;\u003Cspan class=\"hljs-title function_\">getVars\u003C\u002Fspan>();\n    }\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (!\u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan>-&gt;\u003Cspan class=\"hljs-title function_\">startDataCache\u003C\u002Fspan>()) {\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$loadSections\u003C\u002Fspan>();\n    }\n    \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-variable\">$sections\u003C\u002Fspan> = \u003Cspan class=\"hljs-variable\">$loadSections\u003C\u002Fspan>();\n        \u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan>-&gt;\u003Cspan class=\"hljs-title function_\">endDataCache\u003C\u002Fspan>(\u003Cspan class=\"hljs-variable\">$sections\u003C\u002Fspan>);\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$sections\u003C\u002Fspan>;\n    } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (\\\u003Cspan class=\"hljs-built_in\">Throwable\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$error\u003C\u002Fspan>) {\n        \u003Cspan class=\"hljs-variable\">$cache\u003C\u002Fspan>-&gt;\u003Cspan class=\"hljs-title function_\">abortDataCache\u003C\u002Fspan>();\n        \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">$error\u003C\u002Fspan>;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>При попадании в кеш функция загрузки вообще не вызывается. При ошибке незавершённая запись отменяется. Общий ключ подходит только для данных, одинаковых для всех получателей.\u003C\u002Fp>\n\u003Cp>В приложении для части данных также используются теги — отметки зависимости кеша от исходного содержимого. Они позволяют связывать его сброс с изменениями данных. Выбор того, что можно сохранять и когда обновлять, здесь важнее самого вызова кеша: список разделов и персональный расчёт заказа требуют разного подхода.\u003C\u002Fp>\n\u003Ch2 id=\"section-4\">От выбора товара — к данным компании и документам\u003C\u002Fh2>\n\u003Cp>Каталог можно просматривать без входа, а работа с кабинетом требует авторизации. В регистрации мы запрашиваем сведения оптового клиента: контактное лицо, заведение, город и ИНН.\u003C\u002Fp>\n\u003Cp>Для телефона используется vue-3-mask: поле задаёт привычный формат номера. VeeValidate проверяет форму, а Yup описывает правила для полей. Например, маску телефона и проверку электронной почты можно связать так:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: vue\">\u003Ccode class=\"hljs language-vue\">\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">setup\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-javascript\">\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { \u003Cspan class=\"hljs-title class_\">Form\u003C\u002Fspan>, \u003Cspan class=\"hljs-title class_\">Field\u003C\u002Fspan>, \u003Cspan class=\"hljs-title class_\">ErrorMessage\u003C\u002Fspan> } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vee-validate'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { \u003Cspan class=\"hljs-title class_\">MaskInput\u003C\u002Fspan> } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue-3-mask'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> * \u003Cspan class=\"hljs-keyword\">as\u003C\u002Fspan> yup \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'yup'\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> emit = \u003Cspan class=\"hljs-title function_\">defineEmits\u003C\u002Fspan>([\u003Cspan class=\"hljs-string\">'submit'\u003C\u002Fspan>])\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> schema = yup.\u003Cspan class=\"hljs-title function_\">object\u003C\u002Fspan>({\n  \u003Cspan class=\"hljs-attr\">phone\u003C\u002Fspan>: yup.\u003Cspan class=\"hljs-title function_\">string\u003C\u002Fspan>().\u003Cspan class=\"hljs-title function_\">required\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'Укажите телефон'\u003C\u002Fspan>),\n  \u003Cspan class=\"hljs-attr\">email\u003C\u002Fspan>: yup.\u003Cspan class=\"hljs-title function_\">string\u003C\u002Fspan>().\u003Cspan class=\"hljs-title function_\">required\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'Укажите почту'\u003C\u002Fspan>).\u003Cspan class=\"hljs-title function_\">email\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">'Проверьте адрес'\u003C\u002Fspan>)\n})\n\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Form\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:validation-schema\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"schema\"\u003C\u002Fspan> @\u003Cspan class=\"hljs-attr\">submit\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"emit('submit', $event)\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Field\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">name\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"phone\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">v-slot\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"{ value, handleChange, handleBlur }\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>Телефон\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">MaskInput\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:model-value\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"value\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">mask\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"+7 (###) ###-##-##\"\u003C\u002Fspan>\n          @\u003Cspan class=\"hljs-attr\">update:model-value\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"handleChange\"\u003C\u002Fspan> @\u003Cspan class=\"hljs-attr\">blur\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"handleBlur\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">Field\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">ErrorMessage\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">name\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"phone\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>Почта \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Field\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">name\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"email\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">type\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"email\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">ErrorMessage\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">name\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"email\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">type\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"submit\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>Продолжить\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">Form\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Маска помогает вводить номер, а проверка показывает ошибку рядом с полем. В примере валидная форма передаёт значения родительскому компоненту; серверная проверка данных остаётся отдельным этапом.\u003C\u002Fp>\n\u003Cp>После заказа клиент возвращается к прежним закупкам. В кабинете есть список заказов с фильтрами по периоду, статусу и адресу. Для выбора дат мы используем Vue Datepicker. Этот же компонент помогает запросить акт сверки: можно выбрать квартал либо указать другой период.\u003C\u002Fp>\n\u003Cp>Для произвольного периода достаточно двух календарных полей. Здесь начальная дата ограничивает выбор конечной, а кнопка становится доступна после заполнения корректного интервала:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: vue\">\u003Ccode class=\"hljs language-vue\">\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">setup\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-javascript\">\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { ref, computed } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'vue'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Datepicker\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'@vuepic\u002Fvue-datepicker'\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">'@vuepic\u002Fvue-datepicker\u002Fdist\u002Fmain.css'\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> emit = \u003Cspan class=\"hljs-title function_\">defineEmits\u003C\u002Fspan>([\u003Cspan class=\"hljs-string\">'request'\u003C\u002Fspan>])\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> = \u003Cspan class=\"hljs-title function_\">ref\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>)\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> to = \u003Cspan class=\"hljs-title function_\">ref\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>)\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> valid = \u003Cspan class=\"hljs-title function_\">computed\u003C\u002Fspan>(\u003Cspan class=\"hljs-function\">() =&gt;\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> &amp;&amp; to.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> &amp;&amp; to.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan> &gt;= \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">value\u003C\u002Fspan>)\n\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">form\u003C\u002Fspan> @\u003Cspan class=\"hljs-attr\">submit.prevent\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"valid &amp;&amp; emit('request', { from, to })\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>Начало периода\n      \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Datepicker\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">v-model\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"from\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:enable-time-picker\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"false\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>Конец периода\n      \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">Datepicker\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">v-model\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"to\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:min-date\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"from\"\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:enable-time-picker\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"false\"\u003C\u002Fspan> \u002F&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">label\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">:disabled\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">\"!valid\"\u003C\u002Fspan>&gt;\u003C\u002Fspan>Запросить акт\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">form\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">template\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Календарь здесь обслуживает конкретную рабочую задачу: клиент задаёт период документа. В обработчике запроса даты нужно привести к согласованному с сервером формату календарной даты, сохранив выбранный день.\u003C\u002Fp>\n\u003Ch2 id=\"section-5\">Уведомление возвращает клиента к действию\u003C\u002Fh2>\n\u003Cp>Ещё один путь в приложение — push-уведомление. Для него мы используем Firebase Cloud Messaging и нативный плагин, который связывает получение сообщения с интерфейсом на Vue.\u003C\u002Fp>\n\u003Cp>Firebase отвечает за доставку push, приложение — за дальнейшее действие. Когда клиент открывает уведомление из фонового состояния, обработчик может направить его на нужный экран через Vue Router. Так сообщение становится точкой продолжения работы.\u003C\u002Fp>\n\u003Cp>Чтобы по уведомлению открыть список заказов, обработчик принимает заранее предусмотренное событие и выполняет переход:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\" aria-label=\"Блок кода: js\">\u003Ccode class=\"hljs language-js\">\u003Cspan class=\"hljs-comment\">\u002F\u002F Вызывается после готовности нативной среды и настройки push.\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">export\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">connectPush\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">plugin, router, onError\u003C\u002Fspan>) {\n  plugin.\u003Cspan class=\"hljs-title function_\">onMessageReceived\u003C\u002Fspan>(\u003Cspan class=\"hljs-function\">\u003Cspan class=\"hljs-params\">message\u003C\u002Fspan> =&gt;\u003C\u002Fspan> {\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (message.\u003Cspan class=\"hljs-property\">tap\u003C\u002Fspan> !== \u003Cspan class=\"hljs-string\">'background'\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (message.\u003Cspan class=\"hljs-property\">destination\u003C\u002Fspan> !== \u003Cspan class=\"hljs-string\">'order-list'\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>\n    router.\u003Cspan class=\"hljs-title function_\">push\u003C\u002Fspan>({ \u003Cspan class=\"hljs-attr\">name\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">'example-orders'\u003C\u002Fspan> }).\u003Cspan class=\"hljs-title function_\">catch\u003C\u002Fspan>(onError)\n  }, onError)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Здесь \u003Ccode>plugin\u003C\u002Fcode> — нативный Firebase-плагин, а \u003Ccode>example-orders\u003C\u002Fcode> — заранее зарегистрированный маршрут Vue Router. Обработка разрешений и регистрация устройства выполняются при настройке push. Сам переход короткий, потому что навигация уже объединяет экраны приложения.\u003C\u002Fp>\n\u003Ch2 id=\"section-6\">Что получилось\u003C\u002Fh2>\n\u003Cp>Мы выпустили Craft Cartel для iOS и Android и объединили в приложении просмотр ассортимента, заказы, документы и расчёты. Клиент выбирает товары по параметрам закупки, работает со своим кабинетом и возвращается к нужному действию из уведомления.\u003C\u002Fp>\n\u003Cp>В этом проекте технические решения связаны с повседневной работой оптового клиента. Свайп помогает просмотреть подборку, отклик на касание — взаимодействовать с фильтрами, календарь — задать период сверки. Общая навигация соединяет эти действия, а серверный кеш избавляет от повторной подготовки одинаковых ответов.\u003C\u002Fp>\n\u003Cp>Для похожего приложения я бы начинал с пути клиента после выбора товара: какие заказы он должен видеть, какие документы запрашивать и куда возвращаться из уведомления. Ответы на эти вопросы определяют и состав экранов, и взаимодействие с сервером.\u003C\u002Fp>\n\u003Cp>Приведённые примеры кода условны и написаны специально для этого кейса. Они иллюстрируют описанные приёмы и не взяты из исходного кода проекта.\u003C\u002Fp>\n",[6,9,12,15,18,21],{"id":7,"title":8},"section-1","Каталог становится частью рабочего кабинета",{"id":10,"title":11},"section-2","Найти нужное и получить отклик на касание",{"id":13,"title":14},"section-3","Не собирать одинаковые данные заново",{"id":16,"title":17},"section-4","От выбора товара — к данным компании и документам",{"id":19,"title":20},"section-5","Уведомление возвращает клиента к действию",{"id":22,"title":23},"section-6","Что получилось",1790082845804]