koishi-plugin-adapter-onebot-multi 0.0.18 → 1.0.0
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/client/page.vue +969 -60
- package/dist/index.js +5 -1
- package/dist/style.css +1 -1
- package/lib/config-store.d.ts +43 -0
- package/lib/index.d.ts +34 -19
- package/lib/index.js +559 -1775
- package/lib/load-balancer.d.ts +9 -2
- package/package.json +5 -9
package/client/page.vue
CHANGED
|
@@ -1,104 +1,1013 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<k-layout>
|
|
3
|
-
<div
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
3
|
+
<div class="pop-wrapper">
|
|
4
|
+
<div v-if="loading" class="pop-loading">
|
|
5
|
+
<div class="comic-text">LOADING...</div>
|
|
6
|
+
</div>
|
|
7
|
+
<div v-else class="pop-container">
|
|
8
|
+
|
|
9
|
+
<!-- HEADER -->
|
|
10
|
+
<header class="pop-header">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<div class="logo">✦</div>
|
|
13
|
+
<div class="title-box">
|
|
14
|
+
<h1>ONEBOT 控制中心</h1>
|
|
15
|
+
<p>实例管理与负载分发路由</p>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<nav class="pop-tabs">
|
|
20
|
+
<button class="pop-btn" :class="{ active: activeTab === 'bots' }" @click="activeTab = 'bots'">Bot 管理</button>
|
|
21
|
+
<button class="pop-btn" :class="{ active: activeTab === 'balance' }" @click="activeTab = 'balance'">负载均衡</button>
|
|
22
|
+
<button class="pop-btn action-btn" @click="refreshAll">刷新状态</button>
|
|
23
|
+
</nav>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<!-- BOTS TAB -->
|
|
27
|
+
<section v-show="activeTab === 'bots'" class="pop-panel bots-panel">
|
|
28
|
+
<div class="panel-header">
|
|
29
|
+
<div class="panel-title">
|
|
30
|
+
<h2>Bot 状态</h2>
|
|
31
|
+
<p>查看与管理当前运行实例</p>
|
|
32
|
+
</div>
|
|
33
|
+
<button class="pop-btn primary-btn" @click="openCreate">新增节点</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div v-if="bots.length === 0" class="empty-state">
|
|
37
|
+
<div class="comic-speech">无可用实例。请点击“新增节点”以配置。</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div v-else class="bot-grid">
|
|
41
|
+
<article v-for="bot in bots" :key="bot.selfId" class="bot-card" :class="{ disabled: bot.enabled === false }">
|
|
42
|
+
<div class="bot-header">
|
|
43
|
+
<img class="bot-avatar" :src="bot.avatarUrl || ''" alt="avatar" />
|
|
44
|
+
<div class="bot-info">
|
|
45
|
+
<div class="bot-name">{{ bot.name || `Bot ${bot.selfId}` }}</div>
|
|
46
|
+
<div class="bot-sub">{{ bot.nickname || '未知' }} | {{ bot.protocol === 'ws-reverse' ? '反向 WebSocket' : bot.protocol || '反向 WebSocket' }}</div>
|
|
47
|
+
<div class="bot-id">{{ bot.selfId }}</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="status-badge" :class="`status-${bot.status || 'offline'}`">
|
|
50
|
+
{{ statusText(bot.status) }}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="bot-stats">
|
|
55
|
+
<div class="stat-box"><span>群组</span><strong>{{ bot.groupCount ?? '-' }}</strong></div>
|
|
56
|
+
<div class="stat-box"><span>好友</span><strong>{{ bot.friendCount ?? '-' }}</strong></div>
|
|
57
|
+
<div class="stat-box"><span>收</span><strong>{{ bot.messageReceived ?? '-' }}</strong></div>
|
|
58
|
+
<div class="stat-box"><span>发</span><strong>{{ bot.messageSent ?? '-' }}</strong></div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="bot-meta">
|
|
62
|
+
<div><strong>终端:</strong> {{ bot.endpoint || '-' }}</div>
|
|
63
|
+
<div><strong>路径:</strong> {{ bot.path || '/onebot' }}</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="bot-actions">
|
|
67
|
+
<button class="pop-btn mini" @click="openEdit(bot)">编辑</button>
|
|
68
|
+
<button class="pop-btn mini" @click="toggleBot(bot)">{{ bot.enabled === false ? '启用' : '禁用' }}</button>
|
|
69
|
+
<button class="pop-btn mini" @click="restartBot(bot)">重启</button>
|
|
70
|
+
<button class="pop-btn mini" @click="openProfile(bot)">昵称</button>
|
|
71
|
+
<button class="pop-btn mini" @click="openAvatar(bot)">头像</button>
|
|
72
|
+
<button class="pop-btn mini danger" @click="removeBot(bot)">删除</button>
|
|
73
|
+
</div>
|
|
74
|
+
</article>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- MERGED RUNTIME SETTINGS -->
|
|
78
|
+
<div class="panel-header" style="margin-top: 40px;">
|
|
79
|
+
<div class="panel-title">
|
|
80
|
+
<h2>运行参数</h2>
|
|
81
|
+
<p>调整核心连接与重试策略</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="form-grid">
|
|
86
|
+
<label class="pop-label">
|
|
87
|
+
超时时间 (毫秒)
|
|
88
|
+
<input v-model.number="runtime.responseTimeout" type="number" class="pop-input">
|
|
89
|
+
</label>
|
|
90
|
+
<label class="pop-label">
|
|
91
|
+
心跳间隔 (毫秒)
|
|
92
|
+
<input v-model.number="runtime.heartbeatInterval" type="number" class="pop-input">
|
|
93
|
+
</label>
|
|
94
|
+
<label class="pop-label">
|
|
95
|
+
重试次数
|
|
96
|
+
<input v-model.number="runtime.retryTimes" type="number" class="pop-input">
|
|
97
|
+
</label>
|
|
98
|
+
<label class="pop-label">
|
|
99
|
+
重试间隔 (毫秒)
|
|
100
|
+
<input v-model.number="runtime.retryInterval" type="number" class="pop-input">
|
|
101
|
+
</label>
|
|
102
|
+
<label class="pop-label">
|
|
103
|
+
懒重试 (毫秒)
|
|
104
|
+
<input v-model.number="runtime.retryLazy" type="number" class="pop-input">
|
|
105
|
+
</label>
|
|
106
|
+
<label class="pop-label pop-checkbox" style="align-self: center; margin-top: 24px;">
|
|
107
|
+
<input v-model="runtime.advanced.splitMixedContent" type="checkbox" class="pop-check">
|
|
108
|
+
<span>拆分混合内容</span>
|
|
109
|
+
</label>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="panel-footer">
|
|
113
|
+
<button class="pop-btn primary-btn" @click="saveRuntime">保存配置</button>
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<!-- LOAD BALANCE TAB -->
|
|
118
|
+
<section v-show="activeTab === 'balance'" class="pop-panel balance-panel">
|
|
119
|
+
<div class="panel-header">
|
|
120
|
+
<div class="panel-title">
|
|
121
|
+
<h2>负载均衡</h2>
|
|
122
|
+
<p>配置全局流量分发规则</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="form-grid">
|
|
127
|
+
<label class="pop-label pop-checkbox">
|
|
128
|
+
<input v-model="loadBalance.enabled" type="checkbox" class="pop-check">
|
|
129
|
+
<span>启用负载均衡</span>
|
|
130
|
+
</label>
|
|
131
|
+
<label class="pop-label">
|
|
132
|
+
均衡间隔 (秒)
|
|
133
|
+
<input v-model.number="loadBalance.balanceInterval" type="number" class="pop-input">
|
|
134
|
+
</label>
|
|
135
|
+
<label class="pop-label">
|
|
136
|
+
过滤模式
|
|
137
|
+
<select v-model="loadBalance.channelFilterMode" class="pop-select">
|
|
138
|
+
<option value="blacklist">黑名单</option>
|
|
139
|
+
<option value="whitelist">白名单</option>
|
|
140
|
+
</select>
|
|
141
|
+
</label>
|
|
142
|
+
<label class="pop-label">
|
|
143
|
+
默认最大负载 (0=无限)
|
|
144
|
+
<input v-model.number="loadBalance.defaultMaxLoad" type="number" class="pop-input">
|
|
145
|
+
</label>
|
|
146
|
+
<label class="pop-label">
|
|
147
|
+
未分配值
|
|
148
|
+
<input v-model="loadBalance.unassignedValue" type="text" class="pop-input">
|
|
149
|
+
</label>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="split-grid">
|
|
153
|
+
<label class="pop-label">
|
|
154
|
+
群黑名单 (每行一个)
|
|
155
|
+
<textarea v-model="blacklistText" rows="5" class="pop-textarea" />
|
|
156
|
+
</label>
|
|
157
|
+
<label class="pop-label">
|
|
158
|
+
群白名单 (每行一个)
|
|
159
|
+
<textarea v-model="whitelistText" rows="5" class="pop-textarea" />
|
|
160
|
+
</label>
|
|
161
|
+
<label class="pop-label">
|
|
162
|
+
优先群聊 (每行一个)
|
|
163
|
+
<textarea v-model="priorityText" rows="5" class="pop-textarea" />
|
|
164
|
+
</label>
|
|
165
|
+
|
|
166
|
+
<div class="pop-table-editor">
|
|
167
|
+
<div class="table-head">
|
|
168
|
+
<span>节点最大负载</span>
|
|
169
|
+
<button class="pop-btn mini" @click="addBotMaxRow">添加行</button>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="table-body">
|
|
172
|
+
<div v-if="botMaxLoadRows.length === 0" class="empty-row">暂无规则。</div>
|
|
173
|
+
<div v-for="(row, idx) in botMaxLoadRows" :key="`max-${idx}`" class="editor-row">
|
|
174
|
+
<select v-model="row.botId" class="pop-select mini-input">
|
|
175
|
+
<option disabled value="">选择 Bot</option>
|
|
176
|
+
<option v-for="bot in bots" :key="bot.selfId" :value="bot.selfId">
|
|
177
|
+
{{ bot.name || bot.nickname || bot.selfId }}
|
|
178
|
+
</option>
|
|
179
|
+
</select>
|
|
180
|
+
<input v-model.number="row.maxLoad" type="number" min="0" placeholder="最大值" class="pop-input mini-input" />
|
|
181
|
+
<button class="pop-btn mini danger" @click="removeBotMaxRow(idx)">X</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="pop-table-editor full-width">
|
|
187
|
+
<div class="table-head">
|
|
188
|
+
<span>群聊专属节点</span>
|
|
189
|
+
<button class="pop-btn mini" @click="addChannelPriorityRow">添加行</button>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="table-body">
|
|
192
|
+
<div v-if="channelPriorityRows.length === 0" class="empty-row">暂无规则。</div>
|
|
193
|
+
<div v-for="(row, idx) in channelPriorityRows" :key="`priority-${idx}`" class="editor-row" style="display: flex; flex-direction: column;">
|
|
194
|
+
<div style="display: flex; gap: 8px;">
|
|
195
|
+
<input v-model="row.channelId" type="text" placeholder="群号" class="pop-input mini-input" style="flex: 1;" />
|
|
196
|
+
<button class="pop-btn mini danger" @click="removeChannelPriorityRow(idx)">X</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="bot-checkbox-group" style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px;">
|
|
199
|
+
<label v-for="bot in bots" :key="bot.selfId" class="pop-checkbox mini-checkbox">
|
|
200
|
+
<input type="checkbox" :value="bot.selfId" v-model="row.botIds" class="pop-check mini-check">
|
|
201
|
+
<span>{{ bot.name || bot.nickname || bot.selfId }}</span>
|
|
202
|
+
</label>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div class="panel-footer">
|
|
210
|
+
<button class="pop-btn primary-btn" @click="saveLoadBalance">保存规则</button>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- MODALS -->
|
|
216
|
+
<!-- Create/Edit Bot -->
|
|
217
|
+
<div v-if="editing" class="pop-modal-overlay" @click.self="editing = null">
|
|
218
|
+
<div class="pop-modal comic-panel">
|
|
219
|
+
<div class="comic-burst">★</div>
|
|
220
|
+
<h3>{{ editingMode === 'create' ? '创建节点' : '编辑节点' }}</h3>
|
|
221
|
+
<div class="form-grid modal-grid">
|
|
222
|
+
<label class="pop-label">名称<input v-model="editing.name" type="text" class="pop-input" placeholder="选填"></label>
|
|
223
|
+
<label class="pop-label hidden">自身 ID<input v-model="editing.selfId" type="text" class="pop-input"></label>
|
|
224
|
+
<label class="pop-label">访问密钥<input v-model="editing.token" type="text" class="pop-input"></label>
|
|
225
|
+
<label class="pop-label">协议
|
|
226
|
+
<select v-model="editing.protocol" class="pop-select">
|
|
227
|
+
<option value="ws-reverse">反向 WebSocket</option>
|
|
228
|
+
<option value="ws">ws</option>
|
|
229
|
+
</select>
|
|
230
|
+
</label>
|
|
231
|
+
<label class="pop-label">终端地址 (ws)<input v-model="editing.endpoint" type="text" class="pop-input"></label>
|
|
232
|
+
<label class="pop-label">Path (反向 WebSocket)<input v-model="editing.path" type="text" class="pop-input"></label>
|
|
233
|
+
<label class="pop-label pop-checkbox"><input v-model="editing.enabled" type="checkbox" class="pop-check"><span>已启用</span></label>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="modal-actions">
|
|
236
|
+
<button class="pop-btn primary-btn" @click="saveBot">保存</button>
|
|
237
|
+
<button class="pop-btn" @click="editing = null">取消</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Profile Modal -->
|
|
243
|
+
<div v-if="profileTarget" class="pop-modal-overlay" @click.self="profileTarget = null">
|
|
244
|
+
<div class="pop-modal comic-panel">
|
|
245
|
+
<h3>修改昵称</h3>
|
|
246
|
+
<p class="modal-sub">{{ profileTarget.selfId }}</p>
|
|
247
|
+
<label class="pop-label">昵称<input v-model="profileNickname" type="text" class="pop-input"></label>
|
|
248
|
+
<div class="modal-actions">
|
|
249
|
+
<button class="pop-btn primary-btn" @click="saveProfile">保存</button>
|
|
250
|
+
<button class="pop-btn" @click="profileTarget = null">取消</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Avatar Modal -->
|
|
256
|
+
<div v-if="avatarTarget" class="pop-modal-overlay" @click.self="avatarTarget = null">
|
|
257
|
+
<div class="pop-modal comic-panel">
|
|
258
|
+
<h3>修改头像</h3>
|
|
259
|
+
<p class="modal-sub">{{ avatarTarget.selfId }}</p>
|
|
260
|
+
<label class="pop-label">
|
|
261
|
+
选择图片文件
|
|
262
|
+
<input type="file" accept="image/*" class="pop-file-input" @change="onAvatarFileChange">
|
|
263
|
+
</label>
|
|
264
|
+
<div class="modal-actions">
|
|
265
|
+
<button class="pop-btn primary-btn" @click="saveAvatar">保存</button>
|
|
266
|
+
<button class="pop-btn" @click="avatarTarget = null">取消</button>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- Toast -->
|
|
272
|
+
<div v-if="toast.show" class="pop-toast comic-panel" :class="toast.type">
|
|
273
|
+
<span v-if="toast.type === 'success'">✓</span>
|
|
274
|
+
<span v-else>✕</span>
|
|
275
|
+
{{ toast.message }}
|
|
276
|
+
</div>
|
|
9
277
|
</div>
|
|
10
|
-
<iframe
|
|
11
|
-
v-else
|
|
12
|
-
:src="adminUrl"
|
|
13
|
-
class="admin-iframe"
|
|
14
|
-
/>
|
|
15
278
|
</k-layout>
|
|
16
279
|
</template>
|
|
17
280
|
|
|
18
|
-
<script lang="ts"
|
|
19
|
-
import {
|
|
281
|
+
<script setup lang="ts">
|
|
282
|
+
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
20
283
|
import { send } from '@koishijs/client'
|
|
21
284
|
|
|
22
|
-
|
|
23
|
-
|
|
285
|
+
type BotStatus = 'online' | 'offline' | 'connecting'
|
|
286
|
+
|
|
287
|
+
interface ManagedBot {
|
|
288
|
+
selfId?: string
|
|
289
|
+
token?: string
|
|
290
|
+
protocol?: 'ws' | 'ws-reverse'
|
|
291
|
+
endpoint?: string
|
|
292
|
+
path?: string
|
|
293
|
+
name?: string
|
|
294
|
+
enabled?: boolean
|
|
295
|
+
status?: BotStatus
|
|
296
|
+
nickname?: string
|
|
297
|
+
avatarUrl?: string
|
|
298
|
+
groupCount?: number
|
|
299
|
+
friendCount?: number
|
|
300
|
+
messageReceived?: number
|
|
301
|
+
messageSent?: number
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface RuntimeConfig {
|
|
305
|
+
responseTimeout: number
|
|
306
|
+
heartbeatInterval: number
|
|
307
|
+
retryTimes: number
|
|
308
|
+
retryInterval: number
|
|
309
|
+
retryLazy: number
|
|
310
|
+
advanced: {
|
|
311
|
+
splitMixedContent: boolean
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface LoadBalanceConfig {
|
|
24
316
|
enabled: boolean
|
|
25
|
-
|
|
317
|
+
balanceInterval: number
|
|
318
|
+
channelFilterMode: 'blacklist' | 'whitelist'
|
|
319
|
+
channelBlacklist: string[]
|
|
320
|
+
channelWhitelist: string[]
|
|
321
|
+
priorityChannels: string[]
|
|
322
|
+
defaultMaxLoad: number
|
|
323
|
+
botMaxLoad: Record<string, number>
|
|
324
|
+
unassignedValue: string
|
|
325
|
+
channelBotPriority: Record<string, string[]>
|
|
26
326
|
}
|
|
27
327
|
|
|
28
328
|
const loading = ref(true)
|
|
29
|
-
const
|
|
329
|
+
const activeTab = ref<'bots' | 'balance'>('bots')
|
|
330
|
+
const bots = ref<ManagedBot[]>([])
|
|
30
331
|
|
|
31
|
-
const
|
|
332
|
+
const runtime = reactive<RuntimeConfig>({
|
|
333
|
+
responseTimeout: 60000,
|
|
334
|
+
heartbeatInterval: 30000,
|
|
335
|
+
retryTimes: 6,
|
|
336
|
+
retryInterval: 5000,
|
|
337
|
+
retryLazy: 60000,
|
|
338
|
+
advanced: {
|
|
339
|
+
splitMixedContent: true,
|
|
340
|
+
},
|
|
341
|
+
})
|
|
32
342
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
343
|
+
const loadBalance = reactive<LoadBalanceConfig>({
|
|
344
|
+
enabled: false,
|
|
345
|
+
balanceInterval: 600,
|
|
346
|
+
channelFilterMode: 'blacklist',
|
|
347
|
+
channelBlacklist: [],
|
|
348
|
+
channelWhitelist: [],
|
|
349
|
+
priorityChannels: [],
|
|
350
|
+
defaultMaxLoad: 0,
|
|
351
|
+
botMaxLoad: {},
|
|
352
|
+
unassignedValue: '',
|
|
353
|
+
channelBotPriority: {},
|
|
42
354
|
})
|
|
43
355
|
|
|
44
|
-
|
|
356
|
+
const blacklistText = ref('')
|
|
357
|
+
const whitelistText = ref('')
|
|
358
|
+
const priorityText = ref('')
|
|
359
|
+
const botMaxLoadRows = ref<Array<{ botId: string, maxLoad: number }>>([])
|
|
360
|
+
const channelPriorityRows = ref<Array<{ channelId: string, botIds: string[] }>>([])
|
|
361
|
+
|
|
362
|
+
const editingMode = ref<'create' | 'edit'>('create')
|
|
363
|
+
const editingOldSelfId = ref('')
|
|
364
|
+
const editing = ref<ManagedBot | null>(null)
|
|
365
|
+
|
|
366
|
+
const profileTarget = ref<ManagedBot | null>(null)
|
|
367
|
+
const profileNickname = ref('')
|
|
368
|
+
|
|
369
|
+
const avatarTarget = ref<ManagedBot | null>(null)
|
|
370
|
+
const avatarFile = ref('')
|
|
371
|
+
|
|
372
|
+
const toast = reactive({
|
|
373
|
+
show: false,
|
|
374
|
+
type: 'success' as 'success' | 'error',
|
|
375
|
+
message: '',
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
|
379
|
+
let refreshing = false
|
|
380
|
+
|
|
381
|
+
function lineArray(value: string) {
|
|
382
|
+
return value
|
|
383
|
+
.split('\n')
|
|
384
|
+
.map(v => v.trim())
|
|
385
|
+
.filter(Boolean)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function showToast(type: 'success' | 'error', message: string) {
|
|
389
|
+
toast.type = type
|
|
390
|
+
toast.message = message
|
|
391
|
+
toast.show = true
|
|
392
|
+
setTimeout(() => { toast.show = false }, 2000)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function statusText(status?: BotStatus) {
|
|
396
|
+
if (status === 'online') return '在线'
|
|
397
|
+
if (status === 'connecting') return '连接中'
|
|
398
|
+
return '离线'
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function applyLoadBalanceText() {
|
|
402
|
+
blacklistText.value = loadBalance.channelBlacklist.join('\n')
|
|
403
|
+
whitelistText.value = loadBalance.channelWhitelist.join('\n')
|
|
404
|
+
priorityText.value = loadBalance.priorityChannels.join('\n')
|
|
405
|
+
|
|
406
|
+
botMaxLoadRows.value = Object.entries(loadBalance.botMaxLoad || {}).map(([botId, maxLoad]) => ({
|
|
407
|
+
botId,
|
|
408
|
+
maxLoad: Number(maxLoad) || 0,
|
|
409
|
+
}))
|
|
410
|
+
|
|
411
|
+
channelPriorityRows.value = Object.entries(loadBalance.channelBotPriority || {}).map(([channelId, botIds]) => ({
|
|
412
|
+
channelId,
|
|
413
|
+
botIds: Array.isArray(botIds) ? [...botIds] : [],
|
|
414
|
+
}))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function addBotMaxRow() {
|
|
418
|
+
botMaxLoadRows.value.push({ botId: '', maxLoad: 0 })
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function removeBotMaxRow(index: number) {
|
|
422
|
+
botMaxLoadRows.value.splice(index, 1)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function addChannelPriorityRow() {
|
|
426
|
+
channelPriorityRows.value.push({ channelId: '', botIds: [] })
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function removeChannelPriorityRow(index: number) {
|
|
430
|
+
channelPriorityRows.value.splice(index, 1)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function refreshAll() {
|
|
434
|
+
if (refreshing) return
|
|
435
|
+
refreshing = true
|
|
45
436
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
437
|
+
const state = await (send as any)('onebot-multi/state')
|
|
438
|
+
bots.value = state?.bots || []
|
|
439
|
+
Object.assign(runtime, state?.runtime || {})
|
|
440
|
+
Object.assign(loadBalance, state?.loadBalance || {})
|
|
441
|
+
applyLoadBalanceText()
|
|
442
|
+
} catch (error: any) {
|
|
443
|
+
showToast('error', error?.message || '刷新失败')
|
|
49
444
|
} finally {
|
|
50
|
-
|
|
445
|
+
refreshing = false
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function openCreate() {
|
|
450
|
+
editingMode.value = 'create'
|
|
451
|
+
editingOldSelfId.value = ''
|
|
452
|
+
editing.value = {
|
|
453
|
+
token: '',
|
|
454
|
+
protocol: 'ws-reverse',
|
|
455
|
+
endpoint: '',
|
|
456
|
+
path: '/onebot',
|
|
457
|
+
name: '',
|
|
458
|
+
enabled: true,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function openEdit(bot: ManagedBot) {
|
|
463
|
+
editingMode.value = 'edit'
|
|
464
|
+
editingOldSelfId.value = bot.selfId
|
|
465
|
+
editing.value = {
|
|
466
|
+
...bot,
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function saveBot() {
|
|
471
|
+
if (!editing.value) return
|
|
472
|
+
try {
|
|
473
|
+
if (editingMode.value === 'create') {
|
|
474
|
+
await (send as any)('onebot-multi/bots/create', editing.value)
|
|
475
|
+
} else {
|
|
476
|
+
await (send as any)('onebot-multi/bots/update', {
|
|
477
|
+
oldSelfId: editingOldSelfId.value,
|
|
478
|
+
bot: editing.value,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
editing.value = null
|
|
483
|
+
await refreshAll()
|
|
484
|
+
showToast('success', '保存成功')
|
|
485
|
+
} catch (error: any) {
|
|
486
|
+
showToast('error', error?.message || '保存失败')
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function removeBot(bot: ManagedBot) {
|
|
491
|
+
if (!confirm(`确认删除 Bot ${bot.selfId} 吗?`)) return
|
|
492
|
+
try {
|
|
493
|
+
await (send as any)('onebot-multi/bots/delete', { selfId: bot.selfId })
|
|
494
|
+
await refreshAll()
|
|
495
|
+
showToast('success', '删除成功')
|
|
496
|
+
} catch (error: any) {
|
|
497
|
+
showToast('error', error?.message || '删除失败')
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function toggleBot(bot: ManagedBot) {
|
|
502
|
+
try {
|
|
503
|
+
await (send as any)('onebot-multi/bots/toggle', { selfId: bot.selfId })
|
|
504
|
+
await refreshAll()
|
|
505
|
+
showToast('success', '状态已更新')
|
|
506
|
+
} catch (error: any) {
|
|
507
|
+
showToast('error', error?.message || '更新失败')
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function restartBot(bot: ManagedBot) {
|
|
512
|
+
try {
|
|
513
|
+
await (send as any)('onebot-multi/bots/restart', { selfId: bot.selfId })
|
|
514
|
+
await refreshAll()
|
|
515
|
+
showToast('success', '重启已触发')
|
|
516
|
+
} catch (error: any) {
|
|
517
|
+
showToast('error', error?.message || '重启失败')
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function openProfile(bot: ManagedBot) {
|
|
522
|
+
profileTarget.value = bot
|
|
523
|
+
profileNickname.value = bot.nickname || ''
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function saveProfile() {
|
|
527
|
+
if (!profileTarget.value) return
|
|
528
|
+
try {
|
|
529
|
+
const result = await (send as any)('onebot-multi/bots/profile', {
|
|
530
|
+
selfId: profileTarget.value.selfId,
|
|
531
|
+
nickname: profileNickname.value,
|
|
532
|
+
})
|
|
533
|
+
if (!result?.success) throw new Error(result?.error || '保存失败')
|
|
534
|
+
profileTarget.value = null
|
|
535
|
+
await refreshAll()
|
|
536
|
+
showToast('success', '昵称已更新')
|
|
537
|
+
} catch (error: any) {
|
|
538
|
+
showToast('error', error?.message || '更新失败')
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function openAvatar(bot: ManagedBot) {
|
|
543
|
+
avatarTarget.value = bot
|
|
544
|
+
avatarFile.value = ''
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function onAvatarFileChange(e: Event) {
|
|
548
|
+
const target = e.target as HTMLInputElement
|
|
549
|
+
const file = target.files?.[0]
|
|
550
|
+
if (!file) {
|
|
551
|
+
avatarFile.value = ''
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
const reader = new FileReader()
|
|
555
|
+
reader.onload = (ev) => {
|
|
556
|
+
avatarFile.value = ev.target?.result as string
|
|
557
|
+
}
|
|
558
|
+
reader.readAsDataURL(file)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function saveAvatar() {
|
|
562
|
+
if (!avatarTarget.value || !avatarFile.value.trim()) {
|
|
563
|
+
showToast('error', '请先选择图片文件')
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const result = await (send as any)('onebot-multi/bots/avatar', {
|
|
568
|
+
selfId: avatarTarget.value.selfId,
|
|
569
|
+
file: avatarFile.value,
|
|
570
|
+
})
|
|
571
|
+
if (!result?.success) throw new Error(result?.error || '保存失败')
|
|
572
|
+
avatarTarget.value = null
|
|
573
|
+
showToast('success', '头像已更新')
|
|
574
|
+
} catch (error: any) {
|
|
575
|
+
showToast('error', error?.message || '更新失败')
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function saveRuntime() {
|
|
580
|
+
try {
|
|
581
|
+
await (send as any)('onebot-multi/settings/runtime/update', {
|
|
582
|
+
...runtime,
|
|
583
|
+
})
|
|
584
|
+
await refreshAll()
|
|
585
|
+
showToast('success', '运行时设置已保存')
|
|
586
|
+
} catch (error: any) {
|
|
587
|
+
showToast('error', error?.message || '保存失败')
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function saveLoadBalance() {
|
|
592
|
+
try {
|
|
593
|
+
const botMaxLoad: Record<string, number> = {}
|
|
594
|
+
for (const row of botMaxLoadRows.value) {
|
|
595
|
+
const botId = row.botId.trim()
|
|
596
|
+
if (!botId) continue
|
|
597
|
+
const maxLoad = Math.max(0, Number(row.maxLoad || 0))
|
|
598
|
+
botMaxLoad[botId] = maxLoad
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const channelBotPriority: Record<string, string[]> = {}
|
|
602
|
+
for (const row of channelPriorityRows.value) {
|
|
603
|
+
const channelId = row.channelId.trim()
|
|
604
|
+
if (!channelId) continue
|
|
605
|
+
const botIds = (row.botIds || []).filter(Boolean)
|
|
606
|
+
if (botIds.length > 0) {
|
|
607
|
+
channelBotPriority[channelId] = botIds
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const payload = {
|
|
612
|
+
...loadBalance,
|
|
613
|
+
channelBlacklist: lineArray(blacklistText.value),
|
|
614
|
+
channelWhitelist: lineArray(whitelistText.value),
|
|
615
|
+
priorityChannels: lineArray(priorityText.value),
|
|
616
|
+
botMaxLoad,
|
|
617
|
+
channelBotPriority,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
await (send as any)('onebot-multi/settings/load-balance/update', payload)
|
|
621
|
+
await refreshAll()
|
|
622
|
+
showToast('success', '负载均衡设置已保存')
|
|
623
|
+
} catch (error: any) {
|
|
624
|
+
showToast('error', error?.message || '保存失败')
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
onMounted(async () => {
|
|
629
|
+
await refreshAll()
|
|
630
|
+
loading.value = false
|
|
631
|
+
refreshTimer = setInterval(() => {
|
|
632
|
+
refreshAll()
|
|
633
|
+
}, 5000)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
onUnmounted(() => {
|
|
637
|
+
if (refreshTimer) {
|
|
638
|
+
clearInterval(refreshTimer)
|
|
639
|
+
refreshTimer = null
|
|
51
640
|
}
|
|
52
641
|
})
|
|
53
642
|
</script>
|
|
54
643
|
|
|
55
644
|
<style scoped lang="scss">
|
|
56
|
-
|
|
57
|
-
:deep(.k-
|
|
645
|
+
:deep(.k-layout),
|
|
646
|
+
:deep(.k-layout__main) {
|
|
58
647
|
background: transparent !important;
|
|
59
648
|
padding: 0 !important;
|
|
60
649
|
margin: 0 !important;
|
|
650
|
+
height: 100%;
|
|
651
|
+
position: relative;
|
|
61
652
|
}
|
|
62
653
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
654
|
+
.pop-wrapper {
|
|
655
|
+
/* Core Palette */
|
|
656
|
+
--pop-bg: #fffbeb; /* Soft Beige/Warm Yellow */
|
|
657
|
+
--pop-text: #6b5243; /* Soft Brown */
|
|
658
|
+
--pop-border: 3px solid var(--pop-text);
|
|
659
|
+
--pop-shadow-color: var(--pop-text);
|
|
660
|
+
--pop-panel: #ffffff;
|
|
661
|
+
--pop-primary: #ff9a9e; /* Soft Pink */
|
|
662
|
+
--pop-secondary: #a1c4fd; /* Soft Blue */
|
|
663
|
+
--pop-success: #b2fba5; /* Soft Green */
|
|
664
|
+
--pop-danger: #ffb3ba; /* Soft Red */
|
|
665
|
+
|
|
666
|
+
/* Dots for halftone */
|
|
667
|
+
--pop-dot: rgba(107, 82, 67, 0.05); /* very subtle */
|
|
668
|
+
|
|
669
|
+
width: 100%;
|
|
670
|
+
height: 100%;
|
|
671
|
+
box-sizing: border-box;
|
|
672
|
+
color: var(--pop-text);
|
|
673
|
+
font-family: 'Nunito', 'Varela Round', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
674
|
+
|
|
675
|
+
background-color: var(--pop-bg);
|
|
676
|
+
background-image:
|
|
677
|
+
radial-gradient(var(--pop-dot) 15%, transparent 16%),
|
|
678
|
+
radial-gradient(var(--pop-dot) 15%, transparent 16%);
|
|
679
|
+
background-size: 24px 24px;
|
|
680
|
+
background-position: 0 0, 12px 12px;
|
|
681
|
+
|
|
682
|
+
position: absolute;
|
|
683
|
+
top: 0;
|
|
684
|
+
left: 0;
|
|
685
|
+
right: 0;
|
|
686
|
+
bottom: 0;
|
|
687
|
+
z-index: 0;
|
|
688
|
+
overflow-y: auto;
|
|
689
|
+
padding: 24px;
|
|
690
|
+
|
|
691
|
+
/* Bold, uppercase emphasis */
|
|
692
|
+
font-weight: 800;
|
|
68
693
|
}
|
|
69
694
|
|
|
70
|
-
|
|
71
|
-
|
|
695
|
+
.pop-wrapper * {
|
|
696
|
+
box-sizing: border-box;
|
|
72
697
|
}
|
|
73
698
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
699
|
+
/* Base Comic Style */
|
|
700
|
+
.comic-panel {
|
|
701
|
+
background: var(--pop-panel);
|
|
702
|
+
border: var(--pop-border);
|
|
703
|
+
box-shadow: 4px 4px 0 var(--pop-shadow-color);
|
|
704
|
+
border-radius: 16px;
|
|
77
705
|
}
|
|
78
706
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
707
|
+
/* Loading */
|
|
708
|
+
.pop-loading {
|
|
709
|
+
display: flex;
|
|
710
|
+
justify-content: center;
|
|
711
|
+
align-items: center;
|
|
712
|
+
height: 60vh;
|
|
713
|
+
}
|
|
714
|
+
.comic-text {
|
|
715
|
+
font-size: 4rem;
|
|
716
|
+
color: #fff;
|
|
717
|
+
text-shadow: 2px 2px 0 var(--pop-text), -1px -1px 0 var(--pop-text), 1px -1px 0 var(--pop-text), -1px 1px 0 var(--pop-text), 1px 1px 0 var(--pop-text);
|
|
718
|
+
transform: rotate(-2deg);
|
|
719
|
+
animation: pulse 1s infinite alternate;
|
|
84
720
|
}
|
|
85
721
|
|
|
86
|
-
|
|
87
|
-
|
|
722
|
+
@keyframes pulse {
|
|
723
|
+
0% { transform: rotate(-5deg) scale(1); }
|
|
724
|
+
100% { transform: rotate(-5deg) scale(1.1); }
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.pop-container {
|
|
728
|
+
max-width: 1200px;
|
|
729
|
+
margin: 0 auto;
|
|
88
730
|
display: flex;
|
|
89
731
|
flex-direction: column;
|
|
732
|
+
gap: 24px;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* HEADER */
|
|
736
|
+
.pop-header {
|
|
737
|
+
display: flex;
|
|
738
|
+
justify-content: space-between;
|
|
90
739
|
align-items: center;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
740
|
+
gap: 16px;
|
|
741
|
+
flex-wrap: wrap;
|
|
742
|
+
background: var(--pop-secondary);
|
|
743
|
+
border: var(--pop-border);
|
|
744
|
+
box-shadow: 4px 4px 0 var(--pop-shadow-color);
|
|
745
|
+
padding: 20px 24px;
|
|
746
|
+
border-radius: 16px;
|
|
747
|
+
position: relative;
|
|
748
|
+
overflow: hidden;
|
|
749
|
+
}
|
|
750
|
+
.pop-header::before {
|
|
751
|
+
content: '';
|
|
752
|
+
position: absolute;
|
|
753
|
+
top: -50%; left: -50%; width: 200%; height: 200%;
|
|
754
|
+
background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,0.2) 10px, rgba(255,255,255,0.2) 20px);
|
|
755
|
+
z-index: 0;
|
|
756
|
+
}
|
|
757
|
+
.pop-header > * { position: relative; z-index: 1; }
|
|
758
|
+
|
|
759
|
+
.brand { display: flex; align-items: center; gap: 16px; }
|
|
760
|
+
.logo {
|
|
761
|
+
font-size: 42px; background: var(--pop-bg); width: 72px; height: 72px;
|
|
762
|
+
border-radius: 50%; border: var(--pop-border);
|
|
763
|
+
display: flex; align-items: center; justify-content: center;
|
|
764
|
+
box-shadow: 4px 4px 0 var(--pop-shadow-color); transform: rotate(-5deg);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.title-box h1 {
|
|
768
|
+
margin: 0; font-size: 36px; font-weight: 900; color: #fff;
|
|
769
|
+
text-shadow: 2px 2px 0 var(--pop-text), -1px -1px 0 var(--pop-text), 1px -1px 0 var(--pop-text), -1px 1px 0 var(--pop-text), 1px 1px 0 var(--pop-text);
|
|
770
|
+
letter-spacing: 2px; text-transform: uppercase;
|
|
771
|
+
}
|
|
772
|
+
.title-box p {
|
|
773
|
+
margin: 4px 0 0; background: var(--pop-text); color: #fff; display: inline-block;
|
|
774
|
+
padding: 4px 8px; font-size: 14px; transform: skewX(-5deg); text-transform: uppercase;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.pop-tabs { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
778
|
+
|
|
779
|
+
/* BUTTONS */
|
|
780
|
+
.pop-btn {
|
|
781
|
+
background: #fff; color: var(--pop-text); border: var(--pop-border);
|
|
782
|
+
box-shadow: 4px 4px 0 var(--pop-shadow-color); padding: 10px 20px;
|
|
783
|
+
font-size: 16px; font-family: inherit; font-weight: 900; cursor: pointer;
|
|
784
|
+
border-radius: 16px; transition: all 0.1s ease; position: relative; text-transform: uppercase;
|
|
785
|
+
}
|
|
786
|
+
.pop-btn:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--pop-shadow-color); }
|
|
787
|
+
.pop-btn:active { transform: translate(4px, 4px); box-shadow: 0px 0px 0 var(--pop-shadow-color); }
|
|
788
|
+
.pop-btn.active { background: var(--pop-primary); color: #fff; }
|
|
789
|
+
.pop-btn.primary-btn { background: var(--pop-primary); color: #fff; font-size: 18px; }
|
|
790
|
+
.pop-btn.action-btn { background: #ffb347; color: #fff; }
|
|
791
|
+
|
|
792
|
+
.pop-btn.mini { padding: 6px 12px; font-size: 13px; box-shadow: 2px 2px 0 var(--pop-shadow-color); border-width: 2px; }
|
|
793
|
+
.pop-btn.mini:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--pop-shadow-color); }
|
|
794
|
+
.pop-btn.mini:active { transform: translate(3px, 3px); box-shadow: 0px 0px 0 var(--pop-shadow-color); }
|
|
795
|
+
.pop-btn.danger { background: var(--pop-danger); color: #fff; }
|
|
796
|
+
|
|
797
|
+
/* PANELS */
|
|
798
|
+
.pop-panel {
|
|
799
|
+
background: var(--pop-panel); border: var(--pop-border); box-shadow: 4px 4px 0 var(--pop-shadow-color);
|
|
800
|
+
border-radius: 16px; padding: 24px; position: relative;
|
|
94
801
|
}
|
|
802
|
+
.panel-header {
|
|
803
|
+
display: flex; justify-content: space-between; align-items: flex-start;
|
|
804
|
+
margin-bottom: 24px; border-bottom: 3px solid var(--pop-text); padding-bottom: 16px;
|
|
805
|
+
}
|
|
806
|
+
.panel-title h2 { margin: 0; font-size: 28px; color: var(--pop-primary); text-shadow: 2px 2px 0 var(--pop-text); text-transform: uppercase; }
|
|
807
|
+
.panel-title p { margin: 4px 0 0; font-size: 16px; font-weight: bold; text-transform: uppercase; }
|
|
808
|
+
|
|
809
|
+
/* BOT GRID */
|
|
810
|
+
.bot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; }
|
|
811
|
+
.bot-card {
|
|
812
|
+
border: var(--pop-border); border-radius: 16px; background: #fff; padding: 16px;
|
|
813
|
+
box-shadow: 4px 4px 0 var(--pop-shadow-color); transition: transform 0.2s, box-shadow 0.2s;
|
|
814
|
+
position: relative; overflow: hidden;
|
|
815
|
+
}
|
|
816
|
+
.bot-card::before {
|
|
817
|
+
content: ''; position: absolute; top: 0; right: 0; width: 40px; height: 40px;
|
|
818
|
+
background: var(--pop-bg); border-bottom-left-radius: 20px; border-left: var(--pop-border); border-bottom: var(--pop-border);
|
|
819
|
+
}
|
|
820
|
+
.bot-card.disabled { background: #e5e7eb; opacity: 0.8; }
|
|
821
|
+
.bot-card.disabled .bot-avatar { filter: grayscale(1); }
|
|
822
|
+
.bot-card:hover { transform: translate(-4px, -4px); box-shadow: 6px 6px 0 var(--pop-shadow-color); }
|
|
823
|
+
|
|
824
|
+
.bot-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
825
|
+
.bot-avatar {
|
|
826
|
+
width: 64px; height: 64px; border-radius: 50%; border: var(--pop-border);
|
|
827
|
+
background: var(--pop-bg); object-fit: cover; box-shadow: 2px 2px 0 var(--pop-text);
|
|
828
|
+
}
|
|
829
|
+
.bot-info { flex: 1; overflow: hidden; }
|
|
830
|
+
.bot-name { font-size: 20px; font-weight: 900; margin-bottom: 4px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; text-transform: uppercase; }
|
|
831
|
+
.bot-sub {
|
|
832
|
+
font-size: 13px; color: var(--pop-text); background: var(--pop-bg);
|
|
833
|
+
display: inline-block; padding: 2px 6px; border: 2px solid var(--pop-text); border-radius: 4px; margin-bottom: 4px;
|
|
834
|
+
}
|
|
835
|
+
.bot-id { font-family: 'Courier New', Courier, monospace; font-size: 12px; font-weight: bold; }
|
|
836
|
+
|
|
837
|
+
.status-badge {
|
|
838
|
+
border: 3px solid var(--pop-text); border-radius: 16px; padding: 4px 10px;
|
|
839
|
+
font-size: 14px; font-weight: 900; box-shadow: 2px 2px 0 var(--pop-text); transform: rotate(2deg);
|
|
840
|
+
text-transform: uppercase;
|
|
841
|
+
}
|
|
842
|
+
.status-online { background: var(--pop-success); color: var(--pop-text); }
|
|
843
|
+
.status-offline { background: var(--pop-danger); color: #fff; }
|
|
844
|
+
.status-connecting { background: var(--pop-bg); color: var(--pop-text); }
|
|
845
|
+
|
|
846
|
+
.bot-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 16px; }
|
|
847
|
+
.stat-box {
|
|
848
|
+
display: flex; justify-content: space-between; padding: 6px 10px;
|
|
849
|
+
border: 2px solid var(--pop-text); border-radius: 10px; background: #f8f9fa; box-shadow: 2px 2px 0 var(--pop-shadow-color);
|
|
850
|
+
}
|
|
851
|
+
.stat-box span { color: #666; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
|
852
|
+
.stat-box strong { font-size: 16px; }
|
|
853
|
+
|
|
854
|
+
.bot-meta {
|
|
855
|
+
background: var(--pop-secondary); border: 2px solid var(--pop-text); padding: 8px;
|
|
856
|
+
border-radius: 10px; font-size: 12px; margin-bottom: 16px; word-break: break-all;
|
|
857
|
+
}
|
|
858
|
+
.bot-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
859
|
+
|
|
860
|
+
/* FORMS */
|
|
861
|
+
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 24px; }
|
|
862
|
+
.modal-grid { grid-template-columns: 1fr; gap: 12px; max-height: 50vh; overflow-y: auto; padding-right: 8px; }
|
|
863
|
+
|
|
864
|
+
.pop-label { display: flex; flex-direction: column; gap: 8px; font-size: 16px; font-weight: bold; text-transform: uppercase; }
|
|
865
|
+
.pop-label.hidden { display: none; }
|
|
866
|
+
|
|
867
|
+
.pop-input, .pop-select, .pop-textarea {
|
|
868
|
+
border: 3px solid var(--pop-text); border-radius: 16px; padding: 10px 14px;
|
|
869
|
+
font-size: 15px; font-family: inherit; font-weight: bold; background: #fff; color: var(--pop-text);
|
|
870
|
+
box-shadow: inset 2px 2px 0 rgba(0,0,0,0.1); transition: all 0.2s;
|
|
871
|
+
}
|
|
872
|
+
.pop-input:focus, .pop-select:focus, .pop-textarea:focus {
|
|
873
|
+
outline: none; background: #fffde7; box-shadow: 4px 4px 0 var(--pop-shadow-color); transform: translate(-2px, -2px);
|
|
874
|
+
}
|
|
875
|
+
.pop-textarea { resize: vertical; }
|
|
876
|
+
|
|
877
|
+
.pop-file-input {
|
|
878
|
+
border: 3px solid var(--pop-text);
|
|
879
|
+
border-radius: 16px;
|
|
880
|
+
padding: 8px;
|
|
881
|
+
font-size: 14px;
|
|
882
|
+
font-family: inherit;
|
|
883
|
+
font-weight: bold;
|
|
884
|
+
background: #fff;
|
|
885
|
+
color: var(--pop-text);
|
|
886
|
+
box-shadow: inset 2px 2px 0 rgba(0,0,0,0.1);
|
|
887
|
+
transition: all 0.2s;
|
|
888
|
+
cursor: pointer;
|
|
889
|
+
}
|
|
890
|
+
.pop-file-input:focus, .pop-file-input:hover {
|
|
891
|
+
outline: none; background: #fffde7; box-shadow: 4px 4px 0 var(--pop-shadow-color); transform: translate(-2px, -2px);
|
|
892
|
+
}
|
|
893
|
+
.pop-file-input::file-selector-button {
|
|
894
|
+
background: var(--pop-primary);
|
|
895
|
+
color: #fff;
|
|
896
|
+
border: 2px solid var(--pop-text);
|
|
897
|
+
border-radius: 8px;
|
|
898
|
+
padding: 6px 12px;
|
|
899
|
+
font-weight: bold;
|
|
900
|
+
cursor: pointer;
|
|
901
|
+
margin-right: 12px;
|
|
902
|
+
text-transform: uppercase;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.pop-checkbox {
|
|
906
|
+
flex-direction: row; align-items: center; cursor: pointer; background: var(--pop-bg);
|
|
907
|
+
padding: 10px; border: 3px solid var(--pop-text); border-radius: 16px; box-shadow: 2px 2px 0 var(--pop-shadow-color);
|
|
908
|
+
transition: transform 0.1s;
|
|
909
|
+
}
|
|
910
|
+
.pop-checkbox:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--pop-shadow-color); }
|
|
911
|
+
.pop-check { width: 20px; height: 20px; accent-color: var(--pop-primary); border: 2px solid var(--pop-text); cursor: pointer; }
|
|
912
|
+
|
|
913
|
+
.mini-checkbox { padding: 4px 8px; border-radius: 8px; font-size: 13px; gap: 4px; border-width: 2px; }
|
|
914
|
+
.mini-check { width: 14px; height: 14px; }
|
|
915
|
+
|
|
916
|
+
/* SPLIT GRID FOR BALANCE */
|
|
917
|
+
.split-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 24px; }
|
|
918
|
+
|
|
919
|
+
/* TABLE EDITOR */
|
|
920
|
+
.pop-table-editor {
|
|
921
|
+
border: 3px dashed var(--pop-text); background: var(--pop-secondary); border-radius: 16px;
|
|
922
|
+
padding: 16px; display: flex; flex-direction: column; gap: 12px; position: relative;
|
|
923
|
+
}
|
|
924
|
+
.pop-table-editor.full-width { grid-column: 1 / -1; background: #ffb3ff; }
|
|
925
|
+
|
|
926
|
+
.table-head {
|
|
927
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
928
|
+
font-size: 18px; font-weight: 900; background: #fff; padding: 6px 12px;
|
|
929
|
+
border: 3px solid var(--pop-text); border-radius: 16px; box-shadow: 2px 2px 0 var(--pop-text); text-transform: uppercase;
|
|
930
|
+
}
|
|
931
|
+
.table-body { display: flex; flex-direction: column; gap: 8px; max-height: 250px; overflow-y: auto; padding-right: 8px; }
|
|
932
|
+
.empty-row {
|
|
933
|
+
background: #fff; padding: 12px; text-align: center; border: 3px solid var(--pop-text);
|
|
934
|
+
border-radius: 16px; font-weight: bold; text-transform: uppercase;
|
|
935
|
+
}
|
|
936
|
+
.editor-row {
|
|
937
|
+
display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; background: #fff;
|
|
938
|
+
padding: 8px; border: 3px solid var(--pop-text); border-radius: 16px; box-shadow: 2px 2px 0 var(--pop-text);
|
|
939
|
+
}
|
|
940
|
+
.editor-row.wide { grid-template-columns: 1fr 2fr auto; }
|
|
941
|
+
.mini-input { padding: 6px 10px; font-size: 14px; }
|
|
942
|
+
.panel-footer { margin-top: 24px; padding-top: 20px; border-top: 3px solid var(--pop-text); display: flex; justify-content: flex-end; }
|
|
943
|
+
|
|
944
|
+
/* MODALS */
|
|
945
|
+
.pop-modal-overlay {
|
|
946
|
+
position: fixed; inset: 0; background: rgba(107, 82, 67, 0.4); backdrop-filter: blur(4px);
|
|
947
|
+
display: flex; align-items: center; justify-content: center; z-index: 1000;
|
|
948
|
+
}
|
|
949
|
+
.pop-modal { width: min(600px, 90vw); position: relative; padding: 32px; }
|
|
950
|
+
.pop-modal h3 { font-size: 32px; margin: 0 0 8px 0; color: var(--pop-primary); text-shadow: 2px 2px 0 var(--pop-text); text-transform: uppercase; }
|
|
951
|
+
.modal-sub {
|
|
952
|
+
font-family: 'Courier New', Courier, monospace; background: var(--pop-bg); display: inline-block;
|
|
953
|
+
padding: 4px 8px; border: 2px solid var(--pop-text); margin-bottom: 20px; font-weight: bold;
|
|
954
|
+
}
|
|
955
|
+
.comic-burst {
|
|
956
|
+
position: absolute; top: -15px; right: -15px; background: var(--pop-bg); color: var(--pop-text);
|
|
957
|
+
font-size: 24px; font-weight: 900; padding: 10px; border: var(--pop-border); border-radius: 50%;
|
|
958
|
+
transform: rotate(5deg); box-shadow: 4px 4px 0 var(--pop-shadow-color);
|
|
959
|
+
animation: pop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); text-transform: uppercase;
|
|
960
|
+
}
|
|
961
|
+
@keyframes pop {
|
|
962
|
+
0% { transform: scale(0) rotate(0deg); }
|
|
963
|
+
100% { transform: scale(1) rotate(15deg); }
|
|
964
|
+
}
|
|
965
|
+
.modal-actions { display: flex; gap: 12px; margin-top: 24px; justify-content: flex-end; }
|
|
966
|
+
|
|
967
|
+
/* TOAST */
|
|
968
|
+
.pop-toast {
|
|
969
|
+
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
970
|
+
padding: 12px 24px; font-size: 20px; font-weight: 900; z-index: 1100;
|
|
971
|
+
display: flex; align-items: center; gap: 12px; animation: slideUp 0.3s ease-out; text-transform: uppercase;
|
|
972
|
+
}
|
|
973
|
+
.pop-toast.success { background: var(--pop-success); color: var(--pop-text); }
|
|
974
|
+
.pop-toast.error { background: var(--pop-danger); color: #fff; }
|
|
975
|
+
@keyframes slideUp { 0% { transform: translate(-50%, 100%); opacity: 0; } 100% { transform: translate(-50%, 0); opacity: 1; } }
|
|
976
|
+
|
|
977
|
+
/* EMPTY STATE */
|
|
978
|
+
.empty-state { display: flex; justify-content: center; padding: 40px; }
|
|
979
|
+
.comic-speech {
|
|
980
|
+
position: relative; background: #fff; border: 3px solid var(--pop-text); border-radius: 16px;
|
|
981
|
+
padding: 20px 30px; font-size: 24px; font-weight: bold; box-shadow: 4px 4px 0 var(--pop-shadow-color); text-transform: uppercase;
|
|
982
|
+
}
|
|
983
|
+
.comic-speech::after {
|
|
984
|
+
display: none;
|
|
985
|
+
content: ''; position: absolute; bottom: -20px; left: 30px;
|
|
986
|
+
border-width: 12px 12px 0 0; border-style: solid; border-color: #fff transparent transparent transparent;
|
|
987
|
+
}
|
|
988
|
+
.comic-speech::before {
|
|
989
|
+
display: none;
|
|
990
|
+
content: ''; position: absolute; bottom: -26px; left: 28px;
|
|
991
|
+
border-width: 14px 14px 0 0; border-style: solid; border-color: var(--pop-text) transparent transparent transparent; z-index: -1;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/* CUSTOM SCROLLBAR */
|
|
995
|
+
::-webkit-scrollbar { width: 12px; }
|
|
996
|
+
::-webkit-scrollbar-track { background: var(--pop-bg); border-left: 3px solid var(--pop-text); }
|
|
997
|
+
::-webkit-scrollbar-thumb { background: var(--pop-primary); border: 3px solid var(--pop-text); border-radius: 0; }
|
|
95
998
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
color: var(--k-text-dark);
|
|
999
|
+
@media (max-width: 960px) {
|
|
1000
|
+
.split-grid { grid-template-columns: 1fr; }
|
|
99
1001
|
}
|
|
100
1002
|
|
|
101
|
-
|
|
102
|
-
|
|
1003
|
+
@media (max-width: 768px) {
|
|
1004
|
+
.pop-wrapper { padding: 12px; }
|
|
1005
|
+
.pop-header { flex-direction: column; text-align: center; }
|
|
1006
|
+
.pop-tabs { justify-content: center; width: 100%; }
|
|
1007
|
+
.pop-btn { flex: 1; min-width: 45%; text-align: center; }
|
|
1008
|
+
.bot-actions .pop-btn { flex: 1; min-width: 30%; }
|
|
1009
|
+
.pop-modal { padding: 20px; }
|
|
1010
|
+
.comic-speech { font-size: 18px; }
|
|
1011
|
+
.bot-grid { grid-template-columns: 1fr; }
|
|
103
1012
|
}
|
|
104
|
-
</style>
|
|
1013
|
+
</style>
|