@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.58
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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/.prettierignore +20 -0
- package/.vscode/mcp.json +8 -0
- package/AGENTS.md +434 -0
- package/CLAUDE.md +9 -0
- package/Dockerfile +132 -0
- package/Dockerfile.dockerignore +45 -0
- package/README.MD +485 -0
- package/cli/plugins.ts +8 -0
- package/client/agent-annotations-host.ts +19 -0
- package/client/app.ts +38 -0
- package/client/components/back-button.tsx +49 -0
- package/client/components/breadcrumbs.tsx +167 -0
- package/client/components/inbox-badge.ts +49 -0
- package/client/components/inbox-button.tsx +132 -0
- package/client/components/inbox-header-button.tsx +25 -0
- package/client/components/loading.tsx +35 -0
- package/client/components/page-container.tsx +24 -0
- package/client/components/page-header.tsx +33 -0
- package/client/components/route-child-page.tsx +121 -0
- package/client/components/route-dialog.tsx +10 -0
- package/client/components/route-drawer.tsx +10 -0
- package/client/components/route-overlay.tsx +190 -0
- package/client/components/ui/alert.tsx +75 -0
- package/client/components/ui/badge.tsx +51 -0
- package/client/components/ui/breadcrumb.tsx +121 -0
- package/client/components/ui/button.tsx +58 -0
- package/client/components/ui/collapsible.tsx +19 -0
- package/client/components/ui/context-menu.tsx +271 -0
- package/client/components/ui/dialog.tsx +160 -0
- package/client/components/ui/dropdown-menu.tsx +270 -0
- package/client/components/ui/empty.tsx +103 -0
- package/client/components/ui/field.tsx +236 -0
- package/client/components/ui/input-group.tsx +158 -0
- package/client/components/ui/input.tsx +20 -0
- package/client/components/ui/kbd.tsx +26 -0
- package/client/components/ui/label.tsx +18 -0
- package/client/components/ui/popover.tsx +89 -0
- package/client/components/ui/select.tsx +200 -0
- package/client/components/ui/separator.tsx +22 -0
- package/client/components/ui/sheet.tsx +137 -0
- package/client/components/ui/sidebar.tsx +723 -0
- package/client/components/ui/skeleton.tsx +13 -0
- package/client/components/ui/spinner.tsx +20 -0
- package/client/components/ui/tabs.tsx +79 -0
- package/client/components/ui/textarea.tsx +17 -0
- package/client/components/ui/toast.tsx +231 -0
- package/client/components/ui/tooltip.tsx +63 -0
- package/client/components/use-route-overlay.ts +26 -0
- package/client/config/api.ts +13 -0
- package/client/config/app.ts +8 -0
- package/client/config/auth.ts +18 -0
- package/client/config/index.ts +17 -0
- package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
- package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
- package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
- package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
- package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
- package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
- package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
- package/client/extensions/nocobase-file-component-ui/README.md +13 -0
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
- package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
- package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
- package/client/extensions/nocobase-inbox/frame.ts +31 -0
- package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
- package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
- package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
- package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
- package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
- package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
- package/client/extensions/nocobase-inbox/model.ts +428 -0
- package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
- package/client/extensions/nocobase-inbox/registry.ts +303 -0
- package/client/extensions/nocobase-inbox/source.ts +117 -0
- package/client/hooks/use-mobile.ts +21 -0
- package/client/index.tsx +60 -0
- package/client/layouts/app-layout.tsx +105 -0
- package/client/layouts/components/app-brand.tsx +49 -0
- package/client/layouts/components/app-sidebar.tsx +196 -0
- package/client/layouts/components/header-actions.tsx +50 -0
- package/client/layouts/components/language-switcher.tsx +95 -0
- package/client/layouts/components/layout-header.tsx +17 -0
- package/client/layouts/components/navigation-menu.tsx +411 -0
- package/client/layouts/components/sidebar-footer.tsx +107 -0
- package/client/layouts/components/surface-empty.tsx +35 -0
- package/client/layouts/components/user-menu.tsx +127 -0
- package/client/layouts/index.ts +2 -0
- package/client/layouts/settings-layout.tsx +172 -0
- package/client/layouts/use-sidebar-preference.ts +57 -0
- package/client/lib/constants.ts +5 -0
- package/client/lib/toaster.ts +56 -0
- package/client/lib/utils.ts +1 -0
- package/client/locales/en-US.ts +178 -0
- package/client/locales/index.ts +9 -0
- package/client/locales/zh-CN.ts +161 -0
- package/client/pages/auth/device.tsx +41 -0
- package/client/pages/auth/forgot-password.tsx +39 -0
- package/client/pages/auth/login.tsx +66 -0
- package/client/pages/auth/register.tsx +46 -0
- package/client/pages/auth/reset-password.tsx +38 -0
- package/client/pages/auth/shared.tsx +111 -0
- package/client/pages/home.tsx +16 -0
- package/client/pages/inbox.tsx +12 -0
- package/client/plugins.ts +37 -0
- package/client/react-providers.ts +27 -0
- package/client/route-overrides.ts +9 -0
- package/client/routes.ts +75 -0
- package/client/routing/app-router.tsx +127 -0
- package/client/routing/client-route.tsx +129 -0
- package/client/routing/route-context.tsx +85 -0
- package/client/routing/route-navigation.ts +153 -0
- package/client/routing/route-tree.tsx +42 -0
- package/client/routing/standalone-page-layout.tsx +15 -0
- package/client/runtime.ts +26 -0
- package/client/service-provider.ts +40 -0
- package/client/source-extensions.ts +19 -0
- package/client/startup.tsx +22 -0
- package/client/styles.css +122 -0
- package/client/theme/index.ts +4 -0
- package/client/theme/theme-context.ts +12 -0
- package/client/theme/theme-preferences.ts +84 -0
- package/client/theme/theme-presets.ts +7 -0
- package/client/theme/theme-provider.tsx +106 -0
- package/client/theme/theme-settings.tsx +176 -0
- package/client/theme/themes/compact.css +140 -0
- package/client/theme/themes/default.css +117 -0
- package/client/vite-env.d.ts +5 -0
- package/components.json +27 -0
- package/config.example.yml +156 -0
- package/database/main/migrations/.gitkeep +0 -0
- package/database/main/migrations/202610060001_create_device_code.ts +37 -0
- package/database/main/seeds/.gitkeep +0 -0
- package/database/tsconfig.json +4 -0
- package/ecosystem.config.js +35 -0
- package/eslint.config.js +24 -0
- package/index.html +100 -0
- package/package.json +198 -3
- package/playwright.config.ts +10 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/logo-dark.png +0 -0
- package/public/assets/logo-mark-dark.png +0 -0
- package/public/assets/logo-mark.png +0 -0
- package/public/assets/logo.png +0 -0
- package/server/app.ts +50 -0
- package/server/config/api.ts +8 -0
- package/server/config/app.ts +40 -0
- package/server/config/auth.ts +36 -0
- package/server/config/authorization.ts +18 -0
- package/server/config/caching.ts +21 -0
- package/server/config/database.ts +22 -0
- package/server/config/drive.ts +30 -0
- package/server/config/i18n.ts +17 -0
- package/server/config/index.ts +67 -0
- package/server/config/jobs.ts +27 -0
- package/server/config/logging.ts +23 -0
- package/server/config/notification.ts +11 -0
- package/server/config/queue.ts +28 -0
- package/server/config/scheduler.ts +23 -0
- package/server/config/secrets.ts +10 -0
- package/server/config/server.ts +26 -0
- package/server/config/session.ts +60 -0
- package/server/config/snowflake.ts +21 -0
- package/server/config/spa.ts +24 -0
- package/server/config/users.ts +11 -0
- package/server/config/workflow.ts +24 -0
- package/server/config.ts +24 -0
- package/server/embedded.ts +23 -0
- package/server/locales/en-US.ts +10 -0
- package/server/locales/index.ts +10 -0
- package/server/locales/zh-CN.ts +5 -0
- package/server/plugins.ts +37 -0
- package/server/providers/index.ts +5 -0
- package/server/routes/index.ts +6 -0
- package/server/runtime.ts +24 -0
- package/server/standalone.ts +29 -0
- package/server/tsconfig.json +4 -0
- package/tailwind.config.mjs +59 -0
- package/tests/components/app-layout-routes.test.tsx +132 -0
- package/tests/components/app-title.test.tsx +57 -0
- package/tests/components/auth-i18n.test.tsx +87 -0
- package/tests/components/back-button.test.tsx +80 -0
- package/tests/components/file-preview.test.tsx +101 -0
- package/tests/components/header-hover.test.tsx +87 -0
- package/tests/components/inbox.test.tsx +139 -0
- package/tests/components/language-switcher.test.tsx +298 -0
- package/tests/components/layout-containers.test.tsx +15 -0
- package/tests/components/layout-i18n.test.tsx +187 -0
- package/tests/components/loading.test.tsx +25 -0
- package/tests/components/navigation-menu.test.tsx +355 -0
- package/tests/components/page-harness.test.tsx +318 -0
- package/tests/components/primitive-labels.test.tsx +80 -0
- package/tests/components/sidebar-permissions.test.tsx +110 -0
- package/tests/components/sidebar-preference.test.tsx +42 -0
- package/tests/fixtures/theme-tokens.html +12 -0
- package/tests/fixtures/theme-tokens.tsx +111 -0
- package/tests/logic/account-permissions.test.tsx +204 -0
- package/tests/logic/app-locale-coverage.test.ts +136 -0
- package/tests/logic/app-server.test.ts +1589 -0
- package/tests/logic/asset-url-build.test.ts +106 -0
- package/tests/logic/client-auth.test.tsx +122 -0
- package/tests/logic/client-plugin-registry.test.ts +63 -0
- package/tests/logic/client-route-recovery.test.tsx +80 -0
- package/tests/logic/client-routes.test.ts +126 -0
- package/tests/logic/client-runtime.test.ts +112 -0
- package/tests/logic/client-settings.test.tsx +841 -0
- package/tests/logic/client-shell.test.tsx +466 -0
- package/tests/logic/client-theme.test.tsx +441 -0
- package/tests/logic/config.test.ts +197 -0
- package/tests/logic/dev-client-watch.test.ts +108 -0
- package/tests/logic/dev-routes-production-build.test.ts +168 -0
- package/tests/logic/device-code-migration.test.ts +70 -0
- package/tests/logic/lifecycle.test.ts +221 -0
- package/tests/logic/plugin-commands.test.ts +69 -0
- package/tests/logic/plugin-page-authz.test.ts +53 -0
- package/tests/logic/route-navigation.test.ts +66 -0
- package/tests/logic/route-overlay.test.tsx +353 -0
- package/tests/logic/server-imports.test.ts +88 -0
- package/tests/logic/tailwind-sources.test.ts +151 -0
- package/tests/logic/template-package-name.test.ts +88 -0
- package/tests/logic/theme-preferences.test.ts +86 -0
- package/tests/logic/theme-tokens.test.ts +248 -0
- package/tests/logic/toaster.test.tsx +108 -0
- package/tests/logic/user-menu-sign-out.test.tsx +98 -0
- package/tests/setup/client-config.ts +27 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +12 -0
- package/tsconfig.server.json +19 -0
- package/vite.config.ts +92 -0
- package/vitest.config.ts +44 -0
- package/README.md +0 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
3
|
+
import { ArrowLeft } from 'lucide-react';
|
|
4
|
+
import { useMemo, type ReactElement } from 'react';
|
|
5
|
+
import { Link, Navigate, Routes, useLocation, useNavigate } from 'react-router';
|
|
6
|
+
|
|
7
|
+
import { PageBreadcrumbProvider } from '@nocobase/app-client';
|
|
8
|
+
|
|
9
|
+
import { Breadcrumbs } from '#components/breadcrumbs';
|
|
10
|
+
import { Loading } from '#components/loading';
|
|
11
|
+
import { EMPTY_ARRAY } from '#lib/constants';
|
|
12
|
+
|
|
13
|
+
import { renderRouteTree } from '../routing/route-tree.js';
|
|
14
|
+
import { RouteTreeProvider } from '../routing/route-context.js';
|
|
15
|
+
import {
|
|
16
|
+
routeKey,
|
|
17
|
+
matchRouteTree,
|
|
18
|
+
navigationPages,
|
|
19
|
+
selectedNavigationId,
|
|
20
|
+
useRouteNavigation,
|
|
21
|
+
} from '../routing/route-navigation.js';
|
|
22
|
+
import { NavigationMenu } from './components/navigation-menu.js';
|
|
23
|
+
import { LayoutHeader } from './components/layout-header.js';
|
|
24
|
+
import {
|
|
25
|
+
AppSidebar,
|
|
26
|
+
AppSidebarProvider,
|
|
27
|
+
AppSidebarToggle,
|
|
28
|
+
} from './components/app-sidebar.js';
|
|
29
|
+
import { SurfaceEmpty, type SurfaceCopy } from './components/surface-empty.js';
|
|
30
|
+
import { HeaderActions } from './components/header-actions.js';
|
|
31
|
+
|
|
32
|
+
export interface SettingsLayoutProps {
|
|
33
|
+
readonly routeTree: readonly AppClientRegisteredRoute[];
|
|
34
|
+
readonly routes?: readonly AppClientRegisteredRoute[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SettingsLayout({
|
|
38
|
+
routeTree,
|
|
39
|
+
routes = EMPTY_ARRAY,
|
|
40
|
+
}: SettingsLayoutProps): ReactElement {
|
|
41
|
+
const location = useLocation();
|
|
42
|
+
const navigate = useNavigate();
|
|
43
|
+
const { t } = useTranslation();
|
|
44
|
+
const copy: SurfaceCopy = {
|
|
45
|
+
pathPrefix: '/settings',
|
|
46
|
+
title: t('settings.title', { defaultValue: 'Settings' }),
|
|
47
|
+
emptyTitle: t('settings.emptyTitle', {
|
|
48
|
+
defaultValue: 'No settings available',
|
|
49
|
+
}),
|
|
50
|
+
emptyDescription: t('settings.emptyDescription', {
|
|
51
|
+
defaultValue:
|
|
52
|
+
'No enabled plugin contributes a settings page you have access to.',
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const { items: navEntries, loading, denied } = useRouteNavigation(routeTree);
|
|
57
|
+
const selectedKey = selectedNavigationId(
|
|
58
|
+
routeTree,
|
|
59
|
+
location.pathname,
|
|
60
|
+
denied,
|
|
61
|
+
);
|
|
62
|
+
const visible = navigationPages(navEntries);
|
|
63
|
+
// The match below and every consumer of the route tree key off this, so it keeps its identity.
|
|
64
|
+
const allRoutes = useMemo(
|
|
65
|
+
() => [...routeTree, ...routes],
|
|
66
|
+
[routeTree, routes],
|
|
67
|
+
);
|
|
68
|
+
const matches = matchRouteTree(allRoutes, location.pathname);
|
|
69
|
+
if (loading)
|
|
70
|
+
return (
|
|
71
|
+
<Loading
|
|
72
|
+
className='min-h-svh'
|
|
73
|
+
label={t('surface.loading', {
|
|
74
|
+
title: copy.title,
|
|
75
|
+
defaultValue: `Loading ${copy.title}`,
|
|
76
|
+
})}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
if (
|
|
80
|
+
matches
|
|
81
|
+
?.filter(({ route }) => route.componentLoader)
|
|
82
|
+
.slice(0, 1)
|
|
83
|
+
.some(({ route }) => denied.has(routeKey(route))) ||
|
|
84
|
+
!matches?.some(({ route }) => route.componentLoader)
|
|
85
|
+
) {
|
|
86
|
+
return visible[0] ? (
|
|
87
|
+
<Navigate to={visible[0].path} replace />
|
|
88
|
+
) : (
|
|
89
|
+
<SurfaceEmpty copy={copy} />
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
// The header's trail reads the same route tree as the pages, and the trail a page declares.
|
|
95
|
+
<RouteTreeProvider routes={allRoutes}>
|
|
96
|
+
<PageBreadcrumbProvider>
|
|
97
|
+
<AppSidebarProvider>
|
|
98
|
+
<AppSidebar
|
|
99
|
+
label={t('surface.navigation', {
|
|
100
|
+
title: copy.title,
|
|
101
|
+
defaultValue: `${copy.title} navigation`,
|
|
102
|
+
})}
|
|
103
|
+
>
|
|
104
|
+
<NavigationMenu
|
|
105
|
+
items={navEntries}
|
|
106
|
+
label={copy.title}
|
|
107
|
+
selectedKey={selectedKey}
|
|
108
|
+
/>
|
|
109
|
+
</AppSidebar>
|
|
110
|
+
<div className='flex min-w-0 flex-1 flex-col'>
|
|
111
|
+
<LayoutHeader className='sticky top-0 z-40 justify-between'>
|
|
112
|
+
<div className='flex min-w-0 items-center gap-3'>
|
|
113
|
+
<AppSidebarToggle />
|
|
114
|
+
<div className='hidden h-5 w-px bg-border md:block' />
|
|
115
|
+
<Link
|
|
116
|
+
className='inline-flex min-w-0 items-center gap-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground'
|
|
117
|
+
to='/'
|
|
118
|
+
>
|
|
119
|
+
<ArrowLeft className='size-4 shrink-0' />
|
|
120
|
+
<span className='truncate'>
|
|
121
|
+
{t('surface.backToApp', { defaultValue: 'Back to app' })}
|
|
122
|
+
</span>
|
|
123
|
+
</Link>
|
|
124
|
+
<div className='hidden h-5 w-px shrink-0 bg-border md:block' />
|
|
125
|
+
<Breadcrumbs className='hidden md:block' denied={denied} />
|
|
126
|
+
</div>
|
|
127
|
+
<HeaderActions
|
|
128
|
+
showSettings={navigationPages(navEntries).length > 0}
|
|
129
|
+
/>
|
|
130
|
+
</LayoutHeader>
|
|
131
|
+
<main className='relative min-w-0 flex-1 overflow-hidden'>
|
|
132
|
+
{/* main only positions; the page scrolls in here, so a child page layer laid over main is neither
|
|
133
|
+
moved by the page's scrolling nor stretched by its height. */}
|
|
134
|
+
<div className='h-full overflow-y-auto'>
|
|
135
|
+
<label className='sr-only' htmlFor='surface-page'>
|
|
136
|
+
{t('surface.page', {
|
|
137
|
+
title: copy.title,
|
|
138
|
+
defaultValue: `${copy.title} page`,
|
|
139
|
+
})}
|
|
140
|
+
</label>
|
|
141
|
+
<select
|
|
142
|
+
id='surface-page'
|
|
143
|
+
className='m-3 h-9 w-[calc(100%-1.5rem)] min-w-0 rounded-xl border border-border/70 bg-background px-3 text-sm md:hidden'
|
|
144
|
+
value={
|
|
145
|
+
visible.find((route) => routeKey(route) === selectedKey)
|
|
146
|
+
?.path ?? ''
|
|
147
|
+
}
|
|
148
|
+
onChange={(event) => {
|
|
149
|
+
void navigate(event.target.value);
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{visible.map((route) => (
|
|
153
|
+
<option key={routeKey(route)} value={route.path}>
|
|
154
|
+
{t(route.navigation!.title, {
|
|
155
|
+
ns: route.packageName,
|
|
156
|
+
defaultValue: route.navigation!.title,
|
|
157
|
+
})}
|
|
158
|
+
</option>
|
|
159
|
+
))}
|
|
160
|
+
</select>
|
|
161
|
+
<Routes>
|
|
162
|
+
{renderRouteTree(routeTree, copy.pathPrefix)}
|
|
163
|
+
{renderRouteTree(routes, copy.pathPrefix)}
|
|
164
|
+
</Routes>
|
|
165
|
+
</div>
|
|
166
|
+
</main>
|
|
167
|
+
</div>
|
|
168
|
+
</AppSidebarProvider>
|
|
169
|
+
</PageBreadcrumbProvider>
|
|
170
|
+
</RouteTreeProvider>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useSyncExternalStore, type SetStateAction } from 'react';
|
|
2
|
+
|
|
3
|
+
// All applications and layout surfaces on this origin share this preference.
|
|
4
|
+
const storageKey = 'nocobase:sidebar:collapsed';
|
|
5
|
+
const changeEvent = 'nocobase:sidebar-preference-change';
|
|
6
|
+
let memoryValue = false;
|
|
7
|
+
let memoryOnly = false;
|
|
8
|
+
|
|
9
|
+
function getSnapshot(): boolean {
|
|
10
|
+
if (memoryOnly) return memoryValue;
|
|
11
|
+
try {
|
|
12
|
+
memoryValue = localStorage.getItem(storageKey) === 'true';
|
|
13
|
+
} catch {
|
|
14
|
+
// Storage may be unavailable; keep the current session's preference.
|
|
15
|
+
}
|
|
16
|
+
return memoryValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function subscribe(notify: () => void): () => void {
|
|
20
|
+
const onStorage = (event: StorageEvent) => {
|
|
21
|
+
if (event.key === storageKey || event.key === null) notify();
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener('storage', onStorage);
|
|
24
|
+
window.addEventListener(changeEvent, notify);
|
|
25
|
+
return () => {
|
|
26
|
+
window.removeEventListener('storage', onStorage);
|
|
27
|
+
window.removeEventListener(changeEvent, notify);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setCollapsed(update: SetStateAction<boolean>): void {
|
|
32
|
+
const current = getSnapshot();
|
|
33
|
+
memoryValue = typeof update === 'function' ? update(current) : update;
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(storageKey, String(memoryValue));
|
|
36
|
+
} catch {
|
|
37
|
+
memoryOnly = true;
|
|
38
|
+
// The toggle remains usable when storage is blocked.
|
|
39
|
+
}
|
|
40
|
+
window.dispatchEvent(new Event(changeEvent));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getServerSnapshot(): boolean {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useSidebarPreference(): readonly [
|
|
48
|
+
boolean,
|
|
49
|
+
(update: SetStateAction<boolean>) => void,
|
|
50
|
+
] {
|
|
51
|
+
const collapsed = useSyncExternalStore(
|
|
52
|
+
subscribe,
|
|
53
|
+
getSnapshot,
|
|
54
|
+
getServerSnapshot,
|
|
55
|
+
);
|
|
56
|
+
return [collapsed, setCollapsed];
|
|
57
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Toaster, ToastOptions } from '@nocobase/app-client';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { toast } from '#components/ui/toast';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The application's toaster, registered under `toasterToken` in `client/service-provider.ts`. Plugins and pages
|
|
8
|
+
* report through `useToaster()`; this forwards what they report to the Base UI toast manager rendered by the
|
|
9
|
+
* `Toaster` that `client/react-providers.ts` mounts. How a toast is presented is decided here, not at each call site,
|
|
10
|
+
* so changing the toast UI changes this file and nothing that shows a toast.
|
|
11
|
+
*
|
|
12
|
+
* The manager reaches the `Toaster` once it has subscribed, in an effect that runs when the application first renders
|
|
13
|
+
* and before any page does, since pages are lazy routes. A toast shown earlier than that, from a component mounted
|
|
14
|
+
* together with the `Toaster`, would be dropped.
|
|
15
|
+
*/
|
|
16
|
+
export function createToaster(manager: typeof toast = toast): Toaster {
|
|
17
|
+
return {
|
|
18
|
+
show: (options: ToastOptions): string =>
|
|
19
|
+
manager.add({
|
|
20
|
+
id: options.id,
|
|
21
|
+
type: options.type,
|
|
22
|
+
title: options.title,
|
|
23
|
+
description: options.description,
|
|
24
|
+
timeout: options.duration,
|
|
25
|
+
priority: isUrgent(options) ? 'high' : 'low',
|
|
26
|
+
actionProps: options.action
|
|
27
|
+
? { children: options.action.label, onClick: options.action.onClick }
|
|
28
|
+
: undefined,
|
|
29
|
+
onClose: options.onClose,
|
|
30
|
+
}),
|
|
31
|
+
// Base UI closes every toast when given no id; the contract closes one.
|
|
32
|
+
close: (id: string): void => {
|
|
33
|
+
if (id) manager.close(id);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base UI announces a high-priority toast at once, but hides the toast itself from assistive technology until the
|
|
40
|
+
* toast viewport is focused, and repeats its title and description in a visually hidden alert. That suits an error
|
|
41
|
+
* written as plain text. Every other error keeps the default priority: one with an action, or whose title or
|
|
42
|
+
* description is an element rather than text, may hold a control — Hub's technical details toggle is one — which a
|
|
43
|
+
* high priority would hide and duplicate.
|
|
44
|
+
*/
|
|
45
|
+
function isUrgent({ type, title, description, action }: ToastOptions): boolean {
|
|
46
|
+
return (
|
|
47
|
+
type === 'error' &&
|
|
48
|
+
action === undefined &&
|
|
49
|
+
isText(title) &&
|
|
50
|
+
(description === undefined || isText(description))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isText(node: ReactNode): boolean {
|
|
55
|
+
return typeof node === 'string' || typeof node === 'number';
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from 'cn';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { LocaleResource } from '@nocobase/i18n';
|
|
2
|
+
import deviceApprovalEnUS from '#extensions/nocobase-device-approval/locales/en-US';
|
|
3
|
+
import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
4
|
+
|
|
5
|
+
const enUS = {
|
|
6
|
+
// The UI Library block of the `/device` page; the keys below may reword it.
|
|
7
|
+
...deviceApprovalEnUS,
|
|
8
|
+
// The UI Library block of the `/inbox` page; the keys below may reword it.
|
|
9
|
+
...inboxEnUS,
|
|
10
|
+
// The UI Library component of the header's inbox button.
|
|
11
|
+
'inboxButton.title': 'Inbox',
|
|
12
|
+
'inboxButton.pending': 'Inbox, {{count}} waiting',
|
|
13
|
+
'inboxButton.unread': 'Inbox, {{count}} unread',
|
|
14
|
+
'inboxButton.pendingHint':
|
|
15
|
+
'{{count}} waiting for you; it goes down once they are handled.',
|
|
16
|
+
'inboxButton.unreadHint': '{{count}} unread; it goes down as you read them.',
|
|
17
|
+
'auth.welcome': 'Welcome back',
|
|
18
|
+
'auth.loginDescription': 'Sign in with your username or email and password.',
|
|
19
|
+
'auth.registerTitle': 'Create an account',
|
|
20
|
+
'auth.registerDescription': 'Create an account to get started.',
|
|
21
|
+
'auth.forgotTitle': 'Forgot password',
|
|
22
|
+
'auth.forgotDescription':
|
|
23
|
+
'Enter your email and we will send a reset link if the account exists.',
|
|
24
|
+
'auth.resetDescription': 'Choose a new password for your account.',
|
|
25
|
+
'auth.resetTitle': 'Reset password',
|
|
26
|
+
'auth.identifier': 'Username or email',
|
|
27
|
+
'auth.password': 'Password',
|
|
28
|
+
'auth.signIn': 'Sign in',
|
|
29
|
+
'auth.signingIn': 'Signing in…',
|
|
30
|
+
'auth.hidePassword': 'Hide password',
|
|
31
|
+
'auth.showPassword': 'Show password',
|
|
32
|
+
'auth.forgotLink': 'Forgot password?',
|
|
33
|
+
'auth.noAccount': "Don't have an account?",
|
|
34
|
+
'auth.signUp': 'Sign up',
|
|
35
|
+
'auth.createAccount': 'Create account',
|
|
36
|
+
'auth.creatingAccount': 'Creating account…',
|
|
37
|
+
'auth.name': 'Name',
|
|
38
|
+
'auth.username': 'Username',
|
|
39
|
+
'auth.email': 'Email',
|
|
40
|
+
'auth.confirmPassword': 'Confirm password',
|
|
41
|
+
'auth.existingAccount': 'Already have an account?',
|
|
42
|
+
'auth.passwordMismatch': "Passwords don't match.",
|
|
43
|
+
'auth.resetting': 'Resetting…',
|
|
44
|
+
'auth.newPassword': 'New password',
|
|
45
|
+
'auth.confirmNewPassword': 'Confirm new password',
|
|
46
|
+
'auth.invalidResetLink':
|
|
47
|
+
'This password reset link is invalid or has expired.',
|
|
48
|
+
'auth.backToSignIn': 'Back to sign in',
|
|
49
|
+
'auth.sendResetLink': 'Send reset link',
|
|
50
|
+
'auth.sending': 'Sending…',
|
|
51
|
+
'auth.resetSent': 'If the account exists, a reset link has been sent.',
|
|
52
|
+
'auth.rememberPassword': 'Remember your password?',
|
|
53
|
+
'auth.methods': 'Sign-in methods',
|
|
54
|
+
'auth.or': 'Or continue with',
|
|
55
|
+
'auth.continueWith': 'Continue with {provider}',
|
|
56
|
+
'auth.about': 'About this application',
|
|
57
|
+
'auth.platform': 'AI-native application platform',
|
|
58
|
+
'auth.marketingTitleFirst': 'Let AI build freely.',
|
|
59
|
+
'auth.marketingTitleSecond': 'NocoBase keeps it',
|
|
60
|
+
'auth.marketingTitleThird': 'reliable.',
|
|
61
|
+
'auth.marketingDescription':
|
|
62
|
+
'Give AI a flexible frontend framework to shape each experience, while NocoBase secures the data, permissions, workflows and governance underneath.',
|
|
63
|
+
'auth.frontend': 'AI-native frontend',
|
|
64
|
+
'auth.frontendDescription':
|
|
65
|
+
'Compose interfaces freely on a flexible framework.',
|
|
66
|
+
'auth.foundation': 'NocoBase foundation',
|
|
67
|
+
'auth.foundationDescription':
|
|
68
|
+
'Reliable data, access control, workflows and governance.',
|
|
69
|
+
'auth.marketingFooter': 'Freedom above. Confidence below.',
|
|
70
|
+
'status.loading': 'Loading',
|
|
71
|
+
'status.loadingPage': 'Loading page',
|
|
72
|
+
'status.loadingSettings': 'Loading settings',
|
|
73
|
+
'status.denied': 'Access denied',
|
|
74
|
+
'status.pageFailed': 'Unable to load page',
|
|
75
|
+
'status.retry': 'Retry',
|
|
76
|
+
'navigation.brandHome': 'NocoBase home',
|
|
77
|
+
'navigation.brandApps': 'NocoBase applications',
|
|
78
|
+
'routeOverlay.close': 'Close',
|
|
79
|
+
'status.deniedDescription': 'You do not have permission to access {{label}}.',
|
|
80
|
+
'status.routeFailedDescription':
|
|
81
|
+
'Route {{label}} from {{packageName}} could not be loaded.',
|
|
82
|
+
shell: {
|
|
83
|
+
buildFreely: 'AI builds freely.',
|
|
84
|
+
reliability: '<brand>NocoBase</brand> keeps it reliable.',
|
|
85
|
+
},
|
|
86
|
+
surface: {
|
|
87
|
+
backToApp: 'Back to app',
|
|
88
|
+
loading: 'Loading {{title}}',
|
|
89
|
+
navigation: '{{title}} navigation',
|
|
90
|
+
page: '{{title}} page',
|
|
91
|
+
},
|
|
92
|
+
settings: {
|
|
93
|
+
title: 'Settings',
|
|
94
|
+
emptyTitle: 'No settings available',
|
|
95
|
+
emptyDescription:
|
|
96
|
+
'No enabled plugin contributes a settings page you have access to.',
|
|
97
|
+
},
|
|
98
|
+
home: {
|
|
99
|
+
title: 'Start building your application',
|
|
100
|
+
description:
|
|
101
|
+
'Describe what you need to your AI Agent, then build pages, data models, and business workflows.',
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
appearance: {
|
|
105
|
+
title: 'Appearance',
|
|
106
|
+
mode: 'Color mode',
|
|
107
|
+
preset: 'Theme',
|
|
108
|
+
light: 'Light',
|
|
109
|
+
dark: 'Dark',
|
|
110
|
+
system: 'System',
|
|
111
|
+
themes: { default: 'Spacious', compact: 'Compact' },
|
|
112
|
+
},
|
|
113
|
+
app: {
|
|
114
|
+
title: 'NocoBase',
|
|
115
|
+
},
|
|
116
|
+
actions: {
|
|
117
|
+
close: 'Close',
|
|
118
|
+
save: 'Save',
|
|
119
|
+
cancel: 'Cancel',
|
|
120
|
+
confirm: 'Confirm',
|
|
121
|
+
language: 'Language',
|
|
122
|
+
},
|
|
123
|
+
notices: {
|
|
124
|
+
serverLocaleFallback:
|
|
125
|
+
'The server does not support this language, so server messages will use English.',
|
|
126
|
+
languageChangeFailed:
|
|
127
|
+
'Unable to complete the language change. Please try again.',
|
|
128
|
+
},
|
|
129
|
+
account: {
|
|
130
|
+
signOutFailed: 'Unable to sign out. Please try again.',
|
|
131
|
+
openMenu: 'Open account menu',
|
|
132
|
+
fallback: 'Account',
|
|
133
|
+
signOut: 'Sign out',
|
|
134
|
+
signingOut: 'Signing out…',
|
|
135
|
+
},
|
|
136
|
+
navigation: {
|
|
137
|
+
home: 'Home',
|
|
138
|
+
open: 'Open navigation',
|
|
139
|
+
close: 'Close navigation',
|
|
140
|
+
expand: 'Expand navigation',
|
|
141
|
+
collapse: 'Collapse navigation',
|
|
142
|
+
toggle: 'Expand or collapse navigation',
|
|
143
|
+
label: 'Application navigation',
|
|
144
|
+
description: 'Go to a page of this application.',
|
|
145
|
+
breadcrumb: 'Breadcrumb',
|
|
146
|
+
breadcrumbMore: 'Show the levels in between',
|
|
147
|
+
back: 'Back',
|
|
148
|
+
},
|
|
149
|
+
dataTable: {
|
|
150
|
+
noResults: 'No results.',
|
|
151
|
+
sortAscending: 'Asc',
|
|
152
|
+
sortDescending: 'Desc',
|
|
153
|
+
hideColumn: 'Hide',
|
|
154
|
+
view: 'View',
|
|
155
|
+
toggleColumns: 'Toggle columns',
|
|
156
|
+
selectedCount: '{{selected}} of {{total}} row(s) selected.',
|
|
157
|
+
rowsPerPage: 'Rows per page',
|
|
158
|
+
pageOf: 'Page {{page}} of {{pageCount}}',
|
|
159
|
+
firstPage: 'Go to first page',
|
|
160
|
+
previousPage: 'Go to previous page',
|
|
161
|
+
nextPage: 'Go to next page',
|
|
162
|
+
lastPage: 'Go to last page',
|
|
163
|
+
},
|
|
164
|
+
datePicker: {
|
|
165
|
+
placeholder: 'Pick a date',
|
|
166
|
+
rangePlaceholder: 'Pick a date range',
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The shape every locale of this application follows, derived from the English wording above.
|
|
172
|
+
*
|
|
173
|
+
* Anything a plugin does not translate falls back to this namespace, so a term defined here is reused everywhere
|
|
174
|
+
* without each plugin repeating it.
|
|
175
|
+
*/
|
|
176
|
+
export type AppResource = LocaleResource<typeof enUS>;
|
|
177
|
+
|
|
178
|
+
export default enUS;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LocaleLoaders } from '@nocobase/i18n';
|
|
2
|
+
|
|
3
|
+
// One dynamic import per locale, so the browser downloads only the language it is showing.
|
|
4
|
+
const locales: LocaleLoaders = {
|
|
5
|
+
'en-US': () => import('./en-US.js'),
|
|
6
|
+
'zh-CN': () => import('./zh-CN.js'),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default locales;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { AppResource } from './en-US.js';
|
|
2
|
+
import deviceApprovalZhCN from '#extensions/nocobase-device-approval/locales/zh-CN';
|
|
3
|
+
import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
|
|
4
|
+
|
|
5
|
+
const zhCN: AppResource = {
|
|
6
|
+
// The UI Library block of the `/device` page; the keys below may reword it.
|
|
7
|
+
...deviceApprovalZhCN,
|
|
8
|
+
// The UI Library block of the `/inbox` page; the keys below may reword it.
|
|
9
|
+
...inboxZhCN,
|
|
10
|
+
// The UI Library component of the header's inbox button.
|
|
11
|
+
'inboxButton.title': '收件箱',
|
|
12
|
+
'inboxButton.pending': '收件箱,{{count}} 项待处理',
|
|
13
|
+
'inboxButton.unread': '收件箱,{{count}} 条未读',
|
|
14
|
+
'inboxButton.pendingHint': '{{count}} 项等你处理,处理后减少。',
|
|
15
|
+
'inboxButton.unreadHint': '{{count}} 条未读,阅读后减少。',
|
|
16
|
+
'auth.welcome': '欢迎回来',
|
|
17
|
+
'auth.loginDescription': '使用用户名或邮箱和密码登录。',
|
|
18
|
+
'auth.registerTitle': '创建账户',
|
|
19
|
+
'auth.registerDescription': '创建账户以开始使用。',
|
|
20
|
+
'auth.forgotTitle': '忘记密码',
|
|
21
|
+
'auth.forgotDescription': '输入邮箱,如果账户存在,我们将发送重置链接。',
|
|
22
|
+
'auth.resetDescription': '为账户设置新密码。',
|
|
23
|
+
'auth.resetTitle': '重置密码',
|
|
24
|
+
'auth.identifier': '用户名或邮箱',
|
|
25
|
+
'auth.password': '密码',
|
|
26
|
+
'auth.signIn': '登录',
|
|
27
|
+
'auth.signingIn': '正在登录…',
|
|
28
|
+
'auth.hidePassword': '隐藏密码',
|
|
29
|
+
'auth.showPassword': '显示密码',
|
|
30
|
+
'auth.forgotLink': '忘记密码?',
|
|
31
|
+
'auth.noAccount': '还没有账户?',
|
|
32
|
+
'auth.signUp': '注册',
|
|
33
|
+
'auth.createAccount': '创建账户',
|
|
34
|
+
'auth.creatingAccount': '正在创建账户…',
|
|
35
|
+
'auth.name': '姓名',
|
|
36
|
+
'auth.username': '用户名',
|
|
37
|
+
'auth.email': '邮箱',
|
|
38
|
+
'auth.confirmPassword': '确认密码',
|
|
39
|
+
'auth.existingAccount': '已有账户?',
|
|
40
|
+
'auth.passwordMismatch': '两次输入的密码不一致。',
|
|
41
|
+
'auth.resetting': '正在重置…',
|
|
42
|
+
'auth.newPassword': '新密码',
|
|
43
|
+
'auth.confirmNewPassword': '确认新密码',
|
|
44
|
+
'auth.invalidResetLink': '此密码重置链接无效或已过期。',
|
|
45
|
+
'auth.backToSignIn': '返回登录',
|
|
46
|
+
'auth.sendResetLink': '发送重置链接',
|
|
47
|
+
'auth.sending': '正在发送…',
|
|
48
|
+
'auth.resetSent': '如果账户存在,重置链接已发送。',
|
|
49
|
+
'auth.rememberPassword': '记得密码?',
|
|
50
|
+
'auth.methods': '登录方式',
|
|
51
|
+
'auth.or': '或使用以下方式继续',
|
|
52
|
+
'auth.continueWith': '使用 {provider} 继续',
|
|
53
|
+
'auth.about': '关于此应用',
|
|
54
|
+
'auth.platform': 'AI 原生应用平台',
|
|
55
|
+
'auth.marketingTitleFirst': '让 AI 自由构建。',
|
|
56
|
+
'auth.marketingTitleSecond': 'NocoBase 保障',
|
|
57
|
+
'auth.marketingTitleThird': '可靠运行。',
|
|
58
|
+
'auth.marketingDescription':
|
|
59
|
+
'让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限、工作流与治理。',
|
|
60
|
+
'auth.frontend': 'AI 原生前端',
|
|
61
|
+
'auth.frontendDescription': '在灵活的框架上自由构建界面。',
|
|
62
|
+
'auth.foundation': 'NocoBase 基础能力',
|
|
63
|
+
'auth.foundationDescription': '可靠的数据、访问控制、工作流与治理。',
|
|
64
|
+
'auth.marketingFooter': '自由构建,可靠支撑。',
|
|
65
|
+
'status.loading': '加载中',
|
|
66
|
+
'status.loadingPage': '正在加载页面',
|
|
67
|
+
'status.loadingSettings': '正在加载设置',
|
|
68
|
+
'status.denied': '无权访问',
|
|
69
|
+
'status.pageFailed': '无法加载页面',
|
|
70
|
+
'status.retry': '重试',
|
|
71
|
+
'navigation.brandHome': 'NocoBase 首页',
|
|
72
|
+
'navigation.brandApps': 'NocoBase 应用',
|
|
73
|
+
'routeOverlay.close': '关闭',
|
|
74
|
+
'status.deniedDescription': '你没有访问 {{label}} 的权限。',
|
|
75
|
+
'status.routeFailedDescription':
|
|
76
|
+
'无法加载 {{packageName}} 的路由 {{label}}。',
|
|
77
|
+
shell: {
|
|
78
|
+
buildFreely: 'AI 自由构建。',
|
|
79
|
+
reliability: '<brand>NocoBase</brand> 保障可靠。',
|
|
80
|
+
},
|
|
81
|
+
surface: {
|
|
82
|
+
backToApp: '返回应用',
|
|
83
|
+
loading: '正在加载{{title}}',
|
|
84
|
+
navigation: '{{title}}导航',
|
|
85
|
+
page: '{{title}}页面',
|
|
86
|
+
},
|
|
87
|
+
settings: {
|
|
88
|
+
title: '设置',
|
|
89
|
+
emptyTitle: '暂无可用设置',
|
|
90
|
+
emptyDescription: '没有已启用的插件提供你有权访问的设置页面。',
|
|
91
|
+
},
|
|
92
|
+
home: {
|
|
93
|
+
title: '开始构建你的应用',
|
|
94
|
+
description: '向 AI 助手描述你的需求,逐步构建页面、数据模型和业务流程。',
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
appearance: {
|
|
98
|
+
title: '外观',
|
|
99
|
+
mode: '颜色模式',
|
|
100
|
+
preset: '主题',
|
|
101
|
+
light: '浅色',
|
|
102
|
+
dark: '深色',
|
|
103
|
+
system: '跟随系统',
|
|
104
|
+
themes: { default: '宽松', compact: '紧凑' },
|
|
105
|
+
},
|
|
106
|
+
app: {
|
|
107
|
+
title: 'NocoBase',
|
|
108
|
+
},
|
|
109
|
+
actions: {
|
|
110
|
+
close: '关闭',
|
|
111
|
+
save: '保存',
|
|
112
|
+
cancel: '取消',
|
|
113
|
+
confirm: '确认',
|
|
114
|
+
language: '语言',
|
|
115
|
+
},
|
|
116
|
+
notices: {
|
|
117
|
+
serverLocaleFallback: '服务端不支持该语言,服务端内容已回落为英文。',
|
|
118
|
+
languageChangeFailed: '未能完成语言切换,请重试。',
|
|
119
|
+
},
|
|
120
|
+
account: {
|
|
121
|
+
signOutFailed: '退出登录失败,请重试。',
|
|
122
|
+
openMenu: '打开账户菜单',
|
|
123
|
+
fallback: '账户',
|
|
124
|
+
signOut: '退出登录',
|
|
125
|
+
signingOut: '正在退出…',
|
|
126
|
+
},
|
|
127
|
+
navigation: {
|
|
128
|
+
home: '首页',
|
|
129
|
+
open: '打开导航',
|
|
130
|
+
close: '关闭导航',
|
|
131
|
+
expand: '展开导航',
|
|
132
|
+
collapse: '收起导航',
|
|
133
|
+
toggle: '展开或收起导航',
|
|
134
|
+
label: '应用导航',
|
|
135
|
+
description: '前往本应用的页面。',
|
|
136
|
+
breadcrumb: '面包屑',
|
|
137
|
+
breadcrumbMore: '显示中间层级',
|
|
138
|
+
back: '返回',
|
|
139
|
+
},
|
|
140
|
+
dataTable: {
|
|
141
|
+
noResults: '暂无数据。',
|
|
142
|
+
sortAscending: '升序',
|
|
143
|
+
sortDescending: '降序',
|
|
144
|
+
hideColumn: '隐藏',
|
|
145
|
+
view: '视图',
|
|
146
|
+
toggleColumns: '显示列',
|
|
147
|
+
selectedCount: '已选择 {{selected}} / {{total}} 行。',
|
|
148
|
+
rowsPerPage: '每页行数',
|
|
149
|
+
pageOf: '第 {{page}} 页,共 {{pageCount}} 页',
|
|
150
|
+
firstPage: '第一页',
|
|
151
|
+
previousPage: '上一页',
|
|
152
|
+
nextPage: '下一页',
|
|
153
|
+
lastPage: '最后一页',
|
|
154
|
+
},
|
|
155
|
+
datePicker: {
|
|
156
|
+
placeholder: '选择日期',
|
|
157
|
+
rangePlaceholder: '选择日期范围',
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default zhCN;
|