@yyp92-cli/template-react-mobile 1.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +15 -0
  3. package/template/.env.development +5 -0
  4. package/template/.env.production +5 -0
  5. package/template/.env.test +5 -0
  6. package/template/README.md +30 -0
  7. package/template/index.html +13 -0
  8. package/template/package.json +40 -0
  9. package/template/pnpm-lock.yaml +3365 -0
  10. package/template/postcss.config.js +25 -0
  11. package/template/public/vite.svg +1 -0
  12. package/template/src/app.scss +12 -0
  13. package/template/src/app.tsx +14 -0
  14. package/template/src/assets/iconfont/demo.css +539 -0
  15. package/template/src/assets/iconfont/demo_index.html +211 -0
  16. package/template/src/assets/iconfont/iconfont.css +19 -0
  17. package/template/src/assets/iconfont/iconfont.js +1 -0
  18. package/template/src/assets/iconfont/iconfont.json +16 -0
  19. package/template/src/assets/iconfont/iconfont.ttf +0 -0
  20. package/template/src/assets/iconfont/iconfont.woff +0 -0
  21. package/template/src/assets/iconfont/iconfont.woff2 +0 -0
  22. package/template/src/assets/react.svg +1 -0
  23. package/template/src/components/403/index.tsx +21 -0
  24. package/template/src/components/404/index.tsx +23 -0
  25. package/template/src/components/index.ts +3 -0
  26. package/template/src/components/layout/content/index.module.scss +20 -0
  27. package/template/src/components/layout/content/index.tsx +55 -0
  28. package/template/src/components/layout/index.module.scss +5 -0
  29. package/template/src/components/layout/index.tsx +33 -0
  30. package/template/src/components/layout/navBar/index.module.scss +5 -0
  31. package/template/src/components/layout/navBar/index.tsx +48 -0
  32. package/template/src/components/layout/tabBar/index.module.scss +7 -0
  33. package/template/src/components/layout/tabBar/index.tsx +69 -0
  34. package/template/src/components/login/index.module.scss +20 -0
  35. package/template/src/components/login/index.tsx +127 -0
  36. package/template/src/global/constants.ts +4 -0
  37. package/template/src/pages/home/index.module.scss +4 -0
  38. package/template/src/pages/home/index.tsx +55 -0
  39. package/template/src/pages/message/index.module.scss +4 -0
  40. package/template/src/pages/message/index.tsx +13 -0
  41. package/template/src/pages/mine/index.module.scss +3 -0
  42. package/template/src/pages/mine/index.tsx +35 -0
  43. package/template/src/pages/todo/index.module.scss +4 -0
  44. package/template/src/pages/todo/index.tsx +13 -0
  45. package/template/src/router/router.tsx +129 -0
  46. package/template/src/service/api.ts +7 -0
  47. package/template/src/service/config.ts +9 -0
  48. package/template/src/service/index.ts +1 -0
  49. package/template/src/service/request/index.ts +267 -0
  50. package/template/src/service/request/type.ts +5 -0
  51. package/template/src/service/service.ts +27 -0
  52. package/template/src/store/login.ts +39 -0
  53. package/template/src/store/menus.ts +28 -0
  54. package/template/src/store/permission.ts +28 -0
  55. package/template/src/theme/darkTheme.scss +47 -0
  56. package/template/src/theme/lightTheme.scss +49 -0
  57. package/template/src/utils/cache.ts +44 -0
  58. package/template/src/utils/changeTheme.ts +14 -0
  59. package/template/src/utils/filterMenu.ts +21 -0
  60. package/template/src/utils/index.ts +3 -0
  61. package/template/src/vite-env.d.ts +4 -0
  62. package/template/tsconfig.json +42 -0
  63. package/template/tsconfig.node.json +10 -0
  64. package/template/vite.config.ts +57 -0
