koishi-plugin-media-luna 0.0.7 → 0.0.8

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/api.ts CHANGED
@@ -174,6 +174,43 @@ export const settingsApi = {
174
174
  call<{ added: number, updated: number, removed: number }>('media-luna/presets/sync')
175
175
  }
176
176
 
177
+ // 设置向导 API
178
+ export const setupApi = {
179
+ /** 获取设置状态 */
180
+ status: () =>
181
+ call<{
182
+ needsSetup: boolean
183
+ storageConfigured: boolean
184
+ userBound: boolean
185
+ storageBackend: string
186
+ boundUid: number | null
187
+ }>('media-luna/setup/status'),
188
+ /** 获取存储配置字段定义 */
189
+ getStorageFields: () =>
190
+ call<ConfigField[]>('media-luna/setup/storage/fields'),
191
+ /** 获取当前存储配置 */
192
+ getStorageConfig: () =>
193
+ call<Record<string, any>>('media-luna/setup/storage/get'),
194
+ /** 更新存储配置 */
195
+ updateStorageConfig: (config: Record<string, any>) =>
196
+ call<void>('media-luna/setup/storage/update', config),
197
+ /** 生成验证码 */
198
+ generateVerifyCode: (uid: number) =>
199
+ call<{ code: string, expiresIn: number, uid: number }>('media-luna/setup/verify-code/generate', { uid }),
200
+ /** 验证验证码 */
201
+ verifyCode: (code: string, uid: number) =>
202
+ call<{ uid: number }>('media-luna/setup/verify-code/verify', { code, uid }),
203
+ /** 直接绑定 UID */
204
+ bindUid: (uid: number) =>
205
+ call<{ uid: number }>('media-luna/setup/bind-uid', { uid }),
206
+ /** 获取可用用户列表 */
207
+ getUsers: () =>
208
+ call<Array<{ id: number, name: string, authority: number }>>('media-luna/setup/users'),
209
+ /** 完成设置 */
210
+ complete: () =>
211
+ call<void>('media-luna/setup/complete')
212
+ }
213
+
177
214
  // 插件 API
