@svton/cli 1.2.2 → 1.2.4

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 (102) hide show
  1. package/dist/index.js +58 -27
  2. package/dist/index.mjs +58 -27
  3. package/package.json +1 -3
  4. package/templates/apps/admin/next-env.d.ts +0 -2
  5. package/templates/apps/admin/next.config.js +0 -15
  6. package/templates/apps/admin/package.json.tpl +0 -54
  7. package/templates/apps/admin/postcss.config.js +0 -6
  8. package/templates/apps/admin/src/app/globals.css +0 -37
  9. package/templates/apps/admin/src/app/layout.tsx +0 -19
  10. package/templates/apps/admin/src/app/login/page.tsx +0 -96
  11. package/templates/apps/admin/src/app/page.tsx +0 -8
  12. package/templates/apps/admin/src/app/users/page.tsx +0 -165
  13. package/templates/apps/admin/src/components/ui/switch.tsx +0 -29
  14. package/templates/apps/admin/src/hooks/useAPI.ts +0 -130
  15. package/templates/apps/admin/src/lib/api-client.ts +0 -112
  16. package/templates/apps/admin/src/lib/api-server.ts +0 -95
  17. package/templates/apps/admin/tailwind.config.js +0 -54
  18. package/templates/apps/admin/tsconfig.json +0 -22
  19. package/templates/apps/backend/.env.example +0 -29
  20. package/templates/apps/backend/nest-cli.json +0 -8
  21. package/templates/apps/backend/package.json.tpl +0 -57
  22. package/templates/apps/backend/prisma/schema.prisma +0 -72
  23. package/templates/apps/backend/prisma/seed.ts +0 -32
  24. package/templates/apps/backend/src/app.controller.ts +0 -15
  25. package/templates/apps/backend/src/app.module.ts +0 -85
  26. package/templates/apps/backend/src/app.service.ts +0 -12
  27. package/templates/apps/backend/src/auth/auth.controller.ts +0 -31
  28. package/templates/apps/backend/src/auth/auth.module.ts +0 -27
  29. package/templates/apps/backend/src/auth/auth.service.ts +0 -89
  30. package/templates/apps/backend/src/auth/jwt-auth.guard.ts +0 -5
  31. package/templates/apps/backend/src/auth/jwt.strategy.ts +0 -27
  32. package/templates/apps/backend/src/config/env.schema.ts +0 -35
  33. package/templates/apps/backend/src/main.ts +0 -51
  34. package/templates/apps/backend/src/object-storage/object-storage.controller.ts +0 -114
  35. package/templates/apps/backend/src/object-storage/object-storage.module.ts +0 -7
  36. package/templates/apps/backend/src/prisma/prisma.module.ts +0 -9
  37. package/templates/apps/backend/src/prisma/prisma.service.ts +0 -13
  38. package/templates/apps/backend/src/user/user.controller.ts +0 -50
  39. package/templates/apps/backend/src/user/user.module.ts +0 -12
  40. package/templates/apps/backend/src/user/user.service.ts +0 -117
  41. package/templates/apps/backend/tsconfig.json +0 -23
  42. package/templates/apps/mobile/babel.config.js +0 -8
  43. package/templates/apps/mobile/config/index.ts +0 -65
  44. package/templates/apps/mobile/package.json.tpl +0 -48
  45. package/templates/apps/mobile/project.config.json.tpl +0 -17
  46. package/templates/apps/mobile/src/app.config.ts +0 -9
  47. package/templates/apps/mobile/src/app.scss +0 -4
  48. package/templates/apps/mobile/src/app.ts +0 -8
  49. package/templates/apps/mobile/src/hooks/useAPI.ts +0 -285
  50. package/templates/apps/mobile/src/pages/index/index.scss +0 -7
  51. package/templates/apps/mobile/src/pages/index/index.tsx +0 -49
  52. package/templates/apps/mobile/src/services/api.ts +0 -155
  53. package/templates/apps/mobile/src/services/upload.service.ts +0 -41
  54. package/templates/apps/mobile/tsconfig.json +0 -21
  55. package/templates/configs/authz.config.ts +0 -10
  56. package/templates/configs/cache.config.ts +0 -14
  57. package/templates/configs/oauth.config.ts +0 -20
  58. package/templates/configs/payment.config.ts +0 -44
  59. package/templates/configs/queue.config.ts +0 -21
  60. package/templates/configs/rate-limit.config.ts +0 -16
  61. package/templates/configs/sms.config.ts +0 -11
  62. package/templates/configs/storage.config.ts +0 -14
  63. package/templates/examples/README.md +0 -258
  64. package/templates/examples/authz/README.md +0 -273
  65. package/templates/examples/authz/roles.guard.ts +0 -37
  66. package/templates/examples/authz/user.controller.ts +0 -116
  67. package/templates/examples/cache/README.md +0 -82
  68. package/templates/examples/cache/user.controller.ts +0 -42
  69. package/templates/examples/cache/user.service.ts +0 -78
  70. package/templates/examples/oauth/README.md +0 -192
  71. package/templates/examples/oauth/auth.controller.ts +0 -99
  72. package/templates/examples/oauth/auth.service.ts +0 -97
  73. package/templates/examples/payment/README.md +0 -151
  74. package/templates/examples/payment/order.controller.ts +0 -56
  75. package/templates/examples/payment/order.service.ts +0 -132
  76. package/templates/examples/payment/webhook.controller.ts +0 -73
  77. package/templates/examples/queue/README.md +0 -134
  78. package/templates/examples/queue/email.controller.ts +0 -34
  79. package/templates/examples/queue/email.processor.ts +0 -68
  80. package/templates/examples/queue/email.service.ts +0 -64
  81. package/templates/examples/rate-limit/README.md +0 -249
  82. package/templates/examples/rate-limit/api.controller.ts +0 -113
  83. package/templates/examples/sms/README.md +0 -121
  84. package/templates/examples/sms/sms.service.ts +0 -69
  85. package/templates/examples/sms/verification.controller.ts +0 -100
  86. package/templates/examples/storage/README.md +0 -224
  87. package/templates/examples/storage/upload.controller.ts +0 -117
  88. package/templates/examples/storage/upload.service.ts +0 -123
  89. package/templates/packages/types/package.json.tpl +0 -16
  90. package/templates/packages/types/src/api.ts +0 -88
  91. package/templates/packages/types/src/common.ts +0 -89
  92. package/templates/packages/types/src/index.ts +0 -3
  93. package/templates/packages/types/tsconfig.json +0 -16
  94. package/templates/skills/authz.skill.md +0 -42
  95. package/templates/skills/base.skill.md +0 -57
  96. package/templates/skills/cache.skill.md +0 -88
  97. package/templates/skills/oauth.skill.md +0 -41
  98. package/templates/skills/payment.skill.md +0 -129
  99. package/templates/skills/queue.skill.md +0 -140
  100. package/templates/skills/rate-limit.skill.md +0 -38
  101. package/templates/skills/sms.skill.md +0 -39
  102. package/templates/skills/storage.skill.md +0 -42
