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 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.44`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsbp-cli",
3
- "version": "0.2.44",
3
+ "version": "0.2.47",
4
4
  "description": "CLI tool for creating NSBP (Node React SSR by Webpack) projects",
5
5
  "main": "index.js",
6
6
  "homepage": "https://nsbp.erishen.cn/",
@@ -11,3 +11,4 @@ ENABLE_RATE_LIMIT=0
11
11
  # ==================== 调试配置 ====================
12
12
  # 启用详细日志便于调试
13
13
  # DEBUG=nsbp:*
14
+
@@ -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
@@ -1,6 +1,3 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
1
  # Validate commit message format
5
2
  commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert|BREAKING CHANGE)(\(.+\))?: .{1,50}'
6
3
 
@@ -1,4 +1 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
1
  npx lint-staged
@@ -1,4 +1 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
1
  pnpm run lint
@@ -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) -f docker/docker-compose.yml build
22
+ $(COMPOSE) build
23
23
 
24
24
  build-dev: ## Build Docker images for development
25
- $(COMPOSE) -f docker/docker-compose.dev.yml build
25
+ $(COMPOSE) -f docker-compose.dev.yml build
26
26
 
27
27
  dev: ## Start development environment (removes orphan containers)
28
- $(COMPOSE) -f docker/docker-compose.dev.yml up --build --remove-orphans
28
+ $(COMPOSE) -f docker-compose.dev.yml up --build --remove-orphans
29
29
 
30
30
  prod: ## Start production environment (removes orphan containers)
31
- $(COMPOSE) -f docker/docker-compose.yml up -d --remove-orphans
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) -f docker/docker-compose.yml down --remove-orphans || echo "Warning: Failed to stop production containers"
35
+ @$(COMPOSE) down --remove-orphans || echo "Warning: Failed to stop production containers"
36
36
  @echo "Stopping development containers..."
37
- @$(COMPOSE) -f docker/docker-compose.dev.yml down --remove-orphans || echo "Warning: Failed to stop development containers"
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) -f docker/docker-compose.yml down -v --rmi all --remove-orphans
45
- $(COMPOSE) -f docker/docker-compose.dev.yml down -v --rmi all --remove-orphans
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) -f docker/docker-compose.yml logs -f
48
+ $(COMPOSE) logs -f
49
49
 
50
50
  logs-dev: ## View development logs
51
- $(COMPOSE) -f docker/docker-compose.dev.yml logs -f
51
+ $(COMPOSE) -f docker-compose.dev.yml logs -f
52
52
 
53
53
  restart: ## Restart production containers
54
- $(COMPOSE) -f docker/docker-compose.yml restart
54
+ $(COMPOSE) restart
55
55
 
56
56
  restart-dev: ## Restart development containers
57
- $(COMPOSE) -f docker/docker-compose.dev.yml restart
57
+ $(COMPOSE) -f docker-compose.dev.yml restart
58
58
 
59
59
  rebuild: ## Rebuild and restart production containers (removes orphan containers)
60
- $(COMPOSE) -f docker/docker-compose.yml up -d --build --remove-orphans
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/docker-compose.dev.yml up -d --build --remove-orphans
63
+ $(COMPOSE) -f docker-compose.dev.yml up -d --build --remove-orphans
64
64
 
65
65
  shell: ## Open shell in production container
66
- $(COMPOSE) -f docker/docker-compose.yml exec app sh
66
+ $(COMPOSE) exec app sh
67
67
 
68
68
  shell-dev: ## Open shell in development container
69
- $(COMPOSE) -f docker/docker-compose.dev.yml exec app sh
69
+ $(COMPOSE) -f docker-compose.dev.yml exec app sh
70
70
 
71
71
  test: ## Run tests (if configured)
72
- $(COMPOSE) -f docker/docker-compose.yml exec app $(PM) test
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: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
- - "8091:3001"
11
+ - "${HOST_PORT:-8081}:${PORT:-3001}"
12
12
  environment:
13
13
  - NODE_ENV=${NODE_ENV:-production}
14
14
  - PORT=${PORT:-3001}
@@ -22,6 +22,7 @@ public/*.json
22
22
  .env.*.local
23
23
  .env.development
24
24
  .env.production
25
+ !docker/.env
25
26
 
26
27
  # Logs
27
28
  logs
@@ -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.2.0",
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.4.2",
73
- "@testing-library/react": "^15.0.0",
74
- "@testing-library/user-event": "^14.5.2",
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": "^29.7.0",
106
- "jest-environment-jsdom": "^29.7.0",
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.1.2",
120
+ "ts-jest": "^29.4.6",
122
121
  "ts-loader": "^9.5.0",
123
- "typescript": "^5.0.0",
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, { useEffect, useState } from '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
- // 优先使用服务端预取的状态初始化 store
13
- const [store, setStore] = useState(() => {
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
@@ -8,7 +8,7 @@ import '../css/test2.sass'
8
8
  import '../css/test3.scss'
9
9
  import { Container } from '@styled/test'
10
10
 
11
- const Login = ({ query }: any) => {
11
+ const Login = ({ query }: { query?: Record<string, string> }) => {
12
12
  return (
13
13
  <Fragment>
14
14
  <Helmet>
@@ -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
- const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
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
- const photos = Array.isArray(data) ? data : []
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(photos[0] || [0, 0, ''])
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
- ([origW, origH]: any) => (currHeight / origH) * origW
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: any, width: any) => sum - width, 0)
58
+ .reduce((sum: number, width: number) => sum - width, 0)
44
59
 
45
60
  // Calculate position for each photo
46
- const photoPositions = photos.reduce(
47
- (acc: any, [_origW, _origH]: any, i: any, _arr: any) => {
48
- const prevLeft =
49
- i === 0 ? leftStartCoords : acc[i - 1].left + acc[i - 1].width
50
- acc.push({
51
- left: prevLeft,
52
- height: currHeight,
53
- width: widths[i] || 0
54
- })
55
- return acc
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 = ({ target: { value } }: any) => {
63
- setCurrPhoto(value)
80
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
81
+ setCurrPhoto(Number(e.target.value))
64
82
  }
65
83
 
66
- const clickHandler = (btn: any) => {
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: any, index: number) => {
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: any, i: any) => (
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
- const mapStateToProps = (state: any) => {
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 = (dispatch: any) => ({
187
- getPhotoMenu: (dic: any) => {
188
- dispatch(loadDataForContainer(null, dic))
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, any>
5
+ | Record<string, unknown>
6
6
  | Array<{ name: string; cover?: string; count?: number }>
7
7
  }
8
- query?: Record<string, any>
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
- export const homeReducer = (state = { data: {} }, action: any) => {
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
- const queryReducer = (state = {}, action: any) => {
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, any>
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: any
18
+ action: PhotoAction
13
19
  ) => {
14
20
  const { type, data, menu } = action
15
21
 
@@ -7,6 +7,9 @@ import { useCurrentFlag, outPhotoDicPath } from '@utils/config'
7
7
 
8
8
  const app = express()
9
9
 
10
+ // 0. Trust proxy - Important for HTTPS reverse proxy (nginx)
11
+ app.set('trust proxy', true)
12
+
10
13
  // 1. Security headers (helmet)
11
14
  app.use(
12
15
  helmet({
@@ -62,7 +62,16 @@ const getFileMenu = (
62
62
  return result
63
63
  }
64
64
 
65
- export const getPhotoWH = (req: any, res: any) => {
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: any = []
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: any = []
112
- fileList.forEach((item: any, index: number) => {
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 { width, height }: any = probe.sync(data)
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: any) {
122
- console.error('getPhotoWH error:', err)
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: err.message })
137
+ .json({ error: 'Internal Server Error', details: error.message })
126
138
  }
127
139
  }
128
140
 
129
- export const getPhotoMenu = (_req: any, res: any) => {
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: any) {
138
- console.error('getPhotoMenu error:', err)
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: err.message })
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
- export const render = (req: any, res: any) => {
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: any = []
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: any) => {
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 (e) {
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: any) => {
48
- return (dispatch: any) => {
49
- setTimeout(() => {
50
- dispatch({ type: REQUEST_QUERY, query })
51
-
52
- callback && callback()
53
- }, 0)
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 (e) {
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: any = Helmet.renderStatic()
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: any) {
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: any) => {
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
- export const loadData = (resolve: any = null) => {
5
- return (dispatch: any) => {
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: any) => {
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: any) => {
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
- const getPhotoWH = (dispatch: any, callback: any, dic = '') => {
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: any) => {
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((_e: any) => {
32
+ .catch(() => {
21
33
  callback && callback()
22
34
  })
23
35
  }
24
36
 
25
- const getPhotoMenu = (dispatch: any, callback: any) => {
37
+ const getPhotoMenu = (
38
+ dispatch: DispatchFunction,
39
+ callback: CallbackFunction
40
+ ) => {
26
41
  doGet('getPhotoMenu')
27
- .then((res: any) => {
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((_e: any) => {
53
+ .catch(() => {
39
54
  callback && callback()
40
55
  })
41
56
  }
42
57
 
43
- const getData = (callback: any, dic: any) => {
44
- return (dispatch: any) => {
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: any) => {
51
- if (data && data.length > 0) {
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, data[0].name)
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 = (resolve: any = null, query: any = {}) => {
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 = (resolve: any = null, dic = '') => {
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 styled, { createGlobalStyle } from 'styled-components'
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: any) => (props.whiteColor ? 'white' : 'black')};
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: any) => {
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