Это самостоятельный файл docs/examples/platform_playerok.py, а не адаптация конкретного поставщика. Он использует общий интерфейс Lovsel: реальные товары, сделки, связки, статистику, настройки справа, мастер подключения и структурированные события. Права ограничены чтением, хранением данных и уведомлениями. События не закупают и не выдают товары: бизнес-действия добавляются отдельно и явно.
Четыре компоновки для Playerok
| Вариант | Строка запуска | Результат |
|---|---|---|
single | app = build_plugin("single") | Все блоки вместе на 12-колоночной сетке |
tabs | app = build_plugin("tabs") | Обзор и рабочая область во вкладках сверху |
sidebar | app = build_plugin("sidebar") | Внутреннее меню плагина со всеми разделами |
hybrid | app = build_plugin("hybrid") | Верхние разделы и вложенное боковое меню рабочей области |
Меняется только последняя строка или константа LAYOUT. Функции данных, связок, действий и событий остаются теми же. На телефоне панели автоматически становятся в одну колонку.
Готовый файл
Скопируйте весь код в один .py и загрузите через «Плагины → Разработчикам». Подключите Playerok, нажмите «Проверить подключение» и создайте первую связку. После трёх обязательных шагов Lovsel покажет обычный интерфейс. Секреты и ключи подключения площадки в код не вставляются.
# -*- coding: utf-8 -*-
"""Самостоятельный пример SDK 3. Меняется только LAYOUT, обработчики остаются теми же."""
from lovsel_sdk import WebPlugin, ui, ValidationError
MARKETPLACE = 'playerok'
LAYOUT = "hybrid" # single | tabs | sidebar | hybrid
MANIFEST = {
"id": "examples.platform." + MARKETPLACE,
"name": "Рабочее пространство " + 'Playerok',
"version": "1.0.0", "marketplace": MARKETPLACE, "ui_version": 3,
"description": "Обзор, реальные данные площадки, связки, настройка и события в одном стандарте Lovsel.",
"permissions": ["products.read", "orders.read", "storage", "notifications"],
"capabilities": ["web_ui", "settings", "events"],
}
def build_plugin(layout=LAYOUT):
app = WebPlugin(MANIFEST, layout=layout, settings_panel=True,
primary_action=ui.button("Проверить данные", "check", variant="primary", icon="refresh-cw"))
app.settings([ui.field.switch("enabled", "Обрабатывать события", default=False)])
app.section("work", "Рабочее пространство", icon="package")
app.setup_marketplace()
@app.action("check")
def check(ctx, data):
counts = ctx.market.verify()
ctx.log("Проверка данных: " + str(counts))
return ui.result("Доступ к данным проверен")
@app.block("overview", kind="overview", width=8, order=0, requires_connection=True)
def overview(ctx, params):
return [ui.stats([ui.stat(ctx.market.info["products"], len(ctx.market.products())),
ui.stat(ctx.market.info["orders"], len(ctx.market.orders()))]),
ui.automation_status(bool(ctx.settings.get("enabled")))]
@app.block("statistics", kind="statistics", page="overview", width=4, order=1, requires_connection=True)
def statistics(ctx, params):
counts = {}
for order in ctx.market.orders():
status = str(order.get("status") or "Не указан")
counts[status] = counts.get(status, 0) + 1
return [ui.chart([{"label": k, "value": v} for k, v in counts.items()], kind="bars", label="По статусам")] if counts else [ui.state("empty")]
@app.block("products", kind="products", section="work", order=2, requires_connection=True)
def products(ctx, params):
return [ui.market_table("products", ctx.market.products(), marketplace=ctx.marketplace)]
@app.block("orders", kind="orders", section="work", order=3, requires_connection=True)
def orders(ctx, params):
return [ui.market_table("orders", ctx.market.orders(), marketplace=ctx.marketplace)]
@app.block("bindings", kind="bindings", section="work", order=4, requires_connection=True)
def bindings(ctx, params):
rows = ctx.storage.get("bindings", [])
if params.get("id"):
rows = [row for row in rows if row["product_id"] == str(params["id"])]
return [ui.bindings_editor("binding", rows, marketplace=ctx.marketplace, action="save_binding")]
@app.action("save_binding")
def save_binding(ctx, data):
values = data
product_id = str(values.get("product_id") or "")
external_id = str(values.get("external_id") or "").strip()
if product_id not in {p["id"] for p in ctx.market.products()}:
raise ValidationError({"product_id": "Выберите свой товар или лот площадки"})
if not external_id or len(external_id) > 200:
raise ValidationError({"external_id": "Укажите ID сервиса, до 200 символов"})
rows = [row for row in ctx.storage.get("bindings", []) if row["product_id"] != product_id]
ctx.storage.set("bindings", rows + [{"id": product_id, "product_id": product_id, "external_id": external_id}])
ctx.log("Связка сохранена: " + product_id)
return ui.result("Связка сохранена")
@app.onboarding_step("binding", "Создайте первую связку", complete=lambda ctx: bool(ctx.storage.get("bindings", [])))
def first_binding(ctx, params):
return bindings(ctx, params)
@app.block("logs", kind="logs", section="work", order=5)
def logs(ctx, params):
return [ui.logs(), ui.operations()]
@app.block("help", kind="help", order=6)
def help_page(ctx, params):
return [ui.text("Подключите площадку, проверьте чтение и сохраните первую связку. "
"Настройки доступны справа. Этот пример ничего не закупает и не выдаёт автоматически.")]
@app.on('playerok.new_order')
def new_order(ctx, data):
if not ctx.settings.get("enabled"):
return
order = data.get("order") or data
order_id = order.get("id") or order.get("order_id")
if not order_id:
return
ctx.notify("Новый заказ для проверки", cause="Площадка сообщила о новом заказе.",
consequence="Заказ доступен в рабочем пространстве; пример не выполняет выдачу.",
solution="Откройте заказ и проверьте данные.", order_id=order_id,
marketplace=ctx.marketplace, group_key="order:" + str(order_id),
primary_action={"label": "Открыть заказ", "entity": "order", "id": order_id},
secondary_action={"label": "Связки", "page": "bindings"})
return app
app = build_plugin()Как развивать пример
- Добавьте настройки через
app.settingsи валидатор значений. - Добавьте свой обработчик к
app.blockили примените шаблонsupplier,delivery,pricing,sync. - Специфичные действия Playerok вызывайте через
ctx.playerokс объявленными разрешениями. - Переход к связке:
params={"id": product_id}; переход из события к объекту площадки:entityиid. - Длительную работу вынесите в
ctx.operations, а ошибки объединяйте через осмысленныйgroup_key.
Проверка без аккаунта
from lovsel_sdk.testing import load_plugin
plugin = load_plugin("platform_playerok.py", products=[{"id": "101", "name": "Товар"}],
funpay={"lots": [{"id": "101", "title": "Лот"}]},
playerok={"items": [{"id": "101", "title": "Товар"}]})
plugin.page("__setup")
plugin.action("__verify_marketplace")
plugin.action("save_binding", {"product_id": "101", "external_id": "SKU-101"})
assert plugin.app.workspace(plugin.ctx)["onboarding"]["complete"]Подробности: сценарии, компоненты и таблица совместимости, специфичные методы Playerok, компоненты UI.
Частые вопросы
Нужен ли отдельный интерфейс для каждой площадки?
Нет. GGsel, FunPay и Playerok используют один SDK и одни компоненты. Площадка определяет данные, доступные действия, названия объектов и цвет, а не несовместимый набор элементов.
Можно ли сменить компоновку без переписывания логики?
Да. Выберите single, tabs, sidebar или hybrid через layout или app.compose. Функции блоков и действий остаются прежними; меняются только страницы, разделы, ширина и порядок размещения.
Старый плагин продолжит работать после обновления?
Да. Прежние app.page, формы, настройки и адаптеры сохранены. Новый интерфейс включается через ui_version=3 или новые методы композиции. Данные продавца не нужно переносить в новый формат.