nsgm-cli 2.1.14 → 2.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -26
- package/client/components/ClientProviders.tsx +29 -0
- package/client/components/LanguageSwitcher.tsx +59 -0
- package/client/components/SSRSafeAntdProvider.tsx +24 -0
- package/client/components/SuppressHydrationWarnings.tsx +55 -0
- package/client/layout/index.tsx +82 -126
- package/client/styled/common.ts +0 -1
- package/client/styled/layout/index.ts +218 -104
- package/client/styled/template/manage.ts +88 -1
- package/client/utils/i18n.ts +68 -0
- package/client/utils/menu.tsx +42 -36
- package/client/utils/navigation.ts +58 -0
- package/client/utils/suppressWarnings.ts +32 -0
- package/eslint.config.js +17 -20
- package/generation/client/redux/reducers.ts +1 -1
- package/generation/client/utils/menu.tsx +36 -30
- package/generation/env +3 -0
- package/generation/next.config.js +7 -3
- package/generation/package.json +5 -2
- package/generation/tsconfig.json +6 -19
- package/lib/cli/commands/create.js +1 -1
- package/lib/cli/commands/delete.js +1 -1
- package/lib/cli/commands/init.js +6 -6
- package/lib/cli/utils/prompt.d.ts +1 -1
- package/lib/cli/utils/prompt.js +76 -117
- package/lib/constants.d.ts +6 -1
- package/lib/constants.js +12 -2
- package/lib/generate.js +1 -0
- package/lib/generate_create.js +14 -11
- package/lib/generate_delete.js +86 -9
- package/lib/generate_init.d.ts +6 -0
- package/lib/generate_init.js +125 -5
- package/lib/generators/file-generator.d.ts +48 -0
- package/lib/generators/file-generator.js +455 -0
- package/lib/generators/i18n-generator.d.ts +51 -0
- package/lib/generators/i18n-generator.js +320 -0
- package/lib/generators/page-generator.d.ts +6 -2
- package/lib/generators/page-generator.js +182 -156
- package/lib/generators/resolver-generator.d.ts +6 -4
- package/lib/generators/resolver-generator.js +114 -75
- package/lib/generators/service-generator.d.ts +4 -0
- package/lib/generators/service-generator.js +120 -6
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/next-i18next.config.js +18 -0
- package/next.config.js +55 -16
- package/package.json +7 -2
- package/pages/_app.tsx +84 -35
- package/pages/_document.tsx +39 -2
- package/pages/_error.tsx +66 -0
- package/pages/index.tsx +46 -29
- package/pages/login.tsx +58 -33
- package/pages/template/manage.tsx +95 -109
- package/public/locales/en-US/common.json +48 -0
- package/public/locales/en-US/home.json +57 -0
- package/public/locales/en-US/layout.json +22 -0
- package/public/locales/en-US/login.json +13 -0
- package/public/locales/en-US/template.json +42 -0
- package/public/locales/ja-JP/common.json +48 -0
- package/public/locales/ja-JP/home.json +57 -0
- package/public/locales/ja-JP/layout.json +22 -0
- package/public/locales/ja-JP/login.json +13 -0
- package/public/locales/ja-JP/template.json +42 -0
- package/public/locales/zh-CN/common.json +48 -0
- package/public/locales/zh-CN/home.json +57 -0
- package/public/locales/zh-CN/layout.json +22 -0
- package/public/locales/zh-CN/login.json +13 -0
- package/public/locales/zh-CN/template.json +42 -0
- package/server/utils/validation.js +163 -0
- package/types/i18next.d.ts +10 -0
package/README.md
CHANGED
|
@@ -31,22 +31,26 @@ NSGM CLI is a comprehensive full-stack development framework that combines the p
|
|
|
31
31
|
## 🎯 Key Features
|
|
32
32
|
|
|
33
33
|
### 🧙♂️ **Intelligent CLI Wizard**
|
|
34
|
+
|
|
34
35
|
- **Smart Mode Detection**: Automatically switches between interactive and command-line modes
|
|
35
36
|
- **Beginner-Friendly**: Step-by-step guided setup for newcomers
|
|
36
37
|
- **Expert-Efficient**: Quick command-line shortcuts for experienced developers
|
|
37
38
|
|
|
38
39
|
### ⚡ **Rapid Development**
|
|
40
|
+
|
|
39
41
|
- **Code Generation**: Automatic CRUD operations, API endpoints, and database schemas
|
|
40
42
|
- **Hot Reload**: Instant development feedback
|
|
41
43
|
- **Type Safety**: Full TypeScript support throughout the stack
|
|
42
44
|
|
|
43
45
|
### 🔒 **Production-Ready Security**
|
|
46
|
+
|
|
44
47
|
- **CSRF Protection**: Built-in cross-site request forgery prevention
|
|
45
48
|
- **Password Encryption**: bcrypt-based secure authentication
|
|
46
49
|
- **Session Management**: Robust user session handling
|
|
47
50
|
- **CSP Headers**: Content Security Policy implementation
|
|
48
51
|
|
|
49
52
|
### 🏗️ **Modern Tech Stack**
|
|
53
|
+
|
|
50
54
|
- **Frontend**: Next.js 13+, React 18+, Styled-components, Redux Toolkit
|
|
51
55
|
- **Backend**: Express.js, GraphQL, REST APIs
|
|
52
56
|
- **Database**: MySQL with native drivers
|
|
@@ -87,6 +91,7 @@ nsgm init my-awesome-app
|
|
|
87
91
|
```
|
|
88
92
|
|
|
89
93
|
The wizard will guide you through:
|
|
94
|
+
|
|
90
95
|
- ✅ Project name and directory
|
|
91
96
|
- ✅ Database configuration
|
|
92
97
|
- ✅ Security settings
|
|
@@ -99,9 +104,15 @@ cd your-project-name
|
|
|
99
104
|
|
|
100
105
|
# Copy environment template
|
|
101
106
|
cp .env.example .env
|
|
107
|
+
```
|
|
102
108
|
|
|
109
|
+
**Default Login**: `admin/admin123`
|
|
110
|
+
|
|
111
|
+
**To change password** (optional):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
103
114
|
# Generate secure password hash
|
|
104
|
-
npm run generate-password
|
|
115
|
+
npm run generate-password yourNewPassword
|
|
105
116
|
|
|
106
117
|
# Edit .env file with generated hash
|
|
107
118
|
nano .env
|
|
@@ -118,23 +129,24 @@ npm run dev
|
|
|
118
129
|
```
|
|
119
130
|
|
|
120
131
|
Your application will be available at `http://localhost:3000` with:
|
|
132
|
+
|
|
121
133
|
- 🎛️ Admin dashboard with CRUD interface
|
|
122
134
|
- 📊 Data import/export functionality
|
|
123
135
|
- 🗑️ Batch operations support
|
|
124
|
-
- 🔐 Secure login system
|
|
136
|
+
- 🔐 Secure login system (Default: admin/admin123)
|
|
125
137
|
|
|
126
138
|
## 🛠️ CLI Commands
|
|
127
139
|
|
|
128
140
|
### Core Commands
|
|
129
141
|
|
|
130
|
-
| Command
|
|
131
|
-
|
|
132
|
-
| `nsgm init`
|
|
133
|
-
| `nsgm create` | Generate controller with CRUD | Interactive/CLI | `nsgm create user`
|
|
134
|
-
| `nsgm delete` | Remove controller and files
|
|
135
|
-
| `nsgm dev`
|
|
136
|
-
| `nsgm build`
|
|
137
|
-
| `nsgm start`
|
|
142
|
+
| Command | Description | Mode | Example |
|
|
143
|
+
| ------------- | ----------------------------- | --------------- | --------------------- |
|
|
144
|
+
| `nsgm init` | Initialize new project | Interactive/CLI | `nsgm init blog-app` |
|
|
145
|
+
| `nsgm create` | Generate controller with CRUD | Interactive/CLI | `nsgm create user` |
|
|
146
|
+
| `nsgm delete` | Remove controller and files | Interactive/CLI | `nsgm delete product` |
|
|
147
|
+
| `nsgm dev` | Start development server | CLI | `nsgm dev` |
|
|
148
|
+
| `nsgm build` | Build for production | CLI | `nsgm build` |
|
|
149
|
+
| `nsgm start` | Start production server | CLI | `nsgm start` |
|
|
138
150
|
|
|
139
151
|
### Advanced Commands
|
|
140
152
|
|
|
@@ -157,6 +169,7 @@ npm run test:coverage # Test coverage report
|
|
|
157
169
|
Each controller created with `nsgm create` includes:
|
|
158
170
|
|
|
159
171
|
### 🔧 **Backend Components**
|
|
172
|
+
|
|
160
173
|
- **GraphQL Schema**: Typed queries and mutations
|
|
161
174
|
- **GraphQL Resolvers**: Business logic implementation
|
|
162
175
|
- **REST API Endpoints**: RESTful service layer
|
|
@@ -164,6 +177,7 @@ Each controller created with `nsgm create` includes:
|
|
|
164
177
|
- **Data Validation**: Input sanitization and validation
|
|
165
178
|
|
|
166
179
|
### 🎯 **Frontend Components**
|
|
180
|
+
|
|
167
181
|
- **React Components**: Modern functional components with hooks
|
|
168
182
|
- **Styled Components**: CSS-in-JS styling
|
|
169
183
|
- **Redux Integration**: State management
|
|
@@ -171,6 +185,7 @@ Each controller created with `nsgm create` includes:
|
|
|
171
185
|
- **Data Tables**: Sortable, filterable data grids
|
|
172
186
|
|
|
173
187
|
### 📊 **CRUD Operations**
|
|
188
|
+
|
|
174
189
|
- **Create**: Add new records with validation
|
|
175
190
|
- **Read**: List, search, and pagination
|
|
176
191
|
- **Update**: Edit existing records
|
|
@@ -211,7 +226,7 @@ your-project/
|
|
|
211
226
|
# .env file
|
|
212
227
|
NODE_ENV=development
|
|
213
228
|
LOGIN_USERNAME=admin
|
|
214
|
-
LOGIN_PASSWORD_HASH=your_generated_hash
|
|
229
|
+
LOGIN_PASSWORD_HASH=your_generated_hash # Default: admin123
|
|
215
230
|
DATABASE_URL=mysql://user:password@localhost:3306/dbname
|
|
216
231
|
|
|
217
232
|
# Optional
|
|
@@ -229,18 +244,22 @@ module.exports = {
|
|
|
229
244
|
port: process.env.DB_PORT || 3306,
|
|
230
245
|
user: process.env.DB_USER || 'root',
|
|
231
246
|
password: process.env.DB_PASSWORD || '',
|
|
232
|
-
database: process.env.DB_NAME || 'nsgm_db'
|
|
233
|
-
}
|
|
247
|
+
database: process.env.DB_NAME || 'nsgm_db',
|
|
248
|
+
},
|
|
234
249
|
}
|
|
235
250
|
```
|
|
236
251
|
|
|
237
252
|
## 🔒 Security Setup
|
|
238
253
|
|
|
239
|
-
###
|
|
254
|
+
### Default Authentication
|
|
255
|
+
|
|
256
|
+
**Default Login Credentials**: `admin/admin123`
|
|
257
|
+
|
|
258
|
+
### Custom Password Setup (Optional)
|
|
240
259
|
|
|
241
260
|
```bash
|
|
242
|
-
# Generate secure hash
|
|
243
|
-
npm run generate-password
|
|
261
|
+
# Generate secure hash for custom password
|
|
262
|
+
npm run generate-password yourNewPassword
|
|
244
263
|
|
|
245
264
|
# Add to .env file
|
|
246
265
|
LOGIN_PASSWORD_HASH=your_generated_hash_here
|
|
@@ -255,13 +274,15 @@ NSGM CLI includes built-in CSRF protection:
|
|
|
255
274
|
app.use(csrfProtection)
|
|
256
275
|
|
|
257
276
|
// Custom CSP headers
|
|
258
|
-
app.use(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
277
|
+
app.use(
|
|
278
|
+
createCSPMiddleware({
|
|
279
|
+
directives: {
|
|
280
|
+
defaultSrc: ["'self'"],
|
|
281
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
282
|
+
scriptSrc: ["'self'"],
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
)
|
|
265
286
|
```
|
|
266
287
|
|
|
267
288
|
## 🧪 Testing
|
|
@@ -312,7 +333,7 @@ router.get('/stats', (req, res) => {
|
|
|
312
333
|
res.json({
|
|
313
334
|
totalPosts: 42,
|
|
314
335
|
totalUsers: 15,
|
|
315
|
-
lastUpdate: new Date()
|
|
336
|
+
lastUpdate: new Date(),
|
|
316
337
|
})
|
|
317
338
|
})
|
|
318
339
|
|
|
@@ -341,7 +362,7 @@ module.exports = {
|
|
|
341
362
|
createdAt: Date!
|
|
342
363
|
updatedAt: Date!
|
|
343
364
|
}
|
|
344
|
-
|
|
365
|
+
`,
|
|
345
366
|
}
|
|
346
367
|
```
|
|
347
368
|
|
|
@@ -403,6 +424,7 @@ npm run tsbuild
|
|
|
403
424
|
### Common Issues
|
|
404
425
|
|
|
405
426
|
**Port already in use**
|
|
427
|
+
|
|
406
428
|
```bash
|
|
407
429
|
# Kill process on port 3000
|
|
408
430
|
lsof -ti:3000 | xargs kill -9
|
|
@@ -412,6 +434,7 @@ PORT=3001 npm run dev
|
|
|
412
434
|
```
|
|
413
435
|
|
|
414
436
|
**Database connection failed**
|
|
437
|
+
|
|
415
438
|
```bash
|
|
416
439
|
# Check MySQL service
|
|
417
440
|
sudo systemctl status mysql
|
|
@@ -421,6 +444,7 @@ cat .env | grep DB_
|
|
|
421
444
|
```
|
|
422
445
|
|
|
423
446
|
**Permission denied**
|
|
447
|
+
|
|
424
448
|
```bash
|
|
425
449
|
# Fix npm permissions
|
|
426
450
|
sudo chown -R $(whoami) ~/.npm
|
|
@@ -449,4 +473,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
449
473
|
[GitHub](https://github.com/erishen/nsgm) • [npm](https://www.npmjs.com/package/nsgm-cli) • [Issues](https://github.com/erishen/nsgm/issues)
|
|
450
474
|
|
|
451
475
|
</div>
|
|
452
|
-
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { ThemeProvider } from 'styled-components'
|
|
3
|
+
import { GlobalStyle } from '@/styled/common'
|
|
4
|
+
|
|
5
|
+
interface ClientProvidersProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
theme: any
|
|
8
|
+
whiteColor?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ClientProviders: React.FC<ClientProvidersProps> = ({ children, theme, whiteColor = true }) => {
|
|
12
|
+
const [isClient, setIsClient] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// 使用更安全的客户端检测
|
|
16
|
+
setIsClient(true)
|
|
17
|
+
}, [])
|
|
18
|
+
|
|
19
|
+
// 在服务端渲染时,使用一个占位符来保持结构一致性
|
|
20
|
+
// 但不渲染可能引起 useLayoutEffect 警告的组件
|
|
21
|
+
return (
|
|
22
|
+
<ThemeProvider theme={theme}>
|
|
23
|
+
{isClient && <GlobalStyle whiteColor={whiteColor} />}
|
|
24
|
+
{children}
|
|
25
|
+
</ThemeProvider>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default ClientProviders
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Select } from 'antd'
|
|
3
|
+
import { useRouter } from 'next/router'
|
|
4
|
+
import { GlobalOutlined } from '@ant-design/icons'
|
|
5
|
+
|
|
6
|
+
const { Option } = Select
|
|
7
|
+
|
|
8
|
+
interface LanguageSwitcherProps {
|
|
9
|
+
style?: React.CSSProperties
|
|
10
|
+
size?: 'small' | 'middle' | 'large'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ style, size = 'middle' }) => {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const [mounted, setMounted] = useState(false)
|
|
16
|
+
const [currentLocale, setCurrentLocale] = useState('zh-CN')
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
setMounted(true)
|
|
20
|
+
// 只在客户端获取当前语言
|
|
21
|
+
if (typeof window !== 'undefined' && router.locale) {
|
|
22
|
+
setCurrentLocale(router.locale)
|
|
23
|
+
}
|
|
24
|
+
}, [router.locale])
|
|
25
|
+
|
|
26
|
+
const languages = [
|
|
27
|
+
{ code: 'zh-CN', name: '中文', flag: '🇨🇳' },
|
|
28
|
+
{ code: 'en-US', name: 'English', flag: '🇺🇸' },
|
|
29
|
+
{ code: 'ja-JP', name: '日本語', flag: '🇯🇵' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const handleLanguageChange = (locale: string) => {
|
|
33
|
+
if (mounted && typeof window !== 'undefined') {
|
|
34
|
+
const { pathname, asPath, query } = router
|
|
35
|
+
router.push({ pathname, query }, asPath, { locale })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Select
|
|
41
|
+
value={currentLocale}
|
|
42
|
+
onChange={handleLanguageChange}
|
|
43
|
+
style={{ minWidth: 120, ...style }}
|
|
44
|
+
size={size}
|
|
45
|
+
suffixIcon={<GlobalOutlined />}
|
|
46
|
+
placeholder="Language"
|
|
47
|
+
disabled={!mounted}
|
|
48
|
+
>
|
|
49
|
+
{languages.map((language) => (
|
|
50
|
+
<Option key={language.code} value={language.code}>
|
|
51
|
+
<span style={{ marginRight: 8 }}>{language.flag}</span>
|
|
52
|
+
{language.name}
|
|
53
|
+
</Option>
|
|
54
|
+
))}
|
|
55
|
+
</Select>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default LanguageSwitcher
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ConfigProvider } from 'antd'
|
|
3
|
+
|
|
4
|
+
interface SSRSafeAntdProviderProps {
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
locale?: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SSRSafeAntdProvider: React.FC<SSRSafeAntdProviderProps> = ({ children, locale }) => {
|
|
10
|
+
// 在服务端渲染时,我们仍然使用 ConfigProvider,但使用简化的配置
|
|
11
|
+
return (
|
|
12
|
+
<ConfigProvider
|
|
13
|
+
locale={locale}
|
|
14
|
+
theme={{
|
|
15
|
+
// 确保服务端渲染的一致性
|
|
16
|
+
cssVar: false,
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</ConfigProvider>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default SSRSafeAntdProvider
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// 全局抑制 useLayoutEffect 警告的函数
|
|
4
|
+
const suppressUseLayoutEffectWarnings = () => {
|
|
5
|
+
if (typeof window === 'undefined' && process.env.NODE_ENV === 'development') {
|
|
6
|
+
const originalError = console.error
|
|
7
|
+
const originalWarn = console.warn
|
|
8
|
+
|
|
9
|
+
console.error = (...args) => {
|
|
10
|
+
const errorMessage = args[0]
|
|
11
|
+
if (
|
|
12
|
+
typeof errorMessage === 'string' &&
|
|
13
|
+
(errorMessage.includes('useLayoutEffect does nothing on the server') ||
|
|
14
|
+
errorMessage.includes('Warning: useLayoutEffect does nothing on the server'))
|
|
15
|
+
) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
originalError.apply(console, args)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.warn = (...args) => {
|
|
22
|
+
const warnMessage = args[0]
|
|
23
|
+
if (
|
|
24
|
+
typeof warnMessage === 'string' &&
|
|
25
|
+
(warnMessage.includes('useLayoutEffect does nothing on the server') ||
|
|
26
|
+
warnMessage.includes('Warning: useLayoutEffect does nothing on the server'))
|
|
27
|
+
) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
originalWarn.apply(console, args)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 返回清理函数
|
|
34
|
+
return () => {
|
|
35
|
+
console.error = originalError
|
|
36
|
+
console.warn = originalWarn
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 在模块加载时立即执行
|
|
43
|
+
suppressUseLayoutEffectWarnings()
|
|
44
|
+
|
|
45
|
+
const SuppressHydrationWarnings = () => {
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// 在客户端也抑制这些警告(以防万一)
|
|
48
|
+
const cleanup = suppressUseLayoutEffectWarnings()
|
|
49
|
+
return cleanup
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default SuppressHydrationWarnings
|
package/client/layout/index.tsx
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { Layout, Menu,
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { Layout, Menu, Dropdown, Space } from 'antd'
|
|
3
|
+
import {
|
|
4
|
+
Container,
|
|
5
|
+
FlexLayout,
|
|
6
|
+
StyledSider,
|
|
7
|
+
SideMenu,
|
|
8
|
+
ContentLayout,
|
|
9
|
+
StyledHeader,
|
|
10
|
+
StyledBreadcrumb,
|
|
11
|
+
StyledContent,
|
|
12
|
+
} from '@/styled/layout'
|
|
5
13
|
import { useRouter } from 'next/router'
|
|
6
14
|
import _ from 'lodash'
|
|
7
|
-
import menuConfig from '@/utils/menu'
|
|
8
|
-
import { logout } from '@/utils/sso'
|
|
15
|
+
import menuConfig, { getMenuConfig } from '@/utils/menu'
|
|
9
16
|
import getConfig from 'next/config'
|
|
10
|
-
import { LogoutOutlined
|
|
17
|
+
import { LogoutOutlined } from '@ant-design/icons'
|
|
18
|
+
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
|
19
|
+
import { useTranslation } from 'next-i18next'
|
|
20
|
+
import { navigateToLogin } from '@/utils/navigation'
|
|
11
21
|
|
|
12
22
|
interface SubMenuItem {
|
|
13
23
|
key: string
|
|
@@ -23,96 +33,10 @@ interface MenuItem {
|
|
|
23
33
|
subMenus?: SubMenuItem[]
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
const { Header, Content, Sider } = Layout
|
|
27
|
-
|
|
28
36
|
const nextConfig = getConfig()
|
|
29
37
|
const { publicRuntimeConfig } = nextConfig
|
|
30
38
|
const { prefix } = publicRuntimeConfig
|
|
31
39
|
|
|
32
|
-
// styled-components
|
|
33
|
-
const FlexLayout = styled(Layout)`
|
|
34
|
-
display: flex;
|
|
35
|
-
flex: 1;
|
|
36
|
-
`
|
|
37
|
-
const StyledSider = styled(Sider)`
|
|
38
|
-
display: flex;
|
|
39
|
-
flex-direction: column;
|
|
40
|
-
box-shadow: 2px 0 8px -4px rgba(0, 0, 0, 0.1);
|
|
41
|
-
z-index: 5;
|
|
42
|
-
position: relative;
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
const SideMenu = styled(Menu)`
|
|
46
|
-
height: 100%;
|
|
47
|
-
border-right: 0;
|
|
48
|
-
padding: 8px 0;
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
const ContentLayout = styled(Layout)`
|
|
52
|
-
display: flex;
|
|
53
|
-
flex-direction: column;
|
|
54
|
-
flex: 1;
|
|
55
|
-
background: #f5f7fa;
|
|
56
|
-
position: relative;
|
|
57
|
-
z-index: 1;
|
|
58
|
-
`
|
|
59
|
-
const StyledHeader = styled(Header)`
|
|
60
|
-
display: flex;
|
|
61
|
-
align-items: center;
|
|
62
|
-
padding: 0 24px;
|
|
63
|
-
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
|
64
|
-
z-index: 11;
|
|
65
|
-
|
|
66
|
-
.logo {
|
|
67
|
-
margin-right: 24px;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.main-menu {
|
|
71
|
-
flex: 1;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.user-actions {
|
|
75
|
-
display: flex;
|
|
76
|
-
align-items: center;
|
|
77
|
-
|
|
78
|
-
.action-icon {
|
|
79
|
-
font-size: 18px;
|
|
80
|
-
color: rgba(255, 255, 255, 0.85);
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
padding: 0 8px;
|
|
83
|
-
transition: color 0.3s;
|
|
84
|
-
|
|
85
|
-
&:hover {
|
|
86
|
-
color: #fff;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.user-dropdown {
|
|
91
|
-
cursor: pointer;
|
|
92
|
-
padding: 0 8px;
|
|
93
|
-
|
|
94
|
-
.username {
|
|
95
|
-
color: rgba(255, 255, 255, 0.85);
|
|
96
|
-
margin-left: 8px;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
`
|
|
101
|
-
const StyledBreadcrumb = styled(Breadcrumb)`
|
|
102
|
-
margin: 16px 24px;
|
|
103
|
-
font-size: 14px;
|
|
104
|
-
`
|
|
105
|
-
const StyledContent = styled(Content)`
|
|
106
|
-
margin: 0 24px 24px;
|
|
107
|
-
padding: 24px;
|
|
108
|
-
background: #fff;
|
|
109
|
-
border-radius: 4px;
|
|
110
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
111
|
-
min-height: calc(100vh - 180px);
|
|
112
|
-
position: relative;
|
|
113
|
-
z-index: 1;
|
|
114
|
-
`
|
|
115
|
-
|
|
116
40
|
const getLocationKey = () => {
|
|
117
41
|
const result = {
|
|
118
42
|
topMenu: '1',
|
|
@@ -169,7 +93,7 @@ const getLocationKey = () => {
|
|
|
169
93
|
}
|
|
170
94
|
|
|
171
95
|
const routerPush = (router: any, url: string) => {
|
|
172
|
-
if (router && url) {
|
|
96
|
+
if (router && url && typeof window !== 'undefined') {
|
|
173
97
|
if (prefix && url.indexOf(prefix) === -1) {
|
|
174
98
|
url = prefix + url
|
|
175
99
|
}
|
|
@@ -178,10 +102,35 @@ const routerPush = (router: any, url: string) => {
|
|
|
178
102
|
}
|
|
179
103
|
|
|
180
104
|
const LayoutComponent = ({ user, children }) => {
|
|
105
|
+
const { t } = useTranslation(['layout', 'common'])
|
|
181
106
|
const router = useRouter()
|
|
182
107
|
const [topMenuKey, setTopMenuKey] = useState('1')
|
|
183
108
|
const [sliderMenuKey, setSliderMenuKey] = useState('1')
|
|
184
109
|
const [collapsed, setCollapsed] = useState(false)
|
|
110
|
+
const [mounted, setMounted] = useState(false)
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
setMounted(true)
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
// 使用翻译后的菜单配置
|
|
117
|
+
const translatedMenuConfig = getMenuConfig(t)
|
|
118
|
+
|
|
119
|
+
// 自定义退出登录函数,保持语言设置
|
|
120
|
+
const handleLogout = () => {
|
|
121
|
+
if (!mounted || typeof window === 'undefined') return
|
|
122
|
+
|
|
123
|
+
// 删除登录相关的 cookie
|
|
124
|
+
const deleteCookie = (name: string) => {
|
|
125
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deleteCookie('_cas_nsgm')
|
|
129
|
+
deleteCookie('_cas_nsgm_user')
|
|
130
|
+
|
|
131
|
+
// 跳转到登录页面,保持当前语言
|
|
132
|
+
navigateToLogin(router)
|
|
133
|
+
}
|
|
185
134
|
|
|
186
135
|
useEffect(() => {
|
|
187
136
|
const { topMenu, slideMenu } = getLocationKey()
|
|
@@ -192,7 +141,7 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
192
141
|
const menuItems: any = []
|
|
193
142
|
const menuItemsVertical: any = []
|
|
194
143
|
|
|
195
|
-
_.each(
|
|
144
|
+
_.each(translatedMenuConfig, (item) => {
|
|
196
145
|
const { key, text, url, icon, subMenus } = item
|
|
197
146
|
|
|
198
147
|
if (key && text && url) {
|
|
@@ -200,13 +149,15 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
200
149
|
label: text,
|
|
201
150
|
key,
|
|
202
151
|
onClick: () => {
|
|
203
|
-
|
|
204
|
-
|
|
152
|
+
if (mounted) {
|
|
153
|
+
routerPush(router, url)
|
|
154
|
+
setTopMenuKey(key)
|
|
205
155
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
156
|
+
if (subMenus) {
|
|
157
|
+
setSliderMenuKey('1')
|
|
158
|
+
} else {
|
|
159
|
+
setSliderMenuKey('0')
|
|
160
|
+
}
|
|
210
161
|
}
|
|
211
162
|
},
|
|
212
163
|
}
|
|
@@ -225,13 +176,15 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
225
176
|
key: `slider_${subKey}`,
|
|
226
177
|
label: subText,
|
|
227
178
|
onClick: () => {
|
|
228
|
-
|
|
179
|
+
if (mounted) {
|
|
180
|
+
routerPush(router, subUrl)
|
|
229
181
|
|
|
230
|
-
|
|
231
|
-
|
|
182
|
+
const subKeyArr = subKey.split('_')
|
|
183
|
+
const subKeyArrLen = subKeyArr.length
|
|
232
184
|
|
|
233
|
-
|
|
234
|
-
|
|
185
|
+
if (subKeyArrLen >= 1) setTopMenuKey(subKeyArr[0])
|
|
186
|
+
if (subKeyArrLen >= 2) setSliderMenuKey(subKeyArr[1])
|
|
187
|
+
}
|
|
235
188
|
},
|
|
236
189
|
}
|
|
237
190
|
|
|
@@ -260,9 +213,11 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
260
213
|
icon,
|
|
261
214
|
key: `slider_${key}_0`,
|
|
262
215
|
onClick: () => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
216
|
+
if (mounted) {
|
|
217
|
+
routerPush(router, url)
|
|
218
|
+
setTopMenuKey(key)
|
|
219
|
+
setSliderMenuKey('0')
|
|
220
|
+
}
|
|
266
221
|
},
|
|
267
222
|
}
|
|
268
223
|
|
|
@@ -276,7 +231,7 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
276
231
|
<Container>
|
|
277
232
|
<StyledHeader>
|
|
278
233
|
<div className="logo">
|
|
279
|
-
<
|
|
234
|
+
<span className="logo-text">NSGM</span>
|
|
280
235
|
</div>
|
|
281
236
|
<Menu
|
|
282
237
|
theme="dark"
|
|
@@ -288,39 +243,40 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
288
243
|
/>
|
|
289
244
|
<div className="user-actions">
|
|
290
245
|
<Space size={20} align="center">
|
|
291
|
-
<
|
|
246
|
+
<LanguageSwitcher size="small" />
|
|
247
|
+
{/* <Tooltip title="通知">
|
|
292
248
|
<BellOutlined className="action-icon" />
|
|
293
249
|
</Tooltip>
|
|
294
250
|
<Tooltip title="设置">
|
|
295
251
|
<SettingOutlined className="action-icon" />
|
|
296
|
-
</Tooltip>
|
|
252
|
+
</Tooltip> */}
|
|
297
253
|
<Dropdown
|
|
298
254
|
menu={{
|
|
299
255
|
items: [
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
},
|
|
256
|
+
// {
|
|
257
|
+
// key: '1',
|
|
258
|
+
// icon: <UserOutlined />,
|
|
259
|
+
// label: t('layout:layout.userActions.profile'),
|
|
260
|
+
// },
|
|
261
|
+
// {
|
|
262
|
+
// key: '2',
|
|
263
|
+
// icon: <SettingOutlined />,
|
|
264
|
+
// label: t('layout:layout.userActions.settings'),
|
|
265
|
+
// },
|
|
310
266
|
{
|
|
311
267
|
type: 'divider',
|
|
312
268
|
},
|
|
313
269
|
{
|
|
314
270
|
key: '3',
|
|
315
271
|
icon: <LogoutOutlined />,
|
|
316
|
-
label: '
|
|
317
|
-
onClick: () =>
|
|
272
|
+
label: t('layout:layout.userActions.logout'),
|
|
273
|
+
onClick: () => handleLogout(),
|
|
318
274
|
},
|
|
319
275
|
],
|
|
320
276
|
}}
|
|
321
277
|
>
|
|
322
278
|
<Space className="user-dropdown">
|
|
323
|
-
<span className="username">{user?.displayName || '
|
|
279
|
+
<span className="username">{user?.displayName || t('layout:layout.userActions.user')}</span>
|
|
324
280
|
</Space>
|
|
325
281
|
</Dropdown>
|
|
326
282
|
</Space>
|
|
@@ -346,10 +302,10 @@ const LayoutComponent = ({ user, children }) => {
|
|
|
346
302
|
/>
|
|
347
303
|
</div>
|
|
348
304
|
</StyledSider>
|
|
349
|
-
<ContentLayout className="content-layout">
|
|
305
|
+
<ContentLayout collapsed={collapsed} className="content-layout">
|
|
350
306
|
<StyledBreadcrumb
|
|
351
307
|
items={_.compact(
|
|
352
|
-
_.flatMap(
|
|
308
|
+
_.flatMap(translatedMenuConfig, (item, index) => {
|
|
353
309
|
const { key, text, subMenus } = item
|
|
354
310
|
|
|
355
311
|
if (subMenus) {
|