@@ -1,13 +0,0 @@
1
- import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2
- import { PrismaClient } from '@prisma/client';
3
-
4
- @Injectable()
5
- export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
6
- async onModuleInit() {
7
- await this.$connect();
8
- }
9
-
10
- async onModuleDestroy() {
11
- await this.$disconnect();
12
- }
13
- }
@@ -1,50 +0,0 @@
1
- import {
2
- Controller,
3
- Get,
4
- Put,
5
- Delete,
6
- Body,
7
- Param,
8
- Query,
9
- UseGuards,
10
- ParseIntPipe,
11
- } from '@nestjs/common';
12
- import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
13
- import { UserService } from './user.service';
14
- import { JwtAuthGuard } from '../auth/jwt-auth.guard';
15
- import type { UserListParams, UpdateUserDto } from '{{ORG_NAME}}/types';
16
-
17
- @ApiTags('用户管理')
18
- @Controller('users')
19
- @UseGuards(JwtAuthGuard)
20
- @ApiBearerAuth()
21
- export class UserController {
22
- constructor(private userService: UserService) {}
23
-
24
- @Get()
25
- @ApiOperation({ summary: '获取用户列表' })
26
- async findAll(@Query() params: UserListParams) {
27
- return this.userService.findAll(params);
28
- }
29
-
30
- @Get(':id')
31
- @ApiOperation({ summary: '获取用户详情' })
32
- async findOne(@Param('id', ParseIntPipe) id: number) {
33
- return this.userService.findOne(id);
34
- }
35
-
36
- @Put(':id')
37
- @ApiOperation({ summary: '更新用户' })
38
- async update(
39
- @Param('id', ParseIntPipe) id: number,
40
- @Body() dto: UpdateUserDto,
41
- ) {
42
- return this.userService.update(id, dto);
43
- }
44
-
45
- @Delete(':id')
46
- @ApiOperation({ summary: '删除用户' })
47
- async remove(@Param('id', ParseIntPipe) id: number) {
48
- return this.userService.remove(id);
49
- }
50
- }
@@ -1,12 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { UserController } from './user.controller';
3
- import { UserService } from './user.service';
4
- import { PrismaModule } from '../prisma/prisma.module';
5
-
6
- @Module({
7
- imports: [PrismaModule],
8
- controllers: [UserController],
9
- providers: [UserService],
10
- exports: [UserService],
11
- })
12
- export class UserModule {}
@@ -1,117 +0,0 @@
1
- import { Injectable, NotFoundException } from '@nestjs/common';
2
- import { PrismaService } from '../prisma/prisma.service';
3
- import type { UserListParams, UpdateUserDto, PaginatedResponse, UserVo } from '{{ORG_NAME}}/types';
4
-
5
- @Injectable()
6
- export class UserService {
7
- constructor(private prisma: PrismaService) {}
8
-
9
- async findAll(params: UserListParams): Promise<PaginatedResponse<UserVo>> {
10
- const { page = 1, pageSize = 10, keyword, role, status } = params;
11
- const skip = (page - 1) * pageSize;
12
-
13
- const where: any = {};
14
- if (keyword) {
15
- where.OR = [
16
- { phone: { contains: keyword } },
17
- { nickname: { contains: keyword } },
18
- ];
19
- }
20
- if (role) where.role = role;
21
- if (status !== undefined) where.status = status;
22
-
23
- const [list, total] = await Promise.all([
24
- this.prisma.user.findMany({
25
- where,
26
- skip,
27
- take: pageSize,
28
- orderBy: { createdAt: 'desc' },
29
- select: {
30
- id: true,
31
- phone: true,
32
- nickname: true,
33
- avatar: true,
34
- role: true,
35
- status: true,
36
- createdAt: true,
37
- updatedAt: true,
38
- },
39
- }),
40
- this.prisma.user.count({ where }),
41
- ]);
42
-
43
- return {
44
- list: list.map((u) => ({
45
- ...u,
46
- role: u.role as any,
47
- status: u.status as any,
48
- createdAt: u.createdAt.toISOString(),
49
- updatedAt: u.updatedAt.toISOString(),
50
- })),
51
- total,
52
- page,
53
- pageSize,
54
- totalPages: Math.ceil(total / pageSize),
55
- };
56
- }
57
-
58
- async findOne(id: number): Promise<UserVo> {
59
- const user = await this.prisma.user.findUnique({
60
- where: { id },
61
- select: {
62
- id: true,
63
- phone: true,
64
- nickname: true,
65
- avatar: true,
66
- role: true,
67
- status: true,
68
- createdAt: true,
69
- updatedAt: true,
70
- },
71
- });
72
-
73
- if (!user) {
74
- throw new NotFoundException('用户不存在');
75
- }
76
-
77
- return {
78
- ...user,
79
- role: user.role as any,
80
- status: user.status as any,
81
- createdAt: user.createdAt.toISOString(),
82
- updatedAt: user.updatedAt.toISOString(),
83
- };
84
- }
85
-
86
- async update(id: number, dto: UpdateUserDto): Promise<UserVo> {
87
- await this.findOne(id);
88
-
89
- const user = await this.prisma.user.update({
90
- where: { id },
91
- data: dto,
92
- select: {
93
- id: true,
94
- phone: true,
95
- nickname: true,
96
- avatar: true,
97
- role: true,
98
- status: true,
99
- createdAt: true,
100
- updatedAt: true,
101
- },
102
- });
103
-
104
- return {
105
- ...user,
106
- role: user.role as any,
107
- status: user.status as any,
108
- createdAt: user.createdAt.toISOString(),
109
- updatedAt: user.updatedAt.toISOString(),
110
- };
111
- }
112
-
113
- async remove(id: number): Promise<void> {
114
- await this.findOne(id);
115
- await this.prisma.user.delete({ where: { id } });
116
- }
117
- }
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "commonjs",
4
- "declaration": true,
5
- "removeComments": true,
6
- "emitDecoratorMetadata": true,
7
- "experimentalDecorators": true,
8
- "allowSyntheticDefaultImports": true,
9
- "target": "ES2021",
10
- "sourceMap": true,
11
- "outDir": "./dist",
12
- "baseUrl": "./",
13
- "incremental": true,
14
- "skipLibCheck": true,
15
- "strictNullChecks": true,
16
- "noImplicitAny": true,
17
- "strictBindCallApply": true,
18
- "forceConsistentCasingInFileNames": true,
19
- "noFallthroughCasesInSwitch": true,
20
- "esModuleInterop": true,
21
- "resolveJsonModule": true
22
- }
23
- }
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- presets: [
3
- ['taro', {
4
- framework: 'react',
5
- ts: true,
6
- }],
7
- ],
8
- };
@@ -1,65 +0,0 @@
1
- import type { UserConfigExport } from '@tarojs/cli';
2
-
3
- export default {
4
- projectName: 'mobile',
5
- date: '2024-1-1',
6
- designWidth: 750,
7
- deviceRatio: {
8
- 640: 2.34 / 2,
9
- 750: 1,
10
- 828: 1.81 / 2,
11
- },
12
- sourceRoot: 'src',
13
- outputRoot: 'dist',
14
- plugins: [],
15
- defineConstants: {},
16
- copy: {
17
- patterns: [],
18
- options: {},
19
- },
20
- framework: 'react',
21
- compiler: {
22
- type: 'webpack5',
23
- prebundle: {
24
- enable: false,
25
- },
26
- },
27
- mini: {
28
- postcss: {
29
- pxtransform: {
30
- enable: true,
31
- config: {},
32
- },
33
- url: {
34
- enable: true,
35
- config: {
36
- limit: 1024,
37
- },
38
- },
39
- cssModules: {
40
- enable: false,
41
- config: {
42
- namingPattern: 'module',
43
- generateScopedName: '[name]__[local]___[hash:base64:5]',
44
- },
45
- },
46
- },
47
- },
48
- h5: {
49
- publicPath: '/',
50
- staticDirectory: 'static',
51
- postcss: {
52
- autoprefixer: {
53
- enable: true,
54
- config: {},
55
- },
56
- cssModules: {
57
- enable: false,
58
- config: {
59
- namingPattern: 'module',
60
- generateScopedName: '[name]__[local]___[hash:base64:5]',
61
- },
62
- },
63
- },
64
- },
65
- } satisfies UserConfigExport;
@@ -1,48 +0,0 @@
1
- {
2
- "name": "{{ORG_NAME}}/mobile",
3
- "version": "1.0.0",
4
- "description": "{{PROJECT_NAME}} 移动端小程序",
5
- "private": true,
6
- "templateInfo": {
7
- "name": "default",
8
- "typescript": true,
9
- "css": "sass"
10
- },
11
- "scripts": {
12
- "build:weapp": "taro build --type weapp",
13
- "dev": "npm run dev:weapp",
14
- "dev:weapp": "npm run build:weapp -- --watch",
15
- "dev:h5": "taro build --type h5 --watch",
16
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
17
- "type-check": "tsc --noEmit"
18
- },
19
- "dependencies": {
20
- "@babel/runtime": "^7.23.6",
21
- "@svton/api-client": "^1.0.0",
22
- "@svton/hooks": "^1.0.0",
23
- "@svton/taro-ui": "^1.0.0",
24
- "{{ORG_NAME}}/types": "workspace:*",
25
- "@tarojs/components": "3.6.23",
26
- "@tarojs/helper": "3.6.23",
27
- "@tarojs/plugin-framework-react": "3.6.23",
28
- "@tarojs/plugin-platform-weapp": "3.6.23",
29
- "@tarojs/react": "3.6.23",
30
- "@tarojs/runtime": "3.6.23",
31
- "@tarojs/taro": "3.6.23",
32
- "react": "^18.2.0",
33
- "react-dom": "^18.2.0",
34
- "zustand": "^4.4.7"
35
- },
36
- "devDependencies": {
37
- "@babel/core": "^7.23.6",
38
- "@tarojs/cli": "3.6.23",
39
- "@tarojs/webpack5-runner": "3.6.23",
40
- "@types/react": "^18.2.45",
41
- "@typescript-eslint/eslint-plugin": "^6.15.0",
42
- "@typescript-eslint/parser": "^6.15.0",
43
- "babel-preset-taro": "3.6.23",
44
- "eslint": "^8.56.0",
45
- "eslint-config-taro": "3.6.23",
46
- "typescript": "^5.3.3"
47
- }
48
- }
@@ -1,17 +0,0 @@
1
- {
2
- "miniprogramRoot": "dist/",
3
- "projectname": "{{PROJECT_NAME}}",
4
- "description": "{{PROJECT_NAME}} 小程序",
5
- "appid": "",
6
- "setting": {
7
- "urlCheck": true,
8
- "es6": false,
9
- "enhance": false,
10
- "compileHotReLoad": false,
11
- "postcss": false,
12
- "minified": false,
13
- "bundle": false,
14
- "nodeModules": false
15
- },
16
- "compileType": "miniprogram"
17
- }
@@ -1,9 +0,0 @@
1
- export default defineAppConfig({
2
- pages: ['pages/index/index'],
3
- window: {
4
- backgroundTextStyle: 'light',
5
- navigationBarBackgroundColor: '#fff',
6
- navigationBarTitleText: '首页',
7
- navigationBarTextStyle: 'black',
8
- },
9
- });
@@ -1,4 +0,0 @@
1
- page {
2
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
3
- background-color: #f5f5f5;
4
- }
@@ -1,8 +0,0 @@
1
- import { PropsWithChildren } from 'react';
2
- import './app.scss';
3
-
4
- function App({ children }: PropsWithChildren) {
5
- return children;
6
- }
7
-
8
- export default App;
@@ -1,285 +0,0 @@
1
- /**
2
- * Mobile 端 useAPI Hook
3
- * 适配 Taro 的 React Hook
4
- *
5
- * 类型定义通过 {{ORG_NAME}}/types 扩展 @svton/api-client 的全局类型
6
- */
7
-
8
- import { useDidShow } from '@tarojs/taro';
9
- import { useState, useEffect, useRef } from 'react';
10
- import { usePersistFn } from '@svton/hooks';
11
- import type { ApiName, ApiParams, ApiResponse } from '@svton/api-client';
12
- import { apiAsync } from '../services/api';
13
- // 引入类型定义以启用模块增强
14
- import '{{ORG_NAME}}/types';
15
-
16
- /**
17
- * useAPI Hook 配置
18
- */
19
- interface UseAPIConfig {
20
- /**
21
- * 是否立即加载
22
- */
23
- immediate?: boolean;
24
- /**
25
- * 页面显示时是否重新加载
26
- */
27
- refreshOnShow?: boolean;
28
- /**
29
- * 成功回调
30
- */
31
- onSuccess?: (data: any) => void;
32
- /**
33
- * 错误回调
34
- */
35
- onError?: (error: Error) => void;
36
- }
37
-
38
- /**
39
- * useAPI 返回类型
40
- */
41
- interface UseAPIReturn<K extends ApiName> {
42
- data: ApiResponse<K> | null;
43
- loading: boolean;
44
- error: Error | null;
45
- refresh: () => Promise<void>;
46
- }
47
-
48
- export function useAPI<K extends ApiName>(
49
- apiName: K,
50
- params?: ApiParams<K>,
51
- config: UseAPIConfig = {},
52
- ): UseAPIReturn<K> {
53
- const { immediate = true, refreshOnShow = false, onSuccess, onError } = config;
54
-
55
- const [data, setData] = useState<ApiResponse<K> | null>(null);
56
- const [loading, setLoading] = useState(immediate);
57
- const [error, setError] = useState<Error | null>(null);
58
- const mountedRef = useRef(true);
59
-
60
- const fetchData = usePersistFn(async () => {
61
- if (!mountedRef.current) return;
62
-
63
- setLoading(true);
64
- setError(null);
65
-
66
- try {
67
- const result = (
68
- params !== undefined
69
- ? await (apiAsync as any)(apiName, params)
70
- : await (apiAsync as any)(apiName)
71
- ) as ApiResponse<K>;
72
-
73
- if (mountedRef.current) {
74
- setData(result);
75
- onSuccess?.(result);
76
- }
77
- } catch (err: unknown) {
78
- if (mountedRef.current) {
79
- const error = err instanceof Error ? err : new Error(String(err));
80
- setError(error);
81
- onError?.(error);
82
- }
83
- } finally {
84
- if (mountedRef.current) {
85
- setLoading(false);
86
- }
87
- }
88
- });
89
-
90
- useEffect(() => {
91
- if (immediate || params !== undefined) {
92
- fetchData();
93
- }
94
-
95
- return () => {
96
- mountedRef.current = false;
97
- };
98
- }, [apiName, JSON.stringify(params), fetchData, immediate, params]);
99
-
100
- useDidShow(() => {
101
- if (refreshOnShow && data) {
102
- fetchData();
103
- }
104
- });
105
-
106
- return {
107
- data,
108
- loading,
109
- error,
110
- refresh: fetchData,
111
- };
112
- }
113
-
114
- interface UseMutationReturn<K extends ApiName> {
115
- trigger: (params?: ApiParams<K>) => Promise<ApiResponse<K>>;
116
- loading: boolean;
117
- error: Error | null;
118
- data: ApiResponse<K> | null;
119
- reset: () => void;
120
- }
121
-
122
- export function useMutation<K extends ApiName>(apiName: K): UseMutationReturn<K> {
123
- const [loading, setLoading] = useState(false);
124
- const [error, setError] = useState<Error | null>(null);
125
- const [data, setData] = useState<ApiResponse<K> | null>(null);
126
-
127
- const trigger = async (params?: ApiParams<K>): Promise<ApiResponse<K>> => {
128
- setLoading(true);
129
- setError(null);
130
-
131
- try {
132
- const result = (
133
- params !== undefined
134
- ? await (apiAsync as any)(apiName, params)
135
- : await (apiAsync as any)(apiName)
136
- ) as ApiResponse<K>;
137
- setData(result);
138
- return result;
139
- } catch (err: unknown) {
140
- const error = err instanceof Error ? err : new Error(String(err));
141
- setError(error);
142
- throw error;
143
- } finally {
144
- setLoading(false);
145
- }
146
- };
147
-
148
- const reset = () => {
149
- setLoading(false);
150
- setError(null);
151
- setData(null);
152
- };
153
-
154
- return {
155
- trigger,
156
- loading,
157
- error,
158
- data,
159
- reset,
160
- };
161
- }
162
-
163
- type ExtractItemType<T> = T extends { items: (infer Item)[] }
164
- ? Item
165
- : T extends { data: (infer Item)[] }
166
- ? Item
167
- : T extends Array<infer Item>
168
- ? Item
169
- : unknown;
170
-
171
- type PaginationParams<T> = T extends void
172
- ? { pageSize?: number }
173
- : Omit<T, 'page'> & { pageSize?: number };
174
-
175
- interface UsePaginationReturn<TItem> {
176
- data: TItem[];
177
- loading: boolean;
178
- error: Error | null;
179
- page: number;
180
- hasMore: boolean;
181
- loadMore: () => Promise<void>;
182
- refresh: () => Promise<void>;
183
- }
184
-
185
- export function usePagination<K extends ApiName>(
186
- apiName: K,
187
- initialParams?: PaginationParams<ApiParams<K>>,
188
- ): UsePaginationReturn<ExtractItemType<ApiResponse<K>>> {
189
- type ItemType = ExtractItemType<ApiResponse<K>>;
190
-
191
- const [data, setData] = useState<ItemType[]>([]);
192
- const [loading, setLoading] = useState(false);
193
- const [error, setError] = useState<Error | null>(null);
194
- const [page, setPage] = useState(1);
195
- const [hasMore, setHasMore] = useState(true);
196
- const pageSize = initialParams?.pageSize || 10;
197
-
198
- const paramsRef = useRef(initialParams);
199
- paramsRef.current = initialParams;
200
-
201
- const isFirstRenderRef = useRef(true);
202
- const isRefreshingRef = useRef(false);
203
-
204
- const loadMore = usePersistFn(async () => {
205
- if (loading || !hasMore) return;
206
-
207
- setLoading(true);
208
- setError(null);
209
-
210
- try {
211
- const currentPage = isRefreshingRef.current ? 1 : page;
212
-
213
- const paginationParams = {
214
- ...(paramsRef.current || {}),
215
- page: currentPage,
216
- pageSize,
217
- };
218
-
219
- const result = await (apiAsync as any)(apiName, paginationParams);
220
-
221
- let newItems: ItemType[] = [];
222
- if (result && typeof result === 'object') {
223
- if ('items' in result && Array.isArray((result as any).items)) {
224
- newItems = (result as any).items as ItemType[];
225
- } else if ('data' in result && Array.isArray((result as any).data)) {
226
- newItems = (result as any).data as ItemType[];
227
- } else if ('items' in (result as any)?.data && Array.isArray((result as any).data?.items)) {
228
- newItems = (result as any).data.items as ItemType[];
229
- } else if (Array.isArray(result)) {
230
- newItems = result as ItemType[];
231
- }
232
- }
233
-
234
- if (isRefreshingRef.current) {
235
- setData(newItems);
236
- setPage(2);
237
- isRefreshingRef.current = false;
238
- } else {
239
- setData((prev: ItemType[]) => [...prev, ...newItems]);
240
- setPage((p: number) => p + 1);
241
- }
242
-
243
- if (newItems.length < pageSize) {
244
- setHasMore(false);
245
- }
246
- } catch (err: unknown) {
247
- const error = err instanceof Error ? err : new Error(String(err));
248
- setError(error);
249
- } finally {
250
- setLoading(false);
251
- }
252
- });
253
-
254
- const refresh = usePersistFn(async () => {
255
- isRefreshingRef.current = true;
256
- setData([]);
257
- setHasMore(true);
258
- await loadMore();
259
- });
260
-
261
- useEffect(() => {
262
- loadMore();
263
- // eslint-disable-next-line react-hooks/exhaustive-deps
264
- }, []);
265
-
266
- useEffect(() => {
267
- if (isFirstRenderRef.current) {
268
- isFirstRenderRef.current = false;
269
- return;
270
- }
271
-
272
- refresh();
273
- // eslint-disable-next-line react-hooks/exhaustive-deps
274
- }, [JSON.stringify(initialParams)]);
275
-
276
- return {
277
- data,
278
- loading,
279
- error,
280
- page,
281
- hasMore,
282
- loadMore,
283
- refresh,
284
- };
285
- }
@@ -1,7 +0,0 @@
1
- .index {
2
- display: flex;
3
- flex-direction: column;
4
- align-items: center;
5
- justify-content: center;
6
- min-height: 100vh;
7
- }