nsbp-cli 0.1.0
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/CHANGELOG.md +102 -0
- package/README.md +150 -0
- package/bin/nsbp.js +145 -0
- package/package.json +43 -0
- package/scripts/sync-template.js +277 -0
- package/templates/basic/.prettierignore +5 -0
- package/templates/basic/.prettierrc +5 -0
- package/templates/basic/README.md +13 -0
- package/templates/basic/package.json +101 -0
- package/templates/basic/postcss.config.js +7 -0
- package/templates/basic/public/favicon.ico +0 -0
- package/templates/basic/public/images/test/0.jpg +0 -0
- package/templates/basic/public/images/test/1.jpg +0 -0
- package/templates/basic/public/images/test/2.jpg +0 -0
- package/templates/basic/public/images/test/3.jpg +0 -0
- package/templates/basic/public/images/test/4.jpg +0 -0
- package/templates/basic/public/images/test/5.jpg +0 -0
- package/templates/basic/scripts/start.js +3 -0
- package/templates/basic/src/Routers.tsx +40 -0
- package/templates/basic/src/client/index.tsx +37 -0
- package/templates/basic/src/component/Header.tsx +38 -0
- package/templates/basic/src/component/Layout.tsx +24 -0
- package/templates/basic/src/component/Loading.tsx +7 -0
- package/templates/basic/src/component/Theme.tsx +14 -0
- package/templates/basic/src/containers/Home.tsx +435 -0
- package/templates/basic/src/containers/Login.tsx +32 -0
- package/templates/basic/src/containers/Photo.tsx +162 -0
- package/templates/basic/src/css/test.css +13 -0
- package/templates/basic/src/css/test.less +8 -0
- package/templates/basic/src/css/test2.sass +7 -0
- package/templates/basic/src/css/test3.scss +8 -0
- package/templates/basic/src/externals/less.d.ts +4 -0
- package/templates/basic/src/externals/window.d.ts +3 -0
- package/templates/basic/src/reducers/home.ts +17 -0
- package/templates/basic/src/reducers/index.ts +26 -0
- package/templates/basic/src/reducers/photo.ts +23 -0
- package/templates/basic/src/server/index.ts +29 -0
- package/templates/basic/src/server/photo.ts +118 -0
- package/templates/basic/src/server/utils.tsx +158 -0
- package/templates/basic/src/services/home.ts +52 -0
- package/templates/basic/src/services/photo.ts +64 -0
- package/templates/basic/src/store/constants.ts +4 -0
- package/templates/basic/src/store/index.ts +14 -0
- package/templates/basic/src/styled/common.ts +26 -0
- package/templates/basic/src/styled/component/header.ts +166 -0
- package/templates/basic/src/styled/component/layout.ts +10 -0
- package/templates/basic/src/styled/home.ts +789 -0
- package/templates/basic/src/styled/photo.ts +44 -0
- package/templates/basic/src/styled/test.ts +14 -0
- package/templates/basic/src/utils/clientConfig.ts +2 -0
- package/templates/basic/src/utils/config.ts +7 -0
- package/templates/basic/src/utils/fetch.ts +26 -0
- package/templates/basic/src/utils/index.ts +45 -0
- package/templates/basic/tsconfig.json +18 -0
- package/templates/basic/webpack.base.js +262 -0
- package/templates/basic/webpack.client.js +26 -0
- package/templates/basic/webpack.server.js +33 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import Layout from '../component/Layout'
|
|
4
|
+
import { Helmet } from 'react-helmet'
|
|
5
|
+
import {
|
|
6
|
+
GlobalStyle,
|
|
7
|
+
PageWrapper,
|
|
8
|
+
HeroSection,
|
|
9
|
+
HeroContent,
|
|
10
|
+
HeroTitle,
|
|
11
|
+
HeroSubtitle,
|
|
12
|
+
HeroBadge,
|
|
13
|
+
HeroStats,
|
|
14
|
+
StatCard,
|
|
15
|
+
StatValue,
|
|
16
|
+
StatLabel,
|
|
17
|
+
TechSection,
|
|
18
|
+
SectionHeader,
|
|
19
|
+
SectionTitle,
|
|
20
|
+
SectionDescription,
|
|
21
|
+
FeatureGrid,
|
|
22
|
+
FeatureCard,
|
|
23
|
+
CardIcon,
|
|
24
|
+
CardTitle,
|
|
25
|
+
CardDescription,
|
|
26
|
+
CodeExample,
|
|
27
|
+
ComparisonSection,
|
|
28
|
+
ComparisonTable,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
|
+
TableCell,
|
|
32
|
+
TableHeaderCell,
|
|
33
|
+
NsbpJSBadge,
|
|
34
|
+
NextJSBadge,
|
|
35
|
+
PhotoSection,
|
|
36
|
+
PhotoGrid,
|
|
37
|
+
PhotoCard,
|
|
38
|
+
PhotoImageWrapper,
|
|
39
|
+
PhotoImage,
|
|
40
|
+
PhotoName,
|
|
41
|
+
PhotoTitle,
|
|
42
|
+
PhotoCount,
|
|
43
|
+
LoadingContainer,
|
|
44
|
+
LoadingSpinner,
|
|
45
|
+
LoadingText,
|
|
46
|
+
ErrorContainer,
|
|
47
|
+
ErrorTitle,
|
|
48
|
+
ErrorMessage,
|
|
49
|
+
QuickStartSection,
|
|
50
|
+
QuickStartGrid,
|
|
51
|
+
QuickStartCard,
|
|
52
|
+
QuickStartTitle,
|
|
53
|
+
QuickStartCode,
|
|
54
|
+
QuickStartDescription,
|
|
55
|
+
Footer
|
|
56
|
+
} from '../styled/home'
|
|
57
|
+
|
|
58
|
+
interface PhotoMenuItem {
|
|
59
|
+
name: string
|
|
60
|
+
cover?: string
|
|
61
|
+
count?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const Home: React.FC = () => {
|
|
65
|
+
const [menu, setMenu] = useState<PhotoMenuItem[]>([])
|
|
66
|
+
const [loading, setLoading] = useState(true)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (typeof window === 'undefined') return
|
|
70
|
+
|
|
71
|
+
setLoading(true)
|
|
72
|
+
// 先检查服务端是否已预取了图片菜单数据
|
|
73
|
+
const serverMenu = window?.context?.state?.photo?.menu || {}
|
|
74
|
+
const serverMenuArray = Array.isArray(serverMenu) ? serverMenu : []
|
|
75
|
+
|
|
76
|
+
if (serverMenuArray.length > 0) {
|
|
77
|
+
setMenu(serverMenuArray)
|
|
78
|
+
setLoading(false)
|
|
79
|
+
} else {
|
|
80
|
+
// 如果服务端没有预取,则在客户端获取
|
|
81
|
+
fetch('/getPhotoMenu')
|
|
82
|
+
.then(res => {
|
|
83
|
+
if (!res.ok) throw new Error(`Status ${res.status}`)
|
|
84
|
+
return res.json()
|
|
85
|
+
})
|
|
86
|
+
.then(data => {
|
|
87
|
+
setMenu(data?.data || [])
|
|
88
|
+
})
|
|
89
|
+
.catch(err => {
|
|
90
|
+
console.error('Failed to load menu:', err)
|
|
91
|
+
setMenu([])
|
|
92
|
+
})
|
|
93
|
+
.finally(() => setLoading(false))
|
|
94
|
+
}
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const [isLoaded, setIsLoaded] = useState(false)
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
// 模拟页面加载
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
setIsLoaded(true)
|
|
103
|
+
}, 500)
|
|
104
|
+
return () => clearTimeout(timer)
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<GlobalStyle>
|
|
109
|
+
{!isLoaded && (
|
|
110
|
+
<div className="page-loader" id="pageLoader">
|
|
111
|
+
<div className="loader-spinner"></div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
<script dangerouslySetInnerHTML={{
|
|
115
|
+
__html: `
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
const loader = document.getElementById('pageLoader');
|
|
118
|
+
if (loader) {
|
|
119
|
+
loader.classList.add('fade-out');
|
|
120
|
+
setTimeout(() => loader.remove(), 500);
|
|
121
|
+
}
|
|
122
|
+
}, 800);
|
|
123
|
+
`
|
|
124
|
+
}} />
|
|
125
|
+
<Helmet>
|
|
126
|
+
<title>Nsbp.js - 轻量级 React SSR 框架</title>
|
|
127
|
+
<meta name="description" content="Nsbp.js - 一个轻量级 React SSR 框架,专为低资源部署与高度可定制场景而生。与 Next.js 相比,更节省资源,更灵活配置。" />
|
|
128
|
+
<meta name="keywords" content="Nsbp.js, React SSR, 轻量级, SSR, TypeScript, React 19" />
|
|
129
|
+
<meta property="og:title" content="Nsbp.js - 轻量级 React SSR 框架" />
|
|
130
|
+
<meta property="og:description" content="与 Next.js 相比,Nsbp.js 更轻量、更灵活、更可控。" />
|
|
131
|
+
</Helmet>
|
|
132
|
+
|
|
133
|
+
<Layout query={{}}>
|
|
134
|
+
<PageWrapper>
|
|
135
|
+
|
|
136
|
+
{/* ========================================
|
|
137
|
+
Hero Section - 首屏视觉冲击
|
|
138
|
+
======================================== */}
|
|
139
|
+
<HeroSection className="fade-in">
|
|
140
|
+
<HeroContent>
|
|
141
|
+
<div className="hero-glow"></div>
|
|
142
|
+
<div className="hero-glow"></div>
|
|
143
|
+
<HeroBadge className="fade-in" style={{animationDelay: '0.1s'}}>🚀 轻量级 React SSR 框架</HeroBadge>
|
|
144
|
+
<HeroTitle className="fade-in" style={{animationDelay: '0.2s'}}>Nsbp.js</HeroTitle>
|
|
145
|
+
<HeroSubtitle className="fade-in" style={{animationDelay: '0.3s'}}>
|
|
146
|
+
与 Next.js 相比,节省 60% 资源消耗
|
|
147
|
+
<br />
|
|
148
|
+
完全掌控 Webpack 配置,无黑盒限制
|
|
149
|
+
</HeroSubtitle>
|
|
150
|
+
|
|
151
|
+
<HeroStats>
|
|
152
|
+
<StatCard>
|
|
153
|
+
<StatValue>~60%</StatValue>
|
|
154
|
+
<StatLabel>更少资源</StatLabel>
|
|
155
|
+
</StatCard>
|
|
156
|
+
<StatCard>
|
|
157
|
+
<StatValue>512MB</StatValue>
|
|
158
|
+
<StatLabel>最低内存</StatLabel>
|
|
159
|
+
</StatCard>
|
|
160
|
+
<StatCard>
|
|
161
|
+
<StatValue>100%</StatValue>
|
|
162
|
+
<StatLabel>可定制</StatLabel>
|
|
163
|
+
</StatCard>
|
|
164
|
+
<StatCard>
|
|
165
|
+
<StatValue>TS</StatValue>
|
|
166
|
+
<StatLabel>类型安全</StatLabel>
|
|
167
|
+
</StatCard>
|
|
168
|
+
</HeroStats>
|
|
169
|
+
</HeroContent>
|
|
170
|
+
</HeroSection>
|
|
171
|
+
|
|
172
|
+
{/* ========================================
|
|
173
|
+
技术特性展示
|
|
174
|
+
======================================== */}
|
|
175
|
+
<TechSection className="fade-in" style={{animationDelay: '0.4s'}}>
|
|
176
|
+
<SectionHeader>
|
|
177
|
+
<SectionTitle className="fade-in" style={{animationDelay: '0.5s'}}>核心特性</SectionTitle>
|
|
178
|
+
<SectionDescription className="fade-in" style={{animationDelay: '0.6s'}}>
|
|
179
|
+
基于 React 19 + TypeScript,提供完整的 SSR 能力同时保持极致轻量
|
|
180
|
+
</SectionDescription>
|
|
181
|
+
</SectionHeader>
|
|
182
|
+
|
|
183
|
+
<FeatureGrid>
|
|
184
|
+
<FeatureCard>
|
|
185
|
+
<CardIcon>⚡</CardIcon>
|
|
186
|
+
<CardTitle>极速服务端渲染</CardTitle>
|
|
187
|
+
<CardDescription>
|
|
188
|
+
服务端渲染 HTML,SEO 友好,首屏秒开
|
|
189
|
+
</CardDescription>
|
|
190
|
+
<CodeExample>{`// 路由 + 预取数据
|
|
191
|
+
// Routers.tsx
|
|
192
|
+
export default [
|
|
193
|
+
{
|
|
194
|
+
path: '/',
|
|
195
|
+
component: Home,
|
|
196
|
+
exact: true,
|
|
197
|
+
loadData: homeLoadData,
|
|
198
|
+
key: 'home'
|
|
199
|
+
}
|
|
200
|
+
]`}</CodeExample>
|
|
201
|
+
</FeatureCard>
|
|
202
|
+
|
|
203
|
+
<FeatureCard>
|
|
204
|
+
<CardIcon>🔧</CardIcon>
|
|
205
|
+
<CardTitle>完全可控的 Webpack</CardTitle>
|
|
206
|
+
<CardDescription>
|
|
207
|
+
无黑盒配置,自定义任何构建逻辑
|
|
208
|
+
</CardDescription>
|
|
209
|
+
<CodeExample>{`// 自定义 Webpack 配置
|
|
210
|
+
// webpack.server.js
|
|
211
|
+
module.exports = {
|
|
212
|
+
// 你的配置
|
|
213
|
+
}`}</CodeExample>
|
|
214
|
+
</FeatureCard>
|
|
215
|
+
|
|
216
|
+
<FeatureCard>
|
|
217
|
+
<CardIcon>📦</CardIcon>
|
|
218
|
+
<CardTitle>智能代码分割</CardTitle>
|
|
219
|
+
<CardDescription>
|
|
220
|
+
基于 @loadable/component,按需加载
|
|
221
|
+
</CardDescription>
|
|
222
|
+
<CodeExample>{`// 组件懒加载
|
|
223
|
+
import loadable from '@loadable/component'
|
|
224
|
+
|
|
225
|
+
const Home = loadable(() => import('./containers/Home'))`}</CodeExample>
|
|
226
|
+
</FeatureCard>
|
|
227
|
+
|
|
228
|
+
<FeatureCard>
|
|
229
|
+
<CardIcon>🧩</CardIcon>
|
|
230
|
+
<CardTitle>React 19 原生支持</CardTitle>
|
|
231
|
+
<CardDescription>
|
|
232
|
+
利用最新 React 特性,性能和开发体验提升
|
|
233
|
+
</CardDescription>
|
|
234
|
+
<CodeExample>{`// React 19 新特性
|
|
235
|
+
import { use, useTransition } from 'react'
|
|
236
|
+
|
|
237
|
+
// Server Actions
|
|
238
|
+
// Suspense 边界
|
|
239
|
+
// use Optimistic`}</CodeExample>
|
|
240
|
+
</FeatureCard>
|
|
241
|
+
|
|
242
|
+
<FeatureCard>
|
|
243
|
+
<CardIcon>📝</CardIcon>
|
|
244
|
+
<CardTitle>TypeScript 类型安全</CardTitle>
|
|
245
|
+
<CardDescription>
|
|
246
|
+
完整的类型推断,编译时错误检查
|
|
247
|
+
</CardDescription>
|
|
248
|
+
<CodeExample>{`interface PhotoMenuItem {
|
|
249
|
+
name: string
|
|
250
|
+
cover?: string
|
|
251
|
+
count?: number
|
|
252
|
+
}`}</CodeExample>
|
|
253
|
+
</FeatureCard>
|
|
254
|
+
|
|
255
|
+
<FeatureCard>
|
|
256
|
+
<CardIcon>🖼️</CardIcon>
|
|
257
|
+
<CardTitle>内置图片服务</CardTitle>
|
|
258
|
+
<CardDescription>
|
|
259
|
+
开箱即用的图片分类和管理接口
|
|
260
|
+
</CardDescription>
|
|
261
|
+
<CodeExample>{`// 图片服务
|
|
262
|
+
// src/server/photo.ts
|
|
263
|
+
export const getPhotoMenu = (req: any, res: any) => {
|
|
264
|
+
const photosDicPath = getPublicImagesPath()
|
|
265
|
+
const fileMenu = getFileMenu(photosDicPath)
|
|
266
|
+
res.json({ data: fileMenu })
|
|
267
|
+
}`}</CodeExample>
|
|
268
|
+
</FeatureCard>
|
|
269
|
+
</FeatureGrid>
|
|
270
|
+
</TechSection>
|
|
271
|
+
|
|
272
|
+
{/* ========================================
|
|
273
|
+
Nsbp.js vs Next.js 对比
|
|
274
|
+
======================================== */}
|
|
275
|
+
<ComparisonSection className="fade-in" style={{animationDelay: '0.7s'}}>
|
|
276
|
+
<SectionHeader>
|
|
277
|
+
<SectionTitle className="fade-in" style={{animationDelay: '0.8s'}}>Nsbp.js vs Next.js</SectionTitle>
|
|
278
|
+
<SectionDescription className="fade-in" style={{animationDelay: '0.9s'}}>
|
|
279
|
+
对比两个 SSR 框架的关键差异,帮助你做出正确选择
|
|
280
|
+
</SectionDescription>
|
|
281
|
+
</SectionHeader>
|
|
282
|
+
|
|
283
|
+
<ComparisonTable>
|
|
284
|
+
<TableHeader>
|
|
285
|
+
<TableRow>
|
|
286
|
+
<TableHeaderCell>特性</TableHeaderCell>
|
|
287
|
+
<TableHeaderCell><NsbpJSBadge>Nsbp.js</NsbpJSBadge></TableHeaderCell>
|
|
288
|
+
<TableHeaderCell><NextJSBadge>Next.js</NextJSBadge></TableHeaderCell>
|
|
289
|
+
</TableRow>
|
|
290
|
+
</TableHeader>
|
|
291
|
+
<tbody>
|
|
292
|
+
<TableRow>
|
|
293
|
+
<TableCell><strong>运行时体积</strong></TableCell>
|
|
294
|
+
<TableCell>~5MB</TableCell>
|
|
295
|
+
<TableCell>~20MB</TableCell>
|
|
296
|
+
</TableRow>
|
|
297
|
+
<TableRow>
|
|
298
|
+
<TableCell><strong>最低内存</strong></TableCell>
|
|
299
|
+
<TableCell>512MB</TableCell>
|
|
300
|
+
<TableCell>1GB+</TableCell>
|
|
301
|
+
</TableRow>
|
|
302
|
+
<TableRow>
|
|
303
|
+
<TableCell><strong>构建配置</strong></TableCell>
|
|
304
|
+
<TableCell>✅ 完全可控</TableCell>
|
|
305
|
+
<TableCell>❌ 黑盒封装</TableCell>
|
|
306
|
+
</TableRow>
|
|
307
|
+
<TableRow>
|
|
308
|
+
<TableCell><strong>代码分割</strong></TableCell>
|
|
309
|
+
<TableCell>✅ @loadable/component</TableCell>
|
|
310
|
+
<TableCell>✅ 自动(但有限制)</TableCell>
|
|
311
|
+
</TableRow>
|
|
312
|
+
<TableRow>
|
|
313
|
+
<TableCell><strong>SSR 渲染</strong></TableCell>
|
|
314
|
+
<TableCell>✅ 手动控制</TableCell>
|
|
315
|
+
<TableCell>✅ 自动(但可调)</TableCell>
|
|
316
|
+
</TableRow>
|
|
317
|
+
<TableRow>
|
|
318
|
+
<TableCell><strong>学习曲线</strong></TableCell>
|
|
319
|
+
<TableCell>🟡 中等</TableCell>
|
|
320
|
+
<TableCell>🟢 简单</TableCell>
|
|
321
|
+
</TableRow>
|
|
322
|
+
<TableRow>
|
|
323
|
+
<TableCell><strong>生态集成</strong></TableCell>
|
|
324
|
+
<TableCell>✅ 任意 React 库</TableCell>
|
|
325
|
+
<TableCell>⚠️ 需要官方方案</TableCell>
|
|
326
|
+
</TableRow>
|
|
327
|
+
<TableRow>
|
|
328
|
+
<TableCell><strong>适用场景</strong></TableCell>
|
|
329
|
+
<TableCell>博客、官网、教学</TableCell>
|
|
330
|
+
<TableCell>企业应用、电商</TableCell>
|
|
331
|
+
</TableRow>
|
|
332
|
+
</tbody>
|
|
333
|
+
</ComparisonTable>
|
|
334
|
+
</ComparisonSection>
|
|
335
|
+
|
|
336
|
+
{/* ========================================
|
|
337
|
+
快速开始
|
|
338
|
+
======================================== */}
|
|
339
|
+
<QuickStartSection className="fade-in" style={{animationDelay: '1.0s'}}>
|
|
340
|
+
<SectionHeader>
|
|
341
|
+
<SectionTitle className="fade-in" style={{animationDelay: '1.1s'}}>快速开始</SectionTitle>
|
|
342
|
+
<SectionDescription className="fade-in" style={{animationDelay: '1.2s'}}>
|
|
343
|
+
三步启动你的第一个 SSR 项目
|
|
344
|
+
</SectionDescription>
|
|
345
|
+
</SectionHeader>
|
|
346
|
+
|
|
347
|
+
<QuickStartGrid>
|
|
348
|
+
<QuickStartCard>
|
|
349
|
+
<QuickStartTitle>1️⃣ 创建项目</QuickStartTitle>
|
|
350
|
+
<QuickStartCode>$ npx nsbp create my-app</QuickStartCode>
|
|
351
|
+
<QuickStartDescription>
|
|
352
|
+
使用 CLI 工具创建新项目
|
|
353
|
+
</QuickStartDescription>
|
|
354
|
+
</QuickStartCard>
|
|
355
|
+
|
|
356
|
+
<QuickStartCard>
|
|
357
|
+
<QuickStartTitle>2️⃣ 启动开发</QuickStartTitle>
|
|
358
|
+
<QuickStartCode>$ npm run dev</QuickStartCode>
|
|
359
|
+
<QuickStartDescription>
|
|
360
|
+
启动开发服务器,默认端口 3001
|
|
361
|
+
</QuickStartDescription>
|
|
362
|
+
</QuickStartCard>
|
|
363
|
+
|
|
364
|
+
<QuickStartCard>
|
|
365
|
+
<QuickStartTitle>3️⃣ 访问应用</QuickStartTitle>
|
|
366
|
+
<QuickStartCode>http://localhost:3001</QuickStartCode>
|
|
367
|
+
<QuickStartDescription>
|
|
368
|
+
浏览器访问,开始开发
|
|
369
|
+
</QuickStartDescription>
|
|
370
|
+
</QuickStartCard>
|
|
371
|
+
</QuickStartGrid>
|
|
372
|
+
</QuickStartSection>
|
|
373
|
+
|
|
374
|
+
{/* ========================================
|
|
375
|
+
Photo Menu 示例
|
|
376
|
+
======================================== */}
|
|
377
|
+
<PhotoSection className="fade-in" style={{animationDelay: '1.3s'}}>
|
|
378
|
+
<SectionHeader>
|
|
379
|
+
<SectionTitle className="fade-in" style={{animationDelay: '1.4s'}}>图片分类示例</SectionTitle>
|
|
380
|
+
<SectionDescription className="fade-in" style={{animationDelay: '1.5s'}}>
|
|
381
|
+
基于 Nsbp.js 内置的图片服务接口,快速构建图库应用
|
|
382
|
+
</SectionDescription>
|
|
383
|
+
</SectionHeader>
|
|
384
|
+
|
|
385
|
+
{loading ? (
|
|
386
|
+
<LoadingContainer>
|
|
387
|
+
<LoadingSpinner />
|
|
388
|
+
<LoadingText>加载分类...</LoadingText>
|
|
389
|
+
</LoadingContainer>
|
|
390
|
+
) : menu.length > 0 ? (
|
|
391
|
+
<PhotoGrid>
|
|
392
|
+
{menu.map(item => (
|
|
393
|
+
<Link key={item.name} to={`/photo?dic=${item.name}`}>
|
|
394
|
+
<PhotoCard>
|
|
395
|
+
<PhotoImageWrapper>
|
|
396
|
+
<PhotoImage
|
|
397
|
+
src={item.cover}
|
|
398
|
+
alt={item.name}
|
|
399
|
+
loading="lazy"
|
|
400
|
+
/>
|
|
401
|
+
</PhotoImageWrapper>
|
|
402
|
+
<PhotoName>
|
|
403
|
+
<PhotoTitle>{item.name}</PhotoTitle>
|
|
404
|
+
{typeof item.count === 'number' && (
|
|
405
|
+
<PhotoCount>{item.count} 张</PhotoCount>
|
|
406
|
+
)}
|
|
407
|
+
</PhotoName>
|
|
408
|
+
</PhotoCard>
|
|
409
|
+
</Link>
|
|
410
|
+
))}
|
|
411
|
+
</PhotoGrid>
|
|
412
|
+
) : (
|
|
413
|
+
<ErrorContainer>
|
|
414
|
+
<ErrorTitle>❌ 暂无分类</ErrorTitle>
|
|
415
|
+
<ErrorMessage>
|
|
416
|
+
请在 public/images 目录下创建图片文件夹
|
|
417
|
+
</ErrorMessage>
|
|
418
|
+
</ErrorContainer>
|
|
419
|
+
)}
|
|
420
|
+
</PhotoSection>
|
|
421
|
+
|
|
422
|
+
{/* ========================================
|
|
423
|
+
Footer
|
|
424
|
+
======================================== */}
|
|
425
|
+
<Footer>
|
|
426
|
+
<p>© 2025 Nsbp.js. Built with React 19 + TypeScript.</p>
|
|
427
|
+
</Footer>
|
|
428
|
+
|
|
429
|
+
</PageWrapper>
|
|
430
|
+
</Layout>
|
|
431
|
+
</GlobalStyle>
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export default Home
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { Fragment } from 'react'
|
|
2
|
+
import Header from '../component/Header'
|
|
3
|
+
import Layout from '../component/Layout'
|
|
4
|
+
import { Helmet } from 'react-helmet'
|
|
5
|
+
import '../css/test.css'
|
|
6
|
+
import '../css/test.less'
|
|
7
|
+
import '../css/test2.sass'
|
|
8
|
+
import '../css/test3.scss'
|
|
9
|
+
import { Container } from '../styled/test'
|
|
10
|
+
|
|
11
|
+
const Login = ({ query }: any) => {
|
|
12
|
+
return (
|
|
13
|
+
<Fragment>
|
|
14
|
+
<Helmet>
|
|
15
|
+
<title>Login</title>
|
|
16
|
+
<meta name="description" content="Login Description" />
|
|
17
|
+
</Helmet>
|
|
18
|
+
<Header />
|
|
19
|
+
<Layout query={query}>
|
|
20
|
+
<Container>
|
|
21
|
+
<p>login</p>
|
|
22
|
+
<div className="testBox"></div>
|
|
23
|
+
<div className="testBox1"></div>
|
|
24
|
+
<div className="testBox2"></div>
|
|
25
|
+
<div className="testBox3"></div>
|
|
26
|
+
</Container>
|
|
27
|
+
</Layout>
|
|
28
|
+
</Fragment>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default Login
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { Fragment, useState, useEffect } from 'react'
|
|
2
|
+
import { connect } from 'react-redux'
|
|
3
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
4
|
+
import Header from '../component/Header'
|
|
5
|
+
import Layout from '../component/Layout'
|
|
6
|
+
import { Helmet } from 'react-helmet'
|
|
7
|
+
import { Container, Row } from '../styled/photo'
|
|
8
|
+
import { motion } from 'framer-motion'
|
|
9
|
+
import { isSEO, getLocationParams } from '../utils'
|
|
10
|
+
import { useCurrentFlag } from '../utils/clientConfig'
|
|
11
|
+
import _ from 'lodash'
|
|
12
|
+
import { loadData } from '../services/photo'
|
|
13
|
+
|
|
14
|
+
const springSettings = { type: "spring", stiffness: 170, damping: 26 }
|
|
15
|
+
const NEXT = 'show-next'
|
|
16
|
+
|
|
17
|
+
const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
18
|
+
const location = useLocation()
|
|
19
|
+
let { dic, from } = query
|
|
20
|
+
const photos = Array.isArray(data) ? data : []
|
|
21
|
+
const [currPhoto, setCurrPhoto] = useState(0)
|
|
22
|
+
|
|
23
|
+
const [currPhotoData, setCurrPhotoData] = useState(photos[0] || [0, 0, ''])
|
|
24
|
+
|
|
25
|
+
const [currWidth, currHeight] = currPhotoData
|
|
26
|
+
|
|
27
|
+
const widths = photos.map(([origW, origH]:any) => (currHeight / origH) * origW)
|
|
28
|
+
|
|
29
|
+
// 同步 currPhoto 和 currPhotoData
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (photos[currPhoto]) {
|
|
32
|
+
setCurrPhotoData(photos[currPhoto])
|
|
33
|
+
}
|
|
34
|
+
}, [currPhoto, photos])
|
|
35
|
+
|
|
36
|
+
const leftStartCoords = widths
|
|
37
|
+
.slice(0, currPhoto)
|
|
38
|
+
.reduce((sum:any, width:any) => sum - width, 0)
|
|
39
|
+
|
|
40
|
+
// Calculate position for each photo
|
|
41
|
+
const photoPositions = photos.reduce((acc:any, [origW, origH]:any, i:any, arr:any) => {
|
|
42
|
+
const prevLeft = i === 0 ? leftStartCoords : acc[i-1].left + acc[i-1].width
|
|
43
|
+
acc.push({
|
|
44
|
+
left: prevLeft,
|
|
45
|
+
height: currHeight,
|
|
46
|
+
width: widths[i] || 0
|
|
47
|
+
})
|
|
48
|
+
return acc
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
// console.log('photoPositions', photoPositions)
|
|
52
|
+
|
|
53
|
+
const handleChange = ({ target: { value } }: any) => {
|
|
54
|
+
setCurrPhoto(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const clickHandler = (btn: any) => {
|
|
58
|
+
let photoIndex = btn === NEXT ? currPhoto + 1 : currPhoto - 1
|
|
59
|
+
|
|
60
|
+
photoIndex = photoIndex >= 0 ? photoIndex : photos.length - 1
|
|
61
|
+
photoIndex = photoIndex >= photos.length ? 0 : photoIndex
|
|
62
|
+
|
|
63
|
+
setCurrPhoto(photoIndex)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const currentDic = getLocationParams('dic')
|
|
68
|
+
|
|
69
|
+
const doGetPhotoMenu = () => {
|
|
70
|
+
getPhotoMenu(currentDic)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isSEO()) {
|
|
74
|
+
doGetPhotoMenu()
|
|
75
|
+
} else {
|
|
76
|
+
if(from === 'link'){
|
|
77
|
+
doGetPhotoMenu()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 重置到第一张
|
|
82
|
+
setCurrPhoto(0)
|
|
83
|
+
}, [location?.search])
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Fragment>
|
|
87
|
+
<Helmet>
|
|
88
|
+
<title>Photo</title>
|
|
89
|
+
<meta name="description" content="Photo Description" />
|
|
90
|
+
</Helmet>
|
|
91
|
+
<Header />
|
|
92
|
+
|
|
93
|
+
<Layout query={query}>
|
|
94
|
+
<Container>
|
|
95
|
+
<Row>
|
|
96
|
+
{
|
|
97
|
+
_.map(menu, (item:any, index:number) => {
|
|
98
|
+
return (
|
|
99
|
+
<Link key={`menu${index}`} to={`/photo?dic=${item.name}`}>{item.name}</Link>
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
</Row>
|
|
104
|
+
<Row>Scroll Me</Row>
|
|
105
|
+
<Row>
|
|
106
|
+
<button onClick={() => clickHandler('')}>Previous</button>
|
|
107
|
+
<input
|
|
108
|
+
type="range"
|
|
109
|
+
min={0}
|
|
110
|
+
max={photos.length - 1}
|
|
111
|
+
value={currPhoto}
|
|
112
|
+
onChange={handleChange}
|
|
113
|
+
/>
|
|
114
|
+
<button onClick={() => clickHandler(NEXT)}>Next</button>
|
|
115
|
+
</Row>
|
|
116
|
+
<div className="demo4">
|
|
117
|
+
<motion.div
|
|
118
|
+
className="demo4-inner"
|
|
119
|
+
animate={{ height: currHeight, width: currWidth }}
|
|
120
|
+
transition={springSettings}
|
|
121
|
+
>
|
|
122
|
+
{photoPositions.map((pos: any, i: any) => (
|
|
123
|
+
<motion.img
|
|
124
|
+
key={i}
|
|
125
|
+
className="demo4-photo"
|
|
126
|
+
src={useCurrentFlag ? `/images/${photos[i][2]}` : photos[i][2]}
|
|
127
|
+
initial={false}
|
|
128
|
+
animate={{
|
|
129
|
+
left: pos.left,
|
|
130
|
+
height: pos.height,
|
|
131
|
+
width: pos.width
|
|
132
|
+
}}
|
|
133
|
+
transition={springSettings}
|
|
134
|
+
style={{
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
top: 0
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
))}
|
|
140
|
+
</motion.div>
|
|
141
|
+
</div>
|
|
142
|
+
</Container>
|
|
143
|
+
</Layout>
|
|
144
|
+
</Fragment>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mapStateToProps = (state: any) => {
|
|
149
|
+
return {
|
|
150
|
+
query: state?.query,
|
|
151
|
+
menu: state?.photo?.menu,
|
|
152
|
+
data: state?.photo?.data
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mapDispatchToProps = (dispatch: any) => ({
|
|
157
|
+
getPhotoMenu(dic:any) {
|
|
158
|
+
dispatch(loadData(null, dic))
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export default connect(mapStateToProps, mapDispatchToProps)(Photo)
|