@nocobase/app-template-default 1.0.0-beta.67 → 1.0.0-beta.69
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/.agents/rules/components.md +2 -2
- package/.agents/rules/shell.md +5 -1
- package/AGENTS.md +5 -3
- package/Dockerfile.dockerignore +0 -1
- package/README.MD +3 -1
- package/client/app.ts +15 -0
- package/client/components/data-table/column-header.tsx +91 -0
- package/client/components/data-table/data-table.tsx +216 -0
- package/client/components/data-table/index.ts +13 -0
- package/client/components/data-table/pagination.tsx +165 -0
- package/client/components/data-table/view-options.tsx +77 -0
- package/client/components/page-header.tsx +1 -1
- package/client/components/permission-editor.tsx +589 -0
- package/client/components/permission-filter.tsx +455 -0
- package/client/components/route-overlay.tsx +26 -13
- package/client/components/status-badge.tsx +7 -1
- package/client/components/ui/checkbox.tsx +25 -0
- package/client/components/ui/sidebar.tsx +1 -4
- package/client/components/ui/switch.tsx +30 -0
- package/client/components/ui/table.tsx +113 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +44 -58
- package/client/extensions/nocobase-inbox/inbox-list.tsx +73 -75
- package/client/index.tsx +4 -0
- package/client/layouts/components/app-sidebar.tsx +6 -6
- package/client/layouts/components/layout-header.tsx +1 -1
- package/client/layouts/components/navigation-menu.tsx +223 -12
- package/client/layouts/components/sidebar-footer.tsx +1 -1
- package/client/layouts/components/user-menu.tsx +1 -1
- package/client/layouts/use-sidebar-preference.ts +62 -42
- package/client/lib/navigation-transform.ts +8 -0
- package/client/lib/scrollbar.ts +88 -0
- package/client/locales/system/en-US.ts +11 -0
- package/client/locales/system/settings-en-US.ts +141 -0
- package/client/locales/system/settings-zh-CN.ts +135 -0
- package/client/locales/system/zh-CN.ts +22 -12
- package/client/pages/auth/auth-page.css +52 -0
- package/client/pages/auth/login.tsx +1 -1
- package/client/pages/auth/reset-password.tsx +57 -14
- package/client/pages/auth/shared.tsx +26 -24
- package/client/pages/home/index.tsx +2 -2
- package/client/pages/settings/hooks.tsx +137 -0
- package/client/pages/settings/permission-filters.tsx +119 -0
- package/client/pages/settings/permissions-model.ts +260 -0
- package/client/pages/settings/role-form.tsx +426 -0
- package/client/pages/settings/roles.tsx +163 -0
- package/client/pages/settings/routes.ts +79 -0
- package/client/pages/settings/shared.tsx +119 -0
- package/client/pages/settings/user-detail.tsx +268 -0
- package/client/pages/settings/user-form.tsx +427 -0
- package/client/pages/settings/users.tsx +451 -0
- package/client/routing/app-router.tsx +4 -1
- package/client/routing/route-navigation.ts +10 -1
- package/client/routing/session-preparation.tsx +65 -0
- package/client/styles.css +257 -24
- package/client/theme/theme-settings.tsx +12 -7
- package/client/theme/themes/compact.css +9 -14
- package/client/theme/themes/default.css +7 -12
- package/package.json +26 -25
- package/server/app.ts +2 -0
- package/server/config/notification.ts +4 -7
- package/tests/components/app-layout-routes.test.tsx +14 -7
- package/tests/components/auth-i18n.test.tsx +57 -5
- package/tests/components/data-table.test.tsx +127 -0
- package/tests/components/layout-i18n.test.tsx +10 -3
- package/tests/components/navigation-menu.test.tsx +155 -16
- package/tests/components/permission-editor.test.tsx +477 -0
- package/tests/components/permission-filter-binding.test.tsx +150 -0
- package/tests/components/permission-filter.test.tsx +202 -0
- package/tests/components/settings.test.tsx +294 -0
- package/tests/components/sidebar-permissions.test.tsx +17 -19
- package/tests/components/sidebar-preference.test.tsx +209 -14
- package/tests/components/status-badge.test.tsx +22 -0
- package/tests/components/user-permissions.test.tsx +142 -0
- package/tests/fixtures/theme-tokens.tsx +9 -1
- package/tests/logic/asset-url-build.test.ts +0 -1
- package/tests/logic/client-routes.test.ts +40 -0
- package/tests/logic/client-shell.test.tsx +10 -2
- package/tests/logic/config-example.test.ts +30 -0
- package/tests/logic/dev-client-watch.test.ts +8 -1
- package/tests/logic/route-overlay.test.tsx +42 -0
- package/tests/logic/scrollbar.test.ts +88 -0
- package/tests/logic/session-preparation.test.tsx +223 -0
- package/tests/logic/settings-permissions.test.ts +168 -0
- package/tests/logic/theme-tokens.test.ts +1 -0
- package/vite.config.ts +0 -22
- package/vitest.config.ts +1 -0
- package/client/agent-annotations-host.ts +0 -25
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const settings = {
|
|
2
|
+
revokeRoles: 'Revoke roles',
|
|
3
|
+
keyConflict: 'This role key is already in use.',
|
|
4
|
+
userPermissions: 'User management',
|
|
5
|
+
readUsers: 'View users',
|
|
6
|
+
createUsers: 'Create users',
|
|
7
|
+
updateUsers: 'Edit users',
|
|
8
|
+
inviteUsers: 'Invite users',
|
|
9
|
+
deleteUsers: 'Delete users',
|
|
10
|
+
resetUserPassword: 'Reset password',
|
|
11
|
+
assignRole: 'Assign roles',
|
|
12
|
+
|
|
13
|
+
assignedRoles: 'Assigned roles',
|
|
14
|
+
actionSaved: 'Updated access for "{{name}}"',
|
|
15
|
+
|
|
16
|
+
identityConflict: 'This email or username is already in use.',
|
|
17
|
+
signInAgain: 'Sign in again',
|
|
18
|
+
passwordPolicy: 'Password does not meet the current password policy.',
|
|
19
|
+
|
|
20
|
+
reload: 'Reload role',
|
|
21
|
+
title: 'System settings',
|
|
22
|
+
users: 'Users',
|
|
23
|
+
roles: 'Roles and permissions',
|
|
24
|
+
usersDescription: 'Manage accounts, access and active sessions.',
|
|
25
|
+
rolesDescription: 'Define what each role can access and do.',
|
|
26
|
+
userSearch: 'Search names, usernames or emails',
|
|
27
|
+
roleSearch: 'Search role names',
|
|
28
|
+
newUser: 'New user',
|
|
29
|
+
editUser: 'Edit user',
|
|
30
|
+
newRole: 'New role',
|
|
31
|
+
editRole: 'Edit role',
|
|
32
|
+
name: 'Name',
|
|
33
|
+
username: 'Username',
|
|
34
|
+
email: 'Email',
|
|
35
|
+
password: 'Initial password',
|
|
36
|
+
roleName: 'Role name',
|
|
37
|
+
key: 'Key',
|
|
38
|
+
status: 'Status',
|
|
39
|
+
actions: 'Actions',
|
|
40
|
+
actionsFor: 'Actions for {{name}}',
|
|
41
|
+
all: 'All statuses',
|
|
42
|
+
enabled: 'Enabled',
|
|
43
|
+
disabled: 'Disabled',
|
|
44
|
+
clear: 'Clear filters',
|
|
45
|
+
noResults: 'No matching results',
|
|
46
|
+
clearHint: 'Try a different search or clear the filters.',
|
|
47
|
+
noUsers: 'No users yet',
|
|
48
|
+
noRoles: 'No roles yet',
|
|
49
|
+
create: 'Create',
|
|
50
|
+
edit: 'Edit',
|
|
51
|
+
save: 'Save',
|
|
52
|
+
cancel: 'Cancel',
|
|
53
|
+
saving: 'Saving…',
|
|
54
|
+
confirm: 'Confirm',
|
|
55
|
+
enable: 'Enable',
|
|
56
|
+
disable: 'Disable',
|
|
57
|
+
revokeSessions: 'Revoke sessions',
|
|
58
|
+
disableTitle: 'Disable user "{{name}}"?',
|
|
59
|
+
disableHint:
|
|
60
|
+
'This user will be unable to sign in and existing sessions will end.',
|
|
61
|
+
enableTitle: 'Enable user "{{name}}"?',
|
|
62
|
+
enableHint: 'This user will be able to sign in again.',
|
|
63
|
+
revokeTitle: 'Revoke sessions for "{{name}}"?',
|
|
64
|
+
revokeHint: 'This user will need to sign in again.',
|
|
65
|
+
revokeRoleTitle: 'Revoke roles from "{{name}}"?',
|
|
66
|
+
revokeRoleHint: 'Access granted by the listed roles will be removed.',
|
|
67
|
+
created: 'Created user "{{name}}"',
|
|
68
|
+
saved: 'Saved "{{name}}"',
|
|
69
|
+
rolesSaved: 'Updated roles for "{{name}}"',
|
|
70
|
+
required: 'Enter a value for this field',
|
|
71
|
+
invalidEmail: 'Enter a valid email address',
|
|
72
|
+
protected: 'Protected',
|
|
73
|
+
custom: 'Custom',
|
|
74
|
+
protectedHint:
|
|
75
|
+
'This role is protected. Only explicitly permitted changes are available.',
|
|
76
|
+
unsaved: 'Unsaved changes',
|
|
77
|
+
discardTitle: 'Discard changes?',
|
|
78
|
+
discardHint: 'Your unsaved changes will be lost.',
|
|
79
|
+
failed: 'Unable to complete the request. Try again.',
|
|
80
|
+
retry: 'Retry',
|
|
81
|
+
loading: 'Loading',
|
|
82
|
+
denied: 'You do not have access. Contact an administrator.',
|
|
83
|
+
missing: 'This record does not exist or has been deleted.',
|
|
84
|
+
conflict:
|
|
85
|
+
'This role has changed. Reload it and merge your changes before saving.',
|
|
86
|
+
sessionEnded: 'Your session has ended. Sign in again.',
|
|
87
|
+
previous: 'Previous',
|
|
88
|
+
next: 'Next',
|
|
89
|
+
total_one: '{{count}} user',
|
|
90
|
+
total_other: '{{count}} users',
|
|
91
|
+
access: 'Access',
|
|
92
|
+
permissionSearch: 'Search categories, groups or permissions',
|
|
93
|
+
enabledOnly: 'Enabled only',
|
|
94
|
+
noPermissions: 'No permissions to configure',
|
|
95
|
+
permissionCategories: 'Permission categories',
|
|
96
|
+
selectAllPermissions: 'All',
|
|
97
|
+
selectVisiblePermissions: 'Select visible permissions in {{name}}',
|
|
98
|
+
permissionSummary: '{{enabled}} of {{total}} enabled',
|
|
99
|
+
noRecordScope: 'No records (not authorized)',
|
|
100
|
+
permissionFilter: {
|
|
101
|
+
limit: 'Up to 20 conditions',
|
|
102
|
+
configure: 'Conditions',
|
|
103
|
+
title: 'Configure record filter',
|
|
104
|
+
description:
|
|
105
|
+
'Apply conditions to the role draft, then save the role to make them effective.',
|
|
106
|
+
all: 'Match all conditions',
|
|
107
|
+
any: 'Match any condition',
|
|
108
|
+
field: 'Field',
|
|
109
|
+
operator: 'Operator',
|
|
110
|
+
valueType: 'Value type',
|
|
111
|
+
value: 'Value',
|
|
112
|
+
text: 'Text',
|
|
113
|
+
number: 'Number',
|
|
114
|
+
boolean: 'Boolean',
|
|
115
|
+
true: 'Yes',
|
|
116
|
+
false: 'No',
|
|
117
|
+
add: 'Add condition',
|
|
118
|
+
remove: 'Remove',
|
|
119
|
+
cancel: 'Cancel',
|
|
120
|
+
apply: 'Apply',
|
|
121
|
+
invalid:
|
|
122
|
+
'Choose valid fields and complete all values. Include at least one condition.',
|
|
123
|
+
advanced:
|
|
124
|
+
'This existing filter uses advanced conditions. It will be preserved unless you explicitly reconfigure it.',
|
|
125
|
+
reconfigure: 'Reconfigure',
|
|
126
|
+
operators: {
|
|
127
|
+
$eq: 'Equals',
|
|
128
|
+
$ne: 'Does not equal',
|
|
129
|
+
$includes: 'Contains',
|
|
130
|
+
$gt: 'Greater than',
|
|
131
|
+
$gte: 'At least',
|
|
132
|
+
$lt: 'Less than',
|
|
133
|
+
$lte: 'At most',
|
|
134
|
+
$empty: 'Is empty',
|
|
135
|
+
$notEmpty: 'Is not empty',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
recordScope: 'Record scope',
|
|
139
|
+
levelFor: 'Scope for {{name}}',
|
|
140
|
+
};
|
|
141
|
+
export default settings;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const settings = {
|
|
2
|
+
revokeRoles: '撤销角色',
|
|
3
|
+
keyConflict: '此角色标识已被使用。',
|
|
4
|
+
userPermissions: '用户管理',
|
|
5
|
+
readUsers: '查看用户',
|
|
6
|
+
createUsers: '创建用户',
|
|
7
|
+
updateUsers: '编辑用户',
|
|
8
|
+
inviteUsers: '邀请用户',
|
|
9
|
+
deleteUsers: '删除用户',
|
|
10
|
+
resetUserPassword: '重置密码',
|
|
11
|
+
assignRole: '分配角色',
|
|
12
|
+
|
|
13
|
+
assignedRoles: '已分配角色',
|
|
14
|
+
actionSaved: '已更新“{{name}}”的访问状态',
|
|
15
|
+
|
|
16
|
+
identityConflict: '此邮箱或用户名已被使用。',
|
|
17
|
+
signInAgain: '重新登录',
|
|
18
|
+
passwordPolicy: '密码不符合当前密码策略。',
|
|
19
|
+
|
|
20
|
+
reload: '重新加载角色',
|
|
21
|
+
title: '系统设置',
|
|
22
|
+
users: '用户管理',
|
|
23
|
+
roles: '角色与权限',
|
|
24
|
+
usersDescription: '管理用户账户、角色及登录会话。',
|
|
25
|
+
rolesDescription: '设置每个角色可访问的页面与可执行的操作。',
|
|
26
|
+
userSearch: '搜索姓名、用户名或邮箱',
|
|
27
|
+
roleSearch: '搜索角色名称',
|
|
28
|
+
newUser: '新建用户',
|
|
29
|
+
editUser: '编辑用户',
|
|
30
|
+
newRole: '新建角色',
|
|
31
|
+
editRole: '编辑角色',
|
|
32
|
+
name: '姓名',
|
|
33
|
+
username: '用户名',
|
|
34
|
+
email: '邮箱',
|
|
35
|
+
password: '初始密码',
|
|
36
|
+
roleName: '角色名称',
|
|
37
|
+
key: '标识',
|
|
38
|
+
status: '状态',
|
|
39
|
+
actions: '操作',
|
|
40
|
+
actionsFor: '{{name}} 的操作',
|
|
41
|
+
all: '全部状态',
|
|
42
|
+
enabled: '启用',
|
|
43
|
+
disabled: '停用',
|
|
44
|
+
clear: '清除筛选',
|
|
45
|
+
noResults: '没有匹配的结果',
|
|
46
|
+
clearHint: '请更换搜索条件或清除筛选。',
|
|
47
|
+
noUsers: '暂无用户',
|
|
48
|
+
noRoles: '暂无角色',
|
|
49
|
+
create: '创建',
|
|
50
|
+
edit: '编辑',
|
|
51
|
+
save: '保存',
|
|
52
|
+
cancel: '取消',
|
|
53
|
+
saving: '正在保存…',
|
|
54
|
+
confirm: '确认',
|
|
55
|
+
enable: '启用',
|
|
56
|
+
disable: '禁用',
|
|
57
|
+
revokeSessions: '撤销会话',
|
|
58
|
+
disableTitle: '禁用用户“{{name}}”?',
|
|
59
|
+
disableHint: '该用户将无法登录,现有会话将失效。',
|
|
60
|
+
enableTitle: '启用用户“{{name}}”?',
|
|
61
|
+
enableHint: '该用户可以重新登录。',
|
|
62
|
+
revokeTitle: '撤销用户“{{name}}”的会话?',
|
|
63
|
+
revokeHint: '该用户需要重新登录。',
|
|
64
|
+
revokeRoleTitle: '撤销“{{name}}”的角色?',
|
|
65
|
+
revokeRoleHint: '将移除所列角色授予的访问权限。',
|
|
66
|
+
created: '已创建用户“{{name}}”',
|
|
67
|
+
saved: '已保存“{{name}}”',
|
|
68
|
+
rolesSaved: '已更新“{{name}}”的角色',
|
|
69
|
+
required: '请填写此字段',
|
|
70
|
+
invalidEmail: '请输入有效邮箱',
|
|
71
|
+
protected: '受保护',
|
|
72
|
+
custom: '自定义',
|
|
73
|
+
protectedHint: '此角色受系统保护,仅允许明确授权的修改。',
|
|
74
|
+
unsaved: '有未保存的更改',
|
|
75
|
+
discardTitle: '放弃未保存的更改?',
|
|
76
|
+
discardHint: '未保存的更改将丢失。',
|
|
77
|
+
failed: '请求失败,请重试。',
|
|
78
|
+
retry: '重试',
|
|
79
|
+
loading: '加载中',
|
|
80
|
+
denied: '无权查看,请联系管理员。',
|
|
81
|
+
missing: '记录不存在或已被删除。',
|
|
82
|
+
conflict: '角色已被其他人修改,请重新加载后合并。',
|
|
83
|
+
sessionEnded: '会话已结束,请重新登录。',
|
|
84
|
+
previous: '上一页',
|
|
85
|
+
next: '下一页',
|
|
86
|
+
total_one: '共 {{count}} 位用户',
|
|
87
|
+
total_other: '共 {{count}} 位用户',
|
|
88
|
+
access: '访问',
|
|
89
|
+
permissionSearch: '搜索分类、分组或权限',
|
|
90
|
+
enabledOnly: '仅看已开启',
|
|
91
|
+
noPermissions: '暂无可配置权限',
|
|
92
|
+
permissionCategories: '权限分类',
|
|
93
|
+
selectAllPermissions: '全选',
|
|
94
|
+
selectVisiblePermissions: '选择“{{name}}”中显示的权限',
|
|
95
|
+
permissionSummary: '已开启 {{enabled}} / {{total}}',
|
|
96
|
+
noRecordScope: '无记录(未授权)',
|
|
97
|
+
permissionFilter: {
|
|
98
|
+
limit: '最多支持 20 个条件',
|
|
99
|
+
configure: '筛选条件',
|
|
100
|
+
title: '配置记录筛选',
|
|
101
|
+
description: '应用条件后还需保存角色,权限才会生效。',
|
|
102
|
+
all: '满足全部条件',
|
|
103
|
+
any: '满足任一条件',
|
|
104
|
+
field: '字段',
|
|
105
|
+
operator: '比较方式',
|
|
106
|
+
valueType: '值类型',
|
|
107
|
+
value: '值',
|
|
108
|
+
text: '文本',
|
|
109
|
+
number: '数字',
|
|
110
|
+
boolean: '是或否',
|
|
111
|
+
true: '是',
|
|
112
|
+
false: '否',
|
|
113
|
+
add: '添加条件',
|
|
114
|
+
remove: '移除',
|
|
115
|
+
cancel: '取消',
|
|
116
|
+
apply: '应用',
|
|
117
|
+
invalid: '请选择有效字段并填写完整的值,至少保留一个条件。',
|
|
118
|
+
advanced: '现有筛选包含高级条件,将保留原样;重新配置后应用才会替换。',
|
|
119
|
+
reconfigure: '重新配置',
|
|
120
|
+
operators: {
|
|
121
|
+
$eq: '等于',
|
|
122
|
+
$ne: '不等于',
|
|
123
|
+
$includes: '包含',
|
|
124
|
+
$gt: '大于',
|
|
125
|
+
$gte: '大于或等于',
|
|
126
|
+
$lt: '小于',
|
|
127
|
+
$lte: '小于或等于',
|
|
128
|
+
$empty: '为空',
|
|
129
|
+
$notEmpty: '不为空',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
recordScope: '数据范围',
|
|
133
|
+
levelFor: '{{name}} 的范围',
|
|
134
|
+
};
|
|
135
|
+
export default settings;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import settings from './settings-zh-CN.js';
|
|
1
2
|
import type { SystemResource } from './en-US.js';
|
|
2
3
|
import deviceApprovalZhCN from '#extensions/nocobase-device-approval/locales/zh-CN';
|
|
3
4
|
import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
|
|
4
5
|
|
|
5
6
|
/** The Chinese copy of what the template ships; see `./en-US.ts`. */
|
|
6
7
|
const systemZhCN: SystemResource = {
|
|
8
|
+
settings,
|
|
9
|
+
'auth.preparingSession': '正在准备登录会话…',
|
|
10
|
+
'auth.connectionFailed': '连接失败',
|
|
11
|
+
'auth.connectionFailedDescription':
|
|
12
|
+
'实时连接暂时无法准备就绪。请重试后进入应用,或退出登录。',
|
|
13
|
+
|
|
14
|
+
'auth.passwordResetSuccess': '密码已重置,请用新密码登录。',
|
|
15
|
+
'auth.requestNewResetLink': '重新申请重置链接',
|
|
16
|
+
|
|
7
17
|
// The UI Library block of the `/device` page; the keys below may reword it.
|
|
8
18
|
...deviceApprovalZhCN,
|
|
9
19
|
// The UI Library block of the `/inbox` page; the keys below may reword it.
|
|
@@ -15,16 +25,16 @@ const systemZhCN: SystemResource = {
|
|
|
15
25
|
'inboxButton.pendingHint': '{{count}} 项等你处理,处理后减少。',
|
|
16
26
|
'inboxButton.unreadHint': '{{count}} 条未读,阅读后减少。',
|
|
17
27
|
'auth.welcome': '欢迎回来',
|
|
18
|
-
'auth.loginDescription': '
|
|
28
|
+
'auth.loginDescription': '使用用户名或邮箱和密码登录',
|
|
19
29
|
'auth.registerTitle': '创建账户',
|
|
20
|
-
'auth.registerDescription': '
|
|
30
|
+
'auth.registerDescription': '创建账户以开始使用',
|
|
21
31
|
'auth.forgotTitle': '忘记密码',
|
|
22
|
-
'auth.forgotDescription': '
|
|
32
|
+
'auth.forgotDescription': '输入邮箱,如果账户存在,我们将发送重置链接',
|
|
23
33
|
'auth.passwordResetUnavailable': '未开启自助找回,请联系管理员重置密码。',
|
|
24
34
|
'auth.capabilityLoadFailed': '无法检查自助找回是否已开启。',
|
|
25
35
|
'auth.retry': '重试',
|
|
26
36
|
'auth.loading': '加载中',
|
|
27
|
-
'auth.resetDescription': '
|
|
37
|
+
'auth.resetDescription': '为账户设置新密码',
|
|
28
38
|
'auth.resetTitle': '重置密码',
|
|
29
39
|
'auth.identifier': '用户名或邮箱',
|
|
30
40
|
'auth.password': '密码',
|
|
@@ -57,16 +67,16 @@ const systemZhCN: SystemResource = {
|
|
|
57
67
|
'auth.continueWith': '使用 {provider} 继续',
|
|
58
68
|
'auth.about': '关于此应用',
|
|
59
69
|
'auth.platform': 'AI 原生应用平台',
|
|
60
|
-
'auth.marketingTitleFirst': '让 AI
|
|
70
|
+
'auth.marketingTitleFirst': '让 AI 自由构建',
|
|
61
71
|
'auth.marketingTitleSecond': 'NocoBase 保障',
|
|
62
|
-
'auth.marketingTitleThird': '
|
|
72
|
+
'auth.marketingTitleThird': '可靠运行',
|
|
63
73
|
'auth.marketingDescription':
|
|
64
|
-
'让 AI 在灵活的前端框架上构建体验,由 NocoBase
|
|
74
|
+
'让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限与治理',
|
|
65
75
|
'auth.frontend': 'AI 原生前端',
|
|
66
|
-
'auth.frontendDescription': '
|
|
76
|
+
'auth.frontendDescription': '在灵活的框架上自由构建界面',
|
|
67
77
|
'auth.foundation': 'NocoBase 基础能力',
|
|
68
|
-
'auth.foundationDescription': '
|
|
69
|
-
'auth.marketingFooter': '
|
|
78
|
+
'auth.foundationDescription': '可靠的数据、访问控制与治理',
|
|
79
|
+
'auth.marketingFooter': '自由构建,可靠支撑',
|
|
70
80
|
'status.loading': '加载中',
|
|
71
81
|
'status.loadingPage': '正在加载页面',
|
|
72
82
|
'status.denied': '无权访问',
|
|
@@ -79,8 +89,8 @@ const systemZhCN: SystemResource = {
|
|
|
79
89
|
'status.routeFailedDescription':
|
|
80
90
|
'无法加载 {{packageName}} 的路由 {{label}}。',
|
|
81
91
|
shell: {
|
|
82
|
-
buildFreely: 'AI
|
|
83
|
-
reliability: '<brand>NocoBase</brand>
|
|
92
|
+
buildFreely: 'AI 自由构建',
|
|
93
|
+
reliability: '<brand>NocoBase</brand> 保障可靠',
|
|
84
94
|
},
|
|
85
95
|
home: {
|
|
86
96
|
platform: 'NocoBase · AI 原生应用平台',
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.auth-brand-panel {
|
|
2
|
+
background: var(--auth-brand-background);
|
|
3
|
+
background-image: radial-gradient(
|
|
4
|
+
ellipse at top right,
|
|
5
|
+
color-mix(in srgb, var(--auth-brand-highlight) 8%, transparent),
|
|
6
|
+
transparent 65%
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Authentication surfaces have their own palette; application pages keep the selected theme. */
|
|
11
|
+
.dark .auth-page {
|
|
12
|
+
--background: #141923;
|
|
13
|
+
--foreground: #eef1f7;
|
|
14
|
+
--card: #1b2230;
|
|
15
|
+
--muted-foreground: #a4adbd;
|
|
16
|
+
--primary: #526bc7;
|
|
17
|
+
--primary-foreground: #ffffff;
|
|
18
|
+
--border: #30394b;
|
|
19
|
+
--input: #3b475c;
|
|
20
|
+
--ring: #8198ed;
|
|
21
|
+
--auth-brand-background: #192130;
|
|
22
|
+
--auth-brand-highlight: #a2b3ec;
|
|
23
|
+
--auth-brand-muted: #aab5c9;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dark .auth-page > aside {
|
|
27
|
+
border-color: transparent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dark .auth-page [data-slot='input'],
|
|
31
|
+
.dark .auth-page [data-slot='input-group'] {
|
|
32
|
+
background-color: var(--card);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark .auth-page [data-slot='input-group'] input {
|
|
36
|
+
background-color: transparent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Keep login controls comfortable while tightening the form's vertical rhythm. */
|
|
40
|
+
.auth-login-page main header {
|
|
41
|
+
margin-bottom: calc(var(--spacing) * 6);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.auth-login-page form > [data-slot='field-group'] {
|
|
45
|
+
gap: calc(var(--spacing) * 4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.auth-login-page form [data-slot='input'],
|
|
49
|
+
.auth-login-page form [data-slot='input-group'],
|
|
50
|
+
.auth-login-page form button[type='submit'] {
|
|
51
|
+
height: calc(var(--spacing) * 10);
|
|
52
|
+
}
|
|
@@ -21,6 +21,7 @@ export default function LoginPage(): ReactElement {
|
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<AuthPage
|
|
24
|
+
className='auth-login-page'
|
|
24
25
|
description={t('auth.loginDescription')}
|
|
25
26
|
title={t('auth.welcome')}
|
|
26
27
|
>
|
|
@@ -36,7 +37,6 @@ export default function LoginPage(): ReactElement {
|
|
|
36
37
|
{
|
|
37
38
|
content: (
|
|
38
39
|
<PasswordLoginForm
|
|
39
|
-
className='[&_[data-slot=input]]:h-12 [&_[data-slot=input-group]]:h-12 [&_button[type=submit]]:h-12'
|
|
40
40
|
error={login.error?.message}
|
|
41
41
|
footer={
|
|
42
42
|
signUpAvailable ? (
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Button } from '#components/ui/button';
|
|
2
|
+
import { AuthFormStatus } from '#extensions/nocobase-auth-forms/form-parts';
|
|
3
|
+
import { usePasswordResetCapability } from '@nocobase/app-plugin-authentication/client';
|
|
1
4
|
import { usePasswordReset } from '@nocobase/app-plugin-authentication/client/actions';
|
|
2
5
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
6
|
import type { ReactElement } from 'react';
|
|
@@ -10,6 +13,7 @@ import { AuthPage } from './shared.js';
|
|
|
10
13
|
export default function ResetPasswordPage(): ReactElement {
|
|
11
14
|
const { t } = useTranslation();
|
|
12
15
|
const reset = usePasswordReset();
|
|
16
|
+
const capability = usePasswordResetCapability();
|
|
13
17
|
const [searchParams] = useSearchParams();
|
|
14
18
|
// The link in the reset email carries the token; without one the form cannot do anything.
|
|
15
19
|
const token = searchParams.get('token') ?? '';
|
|
@@ -19,20 +23,59 @@ export default function ResetPasswordPage(): ReactElement {
|
|
|
19
23
|
description={t('auth.resetDescription')}
|
|
20
24
|
title={t('auth.resetTitle')}
|
|
21
25
|
>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
{capability.isPending ? (
|
|
27
|
+
<div
|
|
28
|
+
className='h-48 animate-pulse rounded-md bg-muted'
|
|
29
|
+
aria-label={t('auth.loading')}
|
|
30
|
+
/>
|
|
31
|
+
) : capability.isError ? (
|
|
32
|
+
<div className='space-y-4'>
|
|
33
|
+
<AuthFormStatus type='error'>
|
|
34
|
+
{t('auth.capabilityLoadFailed')}
|
|
35
|
+
</AuthFormStatus>
|
|
36
|
+
<Button variant='outline' onClick={() => void capability.refetch()}>
|
|
37
|
+
{t('auth.retry')}
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
) : capability.data?.passwordResetAvailable !== true ? (
|
|
41
|
+
<div className='space-y-4'>
|
|
42
|
+
<AuthFormStatus type='error'>
|
|
43
|
+
{t('auth.passwordResetUnavailable')}
|
|
44
|
+
</AuthFormStatus>
|
|
45
|
+
<Link to='/login'>{t('auth.backToSignIn')}</Link>
|
|
46
|
+
</div>
|
|
47
|
+
) : reset.isSuccess ? (
|
|
48
|
+
<div className='space-y-4'>
|
|
49
|
+
<AuthFormStatus type='success'>
|
|
50
|
+
{t('auth.passwordResetSuccess')}
|
|
51
|
+
</AuthFormStatus>
|
|
52
|
+
<Link to='/login'>{t('auth.backToSignIn')}</Link>
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
<PasswordResetForm
|
|
56
|
+
disabled={!token}
|
|
57
|
+
error={token ? reset.error?.message : t('auth.invalidResetLink')}
|
|
58
|
+
footer={
|
|
59
|
+
<span className='flex flex-wrap justify-center gap-x-4 gap-y-2 [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary'>
|
|
60
|
+
<Link to='/login'>{t('auth.backToSignIn')}</Link>
|
|
61
|
+
{!token || reset.error ? (
|
|
62
|
+
<Link to='/forgot-password'>
|
|
63
|
+
{t('auth.requestNewResetLink')}
|
|
64
|
+
</Link>
|
|
65
|
+
) : null}
|
|
66
|
+
</span>
|
|
67
|
+
}
|
|
68
|
+
labels={{
|
|
69
|
+
confirmPassword: t('auth.confirmNewPassword'),
|
|
70
|
+
password: t('auth.newPassword'),
|
|
71
|
+
passwordMismatch: t('auth.passwordMismatch'),
|
|
72
|
+
submit: t('auth.resetTitle'),
|
|
73
|
+
submitting: t('auth.resetting'),
|
|
74
|
+
}}
|
|
75
|
+
onSubmit={({ password }) => reset.submit({ password, token })}
|
|
76
|
+
submitting={reset.isPending}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
36
79
|
</AuthPage>
|
|
37
80
|
);
|
|
38
81
|
}
|
|
@@ -5,7 +5,10 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { AuthSplitLayout } from '#extensions/nocobase-auth-split-layout/auth-split-layout';
|
|
7
7
|
|
|
8
|
+
import './auth-page.css';
|
|
9
|
+
|
|
8
10
|
export interface AuthPageProps {
|
|
11
|
+
readonly className?: string;
|
|
9
12
|
readonly title: ReactNode;
|
|
10
13
|
readonly description: ReactNode;
|
|
11
14
|
readonly children: ReactNode;
|
|
@@ -18,6 +21,7 @@ export interface AuthPageProps {
|
|
|
18
21
|
*/
|
|
19
22
|
export function AuthPage({
|
|
20
23
|
children,
|
|
24
|
+
className,
|
|
21
25
|
description,
|
|
22
26
|
title,
|
|
23
27
|
}: AuthPageProps): ReactElement {
|
|
@@ -26,6 +30,7 @@ export function AuthPage({
|
|
|
26
30
|
<AuthSplitLayout
|
|
27
31
|
aside={<BrandPanel />}
|
|
28
32
|
asideLabel={t('auth.about')}
|
|
33
|
+
className={['auth-page', className].filter(Boolean).join(' ')}
|
|
29
34
|
description={description}
|
|
30
35
|
logo={
|
|
31
36
|
<>
|
|
@@ -68,42 +73,39 @@ function BrandPanel(): ReactElement {
|
|
|
68
73
|
];
|
|
69
74
|
|
|
70
75
|
return (
|
|
71
|
-
<div className='relative grid h-full place-items-center overflow-hidden
|
|
72
|
-
<div className='
|
|
73
|
-
<div className='relative w-full max-w-xl'>
|
|
76
|
+
<div className='auth-brand-panel relative grid h-full place-items-center overflow-hidden p-12 text-white'>
|
|
77
|
+
<div className='relative w-full max-w-md'>
|
|
74
78
|
<p className='text-xs font-semibold tracking-[0.16em] text-(--auth-brand-highlight) uppercase'>
|
|
75
79
|
{t('auth.platform')}
|
|
76
80
|
</p>
|
|
77
|
-
<h2 className='mt-
|
|
81
|
+
<h2 className='mt-6 text-4xl leading-tight font-semibold tracking-tight'>
|
|
78
82
|
{t('auth.marketingTitleFirst')}
|
|
79
83
|
<br />
|
|
80
84
|
{t('auth.marketingTitleSecond')}
|
|
81
85
|
<br />
|
|
82
86
|
{t('auth.marketingTitleThird')}
|
|
83
87
|
</h2>
|
|
84
|
-
<p className='mt-
|
|
88
|
+
<p className='mt-6 text-sm leading-7 text-(--auth-brand-muted)'>
|
|
85
89
|
{t('auth.marketingDescription')}
|
|
86
90
|
</p>
|
|
87
|
-
<div className='mt-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</p>
|
|
99
|
-
</div>
|
|
91
|
+
<div className='mt-12 space-y-8'>
|
|
92
|
+
{features.map(({ description, icon: Icon, id, title }) => (
|
|
93
|
+
<div className='flex gap-4' key={id}>
|
|
94
|
+
<span className='grid size-10 shrink-0 place-items-center rounded-xl bg-(--auth-brand-highlight)/8 text-(--auth-brand-highlight)'>
|
|
95
|
+
<Icon aria-hidden='true' className='size-5' />
|
|
96
|
+
</span>
|
|
97
|
+
<div>
|
|
98
|
+
<p className='text-sm font-medium'>{title}</p>
|
|
99
|
+
<p className='mt-1.5 text-sm leading-6 text-(--auth-brand-muted)'>
|
|
100
|
+
{description}
|
|
101
|
+
</p>
|
|
100
102
|
</div>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
<div className='mt-12 flex items-center gap-2.5 border-t border-white/8 pt-6 text-xs text-(--auth-brand-muted)'>
|
|
107
|
+
<Blocks aria-hidden='true' className='size-4' />
|
|
108
|
+
<span>{t('auth.marketingFooter')}</span>
|
|
107
109
|
</div>
|
|
108
110
|
</div>
|
|
109
111
|
</div>
|
|
@@ -160,11 +160,11 @@ function CapabilityCard({
|
|
|
160
160
|
onClick={onOpen}
|
|
161
161
|
className='group flex w-full cursor-pointer rounded-xl text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50'
|
|
162
162
|
>
|
|
163
|
-
<Card className='w-full transition-[translate,box-shadow] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:
|
|
163
|
+
<Card className='w-full transition-[translate,box-shadow,background-color] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:bg-hover group-hover:shadow-md motion-reduce:transition-none motion-reduce:group-hover:translate-y-0'>
|
|
164
164
|
<CardHeader className='gap-3'>
|
|
165
165
|
{/* The chip only deepens its tint on hover rather than inverting: swapping a light background and a dark icon
|
|
166
166
|
passes both through the same color halfway, where the icon vanishes for a moment and reads as a flicker. */}
|
|
167
|
-
<span className='flex size-9 items-center justify-center rounded-lg bg-primary/5 text-primary ring-1 ring-transparent transition-[background-color,box-shadow] duration-200 ease-out group-hover:bg-primary/10 group-hover:ring-
|
|
167
|
+
<span className='flex size-9 items-center justify-center rounded-lg bg-primary/5 text-primary ring-1 ring-transparent transition-[background-color,box-shadow] duration-200 ease-out group-hover:bg-primary/10 group-hover:ring-border motion-reduce:transition-none'>
|
|
168
168
|
<Icon
|
|
169
169
|
className='size-4 transition-transform duration-200 ease-out group-hover:scale-110 motion-reduce:transition-none'
|
|
170
170
|
aria-hidden
|