koishi-plugin-mail-manager 1.0.1 → 1.0.3
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 +4 -0
- package/client/components/AccountEditModal.vue +262 -106
- package/client/pages/AccountsView.vue +11 -18
- package/client/pages/MailsView.vue +215 -21
- package/client/pages/index.vue +8 -11
- package/dist/index.js +11 -4
- package/dist/style.css +1 -1
- package/lib/core.d.ts +3 -7
- package/lib/index.js +1203 -820
- package/package.json +6 -6
- package/readme.md +8 -1
- package/lib/api.d.ts +0 -8
- package/lib/cleanup.d.ts +0 -35
- package/lib/commands.d.ts +0 -9
- package/lib/config.d.ts +0 -16
- package/lib/core/accounts.d.ts +0 -15
- package/lib/core/forward.d.ts +0 -41
- package/lib/core/index.d.ts +0 -16
- package/lib/core/mails.d.ts +0 -56
- package/lib/core/rules.d.ts +0 -41
- package/lib/core/state.d.ts +0 -46
- package/lib/database.d.ts +0 -6
- package/lib/html2image.d.ts +0 -42
- package/lib/imap.d.ts +0 -198
- package/lib/index.d.ts +0 -14
- package/lib/logger.d.ts +0 -61
- package/lib/parser.d.ts +0 -77
- package/lib/providers/base.d.ts +0 -168
- package/lib/providers/gmail.d.ts +0 -48
- package/lib/providers/icloud.d.ts +0 -15
- package/lib/providers/index.d.ts +0 -37
- package/lib/providers/netease.d.ts +0 -19
- package/lib/providers/outlook.d.ts +0 -15
- package/lib/providers/qq.d.ts +0 -15
- package/lib/providers/yahoo.d.ts +0 -14
- package/lib/render.d.ts +0 -36
- package/lib/service.d.ts +0 -42
- package/lib/styles.d.ts +0 -16
- package/lib/types.d.ts +0 -547
- package/lib/utils/common.d.ts +0 -45
- package/lib/utils/constants.d.ts +0 -31
- package/lib/utils/crypto.d.ts +0 -74
- package/lib/utils/dns.d.ts +0 -180
- package/lib/utils/errors.d.ts +0 -63
- package/lib/utils/index.d.ts +0 -7
package/client/api.ts
CHANGED
|
@@ -46,6 +46,10 @@ export const accountApi = {
|
|
|
46
46
|
/** 测试连接 */
|
|
47
47
|
test: (id: number) => call<ConnectionTestResult>('mail-manager/accounts/test', id),
|
|
48
48
|
|
|
49
|
+
/** 用当前表单配置测试连接(无需先保存账号) */
|
|
50
|
+
testTemp: (data: Partial<MailAccount>) =>
|
|
51
|
+
call<ConnectionTestResult>('mail-manager/accounts/test-temp', data),
|
|
52
|
+
|
|
49
53
|
/** 连接账号 */
|
|
50
54
|
connect: (id: number) => call<void>('mail-manager/accounts/connect', id),
|
|
51
55
|
|
|
@@ -5,138 +5,161 @@
|
|
|
5
5
|
<span class="ml-modal-title">{{ isEditing ? '编辑账号' : '添加账号' }}</span>
|
|
6
6
|
<button class="ml-modal-close" @click="closeModal"><Icon name="close" /></button>
|
|
7
7
|
</div>
|
|
8
|
+
|
|
8
9
|
<div class="ml-modal-body">
|
|
9
|
-
<!-- 错误提示 -->
|
|
10
10
|
<div v-if="formError" class="form-error">
|
|
11
11
|
<Icon name="alert" /> {{ formError }}
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
<div class="
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<div class="ml-form-group">
|
|
27
|
-
<label class="ml-label">邮箱服务商</label>
|
|
28
|
-
<Select
|
|
29
|
-
v-model="selectedProvider"
|
|
30
|
-
:options="providerOptions"
|
|
31
|
-
placeholder="选择服务商自动填充配置"
|
|
32
|
-
@change="onProviderChange"
|
|
33
|
-
/>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="ml-form-group">
|
|
37
|
-
<label class="ml-label">邮箱地址 <span class="required">*</span></label>
|
|
38
|
-
<input
|
|
39
|
-
v-model="formData.email"
|
|
40
|
-
class="ml-input"
|
|
41
|
-
:class="{ 'has-error': !formData.email && formTouched.email }"
|
|
42
|
-
type="email"
|
|
43
|
-
placeholder="example@mail.com"
|
|
44
|
-
@blur="formTouched.email = true"
|
|
45
|
-
@input="autoFillImapHost"
|
|
46
|
-
/>
|
|
47
|
-
</div>
|
|
14
|
+
<div class="form-section">
|
|
15
|
+
<div class="section-title"><Icon name="user" /> 基本信息</div>
|
|
16
|
+
<div class="ml-form-group">
|
|
17
|
+
<label class="ml-label">名称 <span class="required">*</span></label>
|
|
18
|
+
<input
|
|
19
|
+
v-model="formData.name"
|
|
20
|
+
class="ml-input"
|
|
21
|
+
:class="{ 'has-error': !formData.name && formTouched.name }"
|
|
22
|
+
placeholder="用于标识的名称(如:工作邮箱)"
|
|
23
|
+
@blur="formTouched.name = true"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<div class="password-input-wrapper">
|
|
27
|
+
<div class="ml-form-group">
|
|
28
|
+
<label class="ml-label">邮箱地址 <span class="required">*</span></label>
|
|
52
29
|
<input
|
|
53
|
-
v-model="formData.
|
|
30
|
+
v-model="formData.email"
|
|
54
31
|
class="ml-input"
|
|
55
|
-
:class="{ 'has-error': !
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@blur="formTouched.
|
|
32
|
+
:class="{ 'has-error': !formData.email && formTouched.email }"
|
|
33
|
+
type="email"
|
|
34
|
+
placeholder="example@mail.com"
|
|
35
|
+
@blur="formTouched.email = true"
|
|
36
|
+
@input="autoFillImapHost"
|
|
59
37
|
/>
|
|
60
|
-
<button class="toggle-password-btn" @click="showPassword = !showPassword" tabindex="-1">
|
|
61
|
-
<Icon :name="showPassword ? 'eye' : 'eye-off'" />
|
|
62
|
-
</button>
|
|
63
38
|
</div>
|
|
64
|
-
|
|
65
|
-
<div
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
39
|
+
|
|
40
|
+
<div class="ml-form-group">
|
|
41
|
+
<label class="ml-label">邮箱服务商</label>
|
|
42
|
+
<Select
|
|
43
|
+
v-model="selectedProvider"
|
|
44
|
+
:options="providerOptions"
|
|
45
|
+
placeholder="选择服务商自动填充配置"
|
|
46
|
+
@change="onProviderChange"
|
|
47
|
+
/>
|
|
71
48
|
</div>
|
|
72
|
-
<div v-else class="ml-help"><Icon name="lightbulb" /> 大部分邮箱需要使用授权码,而非登录密码</div>
|
|
73
49
|
</div>
|
|
74
50
|
|
|
75
|
-
<div class="
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
class="
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
51
|
+
<div class="form-section">
|
|
52
|
+
<div class="section-title"><Icon name="network" /> 连接参数</div>
|
|
53
|
+
<div class="ml-form-group">
|
|
54
|
+
<label class="ml-label">IMAP 服务器 <span class="required">*</span></label>
|
|
55
|
+
<div class="server-input-row">
|
|
56
|
+
<input
|
|
57
|
+
v-model="formData.imapHost"
|
|
58
|
+
class="ml-input"
|
|
59
|
+
:class="{ 'has-error': !formData.imapHost && formTouched.imapHost }"
|
|
60
|
+
placeholder="imap.example.com"
|
|
61
|
+
@blur="formTouched.imapHost = true"
|
|
62
|
+
/>
|
|
63
|
+
<Select
|
|
64
|
+
v-model="quickServerSelect"
|
|
65
|
+
:options="quickServerOptions"
|
|
66
|
+
placeholder="快速选择"
|
|
67
|
+
size="small"
|
|
68
|
+
@change="onQuickServerSelect"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="ml-form-row">
|
|
74
|
+
<div class="ml-form-group port-group">
|
|
75
|
+
<label class="ml-label">端口</label>
|
|
76
|
+
<Select
|
|
77
|
+
v-model="formData.imapPort"
|
|
78
|
+
:options="portOptions"
|
|
79
|
+
placeholder="选择端口"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="ml-form-group switch-group">
|
|
84
|
+
<label class="ml-label">TLS 加密</label>
|
|
85
|
+
<div class="switch-wrapper">
|
|
86
|
+
<label class="ml-switch">
|
|
87
|
+
<input v-model="formData.imapTls" type="checkbox" />
|
|
88
|
+
<span class="slider"></span>
|
|
89
|
+
</label>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="ml-form-group switch-group">
|
|
94
|
+
<label class="ml-label">启用</label>
|
|
95
|
+
<div class="switch-wrapper">
|
|
96
|
+
<label class="ml-switch">
|
|
97
|
+
<input v-model="formData.enabled" type="checkbox" />
|
|
98
|
+
<span class="slider"></span>
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
85
102
|
</div>
|
|
86
|
-
</div>
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<div class="server-input-row">
|
|
104
|
+
<div class="ml-form-group">
|
|
105
|
+
<label class="ml-label">代理服务器(可选)</label>
|
|
91
106
|
<input
|
|
92
|
-
v-model="formData.
|
|
107
|
+
v-model="formData.proxyUrl"
|
|
93
108
|
class="ml-input"
|
|
94
|
-
|
|
95
|
-
placeholder="imap.example.com"
|
|
96
|
-
@blur="formTouched.imapHost = true"
|
|
97
|
-
/>
|
|
98
|
-
<Select
|
|
99
|
-
v-model="quickServerSelect"
|
|
100
|
-
:options="quickServerOptions"
|
|
101
|
-
placeholder="快速选择"
|
|
102
|
-
size="small"
|
|
103
|
-
@change="onQuickServerSelect"
|
|
109
|
+
placeholder="如: socks5://127.0.0.1:6780"
|
|
104
110
|
/>
|
|
111
|
+
<div v-if="selectedProviderInfo?.needsProxy" class="ml-help warning">
|
|
112
|
+
<Icon name="alert" />
|
|
113
|
+
{{ selectedProviderInfo.proxyHint || '此邮箱服务可能需要代理才能连接' }}
|
|
114
|
+
</div>
|
|
105
115
|
</div>
|
|
106
116
|
</div>
|
|
107
117
|
|
|
108
|
-
<div class="
|
|
109
|
-
<div class="
|
|
110
|
-
|
|
111
|
-
<Select
|
|
112
|
-
v-model="formData.imapPort"
|
|
113
|
-
:options="portOptions"
|
|
114
|
-
placeholder="选择端口"
|
|
115
|
-
/>
|
|
116
|
-
</div>
|
|
118
|
+
<div class="form-section">
|
|
119
|
+
<div class="section-title"><Icon name="settings" /> 认证与校验</div>
|
|
120
|
+
|
|
117
121
|
<div class="ml-form-group">
|
|
118
|
-
<label class="ml-label"
|
|
119
|
-
<div class="
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
<label class="ml-label">密码/授权码 <span v-if="!isEditing" class="required">*</span></label>
|
|
123
|
+
<div class="password-input-wrapper">
|
|
124
|
+
<input
|
|
125
|
+
v-model="formData.password"
|
|
126
|
+
class="ml-input"
|
|
127
|
+
:class="{ 'has-error': !isEditing && !formData.password && formTouched.password }"
|
|
128
|
+
:type="showPassword ? 'text' : 'password'"
|
|
129
|
+
:placeholder="isEditing ? '留空则保持原密码不变;测试连接需填写' : '授权码(非登录密码)'"
|
|
130
|
+
@blur="formTouched.password = true"
|
|
131
|
+
/>
|
|
132
|
+
<button class="toggle-password-btn" @click="showPassword = !showPassword" tabindex="-1" type="button">
|
|
133
|
+
<Icon :name="showPassword ? 'eye' : 'eye-off'" />
|
|
134
|
+
</button>
|
|
124
135
|
</div>
|
|
136
|
+
|
|
137
|
+
<div v-if="selectedProviderInfo" class="auth-guide">
|
|
138
|
+
<Icon name="info" />
|
|
139
|
+
<span>{{ selectedProviderInfo.authGuide }}</span>
|
|
140
|
+
<a v-if="selectedProviderInfo.helpUrl" :href="selectedProviderInfo.helpUrl" target="_blank" class="guide-link">
|
|
141
|
+
查看教程 <Icon name="external-link" />
|
|
142
|
+
</a>
|
|
143
|
+
</div>
|
|
144
|
+
<div v-else class="ml-help"><Icon name="lightbulb" /> 大部分邮箱需要使用授权码,而非登录密码</div>
|
|
125
145
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<div class="
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</label>
|
|
146
|
+
|
|
147
|
+
<div class="test-box" :class="{ success: testSuccess === true, failed: testSuccess === false }">
|
|
148
|
+
<div class="test-info">
|
|
149
|
+
<Icon :name="testSuccess === true ? 'check' : testSuccess === false ? 'alert-circle' : 'activity'" />
|
|
150
|
+
<span v-if="testSuccess === null">建议保存前先测试连接,避免无效配置反复重连。</span>
|
|
151
|
+
<span v-else>{{ testMessage }}</span>
|
|
133
152
|
</div>
|
|
153
|
+
<button class="ml-btn" type="button" @click="testConnectionWithForm" :disabled="testing || !canTestConfig">
|
|
154
|
+
{{ testing ? '测试中...' : '测试连接' }}
|
|
155
|
+
</button>
|
|
134
156
|
</div>
|
|
135
157
|
</div>
|
|
136
158
|
</div>
|
|
159
|
+
|
|
137
160
|
<div class="ml-modal-footer">
|
|
138
|
-
<button class="ml-btn" @click="closeModal">取消</button>
|
|
139
|
-
<button class="ml-btn primary" @click="saveAccount" :disabled="saving || !isFormValid">
|
|
161
|
+
<button class="ml-btn" @click="closeModal" :disabled="saving || testing">取消</button>
|
|
162
|
+
<button class="ml-btn primary" @click="saveAccount" :disabled="saving || testing || !isFormValid">
|
|
140
163
|
{{ saving ? '保存中...' : '保存' }}
|
|
141
164
|
</button>
|
|
142
165
|
</div>
|
|
@@ -211,7 +234,7 @@ const emailProviders: Record<string, ProviderConfig> = {
|
|
|
211
234
|
authGuide: '请先开启两步验证,然后创建应用专用密码作为授权码使用',
|
|
212
235
|
helpUrl: 'https://support.google.com/accounts/answer/185833',
|
|
213
236
|
needsProxy: true,
|
|
214
|
-
proxyHint: 'Gmail
|
|
237
|
+
proxyHint: 'Gmail 可能需要配置代理'
|
|
215
238
|
},
|
|
216
239
|
outlook: {
|
|
217
240
|
name: 'Outlook / Hotmail',
|
|
@@ -222,7 +245,7 @@ const emailProviders: Record<string, ProviderConfig> = {
|
|
|
222
245
|
authGuide: '请在 Microsoft 账户「安全 > 应用密码」中创建应用密码',
|
|
223
246
|
helpUrl: 'https://support.microsoft.com/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944',
|
|
224
247
|
needsProxy: true,
|
|
225
|
-
proxyHint: 'Outlook
|
|
248
|
+
proxyHint: 'Outlook 可能需要配置代理'
|
|
226
249
|
},
|
|
227
250
|
yahoo: {
|
|
228
251
|
name: 'Yahoo Mail',
|
|
@@ -318,8 +341,11 @@ const imapHostMap: Record<string, string> = {
|
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
const saving = ref(false)
|
|
344
|
+
const testing = ref(false)
|
|
321
345
|
const showPassword = ref(false)
|
|
322
346
|
const formError = ref('')
|
|
347
|
+
const testMessage = ref('')
|
|
348
|
+
const testSuccess = ref<boolean | null>(null)
|
|
323
349
|
const selectedProvider = ref('')
|
|
324
350
|
const quickServerSelect = ref('')
|
|
325
351
|
|
|
@@ -349,6 +375,8 @@ const formData = reactive({
|
|
|
349
375
|
proxyUrl: '',
|
|
350
376
|
})
|
|
351
377
|
|
|
378
|
+
const shouldForceTls = (port: number) => port === 993 || port === 465
|
|
379
|
+
|
|
352
380
|
// 初始化表单
|
|
353
381
|
watch(() => props.visible, (newVal) => {
|
|
354
382
|
if (newVal) {
|
|
@@ -379,9 +407,24 @@ watch(() => props.visible, (newVal) => {
|
|
|
379
407
|
})
|
|
380
408
|
selectedProvider.value = ''
|
|
381
409
|
}
|
|
410
|
+
|
|
411
|
+
testMessage.value = ''
|
|
412
|
+
testSuccess.value = null
|
|
382
413
|
}
|
|
383
414
|
})
|
|
384
415
|
|
|
416
|
+
watch(() => formData.imapPort, (port) => {
|
|
417
|
+
formData.imapTls = shouldForceTls(port)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
watch(
|
|
421
|
+
() => [formData.email, formData.password, formData.imapHost, formData.imapPort, formData.imapTls, formData.proxyUrl],
|
|
422
|
+
() => {
|
|
423
|
+
testSuccess.value = null
|
|
424
|
+
testMessage.value = ''
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
|
|
385
428
|
const resetForm = () => {
|
|
386
429
|
formTouched.name = false
|
|
387
430
|
formTouched.email = false
|
|
@@ -418,6 +461,42 @@ const onProviderChange = (value: string) => {
|
|
|
418
461
|
}
|
|
419
462
|
}
|
|
420
463
|
|
|
464
|
+
const canTestConfig = computed(() => {
|
|
465
|
+
return !!(formData.email && formData.imapHost && formData.password)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const testConnectionWithForm = async () => {
|
|
469
|
+
formError.value = ''
|
|
470
|
+
testMessage.value = ''
|
|
471
|
+
|
|
472
|
+
if (!canTestConfig.value) {
|
|
473
|
+
testSuccess.value = false
|
|
474
|
+
testMessage.value = '测试连接需要填写邮箱地址、IMAP 服务器和密码/授权码。'
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
testing.value = true
|
|
479
|
+
try {
|
|
480
|
+
const result = await accountApi.testTemp({
|
|
481
|
+
name: formData.name,
|
|
482
|
+
email: formData.email,
|
|
483
|
+
password: formData.password,
|
|
484
|
+
imapHost: formData.imapHost,
|
|
485
|
+
imapPort: formData.imapPort,
|
|
486
|
+
imapTls: formData.imapTls,
|
|
487
|
+
proxyUrl: formData.proxyUrl,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
testSuccess.value = result.success
|
|
491
|
+
testMessage.value = result.success ? '连接测试成功,可以安全保存。' : `连接测试失败:${result.message}`
|
|
492
|
+
} catch (e) {
|
|
493
|
+
testSuccess.value = false
|
|
494
|
+
testMessage.value = `连接测试失败:${(e as Error).message}`
|
|
495
|
+
} finally {
|
|
496
|
+
testing.value = false
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
421
500
|
// 快速服务器选择
|
|
422
501
|
const onQuickServerSelect = (value: string) => {
|
|
423
502
|
if (value) {
|
|
@@ -463,6 +542,11 @@ const saveAccount = async () => {
|
|
|
463
542
|
return
|
|
464
543
|
}
|
|
465
544
|
|
|
545
|
+
if (testSuccess.value !== true && canTestConfig.value) {
|
|
546
|
+
const confirmed = confirm('当前配置未通过连接测试,仍要保存吗?')
|
|
547
|
+
if (!confirmed) return
|
|
548
|
+
}
|
|
549
|
+
|
|
466
550
|
formError.value = ''
|
|
467
551
|
saving.value = true
|
|
468
552
|
try {
|
|
@@ -489,7 +573,25 @@ const saveAccount = async () => {
|
|
|
489
573
|
<style scoped>
|
|
490
574
|
.account-modal {
|
|
491
575
|
width: 100%;
|
|
492
|
-
max-width:
|
|
576
|
+
max-width: 640px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.form-section {
|
|
580
|
+
border: 1px solid var(--ml-border);
|
|
581
|
+
border-radius: 10px;
|
|
582
|
+
padding: 14px;
|
|
583
|
+
margin-bottom: 14px;
|
|
584
|
+
background: var(--ml-bg-base);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.section-title {
|
|
588
|
+
display: flex;
|
|
589
|
+
align-items: center;
|
|
590
|
+
gap: 6px;
|
|
591
|
+
font-size: 13px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
color: var(--ml-text-secondary);
|
|
594
|
+
margin-bottom: 10px;
|
|
493
595
|
}
|
|
494
596
|
|
|
495
597
|
.form-error {
|
|
@@ -532,6 +634,37 @@ const saveAccount = async () => {
|
|
|
532
634
|
color: var(--ml-warning);
|
|
533
635
|
}
|
|
534
636
|
|
|
637
|
+
.test-box {
|
|
638
|
+
margin-top: 10px;
|
|
639
|
+
padding: 10px 12px;
|
|
640
|
+
border: 1px solid var(--ml-border);
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
justify-content: space-between;
|
|
645
|
+
gap: 10px;
|
|
646
|
+
background: var(--ml-bg-container);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.test-box.success {
|
|
650
|
+
border-color: rgba(82, 196, 26, 0.45);
|
|
651
|
+
background: rgba(82, 196, 26, 0.08);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.test-box.failed {
|
|
655
|
+
border-color: rgba(255, 77, 79, 0.45);
|
|
656
|
+
background: rgba(255, 77, 79, 0.08);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.test-info {
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
gap: 6px;
|
|
663
|
+
font-size: 13px;
|
|
664
|
+
color: var(--ml-text-secondary);
|
|
665
|
+
line-height: 1.5;
|
|
666
|
+
}
|
|
667
|
+
|
|
535
668
|
.auth-guide {
|
|
536
669
|
display: flex;
|
|
537
670
|
align-items: flex-start;
|
|
@@ -588,6 +721,10 @@ const saveAccount = async () => {
|
|
|
588
721
|
max-width: 200px;
|
|
589
722
|
}
|
|
590
723
|
|
|
724
|
+
.switch-group {
|
|
725
|
+
min-width: 88px;
|
|
726
|
+
}
|
|
727
|
+
|
|
591
728
|
.switch-wrapper {
|
|
592
729
|
padding-top: 8px;
|
|
593
730
|
}
|
|
@@ -641,4 +778,23 @@ const saveAccount = async () => {
|
|
|
641
778
|
width: 1.2em;
|
|
642
779
|
height: 1.2em;
|
|
643
780
|
}
|
|
781
|
+
|
|
782
|
+
@media (max-width: 720px) {
|
|
783
|
+
.account-modal {
|
|
784
|
+
max-width: 96vw;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.test-box {
|
|
788
|
+
flex-direction: column;
|
|
789
|
+
align-items: stretch;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.server-input-row {
|
|
793
|
+
flex-direction: column;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.server-input-row .ml-select-wrapper {
|
|
797
|
+
width: 100%;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
644
800
|
</style>
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
<div v-else>
|
|
123
123
|
<p class="sync-tip">
|
|
124
124
|
<Icon name="lightbulb" />
|
|
125
|
-
|
|
125
|
+
从邮箱服务器重新获取邮件。可用于恢复本地已删除、但服务器仍保留的邮件。
|
|
126
126
|
</p>
|
|
127
127
|
<div class="sync-warning">
|
|
128
128
|
<Icon name="info" />
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
</div>
|
|
154
154
|
<div class="ml-help">
|
|
155
155
|
<Icon name="info" />
|
|
156
|
-
|
|
156
|
+
系统会按 Message-ID 自动去重并优先跳过已存在邮件。同步期间请勿关闭页面。
|
|
157
157
|
</div>
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
</template>
|
|
175
175
|
|
|
176
176
|
<script setup lang="ts">
|
|
177
|
-
import { ref, reactive, computed, onMounted
|
|
177
|
+
import { ref, reactive, computed, onMounted } from 'vue'
|
|
178
178
|
import { send, receive } from '@koishijs/client'
|
|
179
179
|
import { accountApi } from '../api'
|
|
180
180
|
import type { MailAccount } from '../types'
|
|
@@ -209,20 +209,19 @@ const loadAccounts = async () => {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
//
|
|
213
|
-
|
|
212
|
+
// 在 setup 顶层注册监听,确保 Vue 正确绑定生命周期并及时接收事件
|
|
213
|
+
// 处理账号状态变化推送
|
|
214
|
+
receive('mail-manager/account-status-changed', (data: { accountId: number; status: string; error?: string | null }) => {
|
|
214
215
|
const account = accounts.value.find(a => a.id === data.accountId)
|
|
215
216
|
if (account) {
|
|
216
217
|
account.status = data.status as MailAccount['status']
|
|
217
|
-
|
|
218
|
-
account.lastError = data.error
|
|
219
|
-
} else {
|
|
220
|
-
account.lastError = undefined
|
|
221
|
-
}
|
|
222
|
-
// 通知父组件更新统计(已连接数量可能变化)
|
|
218
|
+
account.lastError = data.error || undefined
|
|
223
219
|
emit('refresh')
|
|
220
|
+
} else {
|
|
221
|
+
// 账号不在列表中(如新建账号时的竞态),重新加载以同步最新状态
|
|
222
|
+
loadAccounts().then(() => emit('refresh'))
|
|
224
223
|
}
|
|
225
|
-
}
|
|
224
|
+
})
|
|
226
225
|
|
|
227
226
|
// 同步相关状态
|
|
228
227
|
const showSyncModal = ref(false)
|
|
@@ -329,12 +328,6 @@ const deleteAccount = async (account: MailAccount) => {
|
|
|
329
328
|
|
|
330
329
|
onMounted(() => {
|
|
331
330
|
loadAccounts()
|
|
332
|
-
|
|
333
|
-
// 监听状态变化事件
|
|
334
|
-
// receive 函数会自动在组件卸载时清理监听器
|
|
335
|
-
receive('mail-manager/account-status-changed', (data) => {
|
|
336
|
-
handleStatusChange(data)
|
|
337
|
-
})
|
|
338
331
|
})
|
|
339
332
|
|
|
340
333
|
</script>
|