@nocobase/app-template-default 1.0.0-beta.68 → 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 +1 -1
- package/.agents/rules/shell.md +3 -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/permission-editor.tsx +589 -0
- package/client/components/permission-filter.tsx +455 -0
- package/client/components/route-overlay.tsx +26 -13
- 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/layouts/components/app-sidebar.tsx +1 -1
- package/client/layouts/use-sidebar-preference.ts +62 -42
- package/client/locales/system/en-US.ts +7 -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 +7 -0
- package/client/pages/auth/auth-page.css +15 -0
- package/client/pages/auth/login.tsx +1 -1
- package/client/pages/auth/shared.tsx +3 -1
- 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/session-preparation.tsx +65 -0
- package/client/styles.css +4 -7
- package/package.json +24 -23
- package/tests/components/app-layout-routes.test.tsx +14 -7
- package/tests/components/data-table.test.tsx +127 -0
- package/tests/components/layout-i18n.test.tsx +8 -1
- package/tests/components/navigation-menu.test.tsx +24 -14
- 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 +12 -4
- package/tests/components/sidebar-preference.test.tsx +209 -14
- 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/session-preparation.test.tsx +223 -0
- package/tests/logic/settings-permissions.test.ts +168 -0
- package/vite.config.ts +0 -22
- package/client/agent-annotations-host.ts +0 -25
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot='table-container'
|
|
8
|
+
className='relative w-full overflow-x-auto'
|
|
9
|
+
>
|
|
10
|
+
<table
|
|
11
|
+
data-slot='table'
|
|
12
|
+
className={cn('w-full caption-bottom text-sm', className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|
20
|
+
return (
|
|
21
|
+
<thead
|
|
22
|
+
data-slot='table-header'
|
|
23
|
+
className={cn('[&_tr]:border-b', className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|
30
|
+
return (
|
|
31
|
+
<tbody
|
|
32
|
+
data-slot='table-body'
|
|
33
|
+
className={cn('[&_tr:last-child]:border-0', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
|
40
|
+
return (
|
|
41
|
+
<tfoot
|
|
42
|
+
data-slot='table-footer'
|
|
43
|
+
className={cn(
|
|
44
|
+
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|
53
|
+
return (
|
|
54
|
+
<tr
|
|
55
|
+
data-slot='table-row'
|
|
56
|
+
className={cn(
|
|
57
|
+
'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|
66
|
+
return (
|
|
67
|
+
<th
|
|
68
|
+
data-slot='table-head'
|
|
69
|
+
className={cn(
|
|
70
|
+
'h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|
79
|
+
return (
|
|
80
|
+
<td
|
|
81
|
+
data-slot='table-cell'
|
|
82
|
+
className={cn(
|
|
83
|
+
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function TableCaption({
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}: React.ComponentProps<'caption'>) {
|
|
95
|
+
return (
|
|
96
|
+
<caption
|
|
97
|
+
data-slot='table-caption'
|
|
98
|
+
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
Table,
|
|
106
|
+
TableHeader,
|
|
107
|
+
TableBody,
|
|
108
|
+
TableFooter,
|
|
109
|
+
TableHead,
|
|
110
|
+
TableRow,
|
|
111
|
+
TableCell,
|
|
112
|
+
TableCaption,
|
|
113
|
+
};
|
|
@@ -33,7 +33,7 @@ import { AppBrand } from './app-brand.js';
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The shell's sidebar state: shadcn's provider, with the desktop icon mode kept in the shared sidebar preference
|
|
36
|
-
* (`use-sidebar-preference.ts`) so every layout
|
|
36
|
+
* (`use-sidebar-preference.ts`) so every layout of this application opens it the same way. Whether the phone's sheet is
|
|
37
37
|
* open stays local to the layout.
|
|
38
38
|
*/
|
|
39
39
|
export function AppSidebarProvider({
|
|
@@ -1,57 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useSyncExternalStore,
|
|
5
|
+
type SetStateAction,
|
|
6
|
+
} from 'react';
|
|
7
|
+
|
|
8
|
+
// Each application shares its preference across layouts and tabs, never across scopes.
|
|
9
|
+
export function createSidebarPreferenceStore(scope: string) {
|
|
10
|
+
const storageKey = `nocobase:sidebar:collapsed:${scope}`;
|
|
11
|
+
const changeEvent = `nocobase:sidebar-preference-change:${scope}`;
|
|
12
|
+
let memoryValue = false;
|
|
13
|
+
let memoryOnly = false;
|
|
14
|
+
|
|
15
|
+
function getSnapshot(): boolean {
|
|
16
|
+
if (memoryOnly) return memoryValue;
|
|
17
|
+
try {
|
|
18
|
+
memoryValue = localStorage.getItem(storageKey) === 'true';
|
|
19
|
+
} catch {
|
|
20
|
+
// Storage may be unavailable; keep this application's session preference.
|
|
21
|
+
}
|
|
22
|
+
return memoryValue;
|
|
15
23
|
}
|
|
16
|
-
return memoryValue;
|
|
17
|
-
}
|
|
18
24
|
|
|
19
|
-
function subscribe(notify: () => void): () => void {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
25
|
+
function subscribe(notify: () => void): () => void {
|
|
26
|
+
const onStorage = (event: StorageEvent) => {
|
|
27
|
+
if (event.key === storageKey || event.key === null) notify();
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener('storage', onStorage);
|
|
30
|
+
window.addEventListener(changeEvent, notify);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener('storage', onStorage);
|
|
33
|
+
window.removeEventListener(changeEvent, notify);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
30
36
|
|
|
31
|
-
function setCollapsed(update: SetStateAction<boolean>): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
function setCollapsed(update: SetStateAction<boolean>): void {
|
|
38
|
+
const current = getSnapshot();
|
|
39
|
+
memoryValue = typeof update === 'function' ? update(current) : update;
|
|
40
|
+
try {
|
|
41
|
+
localStorage.setItem(storageKey, String(memoryValue));
|
|
42
|
+
} catch {
|
|
43
|
+
memoryOnly = true;
|
|
44
|
+
// The toggle remains usable when storage is blocked.
|
|
45
|
+
}
|
|
46
|
+
window.dispatchEvent(new Event(changeEvent));
|
|
39
47
|
}
|
|
40
|
-
|
|
48
|
+
|
|
49
|
+
return { getSnapshot, subscribe, setCollapsed };
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
type SidebarPreferenceStore = ReturnType<typeof createSidebarPreferenceStore>;
|
|
53
|
+
|
|
54
|
+
export function createAppSidebarPreferenceStore(basename: string = '') {
|
|
55
|
+
const normalizedBasename = `/${basename.replace(/^\/+|\/+$/g, '')}`;
|
|
56
|
+
return createSidebarPreferenceStore(`app:${normalizedBasename}`);
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
const SidebarPreferenceContext = createContext<SidebarPreferenceStore | null>(
|
|
60
|
+
null,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const SidebarPreferenceProvider = SidebarPreferenceContext.Provider;
|
|
64
|
+
|
|
47
65
|
export function useSidebarPreference(): readonly [
|
|
48
66
|
boolean,
|
|
49
67
|
(update: SetStateAction<boolean>) => void,
|
|
50
68
|
] {
|
|
69
|
+
const store = useContext(SidebarPreferenceContext);
|
|
70
|
+
if (!store) throw new Error('SidebarPreferenceProvider is required.');
|
|
51
71
|
const collapsed = useSyncExternalStore(
|
|
52
|
-
subscribe,
|
|
53
|
-
getSnapshot,
|
|
54
|
-
|
|
72
|
+
store.subscribe,
|
|
73
|
+
store.getSnapshot,
|
|
74
|
+
() => false,
|
|
55
75
|
);
|
|
56
|
-
return [collapsed, setCollapsed];
|
|
76
|
+
return [collapsed, store.setCollapsed];
|
|
57
77
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import settings from './settings-en-US.js';
|
|
1
2
|
import type { LocaleResource } from '@nocobase/i18n';
|
|
2
3
|
import deviceApprovalEnUS from '#extensions/nocobase-device-approval/locales/en-US';
|
|
3
4
|
import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
@@ -7,6 +8,12 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
|
7
8
|
* upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
|
|
8
9
|
*/
|
|
9
10
|
const systemEnUS = {
|
|
11
|
+
settings,
|
|
12
|
+
'auth.preparingSession': 'Preparing your session…',
|
|
13
|
+
'auth.connectionFailed': 'Unable to connect',
|
|
14
|
+
'auth.connectionFailedDescription':
|
|
15
|
+
'The realtime connection could not be prepared. Retry to enter the application, or sign out.',
|
|
16
|
+
|
|
10
17
|
'auth.passwordResetSuccess':
|
|
11
18
|
'Your password has been reset. Sign in with your new password.',
|
|
12
19
|
'auth.requestNewResetLink': 'Request a new reset link',
|
|
@@ -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,16 @@
|
|
|
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
|
+
|
|
7
14
|
'auth.passwordResetSuccess': '密码已重置,请用新密码登录。',
|
|
8
15
|
'auth.requestNewResetLink': '重新申请重置链接',
|
|
9
16
|
|
|
@@ -35,3 +35,18 @@
|
|
|
35
35
|
.dark .auth-page [data-slot='input-group'] input {
|
|
36
36
|
background-color: transparent;
|
|
37
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 ? (
|
|
@@ -8,6 +8,7 @@ import { AuthSplitLayout } from '#extensions/nocobase-auth-split-layout/auth-spl
|
|
|
8
8
|
import './auth-page.css';
|
|
9
9
|
|
|
10
10
|
export interface AuthPageProps {
|
|
11
|
+
readonly className?: string;
|
|
11
12
|
readonly title: ReactNode;
|
|
12
13
|
readonly description: ReactNode;
|
|
13
14
|
readonly children: ReactNode;
|
|
@@ -20,6 +21,7 @@ export interface AuthPageProps {
|
|
|
20
21
|
*/
|
|
21
22
|
export function AuthPage({
|
|
22
23
|
children,
|
|
24
|
+
className,
|
|
23
25
|
description,
|
|
24
26
|
title,
|
|
25
27
|
}: AuthPageProps): ReactElement {
|
|
@@ -28,7 +30,7 @@ export function AuthPage({
|
|
|
28
30
|
<AuthSplitLayout
|
|
29
31
|
aside={<BrandPanel />}
|
|
30
32
|
asideLabel={t('auth.about')}
|
|
31
|
-
className='auth-page'
|
|
33
|
+
className={['auth-page', className].filter(Boolean).join(' ')}
|
|
32
34
|
description={description}
|
|
33
35
|
logo={
|
|
34
36
|
<>
|