@@ -0,0 +1,127 @@
1
+ import React, {useState} from 'react'
2
+ import { useNavigate, useSearchParams } from 'react-router-dom'
3
+ import { Button, Form, Input } from 'antd-mobile'
4
+ import { EyeInvisibleOutline, EyeOutline } from 'antd-mobile-icons'
5
+ import { permissionStore } from '@/store/permission'
6
+ import { menusStore } from '@/store/menus'
7
+ import { filterMenu } from '@/utils'
8
+ import { routerConfig } from '@/router/router'
9
+
10
+ import styles from './index.module.scss'
11
+
12
+ interface LoginProps {
13
+ [key: string]: any
14
+ }
15
+
16
+ type FieldType = {
17
+ username?: string;
18
+ password?: string;
19
+ remember?: string;
20
+ }
21
+
22
+ // todo 测试数据
23
+ const defaultList = [
24
+ 'home',
25
+ 'todo',
26
+ 'message',
27
+ 'personalCenter',
28
+ 'detail',
29
+ ]
30
+
31
+ const Login: React.FC<LoginProps> = ({ }) => {
32
+ // 获取 navigate 方法
33
+ const navigate = useNavigate()
34
+ const [searchParams] = useSearchParams()
35
+ const {
36
+ setPermissions
37
+ } = permissionStore()
38
+ const { setMenus } = menusStore()
39
+
40
+ const [visiblePassword, setVisiblePassword] = useState<boolean>(false)
41
+
42
+
43
+ // ********操作 ******** 
44
+ const onFinish = (values: any) => {
45
+ const {username, password} = values
46
+
47
+ if (username === 'yang' && password === '123456') {
48
+ setPermissions(defaultList)
49
+
50
+ // todo模拟接口
51
+ setTimeout(() => {
52
+ const list = filterMenu((routerConfig as any)[0].children, defaultList)
53
+ setMenus(list)
54
+ }, 0)
55
+
56
+ setTimeout(() => {
57
+ const redirect = searchParams.get('redirect')
58
+ navigate(redirect || '/')
59
+ }, 500)
60
+ }
61
+ }
62
+
63
+
64
+ // ******** 渲染 ******** 
65
+ return (
66
+ <div className={styles.login}>
67
+ <div className={styles.loginInner}>
68
+ <Form
69
+ layout='horizontal'
70
+ onFinish={onFinish}
71
+ footer={
72
+ <Button
73
+ block
74
+ type='submit'
75
+ color='primary'
76
+ size='large'
77
+ >
78
+ 提交
79
+ </Button>
80
+ }
81
+ >
82
+ <Form.Header>登录</Form.Header>
83
+
84
+ <Form.Item
85
+ name='username'
86
+ label='姓名'
87
+ rules={[{ required: true, message: '姓名不能为空' }]}
88
+ >
89
+ <Input
90
+ placeholder='请输入姓名'
91
+ autoComplete="off"
92
+ clearable
93
+ />
94
+ </Form.Item>
95
+
96
+ <Form.Item
97
+ name='password'
98
+ label='密码'
99
+ rules={[{ required: true, message: '密码不能为空' }]}
100
+ extra={
101
+ <>
102
+ {
103
+ !visiblePassword
104
+ ? (
105
+ <EyeInvisibleOutline onClick={() => setVisiblePassword(true)} />
106
+ )
107
+ : (
108
+ <EyeOutline onClick={() => setVisiblePassword(false)} />
109
+ )
110
+ }
111
+ </>
112
+ }
113
+ >
114
+ <Input
115
+ type="password"
116
+ placeholder='请输入姓名'
117
+ autoComplete="off"
118
+ clearable
119
+ />
120
+ </Form.Item>
121
+ </Form>
122
+ </div>
123
+ </div>
124
+ )
125
+ }
126
+
127
+ export default Login
@@ -0,0 +1,4 @@
1
+ export const LOGIN_TOKEN = 'login/token'
2
+ export const USER_INFO = 'userInfo'
3
+ export const MENUS = 'menus'
4
+ export const PERMISSION = 'permisiion'
@@ -0,0 +1,4 @@
1
+
2
+ .home {
3
+ color: var(--text-color);
4
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react'
2
+ import {Button, Input, DatePicker} from 'antd-mobile'
3
+ import {useNavigate, useLocation} from 'react-router-dom'
4
+ import { userInfoStore } from '@/store/login'
5
+ import styles from './index.module.scss'
6
+
7
+ const Home = () => {
8
+ const navigate = useNavigate()
9
+ const {
10
+ userInfo,
11
+ setUserInfo
12
+ } = userInfoStore()
13
+
14
+ return (
15
+ <div className={styles.box}>
16
+ 首页
17
+
18
+ <Button
19
+ color='primary'
20
+ onClick={() => navigate('/403')}
21
+ >403</Button>
22
+
23
+ <Button
24
+ color='primary'
25
+ onClick={() => navigate('/login')}
26
+ >login</Button>
27
+
28
+ <div style={{ marginTop: 20 }}>
29
+ <div>姓名:{userInfo.userName}</div>
30
+
31
+ <Button
32
+ color="primary"
33
+ onClick={() => {
34
+ setUserInfo({
35
+ userName: '小红',
36
+ userId: '222'
37
+ })
38
+ }}
39
+ >设置姓名</Button>
40
+
41
+ <Button
42
+ color="primary"
43
+ disabled
44
+ >设置姓名</Button>
45
+ </div>
46
+
47
+ {/* <DatePicker
48
+ title='时间选择'
49
+ visible={true}
50
+ /> */}
51
+ </div>
52
+ )
53
+ }
54
+
55
+ export default Home
@@ -0,0 +1,4 @@
1
+
2
+ .message {
3
+ color: var(--text-color);
4
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import {Button, Input} from 'antd-mobile'
3
+ import styles from './index.module.scss'
4
+
5
+ const Message = () => {
6
+ return (
7
+ <div className={styles.message}>
8
+ 消息
9
+ </div>
10
+ )
11
+ }
12
+
13
+ export default Message
@@ -0,0 +1,3 @@
1
+ .mine {
2
+ color: var(--text-color);
3
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+ import {useNavigate} from 'react-router-dom'
3
+ import {Button, Input} from 'antd-mobile'
4
+ import { LOGIN_TOKEN, USER_INFO, MENUS, PERMISSION } from '@/global/constants'
5
+ import { localCache } from '@/utils/cache'
6
+ import styles from './index.module.scss'
7
+
8
+ const Mine = () => {
9
+ const navigate = useNavigate()
10
+
11
+ const handleLogout = () => {
12
+ localCache.removeCache(LOGIN_TOKEN)
13
+ localCache.removeCache(USER_INFO)
14
+ localCache.removeCache(MENUS)
15
+ localCache.removeCache(PERMISSION)
16
+
17
+ navigate('/login')
18
+ }
19
+
20
+ return (
21
+ <div className={styles.mine}>
22
+ 我的
23
+
24
+ <Button
25
+ color='primary'
26
+ fill='solid'
27
+ onClick={handleLogout}
28
+ >
29
+ 退出登录
30
+ </Button>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ export default Mine
@@ -0,0 +1,4 @@
1
+
2
+ .todo {
3
+ color: var(--text-color);
4
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import {Button, Input} from 'antd-mobile'
3
+ import styles from './index.module.scss'
4
+
5
+ const Todo = () => {
6
+ return (
7
+ <div className={styles.todo}>
8
+ 代办
9
+ </div>
10
+ )
11
+ }
12
+
13
+ export default Todo
@@ -0,0 +1,129 @@
1
+ import React from 'react'
2
+ import {createBrowserRouter} from 'react-router-dom'
3
+ import {
4
+ AppOutline,
5
+ MessageOutline,
6
+ MessageFill,
7
+ UnorderedListOutline,
8
+ UserOutline,
9
+ } from 'antd-mobile-icons'
10
+ import Home from '@/pages/home'
11
+ import Todo from '@/pages/todo'
12
+ import Message from '@/pages/message'
13
+ import Mine from '@/pages/mine'
14
+ import { Page403 } from '@/components/403'
15
+ import { Page404 } from '@/components/404'
16
+ import Login from '@/components/login'
17
+ import { Layout } from '@/components'
18
+
19
+ // * 解决把 menu 存储在 zustand 中,在 antd Menu组件中使用报错
20
+ export const ComponentMap: any = {
21
+ "icon-1": AppOutline,
22
+ "icon-2": UnorderedListOutline,
23
+ "icon-3": MessageOutline,
24
+ "icon-4": UserOutline,
25
+ }
26
+
27
+ export interface RouterConfigItemProps {
28
+ label?: React.ReactNode,
29
+ key?: string,
30
+ path: string,
31
+ icon?: React.ReactNode,
32
+ element?: React.ReactNode | null,
33
+
34
+ // 是否在菜单里显示
35
+ hideInMenu?: boolean,
36
+
37
+ // 是否全屏显示
38
+ showFullScreen?: boolean,
39
+ children?: RouterConfigItemProps[]
40
+ }
41
+
42
+ export const routerConfig: RouterConfigItemProps[] = [
43
+ {
44
+ path: '/',
45
+ element: <Layout />,
46
+ children: [
47
+ {
48
+ label: '首页',
49
+ key: 'home',
50
+ path: 'home',
51
+ icon: 'icon-1',
52
+ hideInMenu: false,
53
+ element: <Home />,
54
+ },
55
+
56
+ {
57
+ label: '待办',
58
+ key: 'todo',
59
+ path: 'todo',
60
+ icon: 'icon-2',
61
+ hideInMenu: false,
62
+ element: <Todo />,
63
+ },
64
+
65
+ {
66
+ label: '消息',
67
+ key: 'message',
68
+ path: 'message',
69
+ icon: 'icon-3',
70
+ hideInMenu: false,
71
+ element: <Message />,
72
+ },
73
+
74
+ {
75
+ label: '我的',
76
+ key: 'personalCenter',
77
+ path: 'personalCenter',
78
+ icon: 'icon-4',
79
+ hideInMenu: false,
80
+ element: <Mine />,
81
+ },
82
+
83
+ // 需要全屏显示的页面
84
+ {
85
+ label: '详情',
86
+ key: 'detail',
87
+ path: 'detail',
88
+ icon: null,
89
+ hideInMenu: true,
90
+ showFullScreen: true,
91
+ element: <>group22</>,
92
+ },
93
+ ]
94
+ },
95
+
96
+
97
+ // login
98
+ {
99
+ path: 'login',
100
+ label: 'login',
101
+ element: <Login />,
102
+ icon: null,
103
+ hideInMenu: true,
104
+ showFullScreen: true,
105
+ },
106
+
107
+ // 403
108
+ {
109
+ path: '403',
110
+ label: '403',
111
+ element: <Page403 />,
112
+ icon: null,
113
+ hideInMenu: true,
114
+ showFullScreen: true,
115
+ },
116
+
117
+ // 404
118
+ {
119
+ path: '*',
120
+ label: '404',
121
+ element: <Page404 />,
122
+ icon: null,
123
+ hideInMenu: true,
124
+ },
125
+ ]
126
+
127
+ const router = createBrowserRouter(routerConfig)
128
+
129
+ export default router
@@ -0,0 +1,7 @@
1
+ // 接口路径
2
+ export const apiUrl: any = {
3
+ demoUrl: `/api/get`,
4
+ demoUrl1: `/api/post`
5
+ }
6
+
7
+
@@ -0,0 +1,9 @@
1
+ export const BASE_URL = import.meta.env.VITE_BASE_URL
2
+
3
+ export const TIMEOUT = 5000
4
+
5
+ export const ErrorMessageData = {
6
+ ERR_NETWORK: '网络异常',
7
+ ERR_NOT_FOUND: '请求的资源在服务器上不存在',
8
+ ERR_FORBIDDEN: '没有权限'
9
+ }
@@ -0,0 +1 @@
1
+ export * from './service'
@@ -0,0 +1,267 @@
1
+ import axios from 'axios'
2
+ import { Toast } from 'antd-mobile'
3
+ import type { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
4
+ import type { MyInternalAxiosRequestConfig } from './type'
5
+ import { localCache } from '@/utils/cache'
6
+ import { LOGIN_TOKEN, USER_INFO, MENUS, PERMISSION } from '@/global/constants'
7
+ import router from '@/router/router'
8
+
9
+ // * 存储待取消的请求:key 是请求唯一标识,value 是 AbortController 实例
10
+ const pendingRequests = new Map()
11
+
12
+ /**
13
+ * * 生成请求唯一标识
14
+ * @param config - Axios 请求配置
15
+ * @returns 唯一标识字符串
16
+ */
17
+ const generateKey = (config: AxiosRequestConfig) => {
18
+ const {
19
+ method,
20
+ url,
21
+ params,
22
+ data
23
+ } = config
24
+
25
+ // 拼接方法、URL、参数(params 是 URL 参数,data 是请求体参数)
26
+ return [
27
+ method?.toUpperCase(),
28
+ url,
29
+ JSON.stringify(params || {}),
30
+ JSON.stringify(data || {})
31
+ ].join('&')
32
+ }
33
+
34
+ /**
35
+ * * 添加请求到待取消列表(若存在重复请求则先取消)
36
+ * @param config - Axios 请求配置
37
+ */
38
+ const addPendingRequest = (config: AxiosRequestConfig) => {
39
+ const requestKey = generateKey(config)
40
+
41
+ // 若存在重复请求,先取消旧请求
42
+ if (pendingRequests.has(requestKey)) {
43
+ const controller = pendingRequests.get(requestKey)
44
+ controller.abort(`取消重复请求:${requestKey}`)
45
+ pendingRequests.delete(requestKey)
46
+ }
47
+
48
+ // 创建新的 AbortController 并关联到请求
49
+ const controller = new AbortController()
50
+ // 将 signal 绑定到请求配置
51
+ config.signal = controller.signal
52
+ pendingRequests.set(requestKey, controller)
53
+ }
54
+
55
+ /**
56
+ * * 从待取消列表中移除请求(请求完成/失败/取消时调用)
57
+ * @param config - Axios 请求配置
58
+ */
59
+ const removePendingRequest = (config: AxiosRequestConfig) => {
60
+ const requestKey = generateKey(config)
61
+
62
+ if (pendingRequests.has(requestKey)) {
63
+ pendingRequests.delete(requestKey)
64
+ }
65
+ }
66
+
67
+ // * Token 过期处理函数
68
+ function handleTokenExpired() {
69
+ // 1. 清除本地存储的 Token(避免死循环)
70
+ localCache.removeCache(LOGIN_TOKEN)
71
+ localCache.removeCache(USER_INFO)
72
+ localCache.removeCache(MENUS)
73
+ localCache.removeCache(PERMISSION)
74
+
75
+ // 排除登录页,避免死循环
76
+ const currentPath = window.location.pathname
77
+ const isLoginPage = currentPath === '/login'
78
+ if (!isLoginPage) {
79
+ // todo 暂时写死
80
+ router.navigate(`/login?redirect=${currentPath || '/'}`, {
81
+ replace: true,
82
+ })
83
+ }
84
+
85
+ // 3. 可选:显示提示信息
86
+ Toast.show({
87
+ icon: 'fail',
88
+ content: '登录已过期,请重新登录',
89
+ })
90
+ }
91
+
92
+
93
+ // * 创建 Axios 实例
94
+ const instance = axios.create({
95
+ // 从环境变量取 baseURL
96
+ baseURL: import.meta.env.VITE_APP_BASE_API,
97
+ timeout: 5000
98
+ })
99
+
100
+ // * 请求拦截器:添加取消逻辑
101
+ instance.interceptors.request.use(
102
+ (config: MyInternalAxiosRequestConfig) => {
103
+ // 允许通过配置关闭取消功能(例如某些特殊长轮询请求)
104
+ if (config.cancelable !== false) {
105
+ addPendingRequest(config)
106
+ }
107
+
108
+ const token = localCache.getCache(LOGIN_TOKEN)
109
+
110
+ if (config.headers && token) {
111
+ config.headers.Authorization = `Bearer ${token}`
112
+ }
113
+
114
+ return config
115
+ },
116
+ (error) => Promise.reject(error)
117
+ )
118
+
119
+ // * 响应拦截器:清理已完成的请求
120
+ instance.interceptors.response.use(
121
+ (response: AxiosResponse) => {
122
+ const {
123
+ config,
124
+ data: responseData
125
+ } = response ?? {}
126
+ const {
127
+ code,
128
+ message
129
+ } = responseData ?? {}
130
+
131
+ removePendingRequest(config)
132
+
133
+ // * 统一处理接口成功后,返回的 code !== 0 的错误,这样业务代码就不用写错误的逻辑了
134
+ if (code !== 0) {
135
+ // handleTokenExpired()
136
+ message.error(message)
137
+ return Promise.reject(message)
138
+ }
139
+
140
+ // 直接返回响应体数据
141
+ return responseData
142
+ },
143
+ (error: AxiosError) => {
144
+ const {
145
+ config,
146
+ response
147
+ } = error ?? {}
148
+ const { status, statusText } = response ?? {}
149
+
150
+ // 移除已失败/取消的请求
151
+ if (error.config) {
152
+ removePendingRequest(error.config)
153
+ }
154
+
155
+ // 区分取消错误和其他错误
156
+ if (axios.isCancel(error)) {
157
+ // 取消请求不视为错误,返回 null
158
+ return Promise.resolve(null)
159
+ }
160
+
161
+ // * token 过期, 后端返回 401 状态码
162
+ if (status === 401) {
163
+ handleTokenExpired()
164
+
165
+ return Promise.reject(error)
166
+ }
167
+
168
+ // 异常处理程序
169
+ if (response && status) {
170
+ Toast.show({
171
+ icon: 'fail',
172
+ content: statusText,
173
+ })
174
+ }
175
+ else if (!response) {
176
+ Toast.show({
177
+ icon: 'fail',
178
+ content: '您的网络发生异常,无法连接服务器'
179
+ })
180
+ }
181
+
182
+ // 其他错误处理(如网络错误、404等)
183
+ return Promise.reject(error)
184
+ }
185
+ )
186
+
187
+ // 扩展实例:添加手动取消方法
188
+ const requestFn = {
189
+ ...instance,
190
+
191
+ request<T = any>(config: AxiosRequestConfig) {
192
+ // 返回 promise
193
+ return new Promise<T>((resolve, reject) => {
194
+ instance
195
+ .request<any, T>(config)
196
+ .then((res) => {
197
+ resolve(res)
198
+ })
199
+ .catch((err) => {
200
+ reject(err)
201
+ })
202
+ })
203
+ },
204
+
205
+ get(config: AxiosRequestConfig) {
206
+ return this.request({
207
+ ...config,
208
+ method: 'GET'
209
+ })
210
+ },
211
+
212
+ post(config: AxiosRequestConfig) {
213
+ return this.request({
214
+ ...config,
215
+ method: 'POST'
216
+ })
217
+ },
218
+
219
+ delete(config: AxiosRequestConfig) {
220
+ return this.request({
221
+ ...config,
222
+ method: 'DELETE'
223
+ })
224
+ },
225
+
226
+ patch(config: AxiosRequestConfig) {
227
+ return this.request({
228
+ ...config,
229
+ method: 'PATCH'
230
+ })
231
+ },
232
+
233
+ /**
234
+ * 手动取消单个请求
235
+ * @param config - 请求配置(需包含 method、url,可选 params/data)
236
+ * @param message - 取消原因
237
+ * @returns 是否取消成功
238
+ */
239
+ cancelOne: (
240
+ config: AxiosRequestConfig,
241
+ message: string = '手动取消请求'
242
+ ) => {
243
+ const key = generateKey(config)
244
+
245
+ if (pendingRequests.has(key)) {
246
+ pendingRequests.get(key).abort(message)
247
+ pendingRequests.delete(key)
248
+
249
+ return true
250
+ }
251
+
252
+ return false
253
+ },
254
+
255
+ /**
256
+ * 取消所有待处理的请求
257
+ * @param message - 取消原因
258
+ */
259
+ cancelAll: (message: string = '取消所有请求') => {
260
+ pendingRequests.forEach((controller, key) => {
261
+ controller.abort(`${message}:${key}`)
262
+ pendingRequests.delete(key)
263
+ })
264
+ }
265
+ }
266
+
267
+ export default requestFn
@@ -0,0 +1,5 @@
1
+ import type { InternalAxiosRequestConfig } from 'axios'
2
+
3
+ export interface MyInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
4
+ cancelable?: boolean
5
+ }