@zhin.js/client 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.
@@ -0,0 +1,734 @@
1
+ <template>
2
+ <div class="plugins-page">
3
+ <!-- 页面标题 -->
4
+ <div class="page-header mb-4">
5
+ <div class="flex align-items-center">
6
+ <i class="pi pi-th-large text-3xl text-primary mr-3"></i>
7
+ <div>
8
+ <h1 class="page-title">已安装插件</h1>
9
+ <p class="page-subtitle">管理和监控已安装的 Zhin Bot 插件</p>
10
+ </div>
11
+ </div>
12
+ <div class="page-actions">
13
+ <Button
14
+ icon="pi pi-refresh"
15
+ label="刷新"
16
+ @click="refreshData"
17
+ :loading="refreshing"
18
+ />
19
+ <Button
20
+ icon="pi pi-plus"
21
+ label="安装插件"
22
+ severity="success"
23
+ @click="showInstallDialog = true"
24
+ />
25
+ </div>
26
+ </div>
27
+
28
+ <!-- 统计信息 -->
29
+ <div class="grid mb-4">
30
+ <div class="col-12 md:col-4">
31
+ <div class="stats-card stats-total">
32
+ <div class="stats-icon">
33
+ <i class="pi pi-box text-white"></i>
34
+ </div>
35
+ <div class="stats-content">
36
+ <div class="stats-value">{{ pluginsData?.length || 0 }}</div>
37
+ <div class="stats-label">总插件数</div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="col-12 md:col-4">
43
+ <div class="stats-card stats-active">
44
+ <div class="stats-icon">
45
+ <i class="pi pi-check-circle text-white"></i>
46
+ </div>
47
+ <div class="stats-content">
48
+ <div class="stats-value">{{ activePluginsCount }}</div>
49
+ <div class="stats-label">活跃插件</div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="col-12 md:col-4">
55
+ <div class="stats-card stats-commands">
56
+ <div class="stats-icon">
57
+ <i class="pi pi-code text-white"></i>
58
+ </div>
59
+ <div class="stats-content">
60
+ <div class="stats-value">{{ totalCommands }}</div>
61
+ <div class="stats-label">命令总数</div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- 插件列表 -->
68
+ <Card class="plugins-list-card">
69
+ <template #title>
70
+ <div class="flex justify-content-between align-items-center">
71
+ <div class="flex align-items-center">
72
+ <i class="pi pi-list mr-2"></i>
73
+ 插件列表
74
+ </div>
75
+ <div class="flex align-items-center gap-2">
76
+ <Dropdown
77
+ v-model="filterStatus"
78
+ :options="statusOptions"
79
+ optionLabel="label"
80
+ optionValue="value"
81
+ placeholder="筛选状态"
82
+ class="filter-dropdown"
83
+ showClear
84
+ />
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <template #content>
90
+ <div class="plugins-list">
91
+ <div
92
+ v-for="plugin in filteredPlugins"
93
+ :key="plugin.name"
94
+ class="plugin-item"
95
+ >
96
+ <div class="plugin-main-info">
97
+ <div class="plugin-header">
98
+ <div class="plugin-icon">
99
+ <i :class="getPluginIcon(plugin.name)"></i>
100
+ </div>
101
+ <div class="plugin-basic-info">
102
+ <h3 class="plugin-name">{{ plugin.name }}</h3>
103
+ <p class="plugin-description">{{ getPluginDescription(plugin.name) }}</p>
104
+ </div>
105
+ <div class="plugin-status">
106
+ <Tag
107
+ value="活跃"
108
+ severity="success"
109
+ icon="pi pi-check"
110
+ />
111
+ </div>
112
+ </div>
113
+
114
+ <div class="plugin-stats">
115
+ <div class="stat-item">
116
+ <i class="pi pi-sitemap"></i>
117
+ <span>{{ plugin.context_count || 0 }} 个上下文</span>
118
+ </div>
119
+ <div class="stat-item">
120
+ <i class="pi pi-code"></i>
121
+ <span>{{ plugin.command_count || 0 }} 个命令</span>
122
+ </div>
123
+ <div class="stat-item">
124
+ <i class="pi pi-layer-group"></i>
125
+ <span>{{ plugin.middleware_count || 0 }} 个中间件</span>
126
+ </div>
127
+ <div class="stat-item">
128
+ <i class="pi pi-th-large"></i>
129
+ <span>{{ plugin.component_count || 0 }} 个组件</span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="plugin-actions">
135
+ <Button
136
+ icon="pi pi-refresh"
137
+ severity="info"
138
+ text
139
+ rounded
140
+ @click="reloadPlugin(plugin.name)"
141
+ :loading="reloadingPlugins.includes(plugin.name)"
142
+ v-tooltip="'重载插件'"
143
+ />
144
+ <Button
145
+ icon="pi pi-cog"
146
+ severity="secondary"
147
+ text
148
+ rounded
149
+ @click="configurePlugin(plugin)"
150
+ v-tooltip="'配置插件'"
151
+ />
152
+ <Button
153
+ icon="pi pi-info-circle"
154
+ severity="help"
155
+ text
156
+ rounded
157
+ @click="showPluginDetails(plugin)"
158
+ v-tooltip="'查看详情'"
159
+ />
160
+ </div>
161
+ </div>
162
+
163
+ <!-- 空状态 -->
164
+ <div v-if="filteredPlugins.length === 0" class="empty-state">
165
+ <i class="pi pi-inbox text-4xl text-color-secondary mb-3"></i>
166
+ <h3 class="text-color-secondary">暂无插件</h3>
167
+ <p class="text-color-secondary">当前没有找到匹配的插件</p>
168
+ </div>
169
+ </div>
170
+ </template>
171
+ </Card>
172
+
173
+ <!-- 插件详情对话框 -->
174
+ <Dialog
175
+ v-model:visible="detailDialogVisible"
176
+ :header="selectedPlugin?.name + ' 详情'"
177
+ modal
178
+ :style="{ width: '50vw' }"
179
+ :breakpoints="{ '960px': '75vw', '641px': '90vw' }"
180
+ >
181
+ <div v-if="selectedPlugin" class="plugin-detail-content">
182
+ <div class="detail-section">
183
+ <h4>基本信息</h4>
184
+ <div class="detail-grid">
185
+ <div class="detail-item">
186
+ <label>插件名称</label>
187
+ <span>{{ selectedPlugin.name }}</span>
188
+ </div>
189
+ <div class="detail-item">
190
+ <label>版本</label>
191
+ <span>{{ selectedPlugin.version || '未知' }}</span>
192
+ </div>
193
+ <div class="detail-item">
194
+ <label>状态</label>
195
+ <Tag
196
+ :value="selectedPlugin.status"
197
+ :severity="getStatusSeverity(selectedPlugin.status)"
198
+ />
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="detail-section">
204
+ <h4>上下文信息</h4>
205
+ <div class="contexts-list">
206
+ <div
207
+ v-for="(mounted, name) in selectedPlugin.contexts"
208
+ :key="name"
209
+ class="context-item"
210
+ >
211
+ <span class="context-name">{{ name }}</span>
212
+ <Tag
213
+ :value="mounted ? '已挂载' : '未挂载'"
214
+ :severity="mounted ? 'success' : 'danger'"
215
+ />
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <div class="detail-section">
221
+ <h4>统计信息</h4>
222
+ <div class="stats-grid">
223
+ <div class="stat-card">
224
+ <i class="pi pi-code"></i>
225
+ <div class="stat-info">
226
+ <div class="stat-number">{{ selectedPlugin.commands || 0 }}</div>
227
+ <div class="stat-text">命令</div>
228
+ </div>
229
+ </div>
230
+ <div class="stat-card">
231
+ <i class="pi pi-layer-group"></i>
232
+ <div class="stat-info">
233
+ <div class="stat-number">{{ selectedPlugin.middlewares || 0 }}</div>
234
+ <div class="stat-text">中间件</div>
235
+ </div>
236
+ </div>
237
+ <div class="stat-card">
238
+ <i class="pi pi-clock"></i>
239
+ <div class="stat-info">
240
+ <div class="stat-number">{{ formatUptime(selectedPlugin.uptime) }}</div>
241
+ <div class="stat-text">运行时间</div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </Dialog>
248
+
249
+ <!-- 安装插件对话框 -->
250
+ <Dialog
251
+ v-model:visible="showInstallDialog"
252
+ header="安装新插件"
253
+ modal
254
+ :style="{ width: '30vw' }"
255
+ :breakpoints="{ '960px': '50vw', '641px': '90vw' }"
256
+ >
257
+ <div class="install-plugin-content">
258
+ <p class="mb-3">输入插件包名称或本地路径:</p>
259
+ <InputText
260
+ v-model="newPluginName"
261
+ placeholder="例如: @zhin.js/plugin-example"
262
+ class="w-full mb-3"
263
+ />
264
+ </div>
265
+ <template #footer>
266
+ <Button
267
+ label="取消"
268
+ text
269
+ @click="showInstallDialog = false"
270
+ />
271
+ <Button
272
+ label="安装"
273
+ @click="installPlugin"
274
+ :disabled="!newPluginName"
275
+ />
276
+ </template>
277
+ </Dialog>
278
+ </div>
279
+ </template>
280
+
281
+ <script setup lang="ts">
282
+ import { computed, ref } from 'vue'
283
+ import { useCommonStore } from '@zhin.js/client'
284
+
285
+ const commonStore = useCommonStore()
286
+ const refreshing = ref(false)
287
+ const detailDialogVisible = ref(false)
288
+ const showInstallDialog = ref(false)
289
+ const selectedPlugin = ref(null)
290
+ const newPluginName = ref('')
291
+ const filterStatus = ref('')
292
+ const reloadingPlugins = ref<string[]>([])
293
+
294
+ // 插件数据
295
+ const pluginsData = computed(() => (commonStore.store as any).plugins || [])
296
+
297
+ // 状态筛选选项
298
+ const statusOptions = [
299
+ { label: '全部', value: '' },
300
+ { label: '活跃', value: 'active' },
301
+ { label: '已停用', value: 'disposed' }
302
+ ]
303
+
304
+ // 筛选后的插件列表
305
+ const filteredPlugins = computed(() => {
306
+ if (!filterStatus.value) return pluginsData.value
307
+ // 由于API只返回基础数据,所有插件都视为活跃状态
308
+ return pluginsData.value
309
+ })
310
+
311
+ // 统计信息
312
+ const activePluginsCount = computed(() => {
313
+ return pluginsData.value.length // 所有返回的插件都是活跃的
314
+ })
315
+
316
+ const totalCommands = computed(() => {
317
+ return pluginsData.value.reduce((total, plugin) => total + (plugin.command_count || 0), 0)
318
+ })
319
+
320
+ const totalComponents = computed(() => {
321
+ return pluginsData.value.reduce((total, plugin) => total + (plugin.component_count || 0), 0)
322
+ })
323
+
324
+ const totalMiddlewares = computed(() => {
325
+ return pluginsData.value.reduce((total, plugin) => total + (plugin.middleware_count || 0), 0)
326
+ })
327
+
328
+ const totalContexts = computed(() => {
329
+ return pluginsData.value.reduce((total, plugin) => total + (plugin.context_count || 0), 0)
330
+ })
331
+
332
+ // 格式化函数
333
+ const getPluginIcon = (name: string) => {
334
+ if (name.includes('adapter')) return 'pi pi-link'
335
+ if (name.includes('core')) return 'pi pi-star'
336
+ if (name.includes('cli')) return 'pi pi-terminal'
337
+ if (name.includes('http')) return 'pi pi-globe'
338
+ if (name.includes('console')) return 'pi pi-desktop'
339
+ if (name.includes('client')) return 'pi pi-mobile'
340
+ return 'pi pi-puzzle-piece'
341
+ }
342
+
343
+ const getPluginDescription = (name: string) => {
344
+ const descriptions = {
345
+ 'core': '核心框架功能',
346
+ 'cli': '命令行工具',
347
+ 'http': 'HTTP服务器',
348
+ 'console': 'Web控制台',
349
+ 'client': '客户端界面',
350
+ 'hmr': '热模块重载',
351
+ 'icqq': 'ICQQ适配器',
352
+ 'kook': 'KOOK适配器',
353
+ 'process': '进程适配器'
354
+ }
355
+ return descriptions[name] || `${name} 插件`
356
+ }
357
+
358
+ const formatUptime = (seconds?: number) => {
359
+ if (!seconds) return '0秒'
360
+
361
+ const hours = Math.floor(seconds / 3600)
362
+ const minutes = Math.floor((seconds % 3600) / 60)
363
+
364
+ if (hours > 0) {
365
+ return `${hours}小时${minutes}分钟`
366
+ } else if (minutes > 0) {
367
+ return `${minutes}分钟`
368
+ } else {
369
+ return `${Math.floor(seconds)}秒`
370
+ }
371
+ }
372
+
373
+ const getStatusSeverity = (status: string) => {
374
+ switch (status) {
375
+ case 'active': return 'success'
376
+ case 'disposed': return 'danger'
377
+ default: return 'info'
378
+ }
379
+ }
380
+
381
+ const getStatusIcon = (status: string) => {
382
+ switch (status) {
383
+ case 'active': return 'pi pi-check'
384
+ case 'disposed': return 'pi pi-times'
385
+ default: return 'pi pi-question'
386
+ }
387
+ }
388
+
389
+ // 操作函数
390
+ const refreshData = async () => {
391
+ refreshing.value = true
392
+ try {
393
+ // 使用全局API
394
+ if (window.ZhinDataAPI?.updateAllData) {
395
+ await window.ZhinDataAPI.updateAllData()
396
+ // console.log 已替换为注释
397
+ } else {
398
+ throw new Error('全局API未就绪')
399
+ }
400
+ } catch (error) {
401
+ // console.error 已替换为注释
402
+ } finally {
403
+ refreshing.value = false
404
+ }
405
+ }
406
+
407
+ const reloadPlugin = async (pluginName: string) => {
408
+ reloadingPlugins.value.push(pluginName)
409
+
410
+ try {
411
+ // 使用全局API
412
+ if (window.ZhinDataAPI?.reloadPlugin) {
413
+ const result = await window.ZhinDataAPI.reloadPlugin(pluginName)
414
+
415
+ if (result.success) {
416
+ // console.log 已替换为注释
417
+ // 重载成功后刷新插件数据
418
+ await refreshData()
419
+ } else {
420
+ // console.error 已替换为注释
421
+ }
422
+ } else {
423
+ throw new Error('全局API未就绪')
424
+ }
425
+ } catch (error) {
426
+ // console.error 已替换为注释
427
+ } finally {
428
+ reloadingPlugins.value = reloadingPlugins.value.filter(name => name !== pluginName)
429
+ }
430
+ }
431
+
432
+ const configurePlugin = (plugin: any) => {
433
+ // 这里可以跳转到插件配置页面或显示配置对话框
434
+ // console.log 已替换为注释
435
+ }
436
+
437
+ const showPluginDetails = (plugin: any) => {
438
+ selectedPlugin.value = plugin
439
+ detailDialogVisible.value = true
440
+ }
441
+
442
+ const installPlugin = async () => {
443
+ if (!newPluginName.value) return
444
+
445
+ try {
446
+ // 这里应该调用实际的安装API
447
+ // console.log 已替换为注释
448
+ await new Promise(resolve => setTimeout(resolve, 1000))
449
+
450
+ showInstallDialog.value = false
451
+ newPluginName.value = ''
452
+ refreshData()
453
+ } catch (error) {
454
+ // console.error 已替换为注释
455
+ }
456
+ }
457
+ </script>
458
+
459
+ <style scoped>
460
+ .plugins-page {
461
+ padding: 1.5rem;
462
+ }
463
+
464
+ .page-header {
465
+ display: flex;
466
+ justify-content: space-between;
467
+ align-items: flex-start;
468
+ padding: 2rem;
469
+ background: var(--surface-card);
470
+ border-radius: 12px;
471
+ border: 1px solid var(--surface-border);
472
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
473
+ }
474
+
475
+ .page-title {
476
+ margin: 0;
477
+ font-size: 2rem;
478
+ font-weight: 600;
479
+ color: var(--text-color);
480
+ }
481
+
482
+ .page-subtitle {
483
+ margin: 0.5rem 0 0 0;
484
+ color: var(--text-color-secondary);
485
+ font-size: 1rem;
486
+ }
487
+
488
+ .page-actions {
489
+ display: flex;
490
+ gap: 0.75rem;
491
+ }
492
+
493
+ .stats-card {
494
+ background: var(--surface-card);
495
+ border-radius: 12px;
496
+ border: 1px solid var(--surface-border);
497
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
498
+ padding: 1.5rem;
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 1rem;
502
+ }
503
+
504
+ .stats-total .stats-icon { background: var(--blue-500); }
505
+ .stats-commands .stats-icon { background: var(--orange-500); }
506
+ .stats-components .stats-icon { background: var(--purple-500); }
507
+ .stats-contexts .stats-icon { background: var(--cyan-500); }
508
+
509
+ .stats-icon {
510
+ width: 48px;
511
+ height: 48px;
512
+ border-radius: 12px;
513
+ display: flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ font-size: 1.5rem;
517
+ }
518
+
519
+ .stats-value {
520
+ font-size: 2rem;
521
+ font-weight: 700;
522
+ color: var(--text-color);
523
+ line-height: 1;
524
+ }
525
+
526
+ .stats-label {
527
+ font-size: 0.875rem;
528
+ color: var(--text-color-secondary);
529
+ margin-top: 0.25rem;
530
+ }
531
+
532
+ .plugins-list-card :deep(.p-card-body) {
533
+ padding: 1.5rem;
534
+ }
535
+
536
+ .filter-dropdown {
537
+ width: 150px;
538
+ }
539
+
540
+ .plugins-list {
541
+ display: flex;
542
+ flex-direction: column;
543
+ gap: 1rem;
544
+ }
545
+
546
+ .plugin-item {
547
+ display: flex;
548
+ align-items: center;
549
+ padding: 1.5rem;
550
+ background: var(--surface-50);
551
+ border-radius: 12px;
552
+ border: 1px solid var(--surface-border);
553
+ transition: all 0.2s ease;
554
+ }
555
+
556
+ .plugin-item:hover {
557
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
558
+ transform: translateY(-1px);
559
+ }
560
+
561
+ .plugin-main-info {
562
+ flex: 1;
563
+ }
564
+
565
+ .plugin-header {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 1rem;
569
+ margin-bottom: 1rem;
570
+ }
571
+
572
+ .plugin-icon {
573
+ width: 48px;
574
+ height: 48px;
575
+ background: var(--primary-color);
576
+ border-radius: 12px;
577
+ display: flex;
578
+ align-items: center;
579
+ justify-content: center;
580
+ color: white;
581
+ font-size: 1.5rem;
582
+ }
583
+
584
+ .plugin-basic-info {
585
+ flex: 1;
586
+ }
587
+
588
+ .plugin-name {
589
+ margin: 0 0 0.25rem 0;
590
+ font-size: 1.25rem;
591
+ font-weight: 600;
592
+ color: var(--text-color);
593
+ }
594
+
595
+ .plugin-description {
596
+ margin: 0;
597
+ color: var(--text-color-secondary);
598
+ font-size: 0.875rem;
599
+ }
600
+
601
+ .plugin-stats {
602
+ display: flex;
603
+ gap: 2rem;
604
+ }
605
+
606
+ .stat-item {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 0.5rem;
610
+ color: var(--text-color-secondary);
611
+ font-size: 0.875rem;
612
+ }
613
+
614
+ .stat-item i {
615
+ color: var(--primary-color);
616
+ }
617
+
618
+ .plugin-actions {
619
+ display: flex;
620
+ gap: 0.5rem;
621
+ }
622
+
623
+ .empty-state {
624
+ text-align: center;
625
+ padding: 3rem;
626
+ }
627
+
628
+ /* 对话框样式 */
629
+ .plugin-detail-content {
630
+ display: flex;
631
+ flex-direction: column;
632
+ gap: 1.5rem;
633
+ }
634
+
635
+ .detail-section h4 {
636
+ margin: 0 0 1rem 0;
637
+ color: var(--text-color);
638
+ font-size: 1.125rem;
639
+ font-weight: 600;
640
+ }
641
+
642
+ .detail-grid {
643
+ display: grid;
644
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
645
+ gap: 1rem;
646
+ }
647
+
648
+ .detail-item {
649
+ display: flex;
650
+ flex-direction: column;
651
+ gap: 0.5rem;
652
+ }
653
+
654
+ .detail-item label {
655
+ font-size: 0.875rem;
656
+ color: var(--text-color-secondary);
657
+ font-weight: 500;
658
+ }
659
+
660
+ .contexts-list {
661
+ display: flex;
662
+ flex-direction: column;
663
+ gap: 0.75rem;
664
+ }
665
+
666
+ .context-item {
667
+ display: flex;
668
+ justify-content: space-between;
669
+ align-items: center;
670
+ padding: 0.75rem;
671
+ background: var(--surface-100);
672
+ border-radius: 8px;
673
+ }
674
+
675
+ .context-name {
676
+ font-weight: 500;
677
+ }
678
+
679
+ .stats-grid {
680
+ display: grid;
681
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
682
+ gap: 1rem;
683
+ }
684
+
685
+ .stat-card {
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 0.75rem;
689
+ padding: 1rem;
690
+ background: var(--surface-100);
691
+ border-radius: 8px;
692
+ }
693
+
694
+ .stat-card i {
695
+ font-size: 1.5rem;
696
+ color: var(--primary-color);
697
+ }
698
+
699
+ .stat-number {
700
+ font-size: 1.25rem;
701
+ font-weight: 600;
702
+ color: var(--text-color);
703
+ }
704
+
705
+ .stat-text {
706
+ font-size: 0.75rem;
707
+ color: var(--text-color-secondary);
708
+ }
709
+
710
+ @media (max-width: 768px) {
711
+ .plugins-page {
712
+ padding: 1rem;
713
+ }
714
+
715
+ .page-header {
716
+ flex-direction: column;
717
+ gap: 1rem;
718
+ padding: 1.5rem;
719
+ }
720
+
721
+ .plugin-header {
722
+ flex-wrap: wrap;
723
+ }
724
+
725
+ .plugin-stats {
726
+ flex-wrap: wrap;
727
+ gap: 1rem;
728
+ }
729
+
730
+ .plugin-actions {
731
+ margin-top: 1rem;
732
+ }
733
+ }
734
+ </style>