К содержанию
Общее · Разработчикам

Playerok: полный пример плагина SDK 3

Самостоятельный плагин Playerok с обзором, данными, связками, настройкой и всеми четырьмя компоновками.

Содержание

Это самостоятельный файл docs/examples/platform_playerok.py, а не адаптация конкретного поставщика. Он использует общий интерфейс Lovsel: реальные товары, сделки, связки, статистику, настройки справа, мастер подключения и структурированные события. Права ограничены чтением, хранением данных и уведомлениями. События не закупают и не выдают товары: бизнес-действия добавляются отдельно и явно.

Четыре компоновки для Playerok

ВариантСтрока запускаРезультат
singleapp = build_plugin("single")Все блоки вместе на 12-колоночной сетке
tabsapp = build_plugin("tabs")Обзор и рабочая область во вкладках сверху
sidebarapp = build_plugin("sidebar")Внутреннее меню плагина со всеми разделами
hybridapp = build_plugin("hybrid")Верхние разделы и вложенное боковое меню рабочей области

Меняется только последняя строка или константа LAYOUT. Функции данных, связок, действий и событий остаются теми же. На телефоне панели автоматически становятся в одну колонку.

Готовый файл

Скопируйте весь код в один .py и загрузите через «Плагины → Разработчикам». Подключите Playerok, нажмите «Проверить подключение» и создайте первую связку. После трёх обязательных шагов Lovsel покажет обычный интерфейс. Секреты и ключи подключения площадки в код не вставляются.

python
# -*- 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()

Как развивать пример

  1. Добавьте настройки через app.settings и валидатор значений.
  2. Добавьте свой обработчик к app.block или примените шаблон supplier, delivery, pricing, sync.
  3. Специфичные действия Playerok вызывайте через ctx.playerok с объявленными разрешениями.
  4. Переход к связке: params={"id": product_id}; переход из события к объекту площадки: entity и id.
  5. Длительную работу вынесите в ctx.operations, а ошибки объединяйте через осмысленный group_key.

Проверка без аккаунта

python
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 или новые методы композиции. Данные продавца не нужно переносить в новый формат.