@h-ai/ui 0.1.0-alpha.62 → 0.1.0-alpha.64
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 +37 -7
- package/README.ja.md +37 -7
- package/README.md +37 -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/Drawer.svelte +5 -1
- 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/Modal.svelte +5 -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 +23 -11
- package/dist/components/primitives/Select.svelte +90 -18
- package/dist/components/primitives/Textarea.svelte +28 -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 +158 -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 +66 -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 +30 -10
- package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +3 -3
- 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 +14 -7
- 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 +12 -10
- package/dist/i18n.svelte.d.ts +2 -1
- package/dist/i18n.svelte.js +2 -1
- package/dist/internal/form-context.d.ts +18 -0
- package/dist/internal/form-context.js +13 -0
- package/dist/internal/form-validation.svelte.d.ts +16 -0
- package/dist/internal/form-validation.svelte.js +86 -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
|
@@ -1,60 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { DataAttributes } from '../../../types.js';
|
|
3
|
+
import type { CrudFieldDef } from './crud-controller.svelte.js';
|
|
3
4
|
import type { CrudDensity, CrudFormConfig, CrudPaginationConfig, CrudToolbarStyle } from './crud-types.js';
|
|
4
5
|
import type { NavAdapter } from './nav-adapter.js';
|
|
5
|
-
|
|
6
|
-
type FieldDef = {
|
|
7
|
-
/** 字段稳定标识,同时对应数据记录中的属性名。 */
|
|
8
|
-
id: string;
|
|
9
|
-
/** 字段显示文案,允许通过函数延迟读取当前语言。 */
|
|
10
|
-
label: string | (() => string);
|
|
11
|
-
/** 字段控件类型,最终由表单或筛选器解释。 */
|
|
12
|
-
type: string;
|
|
13
|
-
/** 是否出现在列表中;未设置时默认显示。 */
|
|
14
|
-
inList?: boolean;
|
|
15
|
-
/** 是否出现在筛选器中;只有显式为 true 时显示。 */
|
|
16
|
-
filterable?: boolean;
|
|
17
|
-
/** 编辑时是否禁止修改。 */
|
|
18
|
-
readonly?: boolean;
|
|
19
|
-
/** 是否出现在新建表单中;未设置时默认显示。 */
|
|
20
|
-
inCreate?: boolean;
|
|
21
|
-
/** 是否出现在编辑表单中;未设置时默认显示。 */
|
|
22
|
-
inEdit?: boolean;
|
|
23
|
-
/** 是否出现在详情面板中;未设置时默认显示。 */
|
|
24
|
-
inDetail?: boolean;
|
|
25
|
-
/** 选择类字段的静态选项或动态选项工厂。 */
|
|
26
|
-
options?: Array<{
|
|
27
|
-
label: string;
|
|
28
|
-
value: string | number | boolean;
|
|
29
|
-
}> | (() => Array<{
|
|
30
|
-
label: string;
|
|
31
|
-
value: string | number | boolean;
|
|
32
|
-
}>);
|
|
33
|
-
/** 表单校验规则。 */
|
|
34
|
-
validation?: {
|
|
35
|
-
required?: boolean;
|
|
36
|
-
min?: number;
|
|
37
|
-
max?: number;
|
|
38
|
-
pattern?: string;
|
|
39
|
-
message?: string;
|
|
40
|
-
};
|
|
41
|
-
/** 列宽 CSS 值。 */
|
|
42
|
-
width?: string;
|
|
43
|
-
/** 单元格对齐方式。 */
|
|
44
|
-
align?: 'left' | 'center' | 'right';
|
|
45
|
-
/** 列表单元格的自定义渲染函数。 */
|
|
46
|
-
render?: (value: unknown, item: Record<string, unknown>) => string;
|
|
47
|
-
/** 输入控件占位文案。 */
|
|
48
|
-
placeholder?: string | (() => string);
|
|
49
|
-
/** 日期范围起始值 key。 */
|
|
50
|
-
startKey?: string;
|
|
51
|
-
/** 日期范围结束值 key。 */
|
|
52
|
-
endKey?: string;
|
|
53
|
-
/** 新建记录时使用的默认值。 */
|
|
54
|
-
defaultValue?: unknown;
|
|
55
|
-
/** 字段排序权重,数值越小越靠前。 */
|
|
56
|
-
order?: number;
|
|
57
|
-
};
|
|
6
|
+
type FieldDef = CrudFieldDef;
|
|
58
7
|
/** CrudPage 使用的异步资源操作集合。 */
|
|
59
8
|
type CrudApi = {
|
|
60
9
|
/** 获取当前分页、筛选和排序结果。 */
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @module crud-controller
|
|
14
14
|
*/
|
|
15
15
|
import type { PaginatedResult } from '@h-ai/core';
|
|
16
|
+
import type { FormColSpan } from '../../../types.js';
|
|
16
17
|
import type { NavAdapter } from './nav-adapter.js';
|
|
17
18
|
/** 字段展示形式;与 `CrudEditPanel` / `CrudFilterBar` 支持的类型对齐,保留 `string` 以兼容 `@h-ai/kit`。 */
|
|
18
19
|
export type CrudFieldType = 'text' | 'string' | 'textarea' | 'select' | 'radio' | 'multi-select' | 'boolean' | 'checkbox' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime' | 'date-range' | 'custom';
|
|
@@ -51,8 +52,10 @@ export interface CrudFieldDef {
|
|
|
51
52
|
min?: number;
|
|
52
53
|
max?: number;
|
|
53
54
|
pattern?: string;
|
|
54
|
-
message?:
|
|
55
|
+
message?: CrudText;
|
|
55
56
|
};
|
|
57
|
+
/** 表单占用列数,默认 auto;多行文本、多选等自动占两列。 */
|
|
58
|
+
colSpan?: FormColSpan;
|
|
56
59
|
/** 列宽(CSS)。 */
|
|
57
60
|
width?: string;
|
|
58
61
|
/** 列对齐。 */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** CRUD 客户端字段校验;仅返回消息键,避免错误文案锁定在提交时的语言。 */
|
|
2
|
+
import type { FormValidationIssue } from '../../../internal/form-validation.svelte.js';
|
|
3
|
+
import type { CrudFieldDef } from './crud-controller.svelte.js';
|
|
4
|
+
export declare function getCrudValidationIssue(field: CrudFieldDef, value: unknown): FormValidationIssue | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function getCrudValidationIssue(field, value) {
|
|
2
|
+
const rules = field.validation;
|
|
3
|
+
const empty = value === undefined || value === null || (typeof value === 'string' && !value.trim()) || (Array.isArray(value) && value.length === 0);
|
|
4
|
+
if (rules?.required && (empty || (value === false && (field.type === 'boolean' || field.type === 'checkbox'))))
|
|
5
|
+
return { key: 'form_required' };
|
|
6
|
+
if (empty)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (field.type === 'number') {
|
|
9
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
10
|
+
if (!Number.isFinite(number))
|
|
11
|
+
return { key: 'form_number' };
|
|
12
|
+
if (rules?.min !== undefined && number < rules.min)
|
|
13
|
+
return { key: 'form_min', params: { min: rules.min } };
|
|
14
|
+
if (rules?.max !== undefined && number > rules.max)
|
|
15
|
+
return { key: 'form_max', params: { max: rules.max } };
|
|
16
|
+
}
|
|
17
|
+
else if (typeof value === 'string') {
|
|
18
|
+
if (rules?.min !== undefined && value.length < rules.min)
|
|
19
|
+
return { key: 'form_min_length', params: { min: rules.min } };
|
|
20
|
+
if (rules?.max !== undefined && value.length > rules.max)
|
|
21
|
+
return { key: 'form_max_length', params: { max: rules.max } };
|
|
22
|
+
}
|
|
23
|
+
if (field.type === 'email' && !/^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z\d](?:[a-z\d-]*[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]*[a-z\d])?)*$/i.test(String(value)))
|
|
24
|
+
return { key: 'form_email' };
|
|
25
|
+
if (field.type === 'url') {
|
|
26
|
+
if (!URL.canParse(String(value)))
|
|
27
|
+
return { key: 'form_url' };
|
|
28
|
+
}
|
|
29
|
+
if (rules?.pattern) {
|
|
30
|
+
try {
|
|
31
|
+
if (!new RegExp(`^(?:${rules.pattern})$`, 'v').test(String(value)))
|
|
32
|
+
return { key: 'form_pattern' };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { key: 'form_pattern' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* @module crud-types
|
|
6
6
|
*/
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
|
-
import type { DataAttributes, Size } from '../../../types.js';
|
|
8
|
+
import type { DataAttributes, FormLayoutOptions, Size } from '../../../types.js';
|
|
9
|
+
import type { CrudFieldDef } from './crud-controller.svelte.js';
|
|
9
10
|
/** CrudPage 权限控制 */
|
|
10
11
|
export interface CrudPermissions {
|
|
11
12
|
/** 允许新建 */
|
|
@@ -20,7 +21,7 @@ export type CrudFormVariant = 'drawer' | 'modal';
|
|
|
20
21
|
/** 列表显示密度 */
|
|
21
22
|
export type CrudDensity = 'normal' | 'compact';
|
|
22
23
|
/** 新建/编辑表单展示配置 */
|
|
23
|
-
export interface CrudFormConfig {
|
|
24
|
+
export interface CrudFormConfig extends FormLayoutOptions {
|
|
24
25
|
/** 展示形式:'drawer'(抽屉,默认)或 'modal'(弹出窗口) */
|
|
25
26
|
variant?: CrudFormVariant;
|
|
26
27
|
/** 抽屉尺寸预设(variant='drawer',默认 '2xl') */
|
|
@@ -69,41 +70,7 @@ export interface CrudPageProps<T = Record<string, unknown>> extends DataAttribut
|
|
|
69
70
|
name: string;
|
|
70
71
|
label: string | (() => string);
|
|
71
72
|
keyField?: string;
|
|
72
|
-
fields:
|
|
73
|
-
id: string;
|
|
74
|
-
label: string | (() => string);
|
|
75
|
-
type: string;
|
|
76
|
-
inList?: boolean;
|
|
77
|
-
filterable?: boolean;
|
|
78
|
-
readonly?: boolean;
|
|
79
|
-
inCreate?: boolean;
|
|
80
|
-
inEdit?: boolean;
|
|
81
|
-
inDetail?: boolean;
|
|
82
|
-
options?: Array<{
|
|
83
|
-
label: string;
|
|
84
|
-
value: string | number | boolean;
|
|
85
|
-
}> | (() => Array<{
|
|
86
|
-
label: string;
|
|
87
|
-
value: string | number | boolean;
|
|
88
|
-
}>);
|
|
89
|
-
validation?: {
|
|
90
|
-
required?: boolean;
|
|
91
|
-
min?: number;
|
|
92
|
-
max?: number;
|
|
93
|
-
pattern?: string;
|
|
94
|
-
message?: string;
|
|
95
|
-
};
|
|
96
|
-
width?: string;
|
|
97
|
-
align?: 'left' | 'center' | 'right';
|
|
98
|
-
render?: (value: unknown, item: Record<string, unknown>) => string;
|
|
99
|
-
placeholder?: string | (() => string);
|
|
100
|
-
/** 日期范围起始值 key;默认 `${id}Start`。 */
|
|
101
|
-
startKey?: string;
|
|
102
|
-
/** 日期范围结束值 key;默认 `${id}End`。 */
|
|
103
|
-
endKey?: string;
|
|
104
|
-
defaultValue?: unknown;
|
|
105
|
-
order?: number;
|
|
106
|
-
}>;
|
|
73
|
+
fields: CrudFieldDef[];
|
|
107
74
|
defaultPageSize?: number;
|
|
108
75
|
searchable?: boolean;
|
|
109
76
|
searchPlaceholder?: string | (() => string);
|
|
@@ -120,11 +87,11 @@ export interface CrudPageProps<T = Record<string, unknown>> extends DataAttribut
|
|
|
120
87
|
remove?: (id: string) => Promise<void>;
|
|
121
88
|
};
|
|
122
89
|
};
|
|
123
|
-
getListFields: () =>
|
|
124
|
-
getFilterFields: () =>
|
|
125
|
-
getCreateFields: () =>
|
|
126
|
-
getEditFields: () =>
|
|
127
|
-
getDetailFields: () =>
|
|
90
|
+
getListFields: () => CrudFieldDef[];
|
|
91
|
+
getFilterFields: () => CrudFieldDef[];
|
|
92
|
+
getCreateFields: () => CrudFieldDef[];
|
|
93
|
+
getEditFields: () => CrudFieldDef[];
|
|
94
|
+
getDetailFields: () => CrudFieldDef[];
|
|
128
95
|
toTableColumns: () => Array<{
|
|
129
96
|
key: string;
|
|
130
97
|
label: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<!-- 配置驱动认证场景;网络、通知及 Token 存储由 actions 注入。 -->
|
|
2
2
|
<script lang='ts'>
|
|
3
|
-
import type { HaiResult } from '@h-ai/core'
|
|
3
|
+
import type { HaiError, HaiResult } from '@h-ai/core'
|
|
4
|
+
import type { UIMessageKey } from '../../../messages.js'
|
|
4
5
|
import type { AuthLoginMethod, AuthPanelMode, AuthPanelProps } from '../types.js'
|
|
5
6
|
import { onMount, untrack } from 'svelte'
|
|
6
7
|
import { uiM } from '../../../messages.js'
|
|
@@ -16,12 +17,14 @@
|
|
|
16
17
|
import RegisterForm from './RegisterForm.svelte'
|
|
17
18
|
import ResetPasswordForm from './ResetPasswordForm.svelte'
|
|
18
19
|
|
|
19
|
-
const { config, actions, initialMode = 'login', resetToken = '', loginUrl, registerUrl, forgotPasswordUrl, onauthenticated, error = '', onretry, loginHeader, loginFields, loginFooter, registerHeader, registerFields, registerFooter, ...restProps }: AuthPanelProps = $props()
|
|
20
|
+
const { config, actions, columns = 1, labelPosition = 'top', labelWidth = '8rem', initialMode = 'login', resetToken = '', loginUrl, registerUrl, forgotPasswordUrl, onauthenticated, error = '', onretry, loginHeader, loginFields, loginFooter, registerHeader, registerFields, registerFooter, ...restProps }: AuthPanelProps = $props()
|
|
20
21
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
21
22
|
let mode = $state<AuthPanelMode>(untrack(() => initialMode))
|
|
22
23
|
let loading = $state(false)
|
|
23
|
-
let
|
|
24
|
-
let
|
|
24
|
+
let actionError = $state<Pick<HaiError, 'code' | 'message'>>()
|
|
25
|
+
let networkFailed = $state(false)
|
|
26
|
+
const errors = $derived<Record<string, string>>(actionError ? { general: authErrorMessage(actionError) } : networkFailed ? { general: uiM('auth_network_error') } : {})
|
|
27
|
+
let notice = $state<UIMessageKey | ''>('')
|
|
25
28
|
// SSR 表单先显示但不可编辑;客户端绑定事件后开放输入,避免接管时丢失早到的输入。
|
|
26
29
|
let interactive = $state(false)
|
|
27
30
|
onMount(() => {
|
|
@@ -33,13 +36,15 @@
|
|
|
33
36
|
|
|
34
37
|
function switchMode(next: AuthPanelMode) {
|
|
35
38
|
mode = next
|
|
36
|
-
|
|
39
|
+
actionError = undefined
|
|
40
|
+
networkFailed = false
|
|
37
41
|
notice = ''
|
|
38
42
|
}
|
|
39
|
-
async function run(operation: () => Promise<HaiResult<unknown>>, authenticate = false, successNotice = '') {
|
|
43
|
+
async function run(operation: () => Promise<HaiResult<unknown>>, authenticate = false, successNotice: UIMessageKey | '' = '') {
|
|
40
44
|
if (loading)
|
|
41
45
|
return
|
|
42
|
-
|
|
46
|
+
actionError = undefined
|
|
47
|
+
networkFailed = false
|
|
43
48
|
notice = ''
|
|
44
49
|
loading = true
|
|
45
50
|
try {
|
|
@@ -48,10 +53,10 @@
|
|
|
48
53
|
if (mode === 'register' && result.error.code === 'hai:iam:004') {
|
|
49
54
|
// 默认禁用的新账户已完成注册,提示等待启用,避免用户重复注册。
|
|
50
55
|
switchMode('login')
|
|
51
|
-
notice =
|
|
56
|
+
notice = 'auth_registration_pending'
|
|
52
57
|
}
|
|
53
58
|
else {
|
|
54
|
-
|
|
59
|
+
actionError = result.error
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
else if (authenticate) {
|
|
@@ -61,7 +66,7 @@
|
|
|
61
66
|
notice = successNotice
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
|
-
catch {
|
|
69
|
+
catch { networkFailed = true }
|
|
65
70
|
finally { loading = false }
|
|
66
71
|
}
|
|
67
72
|
</script>
|
|
@@ -80,44 +85,47 @@
|
|
|
80
85
|
|
|
81
86
|
<AuthShell {...dataAttributes} {...config?.ui} title={config?.ui.title || title} brandTitle={config?.ui.brandTitle || uiM('auth_brand')}>
|
|
82
87
|
<fieldset class='m-0 min-w-0 border-0 p-0' disabled={!interactive}>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{#if
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{#if notice}<Alert variant='success' class='mb-4'>{notice}</Alert>{/if}
|
|
93
|
-
{#if mode === 'login'}
|
|
94
|
-
<LoginForm {methods} {loading} {errors} header={loginHeader} extraFields={loginFields} showRememberMe={false} showForgotPassword={config.passwordReset.enabled} {forgotPasswordUrl}
|
|
95
|
-
onforgotpassword={forgotPasswordUrl ? undefined : () => switchMode('forgot')}
|
|
96
|
-
otpLength={config.otp.length} resendInterval={config.otp.resendInterval} onsendotp={actions.sendOtp}
|
|
97
|
-
agreements={config.agreements.showOnLogin ? config.agreements : undefined}
|
|
98
|
-
onsubmit={data => run(() => actions.login({ identifier: data.username, password: data.password, method: data.method, code: data.code }), true)}>
|
|
99
|
-
{#snippet footer()}
|
|
100
|
-
{#if loginFooter}{@render loginFooter()}{/if}
|
|
101
|
-
{#if config?.register.enabled}<div class='text-center text-sm'>{uiM('login_no_account')} {@render link('register', uiM('login_register_now'), registerUrl)}</div>{/if}
|
|
102
|
-
{/snippet}
|
|
103
|
-
</LoginForm>
|
|
104
|
-
{:else if mode === 'register'}
|
|
105
|
-
<RegisterForm {loading} {errors} header={registerHeader} extraFields={registerFields} fields={config.passwordReset.mode === 'phone' ? ['username', 'phone', 'password'] : ['username', 'email', 'password']} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
|
|
106
|
-
agreements={config.agreements.showOnRegister ? config.agreements : undefined} onsubmit={data => run(() => actions.register({ username: data.username || '', password: data.password, email: data.email, phone: data.phone, displayName: data.nickname }), true)}>
|
|
107
|
-
{#snippet footer()}
|
|
108
|
-
{#if registerFooter}{@render registerFooter()}{/if}
|
|
109
|
-
<div class='text-center'>{@render link('login', uiM('register_login_now'), loginUrl)}</div>
|
|
110
|
-
{/snippet}
|
|
111
|
-
</RegisterForm>
|
|
112
|
-
{:else if mode === 'forgot'}
|
|
113
|
-
<ForgotPasswordForm {loading} {errors} mode={config.passwordReset.mode ?? 'email'} onsubmit={data => run(() => actions.requestPasswordReset(data.email || data.phone || ''), false, uiM('auth_reset_sent'))}>
|
|
114
|
-
{#snippet footer()}<div class='flex justify-between gap-3'>{@render link('login', uiM('login_submit'), loginUrl)}{@render link('reset', uiM('auth_have_token'))}</div>{/snippet}
|
|
115
|
-
</ForgotPasswordForm>
|
|
116
|
-
{:else}
|
|
117
|
-
<ResetPasswordForm {loading} {errors} showCode={!resetToken} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
|
|
118
|
-
onsubmit={data => run(() => actions.confirmPasswordReset(resetToken || data.code || '', data.newPassword), false, uiM('auth_reset_done'))} />
|
|
88
|
+
<!-- 固定内容包装节点,避免 fieldset 插入通知时丢失容器查询子树的布局。 -->
|
|
89
|
+
<div class='min-w-0'>
|
|
90
|
+
{#if error}
|
|
91
|
+
<Alert variant='error'>{error}</Alert>
|
|
92
|
+
{#if onretry}<Button class='mt-4 w-full' onclick={onretry}>{uiM('crud_retry')}</Button>{/if}
|
|
93
|
+
{:else if !config}
|
|
94
|
+
<div class='flex justify-center' role='status'><Spinner /></div>
|
|
95
|
+
{:else if !available}
|
|
96
|
+
<Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
|
|
119
97
|
<div class='mt-4 text-center'>{@render link('login', uiM('login_submit'), loginUrl)}</div>
|
|
98
|
+
{:else}
|
|
99
|
+
{#if notice}<Alert variant='success' class='mb-4'>{uiM(notice)}</Alert>{/if}
|
|
100
|
+
{#if mode === 'login'}
|
|
101
|
+
<LoginForm {columns} {labelPosition} {labelWidth} {methods} {loading} {errors} header={loginHeader} extraFields={loginFields} showRememberMe={false} showForgotPassword={config.passwordReset.enabled} {forgotPasswordUrl}
|
|
102
|
+
onforgotpassword={forgotPasswordUrl ? undefined : () => switchMode('forgot')}
|
|
103
|
+
otpLength={config.otp.length} resendInterval={config.otp.resendInterval} onsendotp={actions.sendOtp}
|
|
104
|
+
agreements={config.agreements.showOnLogin ? config.agreements : undefined}
|
|
105
|
+
onsubmit={data => run(() => actions.login({ identifier: data.username, password: data.password, method: data.method, code: data.code }), true)}>
|
|
106
|
+
{#snippet footer()}
|
|
107
|
+
{#if loginFooter}{@render loginFooter()}{/if}
|
|
108
|
+
{#if config?.register.enabled}<div class='text-center text-sm'>{uiM('login_no_account')} {@render link('register', uiM('login_register_now'), registerUrl)}</div>{/if}
|
|
109
|
+
{/snippet}
|
|
110
|
+
</LoginForm>
|
|
111
|
+
{:else if mode === 'register'}
|
|
112
|
+
<RegisterForm {columns} {labelPosition} {labelWidth} {loading} {errors} header={registerHeader} extraFields={registerFields} fields={config.passwordReset.mode === 'phone' ? ['username', 'phone', 'password'] : ['username', 'email', 'password']} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
|
|
113
|
+
agreements={config.agreements.showOnRegister ? config.agreements : undefined} onsubmit={data => run(() => actions.register({ username: data.username || '', password: data.password, email: data.email, phone: data.phone, displayName: data.nickname }), true)}>
|
|
114
|
+
{#snippet footer()}
|
|
115
|
+
{#if registerFooter}{@render registerFooter()}{/if}
|
|
116
|
+
<div class='text-center'>{@render link('login', uiM('register_login_now'), loginUrl)}</div>
|
|
117
|
+
{/snippet}
|
|
118
|
+
</RegisterForm>
|
|
119
|
+
{:else if mode === 'forgot'}
|
|
120
|
+
<ForgotPasswordForm {columns} {labelPosition} {labelWidth} {loading} {errors} mode={config.passwordReset.mode ?? 'email'} onsubmit={data => run(() => actions.requestPasswordReset(data.email || data.phone || ''), false, 'auth_reset_sent')}>
|
|
121
|
+
{#snippet footer()}<div class='flex justify-between gap-3'>{@render link('login', uiM('login_submit'), loginUrl)}{@render link('reset', uiM('auth_have_token'))}</div>{/snippet}
|
|
122
|
+
</ForgotPasswordForm>
|
|
123
|
+
{:else}
|
|
124
|
+
<ResetPasswordForm {columns} {labelPosition} {labelWidth} {loading} {errors} showCode={!resetToken} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
|
|
125
|
+
onsubmit={data => run(() => actions.confirmPasswordReset(resetToken || data.code || '', data.newPassword), false, 'auth_reset_done')} />
|
|
126
|
+
<div class='mt-4 text-center'>{@render link('login', uiM('login_submit'), loginUrl)}</div>
|
|
127
|
+
{/if}
|
|
120
128
|
{/if}
|
|
121
|
-
|
|
129
|
+
</div>
|
|
122
130
|
</fieldset>
|
|
123
131
|
</AuthShell>
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
import { uiM } from '../../../messages.js'
|
|
9
9
|
import { cn, getDataAttributes } from '../../../utils.js'
|
|
10
10
|
import Alert from '../../compounds/Alert.svelte'
|
|
11
|
+
import FormField from '../../compounds/FormField.svelte'
|
|
12
|
+
import FormLayout from '../../compounds/FormLayout.svelte'
|
|
11
13
|
import Button from '../../primitives/Button.svelte'
|
|
12
14
|
import { arePasswordsEqual, passwordPolicyError, passwordPolicyHint } from './password-utils.js'
|
|
13
15
|
import PasswordInput from './PasswordInput.svelte'
|
|
14
16
|
|
|
15
17
|
const {
|
|
16
18
|
loading = false,
|
|
19
|
+
columns = 1,
|
|
20
|
+
labelPosition = 'top',
|
|
21
|
+
labelWidth = '8rem',
|
|
17
22
|
disabled = false,
|
|
18
23
|
requireOldPassword = true,
|
|
19
24
|
showPasswordStrength = true,
|
|
@@ -73,71 +78,73 @@
|
|
|
73
78
|
</script>
|
|
74
79
|
|
|
75
80
|
<form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
<FormLayout {columns} {labelPosition} {labelWidth}>
|
|
82
|
+
{#if requireOldPassword}
|
|
83
|
+
<FormField for='old-password' label={uiM('change_password_old')}>
|
|
84
|
+
<PasswordInput
|
|
85
|
+
value={oldPassword}
|
|
86
|
+
id='old-password'
|
|
87
|
+
autocomplete='current-password'
|
|
88
|
+
required
|
|
89
|
+
oninput={(e) => { oldPassword = e.currentTarget.value }}
|
|
90
|
+
placeholder={uiM('change_password_old_placeholder')}
|
|
91
|
+
{disabled}
|
|
92
|
+
error={errors.oldPassword}
|
|
93
|
+
showStrength={false}
|
|
94
|
+
/>
|
|
95
|
+
</FormField>
|
|
96
|
+
{/if}
|
|
97
|
+
|
|
98
|
+
<FormField for='new-password' label={uiM('change_password_new')}>
|
|
81
99
|
<PasswordInput
|
|
82
|
-
value={
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
value={newPassword}
|
|
101
|
+
id='new-password'
|
|
102
|
+
autocomplete='new-password'
|
|
103
|
+
required
|
|
104
|
+
oninput={(e) => { newPassword = e.currentTarget.value }}
|
|
105
|
+
placeholder={uiM('change_password_new_placeholder')}
|
|
85
106
|
{disabled}
|
|
86
|
-
error={errors.
|
|
87
|
-
showStrength={
|
|
107
|
+
error={errors.newPassword}
|
|
108
|
+
showStrength={showPasswordStrength}
|
|
109
|
+
minLength={minPasswordLength}
|
|
88
110
|
/>
|
|
89
|
-
</
|
|
90
|
-
{/if}
|
|
111
|
+
</FormField>
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div class='space-y-1.5'>
|
|
108
|
-
<label class='text-sm font-medium text-base-content/70' for='confirm-password'>
|
|
109
|
-
{uiM('change_password_confirm')}
|
|
110
|
-
</label>
|
|
111
|
-
<PasswordInput
|
|
112
|
-
value={confirmPassword}
|
|
113
|
-
oninput={(e) => { confirmPassword = e.currentTarget.value }}
|
|
114
|
-
placeholder={uiM('change_password_confirm_placeholder')}
|
|
115
|
-
{disabled}
|
|
116
|
-
error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('change_password_mismatch') : '')}
|
|
117
|
-
showStrength={false}
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
113
|
+
<FormField for='confirm-password' label={uiM('change_password_confirm')}>
|
|
114
|
+
<PasswordInput
|
|
115
|
+
value={confirmPassword}
|
|
116
|
+
id='confirm-password'
|
|
117
|
+
autocomplete='new-password'
|
|
118
|
+
required
|
|
119
|
+
oninput={(e) => { confirmPassword = e.currentTarget.value }}
|
|
120
|
+
placeholder={uiM('change_password_confirm_placeholder')}
|
|
121
|
+
{disabled}
|
|
122
|
+
error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('change_password_mismatch') : '')}
|
|
123
|
+
showStrength={false}
|
|
124
|
+
/>
|
|
125
|
+
</FormField>
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
<div class='rounded-lg bg-base-content/3 px-3.5 py-2.5 text-xs text-base-content/50'>
|
|
128
|
+
{uiM('change_password_relogin_hint')}
|
|
129
|
+
</div>
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
{#if errors.general}
|
|
132
|
+
<Alert variant='error'>
|
|
133
|
+
{errors.general}
|
|
134
|
+
</Alert>
|
|
135
|
+
{/if}
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
{#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
|
|
138
|
+
{#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
<Button
|
|
141
|
+
type='submit'
|
|
142
|
+
variant='primary'
|
|
143
|
+
class='w-full sm:w-auto sm:min-w-40'
|
|
144
|
+
disabled={loading || disabled || !canSubmit || Boolean(policyError)}
|
|
145
|
+
{loading}
|
|
146
|
+
>
|
|
147
|
+
{submitText || uiM('change_password_submit')}
|
|
148
|
+
</Button>
|
|
149
|
+
</FormLayout>
|
|
143
150
|
</form>
|