nsbp-cli 0.2.44 → 0.2.47
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 +1 -1
- package/package.json +1 -1
- package/templates/basic/.env.development +1 -0
- package/templates/basic/.env.example +9 -0
- package/templates/basic/.env.production +8 -0
- package/templates/basic/.husky/commit-msg +0 -3
- package/templates/basic/.husky/pre-commit +0 -3
- package/templates/basic/.husky/pre-push +0 -3
- package/templates/basic/Makefile +17 -17
- package/templates/basic/{docker/docker-compose.dev.yml → docker-compose.dev.yml} +3 -3
- package/templates/basic/{docker/docker-compose.yml → docker-compose.yml} +2 -2
- package/templates/basic/gitignore +1 -0
- package/templates/basic/package.json +8 -9
- package/templates/basic/src/client/index.tsx +4 -4
- package/templates/basic/src/containers/Home.tsx +18 -14
- package/templates/basic/src/containers/Login.tsx +1 -1
- package/templates/basic/src/containers/Photo.tsx +58 -33
- package/templates/basic/src/externals/window.d.ts +2 -2
- package/templates/basic/src/reducers/home.ts +6 -1
- package/templates/basic/src/reducers/index.ts +6 -1
- package/templates/basic/src/reducers/photo.ts +8 -2
- package/templates/basic/src/server/index.ts +3 -0
- package/templates/basic/src/server/photo.ts +25 -12
- package/templates/basic/src/server/utils.tsx +33 -18
- package/templates/basic/src/services/home.ts +10 -4
- package/templates/basic/src/services/photo.ts +40 -13
- package/templates/basic/src/styled/common.ts +2 -2
- package/templates/basic/src/utils/fetch.ts +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ node ./bin/nsbp.js --help # Test CLI locally
|
|
|
147
147
|
|
|
148
148
|
- **Package Name**: `nsbp-cli`
|
|
149
149
|
- **Bin Command**: `nsbp` (install globally and run `nsbp --help`)
|
|
150
|
-
- **Version**: `0.2.
|
|
150
|
+
- **Version**: `0.2.47`
|
|
151
151
|
- **Dependencies**: chalk, commander, fs-extra, inquirer
|
|
152
152
|
- **Package Manager**: Uses pnpm (also compatible with npm)
|
|
153
153
|
- **Node Version**: >=16.0.0
|
package/package.json
CHANGED
|
@@ -14,6 +14,14 @@ ENABLE_RATE_LIMIT=0
|
|
|
14
14
|
# Docker 镜像标签(可选)
|
|
15
15
|
# DOCKER_IMAGE_TAG=latest
|
|
16
16
|
|
|
17
|
+
# Docker 主机端口映射(可选,默认值见下方说明)
|
|
18
|
+
# 生产环境主机端口(默认:8081)
|
|
19
|
+
HOST_PORT=8081
|
|
20
|
+
# 开发环境主机端口(默认:3001)
|
|
21
|
+
HOST_PORT_DEV=3001
|
|
22
|
+
# 调试端口(开发环境使用,默认:9229)
|
|
23
|
+
DEBUG_PORT=9229
|
|
24
|
+
|
|
17
25
|
# ==================== 调试配置 ====================
|
|
18
26
|
# 启用详细日志(开发时有用)
|
|
19
27
|
# DEBUG=nsbp:*
|
|
@@ -31,3 +39,4 @@ ENABLE_RATE_LIMIT=0
|
|
|
31
39
|
# 2. 敏感信息(密钥、密码)请放在 .env.local 中
|
|
32
40
|
# 3. .env.local 不会被提交到 Git,适合存放敏感信息
|
|
33
41
|
# 4. 优先级:.env.local > .env > 默认值
|
|
42
|
+
# 5. 修改端口后需要重启 Docker 容器:docker-compose down && docker-compose up -d
|
|
@@ -15,3 +15,11 @@ ENABLE_RATE_LIMIT=1
|
|
|
15
15
|
# ==================== 其他配置 ====================
|
|
16
16
|
# 时区
|
|
17
17
|
TZ=Asia/Shanghai
|
|
18
|
+
|
|
19
|
+
# Docker 主机端口映射(可选,默认值见下方说明)
|
|
20
|
+
# 生产环境主机端口(默认:8081)
|
|
21
|
+
HOST_PORT=8091
|
|
22
|
+
# 开发环境主机端口(默认:3001)
|
|
23
|
+
HOST_PORT_DEV=3001
|
|
24
|
+
# 调试端口(开发环境使用,默认:9229)
|
|
25
|
+
DEBUG_PORT=9229
|
package/templates/basic/Makefile
CHANGED
|
@@ -19,57 +19,57 @@ help: ## Show this help message
|
|
|
19
19
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
|
20
20
|
|
|
21
21
|
build: ## Build Docker images for production
|
|
22
|
-
$(COMPOSE)
|
|
22
|
+
$(COMPOSE) build
|
|
23
23
|
|
|
24
24
|
build-dev: ## Build Docker images for development
|
|
25
|
-
$(COMPOSE) -f docker
|
|
25
|
+
$(COMPOSE) -f docker-compose.dev.yml build
|
|
26
26
|
|
|
27
27
|
dev: ## Start development environment (removes orphan containers)
|
|
28
|
-
$(COMPOSE) -f docker
|
|
28
|
+
$(COMPOSE) -f docker-compose.dev.yml up --build --remove-orphans
|
|
29
29
|
|
|
30
30
|
prod: ## Start production environment (removes orphan containers)
|
|
31
|
-
$(COMPOSE)
|
|
31
|
+
$(COMPOSE) up -d --remove-orphans
|
|
32
32
|
|
|
33
33
|
down: ## Stop and remove containers (including orphan containers)
|
|
34
34
|
@echo "Stopping production containers..."
|
|
35
|
-
@$(COMPOSE)
|
|
35
|
+
@$(COMPOSE) down --remove-orphans || echo "Warning: Failed to stop production containers"
|
|
36
36
|
@echo "Stopping development containers..."
|
|
37
|
-
@$(COMPOSE) -f docker
|
|
37
|
+
@$(COMPOSE) -f docker-compose.dev.yml down --remove-orphans || echo "Warning: Failed to stop development containers"
|
|
38
38
|
@echo "Cleaning up any remaining nsbp containers..."
|
|
39
39
|
@docker ps -a --filter "name=nsbp-" --format "{{.Names}}" | xargs -r docker stop
|
|
40
40
|
@docker ps -a --filter "name=nsbp-" --format "{{.Names}}" | xargs -r docker rm
|
|
41
41
|
@echo "Cleanup complete!"
|
|
42
42
|
|
|
43
43
|
clean: ## Stop containers, remove images and volumes (including orphan containers)
|
|
44
|
-
$(COMPOSE)
|
|
45
|
-
$(COMPOSE) -f docker
|
|
44
|
+
$(COMPOSE) down -v --rmi all --remove-orphans
|
|
45
|
+
$(COMPOSE) -f docker-compose.dev.yml down -v --rmi all --remove-orphans
|
|
46
46
|
|
|
47
47
|
logs: ## View logs
|
|
48
|
-
$(COMPOSE)
|
|
48
|
+
$(COMPOSE) logs -f
|
|
49
49
|
|
|
50
50
|
logs-dev: ## View development logs
|
|
51
|
-
$(COMPOSE) -f docker
|
|
51
|
+
$(COMPOSE) -f docker-compose.dev.yml logs -f
|
|
52
52
|
|
|
53
53
|
restart: ## Restart production containers
|
|
54
|
-
$(COMPOSE)
|
|
54
|
+
$(COMPOSE) restart
|
|
55
55
|
|
|
56
56
|
restart-dev: ## Restart development containers
|
|
57
|
-
$(COMPOSE) -f docker
|
|
57
|
+
$(COMPOSE) -f docker-compose.dev.yml restart
|
|
58
58
|
|
|
59
59
|
rebuild: ## Rebuild and restart production containers (removes orphan containers)
|
|
60
|
-
$(COMPOSE)
|
|
60
|
+
$(COMPOSE) up -d --build --remove-orphans
|
|
61
61
|
|
|
62
62
|
rebuild-dev: ## Rebuild and restart development containers (removes orphan containers)
|
|
63
|
-
$(COMPOSE) -f docker
|
|
63
|
+
$(COMPOSE) -f docker-compose.dev.yml up -d --build --remove-orphans
|
|
64
64
|
|
|
65
65
|
shell: ## Open shell in production container
|
|
66
|
-
$(COMPOSE)
|
|
66
|
+
$(COMPOSE) exec app sh
|
|
67
67
|
|
|
68
68
|
shell-dev: ## Open shell in development container
|
|
69
|
-
$(COMPOSE) -f docker
|
|
69
|
+
$(COMPOSE) -f docker-compose.dev.yml exec app sh
|
|
70
70
|
|
|
71
71
|
test: ## Run tests (if configured)
|
|
72
|
-
$(COMPOSE)
|
|
72
|
+
$(COMPOSE) exec app $(PM) test
|
|
73
73
|
|
|
74
74
|
env-dev: ## Set up development environment
|
|
75
75
|
@if [ -f .env.development ]; then \
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
services:
|
|
5
5
|
app:
|
|
6
6
|
build:
|
|
7
|
-
context:
|
|
7
|
+
context: .
|
|
8
8
|
dockerfile: docker/Dockerfile.dev
|
|
9
9
|
container_name: nsbp-app-dev
|
|
10
10
|
ports:
|
|
11
|
-
- "3001
|
|
12
|
-
- "9229:9229" # Node.js debug port
|
|
11
|
+
- "${HOST_PORT_DEV:-3001}:${PORT:-3001}"
|
|
12
|
+
- "${DEBUG_PORT:-9229}:9229" # Node.js debug port
|
|
13
13
|
environment:
|
|
14
14
|
- NODE_ENV=${NODE_ENV:-development}
|
|
15
15
|
- PORT=${PORT:-3001}
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
services:
|
|
5
5
|
app:
|
|
6
6
|
build:
|
|
7
|
-
context:
|
|
7
|
+
context: .
|
|
8
8
|
dockerfile: docker/Dockerfile
|
|
9
9
|
container_name: nsbp-app
|
|
10
10
|
ports:
|
|
11
|
-
- "
|
|
11
|
+
- "${HOST_PORT:-8081}:${PORT:-3001}"
|
|
12
12
|
environment:
|
|
13
13
|
- NODE_ENV=${NODE_ENV:-production}
|
|
14
14
|
- PORT=${PORT:-3001}
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
"@babel/preset-react": "^7.24.0",
|
|
66
66
|
"@babel/preset-typescript": "^7.25.0",
|
|
67
67
|
"@eslint/js": "^9.39.2",
|
|
68
|
-
"@jest/globals": "^30.
|
|
68
|
+
"@jest/globals": "^30.0.5",
|
|
69
69
|
"@loadable/babel-plugin": "^5.13.2",
|
|
70
70
|
"@loadable/webpack-plugin": "^5.15.0",
|
|
71
71
|
"@playwright/test": "^1.44.0",
|
|
72
|
-
"@testing-library/jest-dom": "^6.
|
|
73
|
-
"@testing-library/react": "^
|
|
74
|
-
"@testing-library/user-event": "^14.
|
|
72
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
73
|
+
"@testing-library/react": "^16.1.0",
|
|
74
|
+
"@testing-library/user-event": "^14.6.1",
|
|
75
75
|
"@types/express": "^5.0.6",
|
|
76
76
|
"@types/jest": "^30.0.0",
|
|
77
77
|
"@types/loadable__component": "^5.13.3",
|
|
@@ -81,7 +81,6 @@
|
|
|
81
81
|
"@types/react": "^19.2.8",
|
|
82
82
|
"@types/react-dom": "^19.2.3",
|
|
83
83
|
"@types/react-helmet": "^6.1.10",
|
|
84
|
-
"@types/react-router-dom": "^5.3.3",
|
|
85
84
|
"@types/serialize-javascript": "^5.0.3",
|
|
86
85
|
"@types/styled-components": "^5.1.36",
|
|
87
86
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -102,8 +101,8 @@
|
|
|
102
101
|
"globals": "^17.0.0",
|
|
103
102
|
"html-webpack-plugin": "^5.6.0",
|
|
104
103
|
"husky": "^9.0.0",
|
|
105
|
-
"jest": "^
|
|
106
|
-
"jest-environment-jsdom": "^
|
|
104
|
+
"jest": "^30.0.5",
|
|
105
|
+
"jest-environment-jsdom": "^30.0.5",
|
|
107
106
|
"less": "^4.2.0",
|
|
108
107
|
"less-loader": "^12.0.0",
|
|
109
108
|
"lint-staged": "^15.0.0",
|
|
@@ -118,9 +117,9 @@
|
|
|
118
117
|
"sass-loader": "^16.0.0",
|
|
119
118
|
"style-loader": "^4.0.0",
|
|
120
119
|
"terser-webpack-plugin": "^5.3.0",
|
|
121
|
-
"ts-jest": "^29.
|
|
120
|
+
"ts-jest": "^29.4.6",
|
|
122
121
|
"ts-loader": "^9.5.0",
|
|
123
|
-
"typescript": "^5.
|
|
122
|
+
"typescript": "^5.8.3",
|
|
124
123
|
"typescript-loadable-components-plugin": "^1.0.2",
|
|
125
124
|
"webpack": "^5.96.0",
|
|
126
125
|
"webpack-cli": "^6.0.1",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
2
|
import { hydrateRoot } from 'react-dom/client'
|
|
3
3
|
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
4
4
|
import routers from '@/Routers'
|
|
@@ -9,13 +9,13 @@ import Theme from '@components/Theme'
|
|
|
9
9
|
import { loadableReady } from '@loadable/component'
|
|
10
10
|
|
|
11
11
|
const App = () => {
|
|
12
|
-
//
|
|
13
|
-
const
|
|
12
|
+
// 使用 useMemo 确保 store 只创建一次,避免每次渲染都创建新 store
|
|
13
|
+
const store = useMemo(() => {
|
|
14
14
|
if (isSEO() && window?.context?.state) {
|
|
15
15
|
return getStore(window.context.state)
|
|
16
16
|
}
|
|
17
17
|
return getStore()
|
|
18
|
-
})
|
|
18
|
+
}, []) // 空依赖数组,只执行一次
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
21
|
<Theme>
|
|
@@ -107,13 +107,30 @@ const Home: React.FC = () => {
|
|
|
107
107
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
108
108
|
|
|
109
109
|
useEffect(() => {
|
|
110
|
-
//
|
|
110
|
+
// 客户端模拟页面加载动画
|
|
111
111
|
const timer = setTimeout(() => {
|
|
112
112
|
setIsLoaded(true)
|
|
113
113
|
}, 500)
|
|
114
114
|
return () => clearTimeout(timer)
|
|
115
115
|
}, [])
|
|
116
116
|
|
|
117
|
+
// 客户端处理页面加载器的淡出和移除
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!isLoaded) return
|
|
120
|
+
|
|
121
|
+
const removeLoader = () => {
|
|
122
|
+
const loader = document.getElementById('pageLoader')
|
|
123
|
+
if (loader) {
|
|
124
|
+
loader.classList.add('fade-out')
|
|
125
|
+
setTimeout(() => loader.remove(), 500)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 延迟移除加载器
|
|
130
|
+
const removeTimer = setTimeout(removeLoader, 300)
|
|
131
|
+
return () => clearTimeout(removeTimer)
|
|
132
|
+
}, [isLoaded])
|
|
133
|
+
|
|
117
134
|
return (
|
|
118
135
|
<GlobalStyle>
|
|
119
136
|
{!isLoaded && (
|
|
@@ -121,19 +138,6 @@ const Home: React.FC = () => {
|
|
|
121
138
|
<div className="loader-spinner"></div>
|
|
122
139
|
</div>
|
|
123
140
|
)}
|
|
124
|
-
<script
|
|
125
|
-
dangerouslySetInnerHTML={{
|
|
126
|
-
__html: `
|
|
127
|
-
setTimeout(() => {
|
|
128
|
-
const loader = document.getElementById('pageLoader');
|
|
129
|
-
if (loader) {
|
|
130
|
-
loader.classList.add('fade-out');
|
|
131
|
-
setTimeout(() => loader.remove(), 500);
|
|
132
|
-
}
|
|
133
|
-
}, 800);
|
|
134
|
-
`
|
|
135
|
-
}}
|
|
136
|
-
/>
|
|
137
141
|
<Helmet>
|
|
138
142
|
<title>Nsbp.js - 轻量级 React SSR 框架</title>
|
|
139
143
|
<meta
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Fragment, useState, useEffect, useRef } from 'react'
|
|
1
|
+
import React, { Fragment, useState, useEffect, useRef, useMemo } from 'react'
|
|
2
2
|
import { connect } from 'react-redux'
|
|
3
3
|
import { Link, useLocation } from 'react-router-dom'
|
|
4
4
|
import Header from '@components/Header'
|
|
@@ -7,29 +7,44 @@ import { Helmet } from 'react-helmet'
|
|
|
7
7
|
import { Container, Row } from '@styled/photo'
|
|
8
8
|
import { motion } from 'framer-motion'
|
|
9
9
|
import { isSEO, getLocationParams, usePreserveNSBP } from '@/utils'
|
|
10
|
-
import { useCurrentFlag } from '@utils/clientConfig'
|
|
11
10
|
import _ from 'lodash'
|
|
12
11
|
import { loadDataForContainer } from '@services/photo'
|
|
13
12
|
|
|
14
13
|
const springSettings = { type: 'spring' as const, stiffness: 170, damping: 26 }
|
|
15
14
|
const NEXT = 'show-next'
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
interface QueryParams {
|
|
17
|
+
from?: string
|
|
18
|
+
nsbp?: string | number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PhotoProps {
|
|
22
|
+
query: QueryParams
|
|
23
|
+
data: [number, number, string][]
|
|
24
|
+
menu: Array<{ name: string; cover?: string; count?: number }>
|
|
25
|
+
getPhotoMenu: (dic: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Photo = ({ query, data, menu, getPhotoMenu }: PhotoProps) => {
|
|
18
29
|
const location = useLocation()
|
|
19
30
|
let { from } = query
|
|
20
31
|
const { withNSBP } = usePreserveNSBP()
|
|
21
|
-
|
|
32
|
+
// 使用 useMemo 缓存 photos,避免每次渲染都创建新数组
|
|
33
|
+
const photos = useMemo(() => (Array.isArray(data) ? data : []), [data])
|
|
22
34
|
const [currPhoto, setCurrPhoto] = useState(0)
|
|
23
35
|
// 使用 ref 来跟踪初始的 dic 值,用于区分首次加载和分类切换
|
|
24
36
|
const initialDicRef = useRef<string | null>(null)
|
|
25
37
|
|
|
26
|
-
const [currPhotoData, setCurrPhotoData] = useState
|
|
38
|
+
const [currPhotoData, setCurrPhotoData] = useState<[number, number, string]>(
|
|
39
|
+
photos[0] || [0, 0, '']
|
|
40
|
+
)
|
|
27
41
|
|
|
28
42
|
const [currWidth, currHeight] = currPhotoData
|
|
29
43
|
|
|
30
|
-
const widths = photos.map(
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const widths = photos.map((photo) => {
|
|
45
|
+
const [origW, origH] = photo
|
|
46
|
+
return (currHeight / origH) * origW
|
|
47
|
+
})
|
|
33
48
|
|
|
34
49
|
// 同步 currPhoto 和 currPhotoData
|
|
35
50
|
useEffect(() => {
|
|
@@ -40,30 +55,33 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
40
55
|
|
|
41
56
|
const leftStartCoords = widths
|
|
42
57
|
.slice(0, currPhoto)
|
|
43
|
-
.reduce((sum:
|
|
58
|
+
.reduce((sum: number, width: number) => sum - width, 0)
|
|
44
59
|
|
|
45
60
|
// Calculate position for each photo
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
interface PhotoPosition {
|
|
62
|
+
left: number
|
|
63
|
+
height: number
|
|
64
|
+
width: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const photoPositions = photos.reduce<PhotoPosition[]>((acc, _item, i) => {
|
|
68
|
+
const prevLeft =
|
|
69
|
+
i === 0 ? leftStartCoords : acc[i - 1].left + acc[i - 1].width
|
|
70
|
+
acc.push({
|
|
71
|
+
left: prevLeft,
|
|
72
|
+
height: currHeight,
|
|
73
|
+
width: widths[i] || 0
|
|
74
|
+
})
|
|
75
|
+
return acc
|
|
76
|
+
}, [])
|
|
59
77
|
|
|
60
78
|
// console.log('photoPositions', photoPositions)
|
|
61
79
|
|
|
62
|
-
const handleChange = (
|
|
63
|
-
setCurrPhoto(value)
|
|
80
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
81
|
+
setCurrPhoto(Number(e.target.value))
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
const clickHandler = (btn:
|
|
84
|
+
const clickHandler = (btn: string) => {
|
|
67
85
|
let photoIndex = btn === NEXT ? currPhoto + 1 : currPhoto - 1
|
|
68
86
|
|
|
69
87
|
photoIndex = photoIndex >= 0 ? photoIndex : photos.length - 1
|
|
@@ -101,6 +119,7 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
101
119
|
|
|
102
120
|
// 重置到第一张
|
|
103
121
|
setCurrPhoto(0)
|
|
122
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
123
|
}, [location?.search, from])
|
|
105
124
|
|
|
106
125
|
return (
|
|
@@ -114,7 +133,7 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
114
133
|
<Layout query={query}>
|
|
115
134
|
<Container>
|
|
116
135
|
<Row>
|
|
117
|
-
{_.map(menu, (item:
|
|
136
|
+
{_.map(menu, (item: { name: string }, index: number) => {
|
|
118
137
|
return (
|
|
119
138
|
<Link
|
|
120
139
|
key={`menu${index}`}
|
|
@@ -143,7 +162,7 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
143
162
|
animate={{ height: currHeight, width: currWidth }}
|
|
144
163
|
transition={springSettings}
|
|
145
164
|
>
|
|
146
|
-
{photoPositions.map((pos
|
|
165
|
+
{photoPositions.map((pos, i) => (
|
|
147
166
|
<motion.img
|
|
148
167
|
key={i}
|
|
149
168
|
className="demo4-photo"
|
|
@@ -175,7 +194,15 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
175
194
|
)
|
|
176
195
|
}
|
|
177
196
|
|
|
178
|
-
|
|
197
|
+
interface RootState {
|
|
198
|
+
query: QueryParams
|
|
199
|
+
photo: {
|
|
200
|
+
menu: Array<{ name: string; cover?: string; count?: number }>
|
|
201
|
+
data: [number, number, string][]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const mapStateToProps = (state: RootState) => {
|
|
179
206
|
return {
|
|
180
207
|
query: state?.query,
|
|
181
208
|
menu: state?.photo?.menu,
|
|
@@ -183,10 +210,8 @@ const mapStateToProps = (state: any) => {
|
|
|
183
210
|
}
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
const mapDispatchToProps =
|
|
187
|
-
getPhotoMenu: (dic:
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
})
|
|
213
|
+
const mapDispatchToProps = {
|
|
214
|
+
getPhotoMenu: (dic: string) => loadDataForContainer(null, dic)
|
|
215
|
+
}
|
|
191
216
|
|
|
192
217
|
export default connect(mapStateToProps, mapDispatchToProps)(Photo)
|
|
@@ -2,10 +2,10 @@ interface ServerState {
|
|
|
2
2
|
photo?: {
|
|
3
3
|
data?: [number, number, string][]
|
|
4
4
|
menu?:
|
|
5
|
-
| Record<string,
|
|
5
|
+
| Record<string, unknown>
|
|
6
6
|
| Array<{ name: string; cover?: string; count?: number }>
|
|
7
7
|
}
|
|
8
|
-
query?: Record<string,
|
|
8
|
+
query?: Record<string, unknown>
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
declare interface Window {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { GITHUB_ZEITNEXT_GET } from '@store/constants'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface HomeAction {
|
|
4
|
+
type: string
|
|
5
|
+
data?: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const homeReducer = (state = { data: {} }, action: HomeAction) => {
|
|
4
9
|
const { type, data } = action
|
|
5
10
|
|
|
6
11
|
switch (type) {
|
|
@@ -2,7 +2,12 @@ import { homeReducer } from './home'
|
|
|
2
2
|
import { photoReducer } from './photo'
|
|
3
3
|
import { REQUEST_QUERY } from '@store/constants'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface QueryAction {
|
|
6
|
+
type: string
|
|
7
|
+
query?: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const queryReducer = (state = {}, action: QueryAction) => {
|
|
6
11
|
const { type, query } = action
|
|
7
12
|
|
|
8
13
|
switch (type) {
|
|
@@ -3,13 +3,19 @@ import { GET_PHOTO_MENU, GET_PHOTO_WIDTH_HEIGHT } from '@store/constants'
|
|
|
3
3
|
interface PhotoState {
|
|
4
4
|
data: [number, number, string][]
|
|
5
5
|
menu:
|
|
6
|
-
| Record<string,
|
|
6
|
+
| Record<string, unknown>
|
|
7
7
|
| Array<{ name: string; cover?: string; count?: number }>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface PhotoAction {
|
|
11
|
+
type: string
|
|
12
|
+
data?: [number, number, string][]
|
|
13
|
+
menu?: PhotoState['menu']
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export const photoReducer = (
|
|
11
17
|
state: PhotoState = { data: [], menu: {} },
|
|
12
|
-
action:
|
|
18
|
+
action: PhotoAction
|
|
13
19
|
) => {
|
|
14
20
|
const { type, data, menu } = action
|
|
15
21
|
|
|
@@ -62,7 +62,16 @@ const getFileMenu = (
|
|
|
62
62
|
return result
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
interface Request {
|
|
66
|
+
query: { dic?: string }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Response {
|
|
70
|
+
status: (code: number) => Response
|
|
71
|
+
json: (data: unknown) => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const getPhotoWH = (req: Request, res: Response) => {
|
|
66
75
|
try {
|
|
67
76
|
const dic = req.query.dic || ''
|
|
68
77
|
const photosDicPath = getPhotosDicPath()
|
|
@@ -72,7 +81,7 @@ export const getPhotoWH = (req: any, res: any) => {
|
|
|
72
81
|
return res.status(400).json({ error: 'Invalid directory name' })
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
const fileList:
|
|
84
|
+
const fileList: string[] = []
|
|
76
85
|
let photoPath = photosDicPath
|
|
77
86
|
if (dic) {
|
|
78
87
|
photoPath = path.join(photosDicPath, dic)
|
|
@@ -108,25 +117,28 @@ export const getPhotoWH = (req: any, res: any) => {
|
|
|
108
117
|
|
|
109
118
|
getFileList(photoPath, fileList)
|
|
110
119
|
|
|
111
|
-
const whArr:
|
|
112
|
-
fileList.forEach((item:
|
|
120
|
+
const whArr: [number, number, string][] = []
|
|
121
|
+
fileList.forEach((item: string, index: number) => {
|
|
113
122
|
const data = fs.readFileSync(item)
|
|
114
123
|
let fileName = path.relative(photosDicPath, fileList[index])
|
|
115
|
-
const
|
|
124
|
+
const result = probe.sync(data)
|
|
125
|
+
const width = result?.width ?? 0
|
|
126
|
+
const height = result?.height ?? 0
|
|
116
127
|
whArr.push([width, height, fileName])
|
|
117
128
|
})
|
|
118
129
|
|
|
119
130
|
// 按前端期望的格式包装
|
|
120
131
|
res.json({ data: whArr })
|
|
121
|
-
} catch (err
|
|
122
|
-
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const error = err as Error
|
|
134
|
+
console.error('getPhotoWH error:', error)
|
|
123
135
|
res
|
|
124
136
|
.status(500)
|
|
125
|
-
.json({ error: 'Internal Server Error', details:
|
|
137
|
+
.json({ error: 'Internal Server Error', details: error.message })
|
|
126
138
|
}
|
|
127
139
|
}
|
|
128
140
|
|
|
129
|
-
export const getPhotoMenu = (_req:
|
|
141
|
+
export const getPhotoMenu = (_req: Request, res: Response) => {
|
|
130
142
|
try {
|
|
131
143
|
const photosDicPath = getPhotosDicPath()
|
|
132
144
|
|
|
@@ -134,10 +146,11 @@ export const getPhotoMenu = (_req: any, res: any) => {
|
|
|
134
146
|
|
|
135
147
|
// 按前端期望的格式包装
|
|
136
148
|
res.json({ data: fileMenu })
|
|
137
|
-
} catch (err
|
|
138
|
-
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const error = err as Error
|
|
151
|
+
console.error('getPhotoMenu error:', error)
|
|
139
152
|
res
|
|
140
153
|
.status(500)
|
|
141
|
-
.json({ error: 'Internal Server Error', details:
|
|
154
|
+
.json({ error: 'Internal Server Error', details: error.message })
|
|
142
155
|
}
|
|
143
156
|
}
|
|
@@ -13,10 +13,25 @@ import Theme from '@components/Theme'
|
|
|
13
13
|
import path from 'path'
|
|
14
14
|
import { ChunkExtractor } from '@loadable/server'
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface Request {
|
|
17
|
+
path: string
|
|
18
|
+
query: Record<string, string | undefined>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Response {
|
|
22
|
+
send: (html: string) => void
|
|
23
|
+
status: (code: number) => { send: (msg: string) => void }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const render = (req: Request, res: Response) => {
|
|
17
27
|
const store = getStore()
|
|
18
28
|
const { path: reqPath, query } = req
|
|
19
|
-
const matchRoutes:
|
|
29
|
+
const matchRoutes: {
|
|
30
|
+
loadData?: (
|
|
31
|
+
resolve: (data?: unknown) => void,
|
|
32
|
+
query: Request['query']
|
|
33
|
+
) => unknown
|
|
34
|
+
}[] = []
|
|
20
35
|
const promises = []
|
|
21
36
|
|
|
22
37
|
let { nsbp } = query
|
|
@@ -29,13 +44,13 @@ export const render = (req: any, res: any) => {
|
|
|
29
44
|
matchPath(reqPath, route.path) ? matchRoutes.push(route) : ''
|
|
30
45
|
})
|
|
31
46
|
|
|
32
|
-
matchRoutes.forEach((item
|
|
47
|
+
matchRoutes.forEach((item) => {
|
|
33
48
|
if (item?.loadData) {
|
|
34
49
|
const promise = new Promise((resolve, reject) => {
|
|
35
50
|
try {
|
|
36
51
|
// 将 query 参数传递给 loadData,确保能正确预取数据
|
|
37
52
|
store.dispatch(item?.loadData(resolve, query))
|
|
38
|
-
} catch
|
|
53
|
+
} catch {
|
|
39
54
|
reject()
|
|
40
55
|
}
|
|
41
56
|
})
|
|
@@ -44,20 +59,20 @@ export const render = (req: any, res: any) => {
|
|
|
44
59
|
}
|
|
45
60
|
})
|
|
46
61
|
|
|
47
|
-
const queryDispatch = (callback:
|
|
48
|
-
return (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
const queryDispatch = (callback: (() => void) | null) => {
|
|
63
|
+
return (
|
|
64
|
+
dispatch: (action: { type: string; query: Request['query'] }) => void
|
|
65
|
+
) => {
|
|
66
|
+
// 直接同步执行,避免 setTimeout 导致的竞态条件
|
|
67
|
+
dispatch({ type: REQUEST_QUERY, query })
|
|
68
|
+
callback && callback()
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
const queryPromise = new Promise((resolve, reject) => {
|
|
58
73
|
try {
|
|
59
|
-
store.dispatch(queryDispatch(resolve))
|
|
60
|
-
} catch
|
|
74
|
+
store.dispatch(queryDispatch(() => resolve()))
|
|
75
|
+
} catch {
|
|
61
76
|
reject()
|
|
62
77
|
}
|
|
63
78
|
})
|
|
@@ -70,7 +85,7 @@ export const render = (req: any, res: any) => {
|
|
|
70
85
|
const sheet = new ServerStyleSheet()
|
|
71
86
|
const serverState = store.getState()
|
|
72
87
|
|
|
73
|
-
const helmet
|
|
88
|
+
const helmet = Helmet.renderStatic()
|
|
74
89
|
|
|
75
90
|
const webStats = path.resolve(__dirname, '../public/loadable-stats.json')
|
|
76
91
|
|
|
@@ -140,14 +155,14 @@ export const render = (req: any, res: any) => {
|
|
|
140
155
|
`
|
|
141
156
|
|
|
142
157
|
res.send(html)
|
|
143
|
-
} catch (e:
|
|
144
|
-
console.error('SSR rendering error:', e)
|
|
158
|
+
} catch (e: Error) {
|
|
159
|
+
console.error('SSR rendering error:', e.message)
|
|
145
160
|
sheet.seal()
|
|
146
161
|
res.status(500).send('Internal Server Error')
|
|
147
162
|
}
|
|
148
163
|
})
|
|
149
|
-
.catch((e:
|
|
150
|
-
console.error('Data loading error:', e)
|
|
164
|
+
.catch((e: Error) => {
|
|
165
|
+
console.error('Data loading error:', e.message)
|
|
151
166
|
res.status(500).send('Data loading failed')
|
|
152
167
|
})
|
|
153
168
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { doGet } from '@utils/fetch'
|
|
2
2
|
import { GET_PHOTO_MENU } from '@store/constants'
|
|
3
|
+
import type { Dispatch } from 'redux'
|
|
4
|
+
import type { AxiosResponse } from 'axios'
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
interface PhotoMenuResponse {
|
|
7
|
+
data?: unknown[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const loadData = (resolve: ((data?: unknown) => void) | null = null) => {
|
|
11
|
+
return (dispatch: Dispatch) => {
|
|
6
12
|
// 预取图片菜单数据
|
|
7
13
|
doGet('/getPhotoMenu')
|
|
8
|
-
.then((res:
|
|
14
|
+
.then((res: AxiosResponse<PhotoMenuResponse>) => {
|
|
9
15
|
// axios 响应结构: { data, status, statusText, headers, config, request }
|
|
10
16
|
if (res.status >= 200 && res.status < 300) {
|
|
11
17
|
// 请求成功,data 已经是解析后的 JSON 对象
|
|
@@ -18,7 +24,7 @@ export const loadData = (resolve: any = null) => {
|
|
|
18
24
|
throw new Error(`Status ${res.status}`)
|
|
19
25
|
}
|
|
20
26
|
})
|
|
21
|
-
.catch((err:
|
|
27
|
+
.catch((err: Error) => {
|
|
22
28
|
console.error('Failed to preload photos:', err)
|
|
23
29
|
// 预取失败但不影响主流程
|
|
24
30
|
resolve && resolve()
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { doGet } from '@utils/fetch'
|
|
2
2
|
import { GET_PHOTO_MENU, GET_PHOTO_WIDTH_HEIGHT } from '@store/constants'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
type DispatchFunction = (action: {
|
|
5
|
+
type: string
|
|
6
|
+
data?: unknown
|
|
7
|
+
menu?: unknown
|
|
8
|
+
}) => void
|
|
9
|
+
|
|
10
|
+
type CallbackFunction = (data?: unknown) => void
|
|
11
|
+
|
|
12
|
+
const getPhotoWH = (
|
|
13
|
+
dispatch: DispatchFunction,
|
|
14
|
+
callback: CallbackFunction,
|
|
15
|
+
dic = ''
|
|
16
|
+
) => {
|
|
5
17
|
let action = 'getPhotoWH'
|
|
6
18
|
if (dic) {
|
|
7
19
|
action += `?dic=${dic}`
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
doGet(action)
|
|
11
|
-
.then((res:
|
|
23
|
+
.then((res: { data?: { data?: unknown } }) => {
|
|
12
24
|
// console.log('getPhotoWH_res', res)
|
|
13
25
|
// axios 响应格式是 { data: { data: [...] }, status: ... },需要取 res.data.data
|
|
14
26
|
dispatch({
|
|
@@ -17,14 +29,17 @@ const getPhotoWH = (dispatch: any, callback: any, dic = '') => {
|
|
|
17
29
|
})
|
|
18
30
|
callback && callback()
|
|
19
31
|
})
|
|
20
|
-
.catch((
|
|
32
|
+
.catch(() => {
|
|
21
33
|
callback && callback()
|
|
22
34
|
})
|
|
23
35
|
}
|
|
24
36
|
|
|
25
|
-
const getPhotoMenu = (
|
|
37
|
+
const getPhotoMenu = (
|
|
38
|
+
dispatch: DispatchFunction,
|
|
39
|
+
callback: CallbackFunction
|
|
40
|
+
) => {
|
|
26
41
|
doGet('getPhotoMenu')
|
|
27
|
-
.then((res:
|
|
42
|
+
.then((res: { data?: { data?: unknown } }) => {
|
|
28
43
|
// console.log('getPhotoMenu_res', res)
|
|
29
44
|
// axios 响应格式是 { data: { data: [...] }, status: ... },需要取 res.data.data
|
|
30
45
|
const { data } = res?.data || {}
|
|
@@ -35,22 +50,28 @@ const getPhotoMenu = (dispatch: any, callback: any) => {
|
|
|
35
50
|
|
|
36
51
|
callback && callback(data)
|
|
37
52
|
})
|
|
38
|
-
.catch((
|
|
53
|
+
.catch(() => {
|
|
39
54
|
callback && callback()
|
|
40
55
|
})
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
interface MenuItem {
|
|
59
|
+
name: string
|
|
60
|
+
cover?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const getData = (callback: CallbackFunction, dic: string) => {
|
|
64
|
+
return (dispatch: DispatchFunction) => {
|
|
45
65
|
if (dic) {
|
|
46
66
|
getPhotoMenu(dispatch, () => {
|
|
47
67
|
getPhotoWH(dispatch, callback, dic)
|
|
48
68
|
})
|
|
49
69
|
} else {
|
|
50
|
-
getPhotoMenu(dispatch, (data
|
|
51
|
-
|
|
70
|
+
getPhotoMenu(dispatch, (data) => {
|
|
71
|
+
const menuData = data as MenuItem[] | undefined
|
|
72
|
+
if (menuData && menuData.length > 0) {
|
|
52
73
|
// data[0] 是对象 {name, cover},需要取 name
|
|
53
|
-
getPhotoWH(dispatch, callback,
|
|
74
|
+
getPhotoWH(dispatch, callback, menuData[0].name)
|
|
54
75
|
}
|
|
55
76
|
})
|
|
56
77
|
}
|
|
@@ -58,13 +79,19 @@ const getData = (callback: any, dic: any) => {
|
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
// 用于路由预取数据的 loadData 函数
|
|
61
|
-
export const loadData = (
|
|
82
|
+
export const loadData = (
|
|
83
|
+
resolve: CallbackFunction | null = null,
|
|
84
|
+
query: { dic?: string } = {}
|
|
85
|
+
) => {
|
|
62
86
|
// 从 URL 查询参数中获取 dic
|
|
63
87
|
const { dic } = query
|
|
64
88
|
return getData(resolve, dic || '')
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
// 用于容器内部调用的 loadData 函数(保持向后兼容)
|
|
68
|
-
export const loadDataForContainer = (
|
|
92
|
+
export const loadDataForContainer = (
|
|
93
|
+
resolve: CallbackFunction | null = null,
|
|
94
|
+
dic = ''
|
|
95
|
+
) => {
|
|
69
96
|
return getData(resolve, dic)
|
|
70
97
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createGlobalStyle } from 'styled-components'
|
|
2
2
|
|
|
3
3
|
export const GlobalStyle = createGlobalStyle`
|
|
4
4
|
html,body,#__next {
|
|
@@ -7,7 +7,7 @@ export const GlobalStyle = createGlobalStyle`
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
body {
|
|
10
|
-
background-color: ${(props:
|
|
10
|
+
background-color: ${(props: { whiteColor?: boolean }) => (props.whiteColor ? 'white' : 'black')};
|
|
11
11
|
font-family: Helvetica;
|
|
12
12
|
margin: 0;
|
|
13
13
|
}
|
|
@@ -2,7 +2,7 @@ import axios from 'axios'
|
|
|
2
2
|
|
|
3
3
|
let prefix = 'http://localhost:3001'
|
|
4
4
|
|
|
5
|
-
export const doGet = (action:
|
|
5
|
+
export const doGet = (action: string) => {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
if (typeof window !== 'undefined') {
|
|
8
8
|
prefix = window.location.origin
|