178
215
  export const pluginApi = {
179
216
  /** 获取所有已加载插件 */
@@ -0,0 +1,284 @@
1
+ <template>
2
+ <div class="setup-wizard">
3
+ <div class="wizard-container">
4
+ <!-- 头部 -->
5
+ <div class="wizard-header">
6
+ <h1>欢迎使用 Media Luna</h1>
7
+ <p>在开始使用前,请完成以下基础配置</p>
8
+ </div>
9
+
10
+ <!-- 步骤指示器 -->
11
+ <div class="steps-indicator">
12
+ <div
13
+ v-for="(step, index) in steps"
14
+ :key="step.id"
15
+ class="step-item"
16
+ :class="{
17
+ active: currentStep === index,
18
+ completed: currentStep > index
19
+ }"
20
+ >
21
+ <div class="step-number">
22
+ <k-icon v-if="currentStep > index" name="check" />
23
+ <span v-else>{{ index + 1 }}</span>
24
+ </div>
25
+ <span class="step-label">{{ step.label }}</span>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- 步骤内容 -->
30
+ <div class="wizard-content">
31
+ <transition name="fade" mode="out-in">
32
+ <!-- 存储配置步骤 -->
33
+ <SetupStorage
34
+ v-if="currentStep === 0"
35
+ key="storage"
36
+ v-model="storageConfig"
37
+ :saving="saving"
38
+ @next="handleStorageNext"
39
+ />
40
+
41
+ <!-- 用户绑定步骤 -->
42
+ <SetupAuth
43
+ v-else-if="currentStep === 1"
44
+ key="auth"
45
+ :saving="saving"
46
+ @complete="handleComplete"
47
+ @skip="handleSkip"
48
+ />
49
+
50
+ <!-- 完成步骤 -->
51
+ <div v-else-if="currentStep === 2" key="complete" class="step-complete">
52
+ <div class="complete-icon">
53
+ <k-icon name="check-circle" />
54
+ </div>
55
+ <h2>配置完成</h2>
56
+ <p>您已完成 Media Luna 的初始设置,现在可以开始使用了。</p>
57
+ <k-button type="primary" size="large" @click="finishSetup">
58
+ 开始使用
59
+ </k-button>
60
+ </div>
61
+ </transition>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import { ref } from 'vue'
69
+ import { message } from '@koishijs/client'
70
+ import { setupApi } from '../api'
71
+ import SetupStorage from './setup/SetupStorage.vue'
72
+ import SetupAuth from './setup/SetupAuth.vue'
73
+
74
+ const emit = defineEmits<{
75
+ (e: 'complete'): void
76
+ }>()
77
+
78
+ // 步骤定义
79
+ const steps = [
80
+ { id: 'storage', label: '存储配置' },
81
+ { id: 'auth', label: '用户绑定' },
82
+ { id: 'complete', label: '完成' }
83
+ ]
84
+
85
+ // 当前步骤
86
+ const currentStep = ref(0)
87
+ const saving = ref(false)
88
+
89
+ // 存储配置(由 SetupStorage 组件动态加载和管理)
90
+ const storageConfig = ref<Record<string, any>>({})
91
+
92
+ // 处理存储配置完成
93
+ const handleStorageNext = async () => {
94
+ saving.value = true
95
+ try {
96
+ await setupApi.updateStorageConfig(storageConfig.value)
97
+ message.success('存储配置已保存')
98
+ currentStep.value = 1
99
+ } catch (e) {
100
+ message.error('保存失败: ' + (e instanceof Error ? e.message : '未知错误'))
101
+ } finally {
102
+ saving.value = false
103
+ }
104
+ }
105
+
106
+ // 处理用户绑定完成
107
+ const handleComplete = () => {
108
+ currentStep.value = 2
109
+ }
110
+
111
+ // 跳过用户绑定
112
+ const handleSkip = () => {
113
+ currentStep.value = 2
114
+ }
115
+
116
+ // 完成设置
117
+ const finishSetup = async () => {
118
+ try {
119
+ await setupApi.complete()
120
+ emit('complete')
121
+ } catch (e) {
122
+ // 忽略错误,直接完成
123
+ emit('complete')
124
+ }
125
+ }
126
+ </script>
127
+
128
+ <style scoped>
129
+ .setup-wizard {
130
+ position: fixed;
131
+ top: 0;
132
+ left: 0;
133
+ right: 0;
134
+ bottom: 0;
135
+ background: var(--k-color-bg-1);
136
+ z-index: 9999;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ overflow: auto;
141
+ }
142
+
143
+ .wizard-container {
144
+ width: 100%;
145
+ max-width: 640px;
146
+ padding: 2rem;
147
+ }
148
+
149
+ .wizard-header {
150
+ text-align: center;
151
+ margin-bottom: 2rem;
152
+ }
153
+
154
+ .wizard-header h1 {
155
+ font-size: 1.75rem;
156
+ font-weight: 600;
157
+ color: var(--k-color-text);
158
+ margin: 0 0 0.5rem 0;
159
+ }
160
+
161
+ .wizard-header p {
162
+ color: var(--k-color-text-description);
163
+ margin: 0;
164
+ }
165
+
166
+ /* 步骤指示器 */
167
+ .steps-indicator {
168
+ display: flex;
169
+ justify-content: center;
170
+ gap: 2rem;
171
+ margin-bottom: 2rem;
172
+ }
173
+
174
+ .step-item {
175
+ display: flex;
176
+ flex-direction: column;
177
+ align-items: center;
178
+ gap: 0.5rem;
179
+ position: relative;
180
+ }
181
+
182
+ .step-item:not(:last-child)::after {
183
+ content: '';
184
+ position: absolute;
185
+ left: calc(50% + 20px);
186
+ top: 16px;
187
+ width: calc(2rem + 20px);
188
+ height: 2px;
189
+ background: var(--k-color-border);
190
+ }
191
+
192
+ .step-item.completed:not(:last-child)::after {
193
+ background: var(--k-color-success);
194
+ }
195
+
196
+ .step-number {
197
+ width: 32px;
198
+ height: 32px;
199
+ border-radius: 50%;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ font-weight: 600;
204
+ font-size: 0.9rem;
205
+ background: var(--k-color-bg-2);
206
+ color: var(--k-color-text-description);
207
+ border: 2px solid var(--k-color-border);
208
+ transition: all 0.2s;
209
+ }
210
+
211
+ .step-item.active .step-number {
212
+ background: var(--k-color-active);
213
+ color: white;
214
+ border-color: var(--k-color-active);
215
+ }
216
+
217
+ .step-item.completed .step-number {
218
+ background: var(--k-color-success);
219
+ color: white;
220
+ border-color: var(--k-color-success);
221
+ }
222
+
223
+ .step-label {
224
+ font-size: 0.85rem;
225
+ color: var(--k-color-text-description);
226
+ }
227
+
228
+ .step-item.active .step-label {
229
+ color: var(--k-color-text);
230
+ font-weight: 500;
231
+ }
232
+
233
+ .step-item.completed .step-label {
234
+ color: var(--k-color-success);
235
+ }
236
+
237
+ /* 步骤内容 */
238
+ .wizard-content {
239
+ background: var(--k-card-bg);
240
+ border: 1px solid var(--k-color-border);
241
+ border-radius: 12px;
242
+ padding: 2rem;
243
+ min-height: 400px;
244
+ }
245
+
246
+ /* 完成步骤 */
247
+ .step-complete {
248
+ display: flex;
249
+ flex-direction: column;
250
+ align-items: center;
251
+ justify-content: center;
252
+ text-align: center;
253
+ padding: 3rem 0;
254
+ }
255
+
256
+ .complete-icon {
257
+ font-size: 4rem;
258
+ color: var(--k-color-success);
259
+ margin-bottom: 1.5rem;
260
+ }
261
+
262
+ .step-complete h2 {
263
+ font-size: 1.5rem;
264
+ font-weight: 600;
265
+ color: var(--k-color-text);
266
+ margin: 0 0 0.75rem 0;
267
+ }
268
+
269
+ .step-complete p {
270
+ color: var(--k-color-text-description);
271
+ margin: 0 0 2rem 0;
272
+ }
273
+
274
+ /* 过渡动画 */
275
+ .fade-enter-active,
276
+ .fade-leave-active {
277
+ transition: opacity 0.2s ease;
278
+ }
279
+
280
+ .fade-enter-from,
281
+ .fade-leave-to {
282
+ opacity: 0;
283
+ }
284
+ </style>
@@ -0,0 +1,323 @@
1
+ <template>
2
+ <div class="setup-auth">
3
+ <h3>用户绑定</h3>
4
+ <p class="step-desc">
5
+ 将 WebUI 与 Koishi 用户绑定。你可以直接配置用户 ID,也可以通过验证码进行验证绑定。
6
+ </p>
7
+
8
+ <div class="config-panel">
9
+ <!-- UID 输入 -->
10
+ <div class="form-row">
11
+ <div class="form-label">用户 ID (UID)</div>
12
+ <div class="field-container">
13
+ <el-input-number
14
+ v-model="uid"
15
+ :min="1"
16
+ :controls="false"
17
+ placeholder="Koishi 用户 ID"
18
+ class="uid-input full-width"
19
+ @input="handleUidChange"
20
+ />
21
+ <div class="field-desc">
22
+ 输入你的 Koishi <code>uid</code>,不是原神的。
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- 验证码显示区域 -->
28
+ <div class="verify-section">
29
+ <div class="verify-card">
30
+ <div class="code-display">
31
+ <div class="code-label">验证码</div>
32
+ <div class="code-value">{{ verifyCode }}</div>
33
+ <div class="code-meta">有效期 {{ expiresIn }} 秒</div>
34
+ </div>
35
+ <div class="verify-guide">
36
+ <p>请在聊天平台向机器人发送以下指令完成绑定:</p>
37
+ <div class="command-box">
38
+ <code>bindui</code>
39
+ </div>
40
+ <p class="small-hint">发送指令后,请根据提示输入左侧的验证码。</p>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="step-actions">
47
+ <k-button @click="$emit('skip')">跳过</k-button>
48
+ <k-button type="primary" :loading="saving" @click="handleSave">
49
+ 保存 / 完成
50
+ </k-button>
51
+ </div>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { ref, onMounted, onUnmounted } from 'vue'
57
+ import { message, receive } from '@koishijs/client'
58
+ import { setupApi } from '../../api'
59
+
60
+ const emit = defineEmits<{
61
+ (e: 'complete'): void
62
+ (e: 'skip'): void
63
+ }>()
64
+
65
+ defineProps<{
66
+ saving: boolean
67
+ }>()
68
+
69
+ const uid = ref<number | null>(null)
70
+ const generating = ref(false)
71
+ const verifyCode = ref('')
72
+ const expiresIn = ref(0)
73
+ const initialBoundUid = ref<number | null>(null)
74
+
75
+ let stopReceive: (() => void) | null = null
76
+ let pollTimer: number | null = null
77
+
78
+ // 加载当前状态
79
+ const loadStatus = async () => {
80
+ try {
81
+ const status = await setupApi.status()
82
+ if (status.boundUid) {
83
+ uid.value = status.boundUid
84
+ initialBoundUid.value = status.boundUid
85
+ }
86
+ } catch (e) {
87
+ console.error('Failed to load status:', e)
88
+ }
89
+ }
90
+
91
+ // 生成验证码
92
+ const generateCode = async () => {
93
+ if (generateTimer) clearTimeout(generateTimer)
94
+ if (!uid.value) return
95
+
96
+ try {
97
+ generating.value = true
98
+ const result = await setupApi.generateVerifyCode(uid.value)
99
+ verifyCode.value = result.code
100
+ expiresIn.value = result.expiresIn
101
+ message.success('验证码已生成,请在聊天平台完成验证')
102
+ } catch (e) {
103
+ message.error('生成验证码失败: ' + (e instanceof Error ? e.message : '未知错误'))
104
+ } finally {
105
+ generating.value = false
106
+ }
107
+ }
108
+
109
+ // 监听 UID 变化自动生成验证码
110
+ let generateTimer: number | null = null
111
+ const handleUidChange = () => {
112
+ if (generateTimer) clearTimeout(generateTimer)
113
+ verifyCode.value = ''
114
+
115
+ if (uid.value) {
116
+ generateTimer = window.setTimeout(generateCode, 1000)
117
+ }
118
+ }
119
+
120
+ // 保存/直接绑定
121
+ const handleSave = async () => {
122
+ if (!uid.value) {
123
+ message.warning('请输入用户 ID')
124
+ return
125
+ }
126
+
127
+ try {
128
+ // 尝试直接更新配置(如果通过验证码流程绑定了,这里再次设置也是安全的)
129
+ await setupApi.bindUid(uid.value)
130
+ message.success('配置已保存')
131
+ emit('complete')
132
+ } catch (e) {
133
+ message.error('保存失败: ' + (e instanceof Error ? e.message : '未知错误'))
134
+ }
135
+ }
136
+
137
+ onMounted(() => {
138
+ loadStatus()
139
+
140
+ // 监听来自 QQ 的绑定请求
141
+ stopReceive = (receive as any)('media-luna/webui-auth/bind-request', (data: { uid: number, code: string, expiresIn: number }) => {
142
+ if (data.uid) {
143
+ uid.value = data.uid
144
+
145
+ if (data.code) {
146
+ verifyCode.value = data.code
147
+ expiresIn.value = data.expiresIn
148
+ message.info('收到绑定请求,请在聊天平台完成验证')
149
+ }
150
+ }
151
+ })
152
+
153
+ // 轮询绑定状态
154
+ pollTimer = window.setInterval(async () => {
155
+ try {
156
+ const status = await setupApi.status()
157
+ if (status.boundUid) {
158
+ // 只有当绑定的 UID 与当前(或请求的)UID 一致时才自动完成
159
+ if (uid.value && status.boundUid !== uid.value) return
160
+
161
+ // 如果绑定状态发生了变化(从无到有,或变更了用户),则自动完成
162
+ // 如果是重新绑定同一个用户,由于状态未变,不自动完成(避免过早提示)
163
+ if (status.boundUid !== initialBoundUid.value) {
164
+ uid.value = status.boundUid
165
+ message.success('检测到绑定成功!')
166
+ emit('complete')
167
+ if (pollTimer) clearInterval(pollTimer)
168
+ }
169
+ }
170
+ } catch {}
171
+ }, 3000)
172
+ })
173
+
174
+ onUnmounted(() => {
175
+ if (stopReceive) stopReceive()
176
+ if (pollTimer) clearInterval(pollTimer)
177
+ })
178
+ </script>
179
+
180
+ <style scoped>
181
+ .setup-auth h3 {
182
+ font-size: 1.25rem;
183
+ font-weight: 600;
184
+ color: var(--k-color-text);
185
+ margin: 0 0 0.5rem 0;
186
+ }
187
+
188
+ .step-desc {
189
+ color: var(--k-color-text-description);
190
+ margin: 0 0 1.5rem 0;
191
+ }
192
+
193
+ .config-panel {
194
+ background: var(--k-card-bg);
195
+ padding: 1.5rem;
196
+ border-radius: 12px;
197
+ border: 1px solid var(--k-color-border);
198
+ margin-bottom: 1.5rem;
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 1.5rem;
202
+ }
203
+
204
+ /* 表单样式 imitation of ConfigRenderer */
205
+ .form-row {
206
+ display: flex;
207
+ align-items: flex-start;
208
+ }
209
+
210
+ .form-label {
211
+ width: 120px;
212
+ flex-shrink: 0;
213
+ color: var(--k-color-text-description);
214
+ padding-top: 6px;
215
+ font-size: 0.9rem;
216
+ }
217
+
218
+ .field-container {
219
+ flex: 1;
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 0.5rem;
223
+ }
224
+
225
+ .uid-input {
226
+ width: 100%;
227
+ }
228
+
229
+ .field-desc {
230
+ font-size: 0.8rem;
231
+ color: var(--k-color-text-description);
232
+ }
233
+
234
+ .field-desc code {
235
+ background: var(--k-color-bg-2);
236
+ padding: 0.1em 0.4em;
237
+ border-radius: 4px;
238
+ font-family: monospace;
239
+ }
240
+
241
+ /* 验证码区域 */
242
+ .verify-section {
243
+ border-top: 1px solid var(--k-color-border);
244
+ padding-top: 1.5rem;
245
+ margin-top: 0.5rem;
246
+ }
247
+
248
+ .verify-card {
249
+ background: var(--k-color-bg-2);
250
+ border-radius: 8px;
251
+ padding: 1.5rem;
252
+ display: flex;
253
+ gap: 2rem;
254
+ align-items: center;
255
+ }
256
+
257
+ .code-display {
258
+ display: flex;
259
+ flex-direction: column;
260
+ align-items: center;
261
+ gap: 0.5rem;
262
+ padding-right: 2rem;
263
+ border-right: 1px dashed var(--k-color-border);
264
+ min-width: 150px;
265
+ }
266
+
267
+ .code-label {
268
+ font-size: 0.85rem;
269
+ color: var(--k-color-text-description);
270
+ }
271
+
272
+ .code-value {
273
+ font-size: 2rem;
274
+ font-family: monospace;
275
+ font-weight: 700;
276
+ color: var(--k-color-active);
277
+ letter-spacing: 0.1em;
278
+ }
279
+
280
+ .code-meta {
281
+ font-size: 0.8rem;
282
+ color: var(--k-color-text-description);
283
+ }
284
+
285
+ .verify-guide {
286
+ flex: 1;
287
+ }
288
+
289
+ .verify-guide p {
290
+ margin: 0 0 0.75rem 0;
291
+ font-size: 0.9rem;
292
+ color: var(--k-color-text);
293
+ }
294
+
295
+ .command-box {
296
+ background: var(--k-card-bg);
297
+ padding: 0.75rem 1rem;
298
+ border-radius: 6px;
299
+ border: 1px solid var(--k-color-border);
300
+ display: inline-block;
301
+ margin-bottom: 0.75rem;
302
+ }
303
+
304
+ .command-box code {
305
+ font-family: monospace;
306
+ color: var(--k-color-active);
307
+ font-weight: 600;
308
+ }
309
+
310
+ .small-hint {
311
+ font-size: 0.8rem !important;
312
+ color: var(--k-color-text-description) !important;
313
+ margin: 0 !important;
314
+ }
315
+
316
+ .step-actions {
317
+ display: flex;
318
+ justify-content: flex-end;
319
+ gap: 0.75rem;
320
+ padding-top: 1.5rem;
321
+ border-top: 1px solid var(--k-color-border);
322
+ }
323
+ </style>