poe-svelte-ui-lib 1.21.0 → 1.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/WidgetDeviceInfo/README.md +82 -0
- package/dist/WidgetDeviceInfo/WidgetDeviceInfo.svelte +44 -36
- package/dist/WidgetHeader.svelte +45 -0
- package/dist/WidgetHeader.svelte.d.ts +10 -0
- package/dist/WidgetWiFi/README.md +84 -0
- package/dist/WidgetWiFi/WidgetWiFi.svelte +82 -72
- package/dist/types.d.ts +2 -0
- package/dist/widgetCollapse.d.ts +2 -0
- package/dist/widgetCollapse.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# WidgetDeviceInfo Component
|
|
2
|
+
|
|
3
|
+
## Описание
|
|
4
|
+
|
|
5
|
+
Смарт-виджет информации об устройстве. Как и `WidgetWiFi`, не привязывается к одной произвольной
|
|
6
|
+
переменной устройства — работает с фиксированным набором полей, соответствующих реальной
|
|
7
|
+
прошивочной конвенции (`main_config.c`, группа `CFG`, ProdFactory-ESP): `DevSN`, `DevID`, `DevFW`,
|
|
8
|
+
`RunCnt` — только чтение; `DevName`, `HostName`, `WebUser`, `WebPsw` — редактируются локально и
|
|
9
|
+
сохраняются одним пакетом по кнопке "Сохранить". Отдельная кнопка "Перезагрузить" рядом с
|
|
10
|
+
заголовком вызывает `onRestart` немедленно (не входит в пакет сохранения полей). Компонент
|
|
11
|
+
ничего не знает про DeviceStore/WebSocket — только `value`/`onSave`/`onRestart`.
|
|
12
|
+
|
|
13
|
+
## Пропсы
|
|
14
|
+
|
|
15
|
+
| Название | Тип | По умолчанию | Описание |
|
|
16
|
+
| ---------------- | ------------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------- |
|
|
17
|
+
| `wrapperClass` | `string` | `""` | CSS-классы для обёртки компонента |
|
|
18
|
+
| `componentClass` | `string` | `""` | "Мастер-цвет" виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`) |
|
|
19
|
+
| `label` | `{ name?: string }` | `{ name: "Информация об устройстве" }` | Заголовок карточки |
|
|
20
|
+
| `value` | `IWidgetDeviceInfoConfig` | внутренний дефолт | Текущая информация об устройстве |
|
|
21
|
+
| `onSave` | `(info: IWidgetDeviceInfoConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — редактируемые поля одним пакетом |
|
|
22
|
+
| `onRestart` | `() => void` | `undefined` | Срабатывает при нажатии "Перезагрузить" |
|
|
23
|
+
|
|
24
|
+
### IWidgetDeviceInfoConfig
|
|
25
|
+
|
|
26
|
+
| Поле | Тип | Только чтение | Описание |
|
|
27
|
+
| ----------- | -------- | :-----------: | ------------------------------------ |
|
|
28
|
+
| `DevSN` | `string` | да | Серийный номер устройства |
|
|
29
|
+
| `DevID` | `string` | да | ID устройства |
|
|
30
|
+
| `DevFW` | `number` | да | Версия ПО |
|
|
31
|
+
| `RunCnt` | `number` | да | Счётчик запусков |
|
|
32
|
+
| `DevName` | `string` | нет | Имя устройства |
|
|
33
|
+
| `HostName` | `string` | нет | Имя хоста |
|
|
34
|
+
| `WebUser` | `string` | нет | Имя пользователя Web-панели |
|
|
35
|
+
| `WebPsw` | `string` | нет | Пароль Web-панели |
|
|
36
|
+
|
|
37
|
+
## Примеры
|
|
38
|
+
|
|
39
|
+
```svelte
|
|
40
|
+
<script>
|
|
41
|
+
import * as UI from "poe-svelte-ui-lib"
|
|
42
|
+
|
|
43
|
+
const handleSave = (info) => {
|
|
44
|
+
console.log("Отправка настроек одним пакетом:", info)
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<UI.WidgetDeviceInfo
|
|
49
|
+
value={{
|
|
50
|
+
DevSN: "00.00.000-00_0000AC670000B22B00007428:DBC5",
|
|
51
|
+
DevID: "20.00.000-01",
|
|
52
|
+
DevFW: 1.4,
|
|
53
|
+
RunCnt: 128,
|
|
54
|
+
DevName: "SYP-Device-01",
|
|
55
|
+
HostName: "syp-device-01",
|
|
56
|
+
WebUser: "admin",
|
|
57
|
+
WebPsw: "",
|
|
58
|
+
}}
|
|
59
|
+
onSave={handleSave}
|
|
60
|
+
onRestart={() => console.log("Перезагрузка устройства")}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Конструктор свойств (WidgetDeviceInfoProps.svelte)
|
|
65
|
+
|
|
66
|
+
### Пропсы конструктора
|
|
67
|
+
|
|
68
|
+
| Название | Тип | Описание |
|
|
69
|
+
| ------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------- |
|
|
70
|
+
| `component` | `UIComponent & { properties: Partial<IWidgetDeviceInfoProps> }` | Объект компонента с его свойствами |
|
|
71
|
+
| `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string }>) => void` | Коллбэк для обновления свойств компонента |
|
|
72
|
+
|
|
73
|
+
Панель минимальна: заголовок и мастер-цвет (через общий `WidgetAccentPicker.svelte`) — полей
|
|
74
|
+
для привязки нет, они фиксированы прошивочной конвенцией.
|
|
75
|
+
|
|
76
|
+
## Заметки
|
|
77
|
+
|
|
78
|
+
- Серийный номер устройства обычно длинный (например
|
|
79
|
+
`00.00.000-00_0000AC670000B22B00007428:DBC5`) — в разметке занимает отдельную строку, чтобы не
|
|
80
|
+
обрезаться в узкой сетке.
|
|
81
|
+
- `WebPsw` в реальном устройстве исторически передаётся обычным текстом; в виджете сделан
|
|
82
|
+
`type="password"` для консистентности с остальным обликом конструктора.
|
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
import type { IWidgetDeviceInfoConfig, IWidgetDeviceInfoProps } from "../types"
|
|
11
11
|
import CpuIcon from "./CpuIcon.svelte"
|
|
12
12
|
import RestartIcon from "./RestartIcon.svelte"
|
|
13
|
+
import WidgetHeader from "../WidgetHeader.svelte"
|
|
13
14
|
import { widgetAccentStyle } from "../widgetAccent"
|
|
15
|
+
import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
|
|
14
16
|
|
|
15
17
|
let {
|
|
16
18
|
wrapperClass = "",
|
|
17
19
|
componentClass = "",
|
|
18
20
|
label = { name: "Информация об устройстве" },
|
|
19
21
|
value = $bindable(),
|
|
22
|
+
persistKey,
|
|
20
23
|
onSave = () => {},
|
|
21
24
|
onRestart = () => {},
|
|
22
25
|
}: IWidgetDeviceInfoProps = $props()
|
|
@@ -36,6 +39,11 @@
|
|
|
36
39
|
},
|
|
37
40
|
)
|
|
38
41
|
|
|
42
|
+
/* Сворачивание тела виджета по клику на значок/заголовок (как аккордеон), но в отличие от
|
|
43
|
+
Accordion переживает перезагрузку страницы через localStorage, если передан persistKey */
|
|
44
|
+
let collapsed = $state(readPersistedCollapsed(persistKey))
|
|
45
|
+
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
46
|
+
|
|
39
47
|
/* Поля редактируются локально (как Argument: NoSend в реальной GUI) — отправка одним пакетом по кнопке */
|
|
40
48
|
let savedSnapshot = $state(JSON.stringify(info))
|
|
41
49
|
let isDirty = $derived(JSON.stringify(info) !== savedSnapshot)
|
|
@@ -50,43 +58,43 @@
|
|
|
50
58
|
style={accentStyle}
|
|
51
59
|
>
|
|
52
60
|
<!-- Заголовок -->
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<h3>{label?.name ?? "Информация об устройстве"}</h3>
|
|
59
|
-
</div>
|
|
60
|
-
<UI.Button wrapperClass="w-auto" componentClass="bg-transparent px-3" content={{ icon: RestartIcon, name: "Перезагрузить" }} onClick={onRestart} />
|
|
61
|
-
</div>
|
|
61
|
+
<WidgetHeader icon={CpuIcon} label={label?.name ?? "Информация об устройстве"} bind:collapsed>
|
|
62
|
+
{#snippet right()}
|
|
63
|
+
<UI.Button wrapperClass="w-auto" componentClass="bg-transparent px-3" content={{ icon: RestartIcon, name: "Перезагрузить" }} onClick={onRestart} />
|
|
64
|
+
{/snippet}
|
|
65
|
+
</WidgetHeader>
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
{#if !collapsed}
|
|
68
|
+
<div class="flex flex-col gap-4" transition:slide={{ duration: 150 }}>
|
|
69
|
+
<!-- Только чтение -->
|
|
70
|
+
<div class="flex flex-col gap-2 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
|
|
71
|
+
<!-- Серийник бывает длинным (напр. 00.00.000-00_0000AC670000B22B00007428:DBC5) — своя строка, чтобы не резался -->
|
|
72
|
+
<UI.Input label={{ name: "Серийный номер" }} value={info.DevSN} readonly help={{ copyButton: true }} />
|
|
73
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
74
|
+
<UI.Input label={{ name: "ID устройства" }} value={info.DevID} readonly />
|
|
75
|
+
<UI.Input label={{ name: "Версия ПО" }} value={info.DevFW} readonly />
|
|
76
|
+
<UI.Input label={{ name: "Запусков" }} value={info.RunCnt} readonly />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
<!-- Редактируемые настройки -->
|
|
81
|
+
<div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3">
|
|
82
|
+
<span class="text-[12px] font-semibold text-(--muted-color)">Настройки</span>
|
|
83
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
84
|
+
<UI.Input label={{ name: "Имя устройства" }} bind:value={info.DevName} maxlength={26} />
|
|
85
|
+
<UI.Input label={{ name: "Имя хоста" }} bind:value={info.HostName} maxlength={32} />
|
|
86
|
+
<UI.Input label={{ name: "Пользователь Web" }} bind:value={info.WebUser} maxlength={16} />
|
|
87
|
+
<UI.Input type="password" label={{ name: "Пароль Web" }} bind:value={info.WebPsw} maxlength={16} help={{ autocomplete: "off" }} />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
<!-- Сохранение -->
|
|
92
|
+
<div class="flex flex-col items-center gap-2">
|
|
93
|
+
{#if isDirty}
|
|
94
|
+
<span class="text-[12px] text-(--muted-color)" transition:slide={{ duration: 100 }}>Есть несохранённые изменения</span>
|
|
95
|
+
{/if}
|
|
96
|
+
<UI.Button wrapperClass="w-40" componentClass="bg-green" content={{ name: "Сохранить" }} disabled={!isDirty} onClick={save} />
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
92
100
|
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!-- $lib/WidgetHeader.svelte — общая шапка смарт-виджетов конструктора: значок + заголовок
|
|
2
|
+
сворачивают/разворачивают тело виджета по клику (как аккордеон), справа — снипет с любым
|
|
3
|
+
содержимым шапки конкретного виджета (переключатель режима, кнопка "Перезагрузить" и т.п.),
|
|
4
|
+
клик по нему сворачивание не триггерит. -->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import type { Component, Snippet } from "svelte"
|
|
7
|
+
import ArrowIcon from "./libIcons/ArrowIcon.svelte"
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
icon,
|
|
11
|
+
label,
|
|
12
|
+
collapsed = $bindable(false),
|
|
13
|
+
right,
|
|
14
|
+
}: {
|
|
15
|
+
icon: Component
|
|
16
|
+
label: string
|
|
17
|
+
collapsed?: boolean
|
|
18
|
+
right?: Snippet
|
|
19
|
+
} = $props()
|
|
20
|
+
|
|
21
|
+
const Icon = $derived(icon)
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="flex min-w-0 cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left"
|
|
28
|
+
onclick={() => (collapsed = !collapsed)}
|
|
29
|
+
aria-expanded={!collapsed}
|
|
30
|
+
>
|
|
31
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-xl bg-(--accent-soft) text-(--accent-color)">
|
|
32
|
+
<Icon />
|
|
33
|
+
</span>
|
|
34
|
+
<h3 class="min-w-0 truncate">{label}</h3>
|
|
35
|
+
<span
|
|
36
|
+
class="flex size-4 shrink-0 items-center justify-center text-(--faint-color) transition-transform duration-150 [&_svg]:h-full [&_svg]:w-full"
|
|
37
|
+
style="transform: rotate({collapsed ? 0 : 90}deg)"
|
|
38
|
+
>
|
|
39
|
+
<ArrowIcon />
|
|
40
|
+
</span>
|
|
41
|
+
</button>
|
|
42
|
+
{#if right}
|
|
43
|
+
{@render right()}
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
icon: Component;
|
|
4
|
+
label: string;
|
|
5
|
+
collapsed?: boolean;
|
|
6
|
+
right?: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const WidgetHeader: Component<$$ComponentProps, {}, "collapsed">;
|
|
9
|
+
type WidgetHeader = ReturnType<typeof WidgetHeader>;
|
|
10
|
+
export default WidgetHeader;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# WidgetWiFi Component
|
|
2
|
+
|
|
3
|
+
## Описание
|
|
4
|
+
|
|
5
|
+
Смарт-виджет настройки WiFi (режимы STA / AP / STA+AP). В отличие от примитивов конструктора,
|
|
6
|
+
не привязывается к одной произвольной переменной устройства через Select — работает с
|
|
7
|
+
фиксированным набором полей, соответствующих реальной прошивочной конвенции (`main_config.c`,
|
|
8
|
+
группа `CFG`, ProdFactory-ESP): `WiFiMode`, `StaSSID`, `StaPSK`, `StaticIP`, `StaIP`, `StaMS`,
|
|
9
|
+
`StaGW`, `ApSSID`, `ApPSK`, `ApIP`, `ApMS`, `ApGW`. Компонент ничего не знает про
|
|
10
|
+
DeviceStore/WebSocket — только `value`/`onSave`, вся отправка на стороне вызывающего кода.
|
|
11
|
+
|
|
12
|
+
Поля редактируются локально и отправляются одним пакетом по кнопке "Сохранить". Переход в
|
|
13
|
+
режим AP (отключает STA) подтверждается отдельно — предупреждение появляется в момент нажатия
|
|
14
|
+
"Сохранить", а не при простом переключении сегмент-контрола, чтобы можно было спокойно открыть
|
|
15
|
+
секцию AP и заполнить SSID/пароль до реальной отправки.
|
|
16
|
+
|
|
17
|
+
## Пропсы
|
|
18
|
+
|
|
19
|
+
| Название | Тип | По умолчанию | Описание |
|
|
20
|
+
| ----------------- | ------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------- |
|
|
21
|
+
| `wrapperClass` | `string` | `""` | CSS-классы для обёртки компонента |
|
|
22
|
+
| `componentClass` | `string` | `""` | "Мастер-цвет" виджета — роль вида `"bg-red"` (см. `optionsStore.COLOR_OPTIONS`); перекрашивает значок, сегмент-контролы и фокус-кольцо у полей |
|
|
23
|
+
| `label` | `{ name?: string }` | `{ name: "WiFi" }` | Заголовок карточки |
|
|
24
|
+
| `value` | `IWidgetWiFiConfig` | внутренний дефолт | Текущая конфигурация WiFi |
|
|
25
|
+
| `allowedModes` | `number[]` | `[1, 2, 3]` | Какие режимы показывать в сегмент-контроле (1=STA, 2=AP, 3=STA+AP) |
|
|
26
|
+
| `confirmOnAP` | `boolean` | `true` | Показывать предупреждение при сохранении чистого режима AP |
|
|
27
|
+
| `onScan` | `() => Promise<{ id: string; name: string; value: string }[]>` | `undefined` | Обработчик сканирования сетей (в проде — `Header: GET, Argument: APsList`) |
|
|
28
|
+
| `onSave` | `(config: IWidgetWiFiConfig) => void` | `undefined` | Срабатывает при нажатии "Сохранить" — конфиг уходит одним пакетом |
|
|
29
|
+
|
|
30
|
+
### IWidgetWiFiConfig
|
|
31
|
+
|
|
32
|
+
| Поле | Тип | Описание |
|
|
33
|
+
| ----------- | -------- | ---------------------------------------------------- |
|
|
34
|
+
| `WiFiMode` | `number` | 0=NULL, 1=STA, 2=AP, 3=STA+AP (как `esp_wifi wifi_mode_t`) |
|
|
35
|
+
| `StaSSID` | `string` | SSID сети в режиме клиента |
|
|
36
|
+
| `StaPSK` | `string` | Пароль сети в режиме клиента |
|
|
37
|
+
| `StaticIP` | `number` | 0=DHCP, 1=Static |
|
|
38
|
+
| `StaIP` | `string` | Статический IP клиента |
|
|
39
|
+
| `StaMS` | `string` | Маска подсети клиента |
|
|
40
|
+
| `StaGW` | `string` | Шлюз клиента |
|
|
41
|
+
| `ApSSID` | `string` | SSID точки доступа |
|
|
42
|
+
| `ApPSK` | `string` | Пароль точки доступа |
|
|
43
|
+
| `ApIP` | `string` | IP адрес точки доступа |
|
|
44
|
+
| `ApMS` | `string` | Маска подсети точки доступа |
|
|
45
|
+
| `ApGW` | `string` | Шлюз точки доступа |
|
|
46
|
+
|
|
47
|
+
## Примеры
|
|
48
|
+
|
|
49
|
+
```svelte
|
|
50
|
+
<script>
|
|
51
|
+
import * as UI from "poe-svelte-ui-lib"
|
|
52
|
+
|
|
53
|
+
const handleScan = async () => {
|
|
54
|
+
/* Header: GET, Argument: APsList */
|
|
55
|
+
return [{ id: "1", name: "MyNetwork", value: "MyNetwork" }]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleSave = (config) => {
|
|
59
|
+
console.log("Отправка конфига одним пакетом:", config)
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<UI.WidgetWiFi label={{ name: "WiFi" }} onScan={handleScan} onSave={handleSave} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Конструктор свойств (WidgetWiFiProps.svelte)
|
|
67
|
+
|
|
68
|
+
### Пропсы конструктора
|
|
69
|
+
|
|
70
|
+
| Название | Тип | Описание |
|
|
71
|
+
| ------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------- |
|
|
72
|
+
| `component` | `UIComponent & { properties: Partial<IWidgetWiFiProps> }` | Объект компонента с его свойствами |
|
|
73
|
+
| `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string }>) => void` | Коллбэк для обновления свойств компонента |
|
|
74
|
+
|
|
75
|
+
Панель проще, чем у примитивов: не "к какой переменной привязать" (полей несколько, и они
|
|
76
|
+
фиксированы прошивочной конвенцией), а "как виджет себя ведёт" — заголовок, разрешённые режимы,
|
|
77
|
+
подтверждение при AP, мастер-цвет (через общий `WidgetAccentPicker.svelte`).
|
|
78
|
+
|
|
79
|
+
## Заметки
|
|
80
|
+
|
|
81
|
+
- Первый по-настоящему составной (multi-field) тип компонента конструктора — примитивы всегда
|
|
82
|
+
привязаны к одной переменной устройства через `properties.id`.
|
|
83
|
+
- Чтение/батч-запись нескольких ключей `DeviceStore` реализуется на стороне приложения
|
|
84
|
+
(`GUIPreview.svelte`), а не внутри библиотеки.
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import SearchIcon from "./SearchIcon.svelte"
|
|
14
14
|
import LoaderIcon from "./LoaderIcon.svelte"
|
|
15
15
|
import WarningIcon from "./WarningIcon.svelte"
|
|
16
|
+
import WidgetHeader from "../WidgetHeader.svelte"
|
|
16
17
|
import { widgetAccentStyle } from "../widgetAccent"
|
|
18
|
+
import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
|
|
17
19
|
|
|
18
20
|
let {
|
|
19
21
|
wrapperClass = "",
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
value = $bindable(),
|
|
23
25
|
allowedModes = [1, 2, 3],
|
|
24
26
|
confirmOnAP = true,
|
|
27
|
+
persistKey,
|
|
25
28
|
onScan,
|
|
26
29
|
onSave = () => {},
|
|
27
30
|
}: IWidgetWiFiProps = $props()
|
|
@@ -83,6 +86,13 @@
|
|
|
83
86
|
scanning = false
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/* Сворачивание тела виджета по клику на значок/заголовок (как аккордеон) — переключатель режима
|
|
90
|
+
в шапке кликом не триггерит, только сама кнопка значок+заголовок (см. WidgetHeader.svelte).
|
|
91
|
+
В отличие от Accordion (только начальное значение из GUI) — переживает перезагрузку страницы
|
|
92
|
+
через localStorage, если передан persistKey. */
|
|
93
|
+
let collapsed = $state(readPersistedCollapsed(persistKey))
|
|
94
|
+
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
95
|
+
|
|
86
96
|
/* Поля редактируются локально (как Argument: NoSend в реальной GUI) — отправка одним пакетом по кнопке */
|
|
87
97
|
let savedSnapshot = $state(JSON.stringify(cfg))
|
|
88
98
|
let isDirty = $derived(JSON.stringify(cfg) !== savedSnapshot)
|
|
@@ -102,86 +112,86 @@
|
|
|
102
112
|
|
|
103
113
|
<div class={twMerge("relative flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)} style={accentStyle}>
|
|
104
114
|
<!-- Заголовок и переключатель режима -->
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
115
|
+
<WidgetHeader icon={WifiIcon} label={label?.name ?? "WiFi"} bind:collapsed>
|
|
116
|
+
{#snippet right()}
|
|
117
|
+
<UI.Select
|
|
118
|
+
wrapperClass="w-full sm:w-72"
|
|
119
|
+
type="buttons"
|
|
120
|
+
value={selectedMode}
|
|
121
|
+
options={MODE_OPTIONS}
|
|
122
|
+
onUpdate={(option) => (cfg.WiFiMode = (option as IOption<number>).value as number)}
|
|
123
|
+
/>
|
|
124
|
+
{/snippet}
|
|
125
|
+
</WidgetHeader>
|
|
126
|
+
|
|
127
|
+
{#if !collapsed}
|
|
128
|
+
<div class="flex flex-col gap-4" transition:slide={{ duration: 150 }}>
|
|
129
|
+
{#if cfg.WiFiMode === 1 || cfg.WiFiMode === 3}
|
|
130
|
+
<!-- Клиентский режим (STA) -->
|
|
131
|
+
<div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3" transition:slide={{ duration: 150 }}>
|
|
132
|
+
<span class="text-[12px] font-semibold text-(--muted-color)">Режим клиента (STA)</span>
|
|
133
|
+
|
|
134
|
+
<div class="flex items-end gap-2">
|
|
135
|
+
<UI.Select
|
|
136
|
+
wrapperClass="flex-1"
|
|
137
|
+
type="input"
|
|
138
|
+
label={{ name: "SSID сети" }}
|
|
139
|
+
value={{ id: "current", name: cfg.StaSSID, value: cfg.StaSSID }}
|
|
140
|
+
options={foundNetworks}
|
|
141
|
+
onUpdate={(option) => (cfg.StaSSID = (option as IOption<string>).name ?? "")}
|
|
142
|
+
/>
|
|
143
|
+
<UI.Button
|
|
144
|
+
wrapperClass="w-9"
|
|
145
|
+
componentClass="p-1"
|
|
146
|
+
content={{ icon: scanning ? LoaderIcon : SearchIcon, info: { text: "Сканировать сети", side: "top" } }}
|
|
147
|
+
disabled={scanning || !onScan}
|
|
148
|
+
onClick={scanNetworks}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<UI.Input type="password" label={{ name: "Пароль" }} bind:value={cfg.StaPSK} maxlength={64} help={{ autocomplete: "off", regExp: PSK_REGEXP }} />
|
|
153
|
+
|
|
154
|
+
<UI.Select wrapperClass="w-48" type="buttons" label={{ name: "Получение IP" }} value={selectedIPMode} options={IP_MODE_OPTIONS}
|
|
155
|
+
onUpdate={(option) => (cfg.StaticIP = (option as IOption<number>).value as number)} />
|
|
156
|
+
|
|
157
|
+
{#if cfg.StaticIP === 1}
|
|
158
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3" transition:slide={{ duration: 150 }}>
|
|
159
|
+
<UI.Input label={{ name: "IP адрес" }} bind:value={cfg.StaIP} maxlength={15} placeholder="192.168.1.100" help={{ regExp: IP_REGEXP }} />
|
|
160
|
+
<UI.Input label={{ name: "Маска подсети" }} bind:value={cfg.StaMS} maxlength={15} placeholder="255.255.255.0" help={{ regExp: IP_REGEXP }} />
|
|
161
|
+
<UI.Input label={{ name: "Шлюз" }} bind:value={cfg.StaGW} maxlength={15} placeholder="192.168.1.1" help={{ regExp: IP_REGEXP }} />
|
|
162
|
+
</div>
|
|
163
|
+
{/if}
|
|
154
164
|
</div>
|
|
155
165
|
{/if}
|
|
156
|
-
</div>
|
|
157
|
-
{/if}
|
|
158
|
-
|
|
159
|
-
{#if cfg.WiFiMode === 2 || cfg.WiFiMode === 3}
|
|
160
|
-
<!-- Точка доступа (AP) -->
|
|
161
|
-
<div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3" transition:slide={{ duration: 150 }}>
|
|
162
|
-
<span class="text-[12px] font-semibold text-(--muted-color)">Точка доступа (AP)</span>
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
167
|
+
{#if cfg.WiFiMode === 2 || cfg.WiFiMode === 3}
|
|
168
|
+
<!-- Точка доступа (AP) -->
|
|
169
|
+
<div class="flex flex-col gap-3 rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-3" transition:slide={{ duration: 150 }}>
|
|
170
|
+
<span class="text-[12px] font-semibold text-(--muted-color)">Точка доступа (AP)</span>
|
|
171
|
+
|
|
172
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
173
|
+
<UI.Input label={{ name: "SSID" }} bind:value={cfg.ApSSID} maxlength={32} />
|
|
174
|
+
<UI.Input type="password" label={{ name: "Пароль" }} bind:value={cfg.ApPSK} maxlength={64} help={{ autocomplete: "off", regExp: PSK_REGEXP }} />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
178
|
+
<UI.Input label={{ name: "IP адрес" }} bind:value={cfg.ApIP} maxlength={15} help={{ regExp: IP_REGEXP }} />
|
|
179
|
+
<UI.Input label={{ name: "Маска подсети" }} bind:value={cfg.ApMS} maxlength={15} help={{ regExp: IP_REGEXP }} />
|
|
180
|
+
<UI.Input label={{ name: "Шлюз" }} bind:value={cfg.ApGW} maxlength={15} help={{ regExp: IP_REGEXP }} />
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
{/if}
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
<!-- Сохранение -->
|
|
186
|
+
<div class="flex flex-col items-center gap-2">
|
|
187
|
+
{#if isDirty}
|
|
188
|
+
<span class="text-[12px] text-(--muted-color)" transition:slide={{ duration: 100 }}>Есть несохранённые изменения</span>
|
|
189
|
+
{/if}
|
|
190
|
+
<UI.Button wrapperClass="w-40" componentClass="bg-green" content={{ name: "Сохранить" }} disabled={!isDirty} onClick={save} />
|
|
173
191
|
</div>
|
|
174
192
|
</div>
|
|
175
193
|
{/if}
|
|
176
194
|
|
|
177
|
-
<!-- Сохранение -->
|
|
178
|
-
<div class="flex flex-col items-center gap-2">
|
|
179
|
-
{#if isDirty}
|
|
180
|
-
<span class="text-[12px] text-(--muted-color)" transition:slide={{ duration: 100 }}>Есть несохранённые изменения</span>
|
|
181
|
-
{/if}
|
|
182
|
-
<UI.Button wrapperClass="w-40" componentClass="bg-green" content={{ name: "Сохранить" }} disabled={!isDirty} onClick={save} />
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
195
|
{#if showAPWarning}
|
|
186
196
|
<!-- Подтверждение локально к виджету (не на весь экран) — предупреждение относится
|
|
187
197
|
именно к этой карточке, а не к странице целиком -->
|
package/dist/types.d.ts
CHANGED
|
@@ -516,6 +516,7 @@ export interface IWidgetWiFiProps {
|
|
|
516
516
|
value?: IWidgetWiFiConfig;
|
|
517
517
|
allowedModes?: number[];
|
|
518
518
|
confirmOnAP?: boolean;
|
|
519
|
+
persistKey?: string;
|
|
519
520
|
onScan?: () => Promise<{
|
|
520
521
|
id: string;
|
|
521
522
|
name: string;
|
|
@@ -541,6 +542,7 @@ export interface IWidgetDeviceInfoProps {
|
|
|
541
542
|
name?: string;
|
|
542
543
|
};
|
|
543
544
|
value?: IWidgetDeviceInfoConfig;
|
|
545
|
+
persistKey?: string;
|
|
544
546
|
onSave?: (info: IWidgetDeviceInfoConfig) => void;
|
|
545
547
|
onRestart?: () => void;
|
|
546
548
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* $lib/widgetCollapse.ts — запоминание свёрнутости смарт-виджетов в localStorage браузера
|
|
2
|
+
(Accordion этого не делает — isOpen у него только начальное значение из сохранённого GUI,
|
|
3
|
+
без переживания перезагрузки; виджеты сделаны надёжнее). typeof window-проверка вместо
|
|
4
|
+
$app/environment — библиотека не должна знать про SvelteKit. */
|
|
5
|
+
const STORAGE_PREFIX = "widget-collapsed:";
|
|
6
|
+
export const readPersistedCollapsed = (persistKey) => {
|
|
7
|
+
if (!persistKey || typeof window === "undefined")
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
return window.localStorage.getItem(STORAGE_PREFIX + persistKey) === "1";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const writePersistedCollapsed = (persistKey, collapsed) => {
|
|
17
|
+
if (!persistKey || typeof window === "undefined")
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(STORAGE_PREFIX + persistKey, collapsed ? "1" : "0");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* приватный режим/заблокировано — молча игнорируем, просто не запомнится */
|
|
24
|
+
}
|
|
25
|
+
};
|