@h-ai/ui 0.1.0-alpha.62 → 0.1.0-alpha.63
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/README.en.md +33 -7
- package/README.ja.md +33 -7
- package/README.md +33 -7
- package/auto-import.d.ts +1 -0
- package/auto-import.js +1 -0
- package/dist/components/compounds/Combobox.svelte +19 -6
- package/dist/components/compounds/Combobox.svelte.d.ts +2 -2
- package/dist/components/compounds/DatePicker.svelte +14 -5
- package/dist/components/compounds/DatePicker.svelte.d.ts +2 -2
- package/dist/components/compounds/Form.svelte +10 -6
- package/dist/components/compounds/FormField.svelte +78 -13
- package/dist/components/compounds/FormLayout.svelte +59 -0
- package/dist/components/compounds/FormLayout.svelte.d.ts +4 -0
- package/dist/components/compounds/TagInput.svelte +1 -1
- package/dist/components/compounds/index.d.ts +1 -0
- package/dist/components/compounds/index.js +1 -0
- package/dist/components/primitives/BareInput.svelte +7 -1
- package/dist/components/primitives/Checkbox.svelte +8 -2
- package/dist/components/primitives/Input.svelte +22 -11
- package/dist/components/primitives/Select.svelte +77 -18
- package/dist/components/primitives/Textarea.svelte +27 -13
- package/dist/components/primitives/Textarea.svelte.d.ts +1 -1
- package/dist/components/scenes/app/FeedbackModal.svelte +29 -19
- package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +2 -2
- package/dist/components/scenes/crud/CrudEditPanel.svelte +148 -118
- package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +4 -25
- package/dist/components/scenes/crud/CrudPage.svelte +5 -41
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -53
- package/dist/components/scenes/crud/crud-controller.svelte.d.ts +4 -1
- package/dist/components/scenes/crud/crud-form-validation.d.ts +4 -0
- package/dist/components/scenes/crud/crud-form-validation.js +39 -0
- package/dist/components/scenes/crud/crud-types.d.ts +9 -42
- package/dist/components/scenes/iam/AuthPanel.svelte +55 -47
- package/dist/components/scenes/iam/ChangePasswordForm.svelte +57 -59
- package/dist/components/scenes/iam/ForgotPasswordForm.svelte +89 -84
- package/dist/components/scenes/iam/LoginForm.svelte +134 -132
- package/dist/components/scenes/iam/PasswordInput.svelte +25 -7
- package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +2 -2
- package/dist/components/scenes/iam/RegisterForm.svelte +95 -97
- package/dist/components/scenes/iam/ResetPasswordForm.svelte +83 -85
- package/dist/components/scenes/iam/UserProfile.svelte +9 -5
- package/dist/components/scenes/storage/FileUpload.svelte +1 -1
- package/dist/components/scenes/storage/ImageUpload.svelte +1 -0
- package/dist/components/scenes/types.d.ts +9 -9
- package/dist/i18n.svelte.d.ts +2 -1
- package/dist/i18n.svelte.js +2 -1
- package/dist/internal/form-context.d.ts +12 -0
- package/dist/internal/form-context.js +9 -0
- package/dist/internal/form-validation.svelte.d.ts +16 -0
- package/dist/internal/form-validation.svelte.js +76 -0
- package/dist/internal/locale.d.ts +2 -0
- package/dist/internal/locale.js +22 -0
- package/dist/messages/en-US.json +13 -1
- package/dist/messages/zh-CN.json +13 -1
- package/dist/messages.js +4 -3
- package/dist/types.d.ts +57 -10
- package/package.json +7 -2
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
import { uiM } from '../../../messages.js'
|
|
15
15
|
import { cn, getDataAttributes } from '../../../utils.js'
|
|
16
16
|
import Alert from '../../compounds/Alert.svelte'
|
|
17
|
+
import FormField from '../../compounds/FormField.svelte'
|
|
18
|
+
import FormLayout from '../../compounds/FormLayout.svelte'
|
|
17
19
|
import Button from '../../primitives/Button.svelte'
|
|
18
20
|
import Input from '../../primitives/Input.svelte'
|
|
19
21
|
|
|
20
22
|
const {
|
|
21
23
|
loading = false,
|
|
24
|
+
columns = 1,
|
|
25
|
+
labelPosition = 'top',
|
|
26
|
+
labelWidth = '8rem',
|
|
22
27
|
disabled = false,
|
|
23
28
|
showTitle = false,
|
|
24
29
|
showDescription = false,
|
|
@@ -69,91 +74,91 @@
|
|
|
69
74
|
</script>
|
|
70
75
|
|
|
71
76
|
<form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<div class='fieldset'>
|
|
94
|
-
<legend class='fieldset-legend'>{uiM('forgot_password_email_label')}</legend>
|
|
95
|
-
<Input
|
|
96
|
-
id='forgot-email'
|
|
97
|
-
name='email'
|
|
98
|
-
type='email'
|
|
99
|
-
placeholder={uiM('forgot_password_email_placeholder')}
|
|
100
|
-
bind:value={email}
|
|
101
|
-
{disabled}
|
|
102
|
-
required
|
|
103
|
-
error={errors.email}
|
|
104
|
-
/>
|
|
105
|
-
</div>
|
|
106
|
-
{:else}
|
|
107
|
-
<div class='fieldset'>
|
|
108
|
-
<legend class='fieldset-legend'>{uiM('forgot_password_phone_label')}</legend>
|
|
109
|
-
<Input
|
|
110
|
-
id='forgot-phone'
|
|
111
|
-
name='phone'
|
|
112
|
-
type='tel'
|
|
113
|
-
placeholder={uiM('forgot_password_phone_placeholder')}
|
|
114
|
-
bind:value={phone}
|
|
115
|
-
{disabled}
|
|
116
|
-
required
|
|
117
|
-
error={errors.phone}
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
120
|
-
{/if}
|
|
121
|
-
|
|
122
|
-
<!-- 提示信息 -->
|
|
123
|
-
<p class='text-sm text-base-content/60'>
|
|
77
|
+
<FormLayout {columns} {labelPosition} {labelWidth}>
|
|
78
|
+
<!-- 标题 -->
|
|
79
|
+
{#if showTitle}
|
|
80
|
+
<h2 class='text-2xl font-semibold text-center mb-4'>{uiM('forgot_password_title')}</h2>
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
<!-- 描述 -->
|
|
84
|
+
{#if showDescription}
|
|
85
|
+
<p class='text-center text-base-content/60 text-sm mb-6'>
|
|
86
|
+
{uiM('forgot_password_desc')}
|
|
87
|
+
</p>
|
|
88
|
+
{/if}
|
|
89
|
+
|
|
90
|
+
<!-- 自定义头部 -->
|
|
91
|
+
{#if header}
|
|
92
|
+
<div class='forgot-password-form-header'>
|
|
93
|
+
{@render header()}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
96
|
+
|
|
97
|
+
<!-- 邮箱输入 -->
|
|
124
98
|
{#if mode === 'email'}
|
|
125
|
-
{uiM('
|
|
99
|
+
<FormField for='forgot-email' label={uiM('forgot_password_email_label')}>
|
|
100
|
+
<Input
|
|
101
|
+
id='forgot-email'
|
|
102
|
+
name='email'
|
|
103
|
+
type='email'
|
|
104
|
+
placeholder={uiM('forgot_password_email_placeholder')}
|
|
105
|
+
bind:value={email}
|
|
106
|
+
{disabled}
|
|
107
|
+
required
|
|
108
|
+
error={errors.email}
|
|
109
|
+
/>
|
|
110
|
+
</FormField>
|
|
126
111
|
{:else}
|
|
127
|
-
{uiM('
|
|
112
|
+
<FormField for='forgot-phone' label={uiM('forgot_password_phone_label')}>
|
|
113
|
+
<Input
|
|
114
|
+
id='forgot-phone'
|
|
115
|
+
name='phone'
|
|
116
|
+
type='tel'
|
|
117
|
+
placeholder={uiM('forgot_password_phone_placeholder')}
|
|
118
|
+
bind:value={phone}
|
|
119
|
+
{disabled}
|
|
120
|
+
required
|
|
121
|
+
error={errors.phone}
|
|
122
|
+
/>
|
|
123
|
+
</FormField>
|
|
124
|
+
{/if}
|
|
125
|
+
|
|
126
|
+
<!-- 提示信息 -->
|
|
127
|
+
<p class='text-sm text-base-content/60'>
|
|
128
|
+
{#if mode === 'email'}
|
|
129
|
+
{uiM('forgot_password_email_hint')}
|
|
130
|
+
{:else}
|
|
131
|
+
{uiM('forgot_password_phone_hint')}
|
|
132
|
+
{/if}
|
|
133
|
+
</p>
|
|
134
|
+
|
|
135
|
+
<!-- 通用错误 -->
|
|
136
|
+
{#if errors.general}
|
|
137
|
+
<Alert variant='error'>
|
|
138
|
+
{errors.general}
|
|
139
|
+
</Alert>
|
|
140
|
+
{/if}
|
|
141
|
+
|
|
142
|
+
<!-- 提交按钮 -->
|
|
143
|
+
<Button
|
|
144
|
+
type='submit'
|
|
145
|
+
variant='primary'
|
|
146
|
+
{loading}
|
|
147
|
+
disabled={loading || disabled}
|
|
148
|
+
class='w-full'
|
|
149
|
+
>
|
|
150
|
+
{submitText || uiM('forgot_password_submit')}
|
|
151
|
+
</Button>
|
|
152
|
+
|
|
153
|
+
<!-- 自定义底部 -->
|
|
154
|
+
{#if footer}
|
|
155
|
+
<div class='forgot-password-form-footer'>
|
|
156
|
+
{@render footer()}
|
|
157
|
+
</div>
|
|
158
|
+
{:else if showBackLink}
|
|
159
|
+
<div class='text-center mt-4 text-sm'>
|
|
160
|
+
<a href={loginUrl} class='link link-primary'>← {uiM('forgot_password_back')}</a>
|
|
161
|
+
</div>
|
|
128
162
|
{/if}
|
|
129
|
-
</
|
|
130
|
-
|
|
131
|
-
<!-- 通用错误 -->
|
|
132
|
-
{#if errors.general}
|
|
133
|
-
<Alert variant='error'>
|
|
134
|
-
{errors.general}
|
|
135
|
-
</Alert>
|
|
136
|
-
{/if}
|
|
137
|
-
|
|
138
|
-
<!-- 提交按钮 -->
|
|
139
|
-
<Button
|
|
140
|
-
type='submit'
|
|
141
|
-
variant='primary'
|
|
142
|
-
{loading}
|
|
143
|
-
disabled={loading || disabled}
|
|
144
|
-
class='w-full'
|
|
145
|
-
>
|
|
146
|
-
{submitText || uiM('forgot_password_submit')}
|
|
147
|
-
</Button>
|
|
148
|
-
|
|
149
|
-
<!-- 自定义底部 -->
|
|
150
|
-
{#if footer}
|
|
151
|
-
<div class='forgot-password-form-footer'>
|
|
152
|
-
{@render footer()}
|
|
153
|
-
</div>
|
|
154
|
-
{:else if showBackLink}
|
|
155
|
-
<div class='text-center mt-4 text-sm'>
|
|
156
|
-
<a href={loginUrl} class='link link-primary'>← {uiM('forgot_password_back')}</a>
|
|
157
|
-
</div>
|
|
158
|
-
{/if}
|
|
163
|
+
</FormLayout>
|
|
159
164
|
</form>
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
用户登录表单组件,搭配 primitives/compounds 使用。
|
|
4
4
|
-->
|
|
5
5
|
<script lang='ts'>
|
|
6
|
+
import type { HaiError } from '@h-ai/core'
|
|
6
7
|
import type { DataAttributes } from '../../../types.js'
|
|
7
8
|
import type { AuthLoginMethod, LoginFormData, LoginFormProps } from '../types.js'
|
|
8
9
|
import { uiM } from '../../../messages.js'
|
|
9
10
|
import { cn, getDataAttributes } from '../../../utils.js'
|
|
10
11
|
import Alert from '../../compounds/Alert.svelte'
|
|
12
|
+
import FormField from '../../compounds/FormField.svelte'
|
|
13
|
+
import FormLayout from '../../compounds/FormLayout.svelte'
|
|
11
14
|
import BareButton from '../../primitives/BareButton.svelte'
|
|
12
15
|
import Button from '../../primitives/Button.svelte'
|
|
13
16
|
import Checkbox from '../../primitives/Checkbox.svelte'
|
|
@@ -22,6 +25,9 @@
|
|
|
22
25
|
resendInterval = 60,
|
|
23
26
|
onsendotp,
|
|
24
27
|
loading = false,
|
|
28
|
+
columns = 1,
|
|
29
|
+
labelPosition = 'top',
|
|
30
|
+
labelWidth = '8rem',
|
|
25
31
|
disabled = false,
|
|
26
32
|
showTitle = false,
|
|
27
33
|
showRememberMe = true,
|
|
@@ -51,7 +57,8 @@
|
|
|
51
57
|
const isOtp = $derived(method === 'email' || method === 'sms')
|
|
52
58
|
let code = $state('')
|
|
53
59
|
let sending = $state(false)
|
|
54
|
-
let
|
|
60
|
+
let sendFailure = $state<Pick<HaiError, 'code' | 'message'> | 'network'>()
|
|
61
|
+
const sendError = $derived(sendFailure === 'network' ? uiM('auth_network_error') : sendFailure ? authErrorMessage(sendFailure) : '')
|
|
55
62
|
let resendAt = $state(0)
|
|
56
63
|
let now = $state(Date.now())
|
|
57
64
|
const remaining = $derived(Math.max(0, Math.ceil((resendAt - now) / 1000)))
|
|
@@ -79,11 +86,11 @@
|
|
|
79
86
|
if (!onsendotp || sending || remaining || !username.trim())
|
|
80
87
|
return
|
|
81
88
|
sending = true
|
|
82
|
-
|
|
89
|
+
sendFailure = undefined
|
|
83
90
|
try {
|
|
84
91
|
const result = await onsendotp(username.trim())
|
|
85
92
|
if (!result.success) {
|
|
86
|
-
|
|
93
|
+
sendFailure = result.error
|
|
87
94
|
}
|
|
88
95
|
else {
|
|
89
96
|
now = Date.now()
|
|
@@ -91,7 +98,7 @@
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
catch {
|
|
94
|
-
|
|
101
|
+
sendFailure = 'network'
|
|
95
102
|
}
|
|
96
103
|
finally { sending = false }
|
|
97
104
|
}
|
|
@@ -125,149 +132,144 @@
|
|
|
125
132
|
</script>
|
|
126
133
|
|
|
127
134
|
<form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<!-- 自定义头部 -->
|
|
134
|
-
{#if header}
|
|
135
|
-
<div class='login-form-header'>
|
|
136
|
-
{@render header()}
|
|
137
|
-
</div>
|
|
138
|
-
{/if}
|
|
135
|
+
<FormLayout {columns} {labelPosition} {labelWidth}>
|
|
136
|
+
<!-- 标题 -->
|
|
137
|
+
{#if showTitle}
|
|
138
|
+
<h2 class='text-xl font-semibold text-center mb-5'>{uiM('login_title')}</h2>
|
|
139
|
+
{/if}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
password = ''
|
|
147
|
-
code = ''
|
|
148
|
-
sendError = ''
|
|
149
|
-
}}>
|
|
150
|
-
{methodLabel(value)}
|
|
151
|
-
</button>
|
|
152
|
-
{/each}
|
|
153
|
-
</div>
|
|
154
|
-
{/if}
|
|
155
|
-
{#if !method}
|
|
156
|
-
<Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
|
|
157
|
-
{:else}
|
|
158
|
-
<div class='space-y-1.5'>
|
|
159
|
-
<label class='text-sm font-medium text-base-content/70' for='login-username'>
|
|
160
|
-
{method === 'email' ? uiM('register_email') : method === 'sms' ? uiM('register_phone') : uiM('login_username')}
|
|
161
|
-
</label>
|
|
162
|
-
<Input
|
|
163
|
-
id='login-username'
|
|
164
|
-
name='username'
|
|
165
|
-
type={method === 'email' ? 'email' : method === 'sms' ? 'tel' : 'text'}
|
|
166
|
-
placeholder={method === 'email' ? uiM('register_email_placeholder') : method === 'sms' ? uiM('register_phone_placeholder') : uiM('login_username_placeholder')}
|
|
167
|
-
bind:value={username}
|
|
168
|
-
autocomplete='username'
|
|
169
|
-
{disabled}
|
|
170
|
-
required
|
|
171
|
-
error={errors.username}
|
|
172
|
-
/>
|
|
173
|
-
</div>
|
|
141
|
+
<!-- 自定义头部 -->
|
|
142
|
+
{#if header}
|
|
143
|
+
<div class='login-form-header'>
|
|
144
|
+
{@render header()}
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
174
147
|
|
|
175
|
-
<!--
|
|
176
|
-
{#if
|
|
177
|
-
<div class='
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
148
|
+
<!-- 用户名 -->
|
|
149
|
+
{#if methods.length > 1}
|
|
150
|
+
<div class='grid grid-cols-2 gap-1 rounded-lg bg-base-200 p-1 sm:flex' aria-label={uiM('login_methods')}>
|
|
151
|
+
{#each methods as value (value)}
|
|
152
|
+
<button type='button' class={cn('flex-1 rounded-md px-2 py-2 text-sm transition-colors', method === value ? 'bg-base-100 font-medium shadow-sm' : 'text-base-content/60')} aria-pressed={method === value} disabled={loading || disabled} onclick={() => {
|
|
153
|
+
selectedMethod = value
|
|
154
|
+
password = ''
|
|
155
|
+
code = ''
|
|
156
|
+
sendFailure = undefined
|
|
157
|
+
}}>
|
|
158
|
+
{methodLabel(value)}
|
|
159
|
+
</button>
|
|
160
|
+
{/each}
|
|
186
161
|
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
{#if !method}
|
|
164
|
+
<Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
|
|
187
165
|
{:else}
|
|
188
|
-
<
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
bind:value={
|
|
195
|
-
|
|
196
|
-
autocomplete='current-password'
|
|
166
|
+
<FormField for='login-username' label={method === 'email' ? uiM('register_email') : method === 'sms' ? uiM('register_phone') : uiM('login_username')}>
|
|
167
|
+
<Input
|
|
168
|
+
id='login-username'
|
|
169
|
+
name='username'
|
|
170
|
+
type={method === 'email' ? 'email' : method === 'sms' ? 'tel' : 'text'}
|
|
171
|
+
placeholder={method === 'email' ? uiM('register_email_placeholder') : method === 'sms' ? uiM('register_phone_placeholder') : uiM('login_username_placeholder')}
|
|
172
|
+
bind:value={username}
|
|
173
|
+
autocomplete='username'
|
|
197
174
|
{disabled}
|
|
198
175
|
required
|
|
199
|
-
error={errors.
|
|
200
|
-
showStrength={false}
|
|
176
|
+
error={errors.username}
|
|
201
177
|
/>
|
|
202
|
-
</
|
|
203
|
-
{/if}
|
|
204
|
-
{/if}
|
|
178
|
+
</FormField>
|
|
205
179
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
180
|
+
<!-- 密码 -->
|
|
181
|
+
{#if isOtp}
|
|
182
|
+
<FormField for='login-code' label={uiM('login_code')}>
|
|
183
|
+
<div class='flex items-start gap-2'>
|
|
184
|
+
<Input id='login-code' name='code' bind:value={code} autocomplete='one-time-code' inputmode='numeric' maxlength={otpLength} pattern={`[0-9]{${otpLength}}`} placeholder={uiM('login_code_placeholder', { length: otpLength })} required {disabled} />
|
|
185
|
+
<Button type='button' variant='outline' loading={sending} disabled={loading || disabled || !username.trim() || remaining > 0 || !onsendotp} onclick={sendOtp}>
|
|
186
|
+
{remaining ? uiM('login_resend_wait', { seconds: remaining }) : uiM('login_send_code')}
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
{#if sendError}<p role='alert' class='text-sm text-error'>{sendError}</p>{/if}
|
|
190
|
+
</FormField>
|
|
216
191
|
{:else}
|
|
217
|
-
<
|
|
192
|
+
<FormField for='login-password' label={uiM('login_password')}>
|
|
193
|
+
<PasswordInput
|
|
194
|
+
id='login-password'
|
|
195
|
+
bind:value={password}
|
|
196
|
+
placeholder={uiM('login_password_placeholder')}
|
|
197
|
+
autocomplete='current-password'
|
|
198
|
+
{disabled}
|
|
199
|
+
required
|
|
200
|
+
error={errors.password}
|
|
201
|
+
showStrength={false}
|
|
202
|
+
/>
|
|
203
|
+
</FormField>
|
|
218
204
|
{/if}
|
|
205
|
+
{/if}
|
|
219
206
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
|
|
207
|
+
<!-- 记住我 & 忘记密码 -->
|
|
208
|
+
{#if method === 'password' && (showRememberMe || showForgotPassword)}
|
|
209
|
+
<div class='flex items-center justify-between'>
|
|
210
|
+
{#if showRememberMe}
|
|
211
|
+
<Checkbox
|
|
212
|
+
bind:checked={rememberMe}
|
|
213
|
+
label={uiM('login_remember_me')}
|
|
214
|
+
{disabled}
|
|
215
|
+
size='sm'
|
|
216
|
+
/>
|
|
229
217
|
{:else}
|
|
230
|
-
<
|
|
231
|
-
{uiM('login_forgot_password')}
|
|
232
|
-
</a>
|
|
218
|
+
<div></div>
|
|
233
219
|
{/if}
|
|
234
|
-
{/if}
|
|
235
|
-
</div>
|
|
236
|
-
{/if}
|
|
237
220
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
221
|
+
{#if showForgotPassword}
|
|
222
|
+
{#if onforgotpassword}
|
|
223
|
+
<BareButton
|
|
224
|
+
type='button'
|
|
225
|
+
class='link link-primary text-sm'
|
|
226
|
+
onclick={handleForgotPassword}
|
|
227
|
+
>
|
|
228
|
+
{uiM('login_forgot_password')}
|
|
229
|
+
</BareButton>
|
|
230
|
+
{:else}
|
|
231
|
+
<a href={forgotPasswordUrl} class='link link-primary text-sm'>
|
|
232
|
+
{uiM('login_forgot_password')}
|
|
233
|
+
</a>
|
|
234
|
+
{/if}
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
{/if}
|
|
238
|
+
|
|
239
|
+
<!-- 通用错误 -->
|
|
240
|
+
{#if extraFields}
|
|
241
|
+
<div class='login-form-extra-fields'>{@render extraFields()}</div>
|
|
242
|
+
{/if}
|
|
243
|
+
{#if errors.general}
|
|
244
|
+
<Alert variant='error'>
|
|
245
|
+
{errors.general}
|
|
246
|
+
</Alert>
|
|
247
|
+
{/if}
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
<!-- 提交按钮 -->
|
|
250
|
+
<Button
|
|
251
|
+
type='submit'
|
|
252
|
+
variant='primary'
|
|
253
|
+
{loading}
|
|
254
|
+
disabled={loading || disabled || !method}
|
|
255
|
+
class='w-full'
|
|
256
|
+
>
|
|
257
|
+
{submitText || uiM('login_submit')}
|
|
258
|
+
</Button>
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
<!-- 协议提示 -->
|
|
261
|
+
<AgreementNotice {agreements} />
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
263
|
+
<!-- 自定义底部 -->
|
|
264
|
+
{#if footer}
|
|
265
|
+
<div class='login-form-footer'>
|
|
266
|
+
{@render footer()}
|
|
267
|
+
</div>
|
|
268
|
+
{:else if showRegisterLink}
|
|
269
|
+
<div class='text-center mt-4 text-sm text-base-content/60'>
|
|
270
|
+
<span>{uiM('login_no_account')}</span>
|
|
271
|
+
<a href={registerUrl} class='link link-primary ml-1'>{uiM('login_register_now')}</a>
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</FormLayout>
|
|
273
275
|
</form>
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
内置多语言支持,自动跟随全局 locale。
|
|
5
5
|
-->
|
|
6
6
|
<script lang='ts'>
|
|
7
|
-
import type { DataAttributes, Size } from '../../../types.js'
|
|
7
|
+
import type { DataAttributes, FormText, Size } from '../../../types.js'
|
|
8
|
+
import { getFormFieldContext } from '../../../internal/form-context.js'
|
|
9
|
+
import { createControlValidation, resolveFormText } from '../../../internal/form-validation.svelte.js'
|
|
8
10
|
import { uiM } from '../../../messages.js'
|
|
9
11
|
import { cn, getDataAttributes } from '../../../utils.js'
|
|
10
12
|
import { getFormControlSizeClasses } from '../../control-size.js'
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
/** 是否必填 */
|
|
36
38
|
required?: boolean
|
|
37
39
|
/** 错误消息 */
|
|
38
|
-
error?:
|
|
40
|
+
error?: FormText
|
|
39
41
|
/** 是否显示切换按钮 */
|
|
40
42
|
showToggle?: boolean
|
|
41
43
|
/** 是否显示密码强度 */
|
|
@@ -76,6 +78,16 @@
|
|
|
76
78
|
}: Props & DataAttributes = $props()
|
|
77
79
|
|
|
78
80
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
81
|
+
const field = getFormFieldContext()
|
|
82
|
+
const validation = createControlValidation(undefined, () => [value, required, readonly, disabled])
|
|
83
|
+
const uniqueId = $props.id()
|
|
84
|
+
const messageId = `${uniqueId}-error`
|
|
85
|
+
const localError = $derived(resolveFormText(error) || validation.message)
|
|
86
|
+
const effectiveError = $derived(field?.error || localError)
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (inputRef)
|
|
89
|
+
inputRef.setCustomValidity(validation.message)
|
|
90
|
+
})
|
|
79
91
|
let showPassword = $state(false)
|
|
80
92
|
const sizeClasses = $derived(getFormControlSizeClasses(size))
|
|
81
93
|
|
|
@@ -124,6 +136,7 @@
|
|
|
124
136
|
})
|
|
125
137
|
|
|
126
138
|
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
139
|
+
validation.input(e.currentTarget)
|
|
127
140
|
value = e.currentTarget.value
|
|
128
141
|
oninput?.(e)
|
|
129
142
|
}
|
|
@@ -142,7 +155,7 @@
|
|
|
142
155
|
<div class={cn(
|
|
143
156
|
'flex items-center w-full rounded-lg border bg-base-100',
|
|
144
157
|
sizeClasses.height,
|
|
145
|
-
|
|
158
|
+
effectiveError
|
|
146
159
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
147
160
|
: 'border-base-content/15 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
148
161
|
'transition-[border-color,box-shadow] duration-150',
|
|
@@ -152,7 +165,7 @@
|
|
|
152
165
|
<BareInput
|
|
153
166
|
type={showPassword ? 'text' : 'password'}
|
|
154
167
|
{id}
|
|
155
|
-
class={cn('flex-1 h-full bg-transparent border-none outline-none placeholder:text-base-content/35', sizeClasses.spacing)}
|
|
168
|
+
class={cn('flex-1 min-w-0 h-full bg-transparent border-none outline-none placeholder:text-base-content/35', sizeClasses.spacing)}
|
|
156
169
|
bind:value
|
|
157
170
|
bind:inputRef={inputRef}
|
|
158
171
|
{placeholder}
|
|
@@ -160,9 +173,14 @@
|
|
|
160
173
|
{disabled}
|
|
161
174
|
{readonly}
|
|
162
175
|
{required}
|
|
176
|
+
ariaInvalid={effectiveError ? true : undefined}
|
|
177
|
+
ariaDescribedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
163
178
|
oninput={handleInput}
|
|
164
179
|
onchange={handleChange}
|
|
165
|
-
oninvalid={
|
|
180
|
+
oninvalid={(e) => {
|
|
181
|
+
validation.invalid(e.currentTarget)
|
|
182
|
+
oninvalid?.(e)
|
|
183
|
+
}}
|
|
166
184
|
/>
|
|
167
185
|
|
|
168
186
|
{#if showToggle}
|
|
@@ -197,7 +215,7 @@
|
|
|
197
215
|
</div>
|
|
198
216
|
{/if}
|
|
199
217
|
|
|
200
|
-
{#if error}
|
|
201
|
-
<p class='mt-1.5 text-xs text-error
|
|
218
|
+
{#if localError && !field?.error}
|
|
219
|
+
<p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
|
|
202
220
|
{/if}
|
|
203
221
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DataAttributes, Size } from '../../../types.js';
|
|
1
|
+
import type { DataAttributes, FormText, Size } from '../../../types.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** 元素 ID */
|
|
4
4
|
id?: string;
|
|
@@ -17,7 +17,7 @@ interface Props {
|
|
|
17
17
|
/** 是否必填 */
|
|
18
18
|
required?: boolean;
|
|
19
19
|
/** 错误消息 */
|
|
20
|
-
error?:
|
|
20
|
+
error?: FormText;
|
|
21
21
|
/** 是否显示切换按钮 */
|
|
22
22
|
showToggle?: boolean;
|
|
23
23
|
/** 是否显示密码强度 */
|