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,17 @@
|
|
|
1
|
+
import { GITHUB_ZEITNEXT_GET } from '../store/constants'
|
|
2
|
+
|
|
3
|
+
export const homeReducer = (state = { data: {} }, action: any) => {
|
|
4
|
+
const { type, data } = action
|
|
5
|
+
let newState = null
|
|
6
|
+
|
|
7
|
+
switch (type) {
|
|
8
|
+
case GITHUB_ZEITNEXT_GET:
|
|
9
|
+
newState = Object.assign({}, state)
|
|
10
|
+
|
|
11
|
+
if (data) newState.data = data
|
|
12
|
+
|
|
13
|
+
return newState
|
|
14
|
+
default:
|
|
15
|
+
return state
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { homeReducer } from './home'
|
|
2
|
+
import { photoReducer } from './photo'
|
|
3
|
+
import { REQUEST_QUERY } from '../store/constants'
|
|
4
|
+
|
|
5
|
+
const queryReducer = (state = {}, action: any) => {
|
|
6
|
+
const { type, query } = action
|
|
7
|
+
let newState = null
|
|
8
|
+
|
|
9
|
+
switch (type) {
|
|
10
|
+
case REQUEST_QUERY:
|
|
11
|
+
newState = {
|
|
12
|
+
...query
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return newState
|
|
16
|
+
default:
|
|
17
|
+
return state
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: (state = 'Erishen Sun') => state,
|
|
23
|
+
query: queryReducer,
|
|
24
|
+
home: homeReducer,
|
|
25
|
+
photo: photoReducer
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GET_PHOTO_MENU, GET_PHOTO_WIDTH_HEIGHT } from '../store/constants'
|
|
2
|
+
|
|
3
|
+
export const photoReducer = (state = { data: [[0, 0]], menu: {} }, action: any) => {
|
|
4
|
+
const { type, data, menu } = action
|
|
5
|
+
let newState = null
|
|
6
|
+
|
|
7
|
+
switch (type) {
|
|
8
|
+
case GET_PHOTO_MENU:
|
|
9
|
+
newState = Object.assign({}, state)
|
|
10
|
+
|
|
11
|
+
if (menu) newState.menu = menu
|
|
12
|
+
|
|
13
|
+
return newState
|
|
14
|
+
case GET_PHOTO_WIDTH_HEIGHT:
|
|
15
|
+
newState = Object.assign({}, state)
|
|
16
|
+
|
|
17
|
+
if (data) newState.data = data
|
|
18
|
+
|
|
19
|
+
return newState
|
|
20
|
+
default:
|
|
21
|
+
return state
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { render } from './utils'
|
|
3
|
+
import { getPhotoWH, getPhotoMenu } from './photo'
|
|
4
|
+
import { useCurrentFlag, outPhotoDicPath } from '../utils/config'
|
|
5
|
+
|
|
6
|
+
const app = express()
|
|
7
|
+
|
|
8
|
+
app.use(express.static('public'))
|
|
9
|
+
!useCurrentFlag && app.use(express.static(outPhotoDicPath))
|
|
10
|
+
|
|
11
|
+
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
|
|
12
|
+
|
|
13
|
+
app.get('/getPhotoWH', (req, res) => {
|
|
14
|
+
getPhotoWH(req, res)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
app.get('/getPhotoMenu', (req, res) => {
|
|
18
|
+
getPhotoMenu(req, res)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Catch-all middleware for SSR
|
|
22
|
+
app.use((req, res) => {
|
|
23
|
+
// console.log('req.url', req.url, req.headers)
|
|
24
|
+
render(req, res)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
app.listen(3001, () => {
|
|
28
|
+
// Server started successfully
|
|
29
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import probe from 'probe-image-size'
|
|
5
|
+
import { useCurrentFlag, outPhotoDic, outPhotoSep, outPhotoDicPath } from '../utils/config'
|
|
6
|
+
|
|
7
|
+
const CURRENT_DIC = 'images'
|
|
8
|
+
|
|
9
|
+
// 获取项目根目录(无论从哪个目录运行服务器)
|
|
10
|
+
const getProjectRoot = () => {
|
|
11
|
+
let currentDir = __dirname
|
|
12
|
+
const maxIterations = 10
|
|
13
|
+
let iterations = 0
|
|
14
|
+
|
|
15
|
+
while (iterations < maxIterations) {
|
|
16
|
+
const packageJsonPath = path.join(currentDir, 'package.json')
|
|
17
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
18
|
+
return currentDir
|
|
19
|
+
}
|
|
20
|
+
currentDir = path.dirname(currentDir)
|
|
21
|
+
iterations++
|
|
22
|
+
}
|
|
23
|
+
return process.cwd()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPublicImagesPath = () => {
|
|
27
|
+
return path.join(getProjectRoot(), 'public/images')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getPhotosDicPath = () => {
|
|
31
|
+
return getPublicImagesPath()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 获取目录下的子目录(分类),并为每个分类找封面图和图片数量
|
|
35
|
+
const getFileMenu = (dir: string): { name: string; cover?: string; count?: number }[] => {
|
|
36
|
+
const arr = fs.readdirSync(dir)
|
|
37
|
+
const result: { name: string; cover?: string; count?: number }[] = []
|
|
38
|
+
|
|
39
|
+
arr.forEach((item) => {
|
|
40
|
+
const fullPath = path.join(dir, item)
|
|
41
|
+
const stats = fs.statSync(fullPath)
|
|
42
|
+
if (stats.isDirectory()) {
|
|
43
|
+
// 分类名
|
|
44
|
+
const name = item
|
|
45
|
+
// 计算图片数量
|
|
46
|
+
const files = fs.readdirSync(fullPath).filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f))
|
|
47
|
+
const count = files.length
|
|
48
|
+
|
|
49
|
+
// 在该目录下找 cover.jpg
|
|
50
|
+
let cover = undefined
|
|
51
|
+
const coverPath = path.join(fullPath, 'cover.jpg')
|
|
52
|
+
if (fs.existsSync(coverPath)) {
|
|
53
|
+
// 转成相对 public 的 URL 路径
|
|
54
|
+
cover = `/images/${item}/cover.jpg`
|
|
55
|
+
} else if (files.length > 0) {
|
|
56
|
+
// 否则找第一张图片
|
|
57
|
+
cover = `/images/${item}/${files[0]}`
|
|
58
|
+
}
|
|
59
|
+
result.push({ name, cover, count })
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const getPhotoWH = (req: any, res: any) => {
|
|
66
|
+
try {
|
|
67
|
+
const dic = req.query.dic || ''
|
|
68
|
+
const photosDicPath = getPhotosDicPath()
|
|
69
|
+
|
|
70
|
+
const fileList: any = []
|
|
71
|
+
let photoPath = photosDicPath
|
|
72
|
+
if (dic) {
|
|
73
|
+
photoPath = path.join(photosDicPath, dic)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const getFileList = (dir: string, list: string[]) => {
|
|
77
|
+
const arr = fs.readdirSync(dir)
|
|
78
|
+
arr.forEach((item) => {
|
|
79
|
+
const fullPath = path.join(dir, item)
|
|
80
|
+
const stats = fs.statSync(fullPath)
|
|
81
|
+
if (stats.isDirectory()) {
|
|
82
|
+
getFileList(fullPath, list)
|
|
83
|
+
} else {
|
|
84
|
+
list.push(fullPath)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
return list
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getFileList(photoPath, fileList)
|
|
91
|
+
|
|
92
|
+
const whArr: any = []
|
|
93
|
+
fileList.forEach((item: any, index: number) => {
|
|
94
|
+
const data = fs.readFileSync(item)
|
|
95
|
+
let fileName = path.relative(photosDicPath, fileList[index])
|
|
96
|
+
const { width, height }: any = probe.sync(data)
|
|
97
|
+
whArr.push([width, height, fileName])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 按前端期望的格式包装
|
|
101
|
+
res.json({ data: whArr })
|
|
102
|
+
} catch (err) {
|
|
103
|
+
res.status(500).json({ error: 'Internal Server Error', details: err.message })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const getPhotoMenu = (req: any, res: any) => {
|
|
108
|
+
try {
|
|
109
|
+
const photosDicPath = getPhotosDicPath()
|
|
110
|
+
|
|
111
|
+
const fileMenu = getFileMenu(photosDicPath)
|
|
112
|
+
|
|
113
|
+
// 按前端期望的格式包装
|
|
114
|
+
res.json({ data: fileMenu })
|
|
115
|
+
} catch (err) {
|
|
116
|
+
res.status(500).json({ error: 'Internal Server Error', details: err.message })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from 'react' //引入React以支持JSX的语法
|
|
2
|
+
import { renderToString } from 'react-dom/server' //引入renderToString方法
|
|
3
|
+
import { StaticRouter, Routes, Route, matchPath } from 'react-router-dom'
|
|
4
|
+
import routers from '../Routers'
|
|
5
|
+
import { Provider } from 'react-redux'
|
|
6
|
+
import getStore from '../store'
|
|
7
|
+
import serialize from 'serialize-javascript'
|
|
8
|
+
import { REQUEST_QUERY } from '../store/constants'
|
|
9
|
+
import { Helmet } from 'react-helmet'
|
|
10
|
+
import { ServerStyleSheet } from 'styled-components'
|
|
11
|
+
import Theme from '../component/Theme'
|
|
12
|
+
import path from 'path'
|
|
13
|
+
import { ChunkExtractor } from '@loadable/server'
|
|
14
|
+
|
|
15
|
+
const removeCommentsAndSpacing = (str = '') =>
|
|
16
|
+
str.replace(/\/\*.*\*\//g, ' ').replace(/\s+/g, ' ')
|
|
17
|
+
|
|
18
|
+
const removeSpacing = (str = '') => str.replace(/\s+/g, ' ')
|
|
19
|
+
|
|
20
|
+
export const render = (req: any, res: any) => {
|
|
21
|
+
const store = getStore()
|
|
22
|
+
const { path:reqPath, query, url } = req
|
|
23
|
+
const matchRoutes: any = []
|
|
24
|
+
const promises = []
|
|
25
|
+
|
|
26
|
+
let { seo } = query
|
|
27
|
+
|
|
28
|
+
if (seo !== undefined && seo !== '') {
|
|
29
|
+
seo = parseInt(seo, 10)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
routers.some((route) => {
|
|
33
|
+
matchPath({ path: route.path, end: true }, reqPath) ? matchRoutes.push(route) : ''
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
matchRoutes.forEach((item: any) => {
|
|
37
|
+
if (item?.loadData) {
|
|
38
|
+
const promise = new Promise((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
store.dispatch(item?.loadData(resolve))
|
|
41
|
+
} catch (e) {
|
|
42
|
+
reject()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
promises.push(promise)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
matchRoutes.forEach((item: any) => {
|
|
51
|
+
if (item?.loadData) {
|
|
52
|
+
const promise = new Promise((resolve, reject) => {
|
|
53
|
+
try {
|
|
54
|
+
store.dispatch(item?.loadData(resolve))
|
|
55
|
+
} catch (e) {
|
|
56
|
+
reject()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
promises.push(promise)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const queryDispatch = (callback: any) => {
|
|
65
|
+
return (dispatch: any) => {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
dispatch({ type: REQUEST_QUERY, query })
|
|
68
|
+
|
|
69
|
+
callback && callback()
|
|
70
|
+
}, 0)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const queryPromise = new Promise((resolve, reject) => {
|
|
75
|
+
try {
|
|
76
|
+
store.dispatch(queryDispatch(resolve))
|
|
77
|
+
} catch (e) {
|
|
78
|
+
reject()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
promises.push(queryPromise)
|
|
83
|
+
|
|
84
|
+
Promise.all(promises)
|
|
85
|
+
.then(() => {
|
|
86
|
+
const nodeEnv = process.env.NODE_ENV
|
|
87
|
+
const sheet = new ServerStyleSheet()
|
|
88
|
+
const serverState = store.getState()
|
|
89
|
+
|
|
90
|
+
const helmet: any = Helmet.renderStatic()
|
|
91
|
+
|
|
92
|
+
const webStats = path.resolve(
|
|
93
|
+
__dirname,
|
|
94
|
+
'../public/loadable-stats.json',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
let webEntryPoints = ['client', 'vendor']
|
|
99
|
+
|
|
100
|
+
if (nodeEnv === 'production') {
|
|
101
|
+
webEntryPoints = ['client']
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const webExtractor = new ChunkExtractor({
|
|
105
|
+
statsFile: webStats,
|
|
106
|
+
entrypoints: webEntryPoints,
|
|
107
|
+
publicPath: '/'
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const jsx = webExtractor.collectChunks(sheet.collectStyles(
|
|
111
|
+
<Theme>
|
|
112
|
+
<Provider store={store}>
|
|
113
|
+
<StaticRouter location={reqPath}>
|
|
114
|
+
<Routes>
|
|
115
|
+
{routers.map((router) => (
|
|
116
|
+
<Route key={router.key} path={router.path} element={router.element} />
|
|
117
|
+
))}
|
|
118
|
+
</Routes>
|
|
119
|
+
</StaticRouter>
|
|
120
|
+
</Provider>
|
|
121
|
+
</Theme>
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const content = renderToString(jsx)
|
|
126
|
+
const styleTags = sheet.getStyleTags()
|
|
127
|
+
|
|
128
|
+
const html = `
|
|
129
|
+
<!DOCTYPE html>
|
|
130
|
+
<html>
|
|
131
|
+
<head>
|
|
132
|
+
<meta charset="utf-8">
|
|
133
|
+
${helmet?.title?.toString()}
|
|
134
|
+
${helmet?.meta?.toString()}
|
|
135
|
+
${styleTags}
|
|
136
|
+
${webExtractor.getLinkTags()}
|
|
137
|
+
${webExtractor.getStyleTags()}
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<div id="root">${content}</div>
|
|
141
|
+
<script type="text/javascript">
|
|
142
|
+
window.context = { state: ${serialize(serverState)} }
|
|
143
|
+
window.query = ${serialize(query)}
|
|
144
|
+
</script>
|
|
145
|
+
${webExtractor.getScriptTags()}
|
|
146
|
+
</body>
|
|
147
|
+
</html>
|
|
148
|
+
`
|
|
149
|
+
|
|
150
|
+
res.send(html)
|
|
151
|
+
} catch (e) {
|
|
152
|
+
sheet.seal()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.catch((e) => {
|
|
156
|
+
// Error handling
|
|
157
|
+
})
|
|
158
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { doGet } from '../utils/fetch'
|
|
2
|
+
import { GITHUB_ZEITNEXT_GET, GET_PHOTO_MENU } from '../store/constants'
|
|
3
|
+
|
|
4
|
+
const getData = (callback: any) => {
|
|
5
|
+
|
|
6
|
+
return (dispatch: any) => {
|
|
7
|
+
doGet('https://api.apiopen.top/getJoke?page=1&count=2&type=video')
|
|
8
|
+
.then((res:any) => {
|
|
9
|
+
// console.log('res', res)
|
|
10
|
+
dispatch({
|
|
11
|
+
type: GITHUB_ZEITNEXT_GET,
|
|
12
|
+
data: res?.data
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
callback && callback()
|
|
16
|
+
})
|
|
17
|
+
.catch((e:any) => {
|
|
18
|
+
// console.log('e', e.response)
|
|
19
|
+
dispatch({
|
|
20
|
+
type: GITHUB_ZEITNEXT_GET,
|
|
21
|
+
data: e?.response?.data
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
callback && callback()
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const loadData = (resolve: any = null) => {
|
|
30
|
+
return (dispatch: any) => {
|
|
31
|
+
// 预取图片菜单数据
|
|
32
|
+
doGet('/getPhotoMenu')
|
|
33
|
+
.then((res: any) => {
|
|
34
|
+
// axios 响应结构: { data, status, statusText, headers, config, request }
|
|
35
|
+
if (res.status >= 200 && res.status < 300) {
|
|
36
|
+
// 请求成功,data 已经是解析后的 JSON 对象
|
|
37
|
+
dispatch({
|
|
38
|
+
type: GET_PHOTO_MENU,
|
|
39
|
+
menu: res.data?.data || []
|
|
40
|
+
})
|
|
41
|
+
resolve && resolve(res.data)
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Status ${res.status}`)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.catch((err: any) => {
|
|
47
|
+
console.error('Failed to preload photos:', err)
|
|
48
|
+
// 预取失败但不影响主流程
|
|
49
|
+
resolve && resolve()
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { doGet } from '../utils/fetch'
|
|
2
|
+
import { GET_PHOTO_MENU, GET_PHOTO_WIDTH_HEIGHT } from '../store/constants'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const getPhotoWH = (dispatch: any, callback: any, dic = '') => {
|
|
6
|
+
let action = 'getPhotoWH'
|
|
7
|
+
if (dic) {
|
|
8
|
+
action += `?dic=${dic}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
doGet(action)
|
|
12
|
+
.then((res:any) => {
|
|
13
|
+
// console.log('getPhotoWH_res', res)
|
|
14
|
+
// axios 响应格式是 { data: { data: [...] }, status: ... },需要取 res.data.data
|
|
15
|
+
dispatch({
|
|
16
|
+
type: GET_PHOTO_WIDTH_HEIGHT,
|
|
17
|
+
data: res?.data?.data || []
|
|
18
|
+
})
|
|
19
|
+
callback && callback()
|
|
20
|
+
})
|
|
21
|
+
.catch((e:any) => {
|
|
22
|
+
callback && callback()
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPhotoMenu = (dispatch:any, callback:any) => {
|
|
27
|
+
doGet('getPhotoMenu')
|
|
28
|
+
.then((res:any) => {
|
|
29
|
+
// console.log('getPhotoMenu_res', res)
|
|
30
|
+
// axios 响应格式是 { data: { data: [...] }, status: ... },需要取 res.data.data
|
|
31
|
+
const { data } = res?.data || {}
|
|
32
|
+
dispatch({
|
|
33
|
+
type: GET_PHOTO_MENU,
|
|
34
|
+
menu: data
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
callback && callback(data)
|
|
38
|
+
})
|
|
39
|
+
.catch((e:any) => {
|
|
40
|
+
callback && callback()
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getData = (callback:any, dic:any) => {
|
|
45
|
+
return (dispatch: any) => {
|
|
46
|
+
|
|
47
|
+
if (dic) {
|
|
48
|
+
getPhotoMenu(dispatch, () => {
|
|
49
|
+
getPhotoWH(dispatch, callback, dic)
|
|
50
|
+
})
|
|
51
|
+
} else {
|
|
52
|
+
getPhotoMenu(dispatch, (data:any) => {
|
|
53
|
+
if (data && data.length > 0) {
|
|
54
|
+
// data[0] 是对象 {name, cover},需要取 name
|
|
55
|
+
getPhotoWH(dispatch, callback, data[0].name)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const loadData = (resolve: any = null, dic='') => {
|
|
63
|
+
return getData(resolve, dic)
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { configureStore, combineReducers } from '@reduxjs/toolkit'
|
|
2
|
+
import reducers from '../reducers'
|
|
3
|
+
|
|
4
|
+
const combineReducer = combineReducers({ ...reducers })
|
|
5
|
+
|
|
6
|
+
const getStore = (stateParam = {}) => {
|
|
7
|
+
return configureStore({
|
|
8
|
+
reducer: (state: any, action: any) => combineReducer(state || stateParam, action),
|
|
9
|
+
preloadedState: stateParam || {},
|
|
10
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware()
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default getStore
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import styled, { createGlobalStyle } from 'styled-components'
|
|
2
|
+
|
|
3
|
+
export const GlobalStyle = createGlobalStyle`
|
|
4
|
+
html,body,#__next {
|
|
5
|
+
height: 100%;
|
|
6
|
+
border: 1px solid white;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
background-color: ${(props: any) => (props.whiteColor ? 'white' : 'black')};
|
|
11
|
+
font-family: Helvetica;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
export const theme = {
|
|
17
|
+
colors: {
|
|
18
|
+
primary: '#0070f3'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const theme2 = {
|
|
23
|
+
colors: {
|
|
24
|
+
primary: 'black'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
|
|
3
|
+
export const Container = styled.header`
|
|
4
|
+
position: relative;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
padding: 0.75rem 2rem;
|
|
9
|
+
background: linear-gradient(135deg, rgba(255,255,255, 0.95), rgba(255,255,255, 0.85));
|
|
10
|
+
backdrop-filter: blur(20px);
|
|
11
|
+
-webkit-backdrop-filter: blur(20px);
|
|
12
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
13
|
+
box-shadow:
|
|
14
|
+
0 2px 12px rgba(0, 0, 0, 0.06),
|
|
15
|
+
0 1px 3px rgba(0, 0, 0, 0.04);
|
|
16
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
17
|
+
|
|
18
|
+
&::before {
|
|
19
|
+
content: '';
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0;
|
|
22
|
+
left: 0;
|
|
23
|
+
right: 0;
|
|
24
|
+
height: 2px;
|
|
25
|
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (max-width: 768px) {
|
|
29
|
+
padding: 0.625rem 1rem;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
gap: 0.875rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (max-width: 480px) {
|
|
35
|
+
padding: 0.5rem 1rem;
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
export const Brand = styled.div`
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.5rem;
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
export const LogoWrapper = styled.div`
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
font-size: 1.375rem;
|
|
48
|
+
font-weight: 800;
|
|
49
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
50
|
+
-webkit-background-clip: text;
|
|
51
|
+
-webkit-text-fill-color: transparent;
|
|
52
|
+
background-clip: text;
|
|
53
|
+
letter-spacing: -0.5px;
|
|
54
|
+
transition: all 0.3s ease;
|
|
55
|
+
|
|
56
|
+
span:first-child {
|
|
57
|
+
font-size: 1.625rem;
|
|
58
|
+
color: #667eea;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&:hover {
|
|
62
|
+
transform: scale(1.03);
|
|
63
|
+
filter: drop-shadow(0 2px 6px rgba(102, 126, 234, 0.25));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (max-width: 768px) {
|
|
67
|
+
font-size: 1.1875rem;
|
|
68
|
+
|
|
69
|
+
span:first-child {
|
|
70
|
+
font-size: 1.4375rem;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
export const Nav = styled.nav`
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 0.375rem;
|
|
78
|
+
align-items: center;
|
|
79
|
+
|
|
80
|
+
@media (max-width: 768px) {
|
|
81
|
+
width: 100%;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
export const NavLink = styled.div<{ $active?: boolean }>`
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.3125rem;
|
|
92
|
+
padding: 0.5rem 1rem;
|
|
93
|
+
font-size: 0.875rem;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
color: #4a5568;
|
|
96
|
+
text-decoration: none;
|
|
97
|
+
border-radius: 999px;
|
|
98
|
+
background: rgba(102, 126, 234, 0.05);
|
|
99
|
+
border: 1px solid rgba(102, 126, 234, 0.1);
|
|
100
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
101
|
+
backdrop-filter: blur(10px);
|
|
102
|
+
-webkit-backdrop-filter: blur(10px);
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
|
|
105
|
+
&::before {
|
|
106
|
+
content: '';
|
|
107
|
+
position: absolute;
|
|
108
|
+
bottom: 0;
|
|
109
|
+
left: 50%;
|
|
110
|
+
transform: translateX(-50%) scale(0);
|
|
111
|
+
width: 70%;
|
|
112
|
+
height: 1.5px;
|
|
113
|
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
114
|
+
border-radius: 999px;
|
|
115
|
+
transition: transform 0.3s ease;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
color: #667eea;
|
|
120
|
+
background: rgba(102, 126, 234, 0.1);
|
|
121
|
+
border-color: rgba(102, 126, 234, 0.2);
|
|
122
|
+
transform: translateY(-1px);
|
|
123
|
+
box-shadow:
|
|
124
|
+
0 3px 8px rgba(102, 126, 234, 0.12),
|
|
125
|
+
0 1px 3px rgba(102, 126, 234, 0.08);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&:hover::before {
|
|
129
|
+
transform: translateX(-50%) scale(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
${(props) =>
|
|
133
|
+
props.$active &&
|
|
134
|
+
`
|
|
135
|
+
color: #ffffff;
|
|
136
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
137
|
+
border-color: transparent;
|
|
138
|
+
box-shadow:
|
|
139
|
+
0 3px 8px rgba(102, 126, 234, 0.2),
|
|
140
|
+
0 1px 3px rgba(102, 126, 234, 0.12);
|
|
141
|
+
|
|
142
|
+
&::before {
|
|
143
|
+
transform: translateX(-50%) scale(1);
|
|
144
|
+
background: rgba(255,255,255, 0.3);
|
|
145
|
+
height: 1.5px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&:hover {
|
|
149
|
+
background: linear-gradient(135deg, #5a67d8 0%, #6b3e8e 100%);
|
|
150
|
+
transform: translateY(-1px);
|
|
151
|
+
}
|
|
152
|
+
`}
|
|
153
|
+
|
|
154
|
+
@media (max-width: 768px) {
|
|
155
|
+
padding: 0.4375rem 0.875rem;
|
|
156
|
+
font-size: 0.8125rem;
|
|
157
|
+
gap: 0.25rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media (max-width: 480px) {
|
|
161
|
+
padding: 0.375rem 0.75rem;
|
|
162
|
+
font-size: 0.75rem;
|
|
163
|
+
width: 100%;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
}
|
|
166
|
+
`
|