.. NodaLogic documentation master file, created by sphinx-quickstart on Wed Nov 5 07:29:33 2025. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Клиентская часть(мобильный и веб-клиент) ============================================ Для всех видов клиентов соблюдаются единые подходы и единый синтаксис, но все же, учитывая разные задачи, которые возлагаются на клиенты и разный функционал есть отличия между мобильным и веб-клиентом. Возможности и отличия веб-клиента и мобильного клиента --------------------------------------------------------- С узлами можно работать напрямую на сервере, используя веб-клиент, которой непосредственно подключен к БД узлов и работает с сервером узлов напрямую через локальное хранение и исполнение кода обработчиков сразу на сервере. И также можно работать через мобильное приложение на Android, причем при работе через Андроид-приложение (мобильный клиент) по умолчанию узлы скачиваются локально и обработчики узлов выполняются также локально на устройстве. Но при наличии связи это происходит мгновенно, образуя что называется "псевдо-онлайн". Однако при отсутствии связи узлы также продолжают функционировать - им не нужен сервер. Отсылку данных с мобильного клиента на сервер разработчик организует сам, как вариант, это может быть исполнение метода узла на сервере и передача ему данных. Таким образом можно сказать, что главное отличие веб-клиента от мобильного клиента заключается в том что веб-клиент работает только чисто онлайн, мобильный - в основном локально, независимо от связи с сервером. Репозиторий ~~~~~~~~~~~~~~~~ .. image:: _static/repo_web.png :scale: 55% :align: center Для того чтобы начать работать с конфигурацией, ее нужно сохранить локально для клиента в репозиторий. Это касается и веб-клиента, не смотря на то, что он чисто онлайн. И мобильный и веб-клиент работают со своим набором конфигураций - репозиторием. Для веб-клиента, пользователь может установить те конфигурации, для которых у него есть доступ. Работа с обработчиками на сервере имеет особенность - при сохранении конфигурации, файл обработчиков конфигурации генерируется и клиент работает с ним непосредственно (т.е. не достает каждый раз из репозитория), при этом сама конфигурация храниться в репозитории и клиент работает с ней, в случае изменений надо обновить ее в клиенте. Такой подход позволяет отлаживать обработчики. Работа с несколькими комнатами и регистрация в комнатах ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ В то время как мобильный клиент может быть подключен только к одной комнате, веб-клиент может регистрировать изменения в разных комнатах (мобильный клиент все же может использовать команду register для разных комнат). У каждого класса на закладке Миграция можно определить альяс комнаты по умолчанию, а в разделе конфигурации "Комнаты" можно сопоставить псевдонимам реальные комнаты конкретного инстанса, тогда для каждого класса изменения будут уходить в свою комнату к которой подключена своя группа мобильных клиентов. .. image:: _static/migration.png :scale: 55% :align: center На вкладке Миграция можно задать регистрацию при записи - при записи объекта в клиентах будет происходить регистрация в комнате по умолчанию для класса (для веб-клиента) или в комнате из настроек (для мобильного клиента). Также можно включить "Команда для регистрации" - будет добавлена кнопка "Регистраци" в блоке стандартных команд. Обложки для мобильного и веб-клиента ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. image:: _static/covers.png :scale: 55% :align: center Узлы отображаются в разделах, в списках узлов и им нужно некое представление для пользователя, нужно выводить какие то данные. Помимо списков разделов узлы могут быть представлены в других элементах - списках, полях выбора и т.д. У узла в классе есть поле Обложка (Cover), в ней можно задать макет, по общим правилам разметки (общие правила для экранов, обложек, диалогов и т.д.) и он будет применяться ко всем узлам. Но для веб клиента можно задать особую обложку "Особая обложка для веб-клиента" - по тем же принципам, просто она будет отлична от мобильного клиента. Также для веб-клиента можно задать обложку для табличного представления клиента в списке (в списке узлов есть переключатель вида) в формате Загловок|ключ И наконец, для каждого узла в _data можно задать непосредственно обложку для конкретного узла с помощью свойства **_cover**. Это имеет наивысший приоритет и перекрывает предыдущие способы. С помощью ``_cover`` можно делать свои обложки для каждого узла, т.е. играться не только содержанием, но и формой. Активные обложки ~~~~~~~~~~~~~~~~~~~~~ .. image:: _static/active_cover.png :scale: 55% :align: center В обложках можно выводить не только данные или статическую разметку, но и активные элементы, генерирующие события (например кнопки, галочки). Особенность заключается в том, что эти события направляются в тот узел, к которому принадлежит обложка. Т.е. события обработаются непосредсвенно в этом узле, даже если это например список узлов, в каком то другом узле. Стандартные команды ~~~~~~~~~~~~~~~~~~~~~~~~~ В клиентах можно разместить "Стандартные команды" включив в классе галочку "Использовать стандартные команды". Их можно сделать и вручную - элементами и обработчиками, но так проще. Становятся доступны: * Сохранить и закрыть * Сохранить * Регистрация (если настроена) * Удалить * Закрыть Пользователи ~~~~~~~~~~~~~~~~~~~~ Для веб-клиента можно завести пользователей и разграничить их доступ в разделе Пользователи. По умолчанию, единственный пользователь имеет полный доступ. Вообще управлять доступом можно только с уровнем доступа Конфигуратор. Сейчас доступно: * доступ на уровне модулей: веб-клиент, API (имеется ввиду прием REST-запросов), Конфигуратор. * доступ к конфигурациям - можно выбрать конфигурации, доступные пользователю. Влияет на доступность выбора конфигураций, при добавлении в Репозиторий, т.е. на список выбора конфигураций Будет доступно (планируется): * доступ на уровне классов * доступ на уровне узлов (RLS) Работа с аппаратным сканером штрихкодов ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Для получения событий onBarcode на мобильном устройстве и десктопе используются разные подходы. На мобильном клиенте (в случае работы на терминале сбора данных, ТСД) используется соединение с программой-драйвером штрихкодов через подписку на события сканера. В настройках надо указать имя события, переменную, и, опционально, длину поля (если программа-драйвер выгружает данные не в виде строки, а в виде байт-массива). .. image:: _static/barcode_web.png :scale: 55% :align: center В веб-клиенте на компьютере пользователя необходимо запустить драйвер, а в браузере настроить соединение с драйвером (драйвер поднимает веб-сокет как сервер локально на компьютере, браузер является его клиентом). Драйвер работает как сервис, резидентно, с запуском из командной строки. Программу можно скомпиллировать pyinstaller под нужную ОС, скачав с GitHub или просто запустить под Python, установив зависимости. Либо можно скачать с exe-файл. Параметры задаются в командной строке. По умолчанию настроен перехват в режиме эмуляции клавиатуры. Можно задать хост и порт. Параметр --keyboard по умолчанию: ``barcode_scanner_ws --keyboard --host 127.0.0.1 --port 8765`` Для подключения через COM-порт: ``barcode_scanner_ws --serial COM3 --baud 9600 --host 127.0.0.1 --port 8765`` Общие функции мобильного и веб-клиента ------------------------------------------ Общее устройство интерфейса и событий ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Все визуальные узлы (узлы данных и пользовательские процессы) располагаются в разделах. .. image:: _static/sections.png :scale: 55% :align: center Разделы задаются в секции конфигурации Разделы в виде ИД и заголовка. Также для раздела можно прописать команды, которые будут показаны в виде кнопок снизу раздела в виде списка через запятую <Заголовок команды>|<ид команды>. При нажатии на кнопку генерируется общее событие onStartMenuCommand в котором в качестве параметра передается ИД команды Класс узла в свою очередь относится к разделу через свойство Код раздела где нужно выбрать существующий раздел Все узлы в разделах могут иметь обложку или так называемую обложку по умолчанию. Обложка задается в виде стандартной разметки (см. далее) в классе узла, но также может быть задана в _data (свойство "_cover") – таким образом узлы одного класса могут иметь разную разметку. И также может переопределяться для конкретного узла методом SetCover .. image:: _static/cover.png :scale: 55% :align: center При нажатии на узел в разделе открывается форма узла и генерируется событие onShow в котором можно прописать обработчик для отрисовки экрана, иначе он может оказаться пустым. С этого экрана пользователь может уйти, а потом вернуться (например открыв другой узел в другом экране), поэтому в таких случаях, в случае возврата на экран генерируется событие onResume в котором можно прописать тоже самое что и в onShow либо что то особенное. Принципы разметки экрана и других визуальных форм. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Доступность: мобильный клиент и веб-клиент В системе используется 2 альтернативных подхода к разметке, которые можно комбинировать. По умолчанию (будем называть это основной подход) используется принцип разметки «по строкам» т.е. список вертикальных «строк», каждая строка которого – это массив элементов в горизонтали (каждый элемент – визуальный объект какого то типа type). Понятно, что такой подход не исчерпывающий, но для большинства бизнес-приложений его хватает. Высоту и поведение как строк и элементов при желании можно настроить. Альтернативой этому можно считать разметку контейнерами – вертикальными, горизонтальными и вертикальными/горизонтальными скроллами. Причем оба подхода можно смешивать, а можно например просто в 1ю строку добавить контейнер и весь экран выстроить на контейнерах (полностью перешагнуть через разметку строками не удастся – разметка все равно должна иметь хотя бы 1 строку). Для разметки экрана, обложки узла, диалога, элемента списка применяется единый подход в виде строки JSON-макета (в параметрах python-методов это внутренние типы list/dict) такого вида: .. code-block:: Python [ #общий вертикальный список сверху-вниз [{"type":…},{"type":…}], #горизонтальная строка элементов [{"type":…},{"type"":…}], #горизонтальная строка элементов ... ] Пример: .. code-block:: Python Layout1 =[ [{"type":"Table","id":"l1","value":table_data,"layout":table_layout }], [{"type":"Button","id":"btn_repl","caption":"@caption"}] ] В «строках» располагаются визуальные элементы. Каждый визуальный элемент имеет свойства: * **"type"** (string)- тип элемента, * **"id"** (string)- идентификатор элемента. Для активных элементов (с которыми взаимодействует пользователь) как правило генерируется событие со свойством listener, равным id элемента. Также по id можно обратиться к элементу и задать его свойства * **"visible"** (int) – видимость элемента: 1 – видим, 0- невидим (но занимает место на экране), -1- невидим (и не занимает место на экране) * **"w"** – вес элемента. Вес элемента по горизонтали/вертикали (в зависимости от родителя-контейнера) относительно других элементов. По умолчанию w=1. Пример: ``[{"type":"Button","id":"back","caption":"Back"},{"type":"Button","id":"next","caption":"Forward","w":2}] #кнопка next в два раза шире, потому что у нее w=2 (у back w=1 по умолчанию).`` В сочетании с высотой/шириной можно управлять разметкой. Зависит от поведения родительского контейнера. Если контейнер «по размеру элементов» то вес не имеет смысла, имеет смысл толкьо если «на всю ширину» или 0. * **"width"** – ширина. Может задаваться как в относительных числовых размерах, так и в виде абсолютных значений: -1 – на весь контейнер, -2 – по высоте элементов. Зависит от поведения корневого контейнера. Если контейнер имеет измерение «по ширине элементов», то «на всю ширину» не имеет смысла * **"height"** – высота. Может задаваться как в относительных числовых размерах, так и в виде абсолютных значений: -1 – на весь контейнер, -2 – по высоте элементов. Зависит от поведения корневого контейнера. Если контейнер имеет измерение «по ширине элементов», то «на всю ширину» не имеет смысла * **padding** - внутренние отступы * **gravity** - выравнивание по горизонтали. Значения: left, center, rigth Правила по умолчанию такие: * Корневой вертикальный список растянут на весь экран без прокрутки. * Все элементы в строке по умолчанию имеют высоту «по высоте элементов», а сама строка растянута по ширине «на весь контейнер» (т.е. по горизонтали от края до края экрана) и не имеет веса. * Но можно поменять свойство горизонтальной строки с помощью добавления элемента "Parameters" с свойствами "w","width" и "height" – т.е. установить для горизонтальной строки (она является контейнером) вес ширину и высоту. Это важно, если требуется более сложная разметка. Например в этой разметке нижняя строка занимает всю оставшуюся высоту, за счет того, что у нее вес = 1, тогда, в элементе Input в этой строке имеет смысл применить высоту = -1 (на всю высоту) .. code-block:: Python [ [{"type":"Input","id":"title","caption":"Subject","value":"@title"}], [{"type":"Parameters","w":1},{"type":"Input","height":-1,"id":"body","caption":"Text","value":"@body","input_type":"multiline"}] ] Сама горизонтальная строка является контейнером, но в нее также можно вписывать другие контейнеры. В принципе можно полностью отказаться от разметки строками и вывести только 1 строку с высотой – на весь экран, а внутри нее сделать все элементами -контейнерами. Контейнеры нужны как для разметки так и для того, чтобы собрать несколько элементов в группу и управлять ими вместе (например видимостью) .. warning:: Крайне важно помнить – если вы используете контейнеры, у элементов обязательно надо задавать ширину и высоту. У самих контейнеров как правило тоже. И также возможно Parameters у «строки» У контейнеров есть свойства: * Общие свойства (тип, вес, ширина, высота) * Свойство value – список элементов. Для Card в виде общей разметки "строками" (массив массивов)–[[]], а для остальных контейнеров – в виде массива/списка ( [] ) элементов. Которе соответственно выстраиваются по горизонтали или вертикали в зависимости от типа контейнера. Существуют такие варианты контейнеров: * **"VerticalLayout"** – вертикальный не визуальный контейнер без прокрутки * **"HorizontalLayout"** – горизонтальный не визуальный контейнер без прокрутки * **"VerticalScroll"** – вертикальная прокрутка. Имейте ввиду, что прокрутка «уничтожает» свойства элемента «на всю высоту» * **"HorizontalScroll"** – горизонтальная прокрутка * **"Card"** – визуальный контейнер «карточка» служит для визуального оформления как элементов списка, так и группировки элементов на экране. Статическая и динамическая разметка формы узла ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Разметка узла может быть задана через команду Show (т.е. нужно сделать метод и подцепить его на событие onShow), переопределена другими динамическими методами. Этоназывается *динамическая разметка* Но иногда удобнее не делать этого, а просто указать в карточке класса. Это удобно если экран один. Если у узла есть какие то переключающиеся экраны, то их все равно придется выводить командой Show.Такая разметка называется *статическая* и есть несколько вариантов как ее задать: * Задать в классе узла непосредственно в виде разметки (JSON). Больше ничего не надо делать - узел при открытии отрисуется по этой разметке. * Создать элемент разметки (раздел "Общая разметка") и использовать его в классе. Там можно выбрать из имеющихся вариантов * В _data узла поместить разметку в ключ _layout. У этого варианта - наивысший приоритет. Виды визуальных объектов и их уникальные свойства ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Надпись """""""""" Доступность: мобильный клиент и веб-клиент Вывод текстовой строки. "type" : "Text" "value": строковая константа либо ссылка на переменную с префиксом @. Можно использовать html разметку (как и во всех остальных надписях) "text_color" – цвет текста в HEX-формате (пример #F54927) "radius" - закругление фона (число) "stroke" - обводка линией (число) "background" - цвет фона в HEX-формате (пример #F54927) "size" – размер (целое число) Картинка """"""""""" .. image:: _static/picture.png :scale: 55% :align: center Вывод картинки "type" : "Picture" "value" – абсолютный путь к файлу. Константа либо ссылка на переменную _data с префиксом @ Пример: .. code-block:: Python my_layout = [ [{"type":"Picture","value":"@image_path"}] ] Слайдер изображений """"""""""""""""""""""""" .. image:: _static/qs_photo_notes_imageslider.png :scale: 70% :align: center Вывод картинки "type" : "ImageSlider" "value" – список имен файлов (для мобильной - абсольюный путь, для веб-имя файла) либо ссылка на переменную в _data, содержащую такой список "captions" - список заголовков (необязательный) либо ссылка на такой список "cover" - (для web) - подстраивает пропорции под размер элемента Пример: .. code-block:: Python my_slider = { "type": "ImageSlider", "value": "@pic_files_web", "width": 100, "height": 100 , "cover":true, "captions":["1","2","3"]} Особенности работы с изображениями в мобильном и веб-клиенте """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" Изображение (файл, медиафайл) всегда храниться на диске. В веб-клиенте: в папке UserFiles\ сервера В мобильном : во внутренней папке приложения, связанной с конфигурацией Ссылка на него (в элементе Picture, ImageSlider) задается в виде имени файла, но по разному – в мобильном – это абсолютный путь, в веб-клиенте – имя файла (он сам добавляет путь). На сервере, разместив файл каким либо образом в папке конфигурации (помимо обработчиков, этом можно сделать через галереи Медиа и Файлы или просто через API) обратиться в элементе к нему можно через имя файла На сервере, для примера реализовано два роута для работы с картинками: ``POST /api/userfiles//images – принимает список файлов и сохраняет их в нужном формате. Пример обращения описан в Быстром старте`` ``GET /api/userfiles//raw/ - получить картику по имени файла`` Поле HTML """""""""""" Доступность: мобильный клиент и веб-клиент Вывод html-документа "type" : "HTML" "value" – строка в формате HTML Кнопка """"""""" Доступность: мобильный клиент и веб-клиент Вывод кнопки. При нажатии на кнопку экрана генерируется события с listener= . При использовании в списках см. раздел Активные элементы списков "type" : "Button" "caption" – надпись на кнопке "small" - кнопка в виде маленькой круглой кнопки "background" – цвет фона кнопки в HEX-формате Пример: ``{"type":"Button","id":"btn_update","caption":"Simple button"}`` Список нижних кнопок """"""""""""""""""""""""""" Доступность: мобильный клиент .. image:: _static/bottom_buttons.png :scale: 55% :align: center Выводит горизонтальный массив кнопок внизу экрана. Только для размещения в экране. "type" : "BottomButtons" "value" – список кнопок с id. Параметры каждой кнопки такие же как у Button Пример: ``{"type":"BottomButtons","id":"bottom","value":[{"type":"Button","id":"back","background":"#F54927","caption":"Back"},{"type":"Button","id":"next","background":"#25a018","caption":"Forward","w":2}]}`` Переключатель """"""""""""""""""" Доступность: мобильный клиент и веб-клиент Элемент переключателя. Генерирует событие и пишет в переменную (id элемента) текущее значение переключателя. "type": "Switch" "caption" –(строка) заголовок элемента "value" –(булево) значение элемента для инициализации – в виде константы или ссылки на переменную. Как правило сюда ставят ссылку на id Пример: ``{"type":"Switch","caption":"Setting 1","id":"sw1","value":"@sw1"}`` Галочка """""""""" Доступность: мобильный клиент и веб-клиент Переключатель Элемент переключателя. Генерирует событие и пишет в переменную (id элемента) текущее значение переключателя. "type" : "CheckBox" "caption" –(строка) заголовок элемента "value" –(булево) значение элемента для инициализации – в виде константы или ссылки на переменную. Как правило сюда ставят ссылку на id Пример: ``{"type":"CheckBox","caption":"Setting 2","id":"cb1","value":"@cb1"}`` Списки """"""""""""" Доступность: мобильный клиент и веб-клиент .. image:: _static/tables.png :scale: 55% :align: center Выводит различные списки элементов. В качестве наполнения можно использовать как список, созданный в обработчике так и датасет и список узлов класса. В элементах возможно размещение активных элементов. Для более красивого оформления элементы списка можно упаковать в Card. По умолчанию макет элемента – автогенерируемый, но его можно переопределить как для всего списка, так и для любого элемента. "type": "Table" "value" – источник данных списка. Может быть определен как просто «список словарей» в python что делает его хорошо совместимым с NoSQL такими как Pelican например Пример такого определения: .. code-block:: Python table_data = [{"name": "element 1", "key":"el1"},{"name": "element 2", "key":"el2"}] {"type":"Table","id":"l1","value":table_data} Либо (если используются датасеты) может быть указана ссылка на датасет: ``{"type":"Table","id":"l2","value":"goods" }`` Либо, если используется свойство nodes_source можно вывести список узлов (тогда при нажатии будет открываться узел).Это может быть поиск (отбор) узлов или просто какой то произвольный список. Узлы перечисляются в виде uid-ов. Преобразовать список объектов в список uid-ов можно с помощью функции to_uid Пример (вывод списка всех узлов класса Note): ``{"type":"Table","id":"t1","nodes_source":True,"value":to_uid(Note.get_all())}`` "layout" – макет списка в целом в стандартном «строчном» формате. Пример: .. code-block:: Python table_layout = [ [{"type":"Text","value":"@_id","bold":True}], [{"type":"Text","value":"@name"},{"type":"Text","value":"@barcode"}] ] {"type":"Table","id":"l1","value":table_data, "layout": table_layout } *_layout* в элементе списка – делает то же что и layout только для конкретного элемента списка. Это свойство просто добавляется в источник данных: ``table_data = [{"name": "element 1", "key":"el1","_layout":layout2},{"name": "element 2", "key":"el2"}]`` *_background* в элементе списка – раскрашивает фон в заданный HEX-цвет ``table_data = [{"name": "element 1", "key":"el1","_background":"#701705" },{"name": "element 2", "key":"el2"}]`` "columns_count" – количество столбцов таблицы. Выводит список в виде нескольких столбцов "horizontal" – горизонтальная ориентация списка. "search_enabled" – включает режим поиска Для датасета своя секция настроек поиска dataset_search в которой есть method(метод поиска), keys(поля поиска) и min_length (необязательно) минимальная длина с которой начинается поиск. Метод может быть: * text - для обычного поиска по вхождению строки * levenshtein - для нечеткого поиска по расстоянию Левенштейна (будут выведены результаты в порядке убывания точности, с отбором >75, сама точность добавляется в записи в поле _confidence Также для датасета всегда включена пагинация (невидимая) но можно настроить размер страницы – свойство page_size Пример: .. code-block:: Python dataset_search = {"dataset":"goods","keys":"name","method":"text","min_length":2} self.Show([ [ {"type":"Table","id":"l2","value":"goods" ,"search_enabled":True,"dataset_search":dataset_search}] ]) Вывод списка в виде таблицы """"""""""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент .. image:: _static/flat_table.png :scale: 55% :align: center В объекте Table можно определить свойство table=true и использовать свойство table_header c заголовками в формате ``["Заголовок|Ключ"]`` либо ``["Заголовок|Ключ|Вес"]`` тогда список будет выведен в виде плоской таблицы со столбцами. В остальном все тоже самое - используются те же источники (в случае источников - узлов будет выведена не обложка а поля в соответствии с table_header) Пример: .. code-block:: Python {"type":"Table","id":"tab4","value":lines,"table":True, "table_header":["#|n","Position|position","Qty|qty"]} Список подчиненных узлов """""""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент .. image:: _static/children.png :scale: 55% :align: center Выводит иерархический список подчиненных узлов и их потомков "type" : "NodeChildren" Без параметров Закладки/Страницы --------------------- .. image:: _static/tabs.png :scale: 55% :align: center Доступность: мобильный клиент и веб-клиент "type": "Tabs" Группирует данные по закладкам (объектам "type":"Tab"). Внутри каждой закладки – контейнер с о обычной строчной разверткой Общий синтаксис такой: {"type":"Tabs","value":[ {"type":"Tab","id":"tab1","caption":"Base elements","layout":tab1_layout}, {"type":"Tab","id":"tab12","caption":"List as Table","layout":tab2_layout} ]} У таб есть одно свойство - ``value`` - список закладок Tab У каждой Tab есть: * id * caption * layout - собстввенно, содержимое страницы Пример .. code-block:: Python tab1_layout = [ [{"type":"Spinner","id":"my_spinner","caption":"my select:","value":"@my_spinner", "dataset":spinner_dataset}], [{"type":"NodeInput","id":"ni","value":"@ni","caption":"Заметка","dataset":"Note"}] ] lines = [ {"n":1,"position":"Position #1","qty":100}, {"n":2,"position":"Position #2","qty":120}, {"n":3,"position":"Position #3","qty":10} ] tab2_layout = [ [{"type":"Table","id":"tab4","value":lines,"table":True, "table_header":["#|n|1","Position|position|7","Qty|qty|1"]}] ] self.Show([[{"type":"Tabs","value":[ {"type":"Tab","id":"tab1","caption":"Base elements","layout":tab1_layout}, {"type":"Tab","id":"tab12","caption":"List as Table","layout":tab2_layout} ]}]]) Поля ввода """"""""""""" Доступность: мобильный клиент и веб-клиент .. image:: _static/inputs.png :scale: 55% :align: center Вся палитра полей ввода (кроме полей датасета, которые настраиваются аналогично) имеет тип Input – обычные поля, многострочные, выпадающие списки и т.д. Все введенные символы сразу записываются в _data узла. Если стоит автосохранение в классе узла то узел сразу же сохраняется при этом. "type" : "Input" "input_type" тип значений ввода (по умолчанию – просто текст, input_type можно не указывать): * NUMBER – числа. Причем числа, при записи в переменную автоматически распознаются на float/integer в зависимости от наличия дробной части * PASSWORD – ввод пароля (закрывается звездочками) * MULTILINE – многострочный текст * DATE – дата. При выборе даты в _data попадает 2 значения – представление даты (в переменную=id) и в _d - дата в формате ISO "events" – флаг, подключающий генерацию событий при вводе в поле ввода "value" – отображаемое значение по умолчанию "caption" заголовок поля "spinner_mode" – режим выпадающего списка. Необходимо задать в таком случае значение свойства source – список значений в виде строки с разделителем ";" "autocomplete" -режим подбора по первым символам. Также должен быть задан source Выпадающий список """""""""""""""""""""" .. image:: _static/inputs.png :scale: 55% :align: center Доступность: мобильный клиент и веб-клиент "type" : "Spinner" Элемент позволяет отобразить датасет в виде выпадающего списка и получить выбранное значение. Датасет элемента задается в виде списка словарей с обязательными полями: * _id ключ варианта * _view представление выбранного значения Остальные поля могут быть любые, они не отображаются но участвуют в результате В переменную, равную id возвращается _id из выбранного элемента. Этим же _id можно инициализировать элемент. Пример: .. code-block:: Python spinner_dataset = [{"_id":"item1","_view":"item 1","some_key1":"some_value1"}, {"_id":"item2","_view":"item 2","some_key1":"some_value2"}, {"_id":"item3","_view":"item 3","some_key1":"some_value3"}] {"type":"Spinner","id":"my_spinner","caption":"my select:","value":"@my_spinner", "dataset":spinner_dataset} Поле ввода узел """""""""""""""""""" .. image:: _static/node_input.png :scale: 55% :align: center Доступность: мобильный клиент и веб-клиент "type" : "NodeInput" "dataset" – имя класса узлов. При выборе будет открыта форма выбора с поиском, сортировкой, видимостью и обложками, заданными для данного класса. Элемент позволяет выбрать узел в качестве значения в каком то другом узле, то есть указать ссылку на узел. Значение возвращается/устанавливается в универсальном формате ссылки на узел ``$`` Поля ввода датасетов """""""""""""""""""""" Доступность: мобильный клиент и веб-клиент Используются для выбора конкретного значения датасета на форме – элемента справочника, документа. Удобство заключается в том, что выбирается ссылка на объект по которой можно получить объект целиком или представление объекта. *На картинке в разделе Поля ввода Production - поле датасета, в котором отображается имя продукции и код - такой задан шаблон* Представление объекта датасета задается в поле Шаблон записи в датасете. Там также можно использовать html "type" : "DatasetField" "dataset" – имя датасета "spinner" – режим выбора из списка (исключает режим автоподстановки) "autocomplete" – режим автоподстановки. "caption" – заголовок поля "value" – значение по умолчанию. Задается в виде ссылки на элемент датасета <имя датасета>$ Надписи без объекта """"""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент В разметка можно просто указать строку вместо объекта с type=Text, тогда будет выведена строка или , если указан префикс @ значение из _data, ``["Hello world"]`` Как вариант можно использовать строковую конструкцию <заголовок>|<значение> тогда также будет выведено Строкове значение но в виде своеобразной Card с заголовком ``["title|Hello world"]`` Подобные конструкции можно использовать для упрощения, вместо Text, Card Активные элементы списков (в Таблица) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Доступность: мобильный клиент и веб-клиент В разметке элементов списка можно использовать не только надписи и картинки но и некоторые активные элементы, например, кнопки. При взаимодействии с ними в узел поступают несколько другие (расширенные) данные чем при размещении в экране. Разработчик получает имя списка, имя активного элемента, позицию и значение (для элементов ввода значения) Например Кнопка (Button). Переменные, при событии нажатия (onInput): listener – поступает в формате ``_input`` ``_input_position`` - в эту переменную возвращается позиция элемента списка, в котором произошло нажатие Если в _data есть key тогда также возвращается ключ: ``_input_key`` - значение key элемента Для полей ввода – CheckBox/Switch/Input добавляется еще само введенное значение в переменную _data - ``_input`` Доступны активные элементы: * Кнопка * Поля ввода (Input) * Галочка * Переключатель Функции/методы UI/UX мобильного клиента ---------------------------------------------- Методы узла (на устройстве) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Метод отрисовки экрана Show """""""""""""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент **Show(layout)** отрисовка экрана на странице узла. Этот метод «очищает холст» и выводит разметку , принципы которой описаны в разделе «Разметка экрана». Как минимум его надо прописать в обработчике onShow чтобы что то появилось. Также он может быть вызван в других обработчиках чтобы обновить экран с новыми данными, перерисовать. Хотя для этого есть более экономная команда UpdateView, но иногда перерисовать целиком – проще. .. code-block:: Python self.Show([ [{"type":"Input","id":"title","caption":"Subject","value":"@title"}], [{"type":"Parameters","w":1},{"type":"Input","height":-1,"id":"body","caption":"Text","value":"@body","input_type":"multiline"}] ]) Метод подключения механизмов экрана PlugIn """""""""""""""""""""""""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент(только указанные) **PlugIn(elements)** подключает несколько элементов к экрану. Это и аппаратные механизмы, такие как аппаратный сканер и визуальные элементы вне разметки (иначе бы они были выведеные в Show). Параметр команды – одномерный массив объектов (в python- список) с объектами вида {"type":"type_of_element","id":"element_id"}. .. attention:: Команда очищает все элементы перед доабавлением (self.PlugIn([])- все очистит), поэтому в ней надо сразу перечислить все элементы. Типы объектов(ключ type) бывают следующие: **CameraBarcodeScannerButton* (только мобильный) – экранный элемент сканирования штрих-кода. Это кнопка на экране которая вызывает камеру для сканирования. При сканировании генеририруется событие с listener=<ИД элемента> и в _data[<ИД элемента>] будет помещен штрихкод Пример: ``self.PlugIn([{"type":"CameraBarcodeScannerButton", "id":"barcode_cam"}])`` **BarcodeScanner** – подключение перехвата штрихкода с аппаратного сканера (для терминалов сбора данных). Аппаратный сканер должен быть настроен в Настройках в разделе Настройка аппаратного сканера. Должна быть активирована подписка на события аппаратного сканера и указаны данные, которые через Intent отдает ПО вашего сканера, а также в самом сканере надо настроить чтобы он отдавал штрихкоды в Intent broadcast а не в клавиатуру например. При сканировании генерируется событие с listener=<ИД элемента> и в _data[<ИД элемента>] будет помещен штрихкод **FloatingButton** (только мобильный) – добавляет плавающую кнопку (их может быть несколько) справа внизу экрана. У этого элемента, помимо id (по которому как у всех элементов генерируется событие) есть поле caption для вывода текста кнопки и также есть поля, позволяющие настроить иконку svg (подробности смотри в разделе «svg-иконки») Пример: ``self.PlugIn([{"type":"FloatingButton","id":"add_child","caption":"Add line"}])`` **ToolbarButton** (только мобильный) – добавляет кнопку в тулбаре. У этого элемента, помимо id (по которому как у всех элементов генерируется событие) есть поле caption для вывода текста кнопки и также есть поля, позволяющие настроить иконку svg (подробности смотри в разделе «svg-иконки») Пример: ``self.PlugIn([{"type":"ToolbarButton","id":"pin","caption":"Save","svg":svg2,"svg_size":24,"svg_color":"#FFFFFF"} ]])`` **PhotoButton** (только мобильный) – открывает камеру для фотографирования. Снимок сохраняется в файл, путь к файлу – в _data[<ИД элемента>]. Если подключена MediaGallery то снимок автоматически добавляется в галерею. Автоматическое добавление можно отключить поместив флаг OFFAutoupdateMediaGallery в _data узла. Тогда фото будет перехватываться в обработчике и с ним можно проводить какую то обработку перед добавлением в галерею. Пример ручной обработки .. code-block:: Python def CaptureImage(self, input_data=None): gallery_array = self._data["pic_files"] base64 = getBase64FromImageFile(this._data["result_file"],50,50) #ни к чему не ведет, просто получили кроп в base64 gallery_array.append(this._data["result_file"]) toast(self._data["result_file"]) UpdateMediaGallery(gallery_array ) return True,{} **GalleryButton** (мобильный, ожидается на веб) – открывает камеру для прикрепления медиафайла из галереи. Снимок сохраняется в файл, путь к файлу – в _data[<ИД элемента>]. Аналогичен PhotoButton. **MediaGallery** (мобильный, ожидается на веб) – галерея массива меддиафайлов внизу экрана с возможностью отркытия, удаления. По ключу = id элемента хранится массив путей к файлам, который отображается в галерее. С ним по умолчанию автоматически взаимодействуют PhotoButton и GalleryButton но также этот массив можно редактировать из обработчика (например поместить туда фото). Также доступно удаление пользователем. После удаления генерируется событие <имя_переменной>_delete в котором можно например забрать измененный массив файлов галереи. .. image:: _static/inputs.png :scale: 90% :align: center Метод открытия узла в интефейсе _open() """"""""""""""""""""""""""""""""""""""""" Доступность: мобильный клиент и веб-клиент **_open(method=None)** – открывает форму узла, как если бы его открыл пользователь. По умолчанию сработает onShow и метод, прописанный в конфигурации, но его можно переопределить в параметре method Метод обновления элементов на экране UpdateView """"""""""""""""""""""""""""""""""""""""""""""""" Доступность: мобильный клиент **UpdateView(id,element=None)** – команда для выборочного обновления элементов. Рекомендуется использовать для высоконагруженных интерфейсов (например ActiveCV) Работает в нескольких режимах: 1. Просто перерисовывает элемент по id. Это имеет смысле если значение элемента задано переменной (через @) а не константой. Пример: ``self.UpdateView("btn_repl",None)`` 2. Меняет свойства элемента. Пример: ``self.UpdateView("btn_repl",{"background":"#C82909"})`` 3. Заменяет элемент на другой элемент. Можно например проделывать это с контейнерами. Пример : ``self.UpdateView("btn_repl",{"type":"Input","id":"inp1","caption":"New input---"})`` Общие методы UI/UX для клиента ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **SetTitle (title)** (мобильный клиент) – устанавливает заголовок окна узла **RefreshTab()** (мобильный клиент) – обновляет текущую вкладку(раздел), из которой открыт узел. Имеет смысл когда узел добавили/изменили и возвращаемся в список **CloseNode()** (мобильный клиент) – закрывает форму узла **RunGPS/ StopGPS** (мобильный клиент) – команды запуска/остановки GPS. Может потребоваться разрешение от пользователя. Запущенный GPS считывает данные с устройства через промежутки времени и первые данные будут не сразу, а также может меняться точность, поэтому для чтения данных GetLocation лучше использовать таймер **GetLocation** (мобильный клиент) – получает данные GPS в формате сериализованного в строку JSON-объекта с полями: *altitude* – высота *latitude* – широта *longitude* – долгота *accuracy* – точность, в метрах *provider* – провайдер данных **ScanBarcode(listener)** (мобильный клиент) – запускает сканирование камерой, как если бы пользователь нажал на кнопку сканера, подключенную через PlugIn. По результату возвращается событие, указанное в параметре. **Dialog(id,title,yes_caption="",no_caption="",layout=None)** (мобильный и веб-клиент) – метод для показа как простых диалогов, так и диалогов с разметкой. Минимально – только заголовок, также можно переопределить 2 кнопки и сделать любую разметку layout по общим правилам разметки. .. image:: _static/dialog.png :scale: 90% :align: center Возвращает событие с ``listener=_positive/_negative`` в зависимости от того, какую кнопку нажал пользователь. Если диалог с разметкой, и там есть какие то инпуты, то данные пишутся прямо в _data Примеры: .. code-block:: Python def simple_dialog(self, input_data=None): Dialog("dlg1","To be or not to be?","To be","Not to be") return True,{} def layout_dialog(self, input_data=None): Dialog("dlg2","Enter quantity","Ok",None,[[{"type":"Input","id":"qty_dialog","caption":"Quantity","input_type":"number"}]]) return True,{} def input(self, input_data=None): if self._data.get("listener") == "dlg1_positive": toast("To be") elif self._data.get("listener") == "dlg1_negative": toast("Not to be") elif self._data.get("listener") == "dlg2_positive": toast(str(self._data.get("qty_dialog") )) **AddTimer(key, period)/ StopTimer(key)** – запускает/останавливает таймер с нужным ключом. Пример: ``AddTimer("my_timer",5)`` **ShowProgressButton/ HideProgressButton** – рисует колесико прогресс-бара на кнопке и делает ее неактивной пока не будет выполнено HideProgressButton Пример: .. code-block:: Python def worker(): time.sleep(2) HideProgressButton("button1") ShowProgressButton("button1") t = threading.Thread(target=worker) t.start() **ShowProgressGlobal/ HideProgressGlobal** (мобильный клиент) – показывает общий (закрывающий весь экран) прогресс-бар/ снимает его. **SetCover(node,layout)** – устанавливает новый макет для узла. Это именно макет, а не данные. Если у вас значения в макете заданы через @ - они и так обновятся. **UpdateMediaGallery()** (мобильный клиент) – обновляет галерею. Нужно выполнять после какиз либо действий с составом галереи. Функции модуля android (мобильный клиент) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Интерфейсные команды: * toast(String toast) – вывести сообщение Андроид * message(String text) – вывести сообщение * speak(String text) – произнести текст (TTS engine) * listen() – запустить ожидание распознавания речи * vibrate() и vibrate(int duration) – вибрация и вибрация заданной длительности * beep()/beep(int tone)/ beep(int tone,int beep_duration,int beep_volume) – звуковой сигнал, т.ч. с возможностью выбрать тон (от 1 до 99), продолжительность и громкость (по умолчанию – 100) * notification(String message)/ notification (String message,String title)/ notification(String message,String title,int number) – уведомление в шторке уведомлений. Number – идентификатор уведомления, по которому к нему можно потом обратиться, чтобы либо убрать, либо перезаписать (обновить) * notification_progress(String message,String title,int number,int progress) – уведомление с прогресс-баром (от 0 до 100) notification_cancel(int number) – скрыть уведомление Некоторые общие свойства элементов ----------------------------------- html текст в надписях ~~~~~~~~~~~~~~~~~~~~~~~~~ Везде, где в интерфейсе присутствует текст, можно использовать html-теги для разметки текста. Например Привет мир svg-иконки ~~~~~~~~~~~~~~~ .. image:: _static/svg.png :scale: 100% :align: center Для элементов, где предусмотрены иконки, можно использовать иконки svg. Способ такой: 1. Скачиваете и сохраняете svg-файл 2. Открываете файл текстовым редактором и копируете текст в переменную 3. Можно использовать эту переменную в свойстве элемента svg (также можно установить размер и цвет svg_size и svg_color) .. code-block:: Python svg1 = '' self.PlugIn([ {"type":"FloatingButton","id":"add","svg":svg1,"svg_size":48,"svg_color":"#000000"} ]) Функции для преобразования изображений и галереи ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **getBase64FromImageFile(path_to_image)** - возращает строку base64 из файла. **convertImageFilesToBase64Array(paths_to_images_array)** - удобно для конвератции "Галереи" в одну строку: конвентирует массив путей в массив строк base64 **saveBase64ToFile (base64_string)** - сохраняет base64 строку во временный файл и возвращает путь к нему **convertBase64ArrayToFilePaths** - сохраняет массив строк base64 в масисив путей (временных файлов), подходящий для использования в Галерее. Общие события клиента. Обработка событий приложения. ------------------------------------------------------------------- Некоторые события возникают не в узлах, а в целом в приложении, например событие при запуске платформы, при сканировании штрихкода вне узла и т.д. Они обрабатываются по тому же принципу что и события узлов – то есть имеют назначенное событие и обработчик, только не в структуре узла, а в структуре конфигурации на вкладке Общие события (в конфигурации это раздел CommonEvents) Обработчики python для подобных событий, соответственно прописываются не в классах узлов, а просто как функции в коде обработчиков. Некоторые события также содержат данные, они передаются в input_data в соответствующий ключ/ Пример: .. code-block:: Python def onBarcode(input_data): toast(input_data.get("barcode")) return True,{} Они должны иметь параметр input_data типа словарь. Перечень типов общих событий (мобильный клиент): * **onLaunch** – событие при загрузке конфигурации (либо при перезапуске). Без параметров. * **onTimer** – событие таймера. В input_data ключ “timer_key” – ключ сработавшего таймера * **onJSONFile** – событие открытия JSON-файла приложением (через Открыть или Поделиться). В input_data ключ "content" – содержимое файла в виде строки * **onTextFile** – событие открытия текстового файла приложением (через Открыть или Поделиться). В input_data ключ "content" – содержимое файла в виде строки * **onBarcode** – событие штрихкода, сканируемого через ScanBarcode т.е. вне узла, из главного меню * **onStartMenuCommand** – нажатие на команду в разделе конфигурации (те команды, что добавляются через Разделы конфигурации) * **onDialogResult** – событие диалога, вызванного из главного меню. Т.к. диалог вызывается с определенным идентификатором то в result возвещается либо result_positive либо result_negative а в ключ result_data возвращается данные из элементов ввода диалога, если таковые были. Перечень типов общих событий (веб-клиент): * **onBarcode** – событие штрихкода, сканируемого через ScanBarcode т.е. вне узла, из главного меню * **onStartMenuCommand** – нажатие на команду в разделе конфигурации (те команды, что добавляются через Разделы конфигурации) * **onDialogResult** – событие диалога, вызванного из главного меню. Т.к. диалог вызывается с определенным идентификатором то в result возвещается либо result_positive либо result_negative а в ключ result_data возвращается данные из элементов ввода диалога, если таковые были. Поиск, сортировка и видимость узлов (клиент) -------------------------------------------------- В мобильном клиенте NodaLogic есть встроенные механизмы поиска, сортировки и скрытия узлов, которые работают на стороне клиента и не требуют дополнительного кода или API. Эти механизмы управляются через специальные служебные поля в _data узла. Поиск по узлам ~~~~~~~~~~~~~~~~ Поведение по умолчанию Если в узле не задано специальное поле для поиска, приложение выполняет поиск: по всем ключам словаря _data поиск осуществляется по строковому представлению значений Поле _search_index Чтобы управлять поиском явно и повысить производительность, рекомендуется использовать поле: ``_search_index`` Если оно задано: * поиск выполняется только по этому полю * поиск становится быстрее и более контролируемым Пример .. code-block:: Python self._data["_search_index"] = ( self._data.get("number", "") + " " + self._data.get("customer_name", "") ) ``_search_index`` должен быть обычной строкой Сортировка узлов ~~~~~~~~~~~~~~~~~~ Сортировка списков узлов также выполняется на стороне клиента. Для этого используются специальные поля: ``_sort_string`` — сортировка по возрастанию ``_sort_string_desc`` — сортировка по убыванию Пример сортировки Сортировка по дате (по возрастанию): ``self._data["_sort_string"] = self._data.get("plan_date")`` Сортировка по дате (по убыванию): ``self._data["_sort_string_desc"] = self._data.get("created_at")`` Скрытие узлов ~~~~~~~~~~~~~~~~~~ Узел можно скрыть из интерфейса, не удаляя его физически. Для этого используется поле: _hidden Если установлено значение True: узел не отображается в списках узел остаётся доступным через код и API Пример скрытия узла ``self._data["_hidden"] = True``