@svton/cli 1.2.2 → 1.2.3

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 +6 -13
  2. package/dist/index.mjs +6 -13
  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
package/dist/index.js CHANGED
@@ -132,18 +132,11 @@ async function copyTemplateFiles(config) {
132
132
  let templateDir = null;
133
133
  let needCleanup = false;
134
134
  const cliPackageRoot = import_path2.default.dirname(__dirname);
135
- const packagedTemplateDir = import_path2.default.join(cliPackageRoot, "../templates");
136
- if (await import_fs_extra2.default.pathExists(packagedTemplateDir)) {
137
- templateDir = packagedTemplateDir;
138
- logger.debug(`Using packaged template directory: ${templateDir}`);
139
- }
140
- if (!templateDir) {
141
- const frameworkRoot = import_path2.default.dirname(import_path2.default.dirname(cliPackageRoot));
142
- const localTemplateDir = import_path2.default.join(frameworkRoot, "templates");
143
- if (await import_fs_extra2.default.pathExists(localTemplateDir)) {
144
- templateDir = localTemplateDir;
145
- logger.debug(`Using local template directory: ${templateDir}`);
146
- }
135
+ const frameworkRoot = import_path2.default.dirname(import_path2.default.dirname(cliPackageRoot));
136
+ const localTemplateDir = import_path2.default.join(frameworkRoot, "templates");
137
+ if (await import_fs_extra2.default.pathExists(localTemplateDir)) {
138
+ templateDir = localTemplateDir;
139
+ logger.debug(`Using local template directory: ${templateDir}`);
147
140
  }
148
141
  if (!templateDir) {
149
142
  logger.info("Downloading templates from GitHub...");
@@ -1084,7 +1077,7 @@ async function createProjectFromTemplate(config) {
1084
1077
  }
1085
1078
 
1086
1079
  // package.json
1087
- var version = "1.2.2";
1080
+ var version = "1.2.3";
1088
1081
 
1089
1082
  // src/index.ts
1090
1083
  async function cli() {
package/dist/index.mjs CHANGED
@@ -105,18 +105,11 @@ async function copyTemplateFiles(config) {
105
105
  let templateDir = null;
106
106
  let needCleanup = false;
107
107
  const cliPackageRoot = path2.dirname(__dirname);
108
- const packagedTemplateDir = path2.join(cliPackageRoot, "../templates");
109
- if (await fs2.pathExists(packagedTemplateDir)) {
110
- templateDir = packagedTemplateDir;
111
- logger.debug(`Using packaged template directory: ${templateDir}`);
112
- }
113
- if (!templateDir) {
114
- const frameworkRoot = path2.dirname(path2.dirname(cliPackageRoot));
115
- const localTemplateDir = path2.join(frameworkRoot, "templates");
116
- if (await fs2.pathExists(localTemplateDir)) {
117
- templateDir = localTemplateDir;
118
- logger.debug(`Using local template directory: ${templateDir}`);
119
- }
108
+ const frameworkRoot = path2.dirname(path2.dirname(cliPackageRoot));
109
+ const localTemplateDir = path2.join(frameworkRoot, "templates");
110
+ if (await fs2.pathExists(localTemplateDir)) {
111
+ templateDir = localTemplateDir;
112
+ logger.debug(`Using local template directory: ${templateDir}`);
120
113
  }
121
114
  if (!templateDir) {
122
115
  logger.info("Downloading templates from GitHub...");
@@ -1057,7 +1050,7 @@ async function createProjectFromTemplate(config) {
1057
1050
  }
1058
1051
 
1059
1052
  // package.json
1060
- var version = "1.2.2";
1053
+ var version = "1.2.3";
1061
1054
 
1062
1055
  // src/index.ts
1063
1056
  async function cli() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svton/cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Svton CLI - Create full-stack applications with NestJS, Next.js, and Taro",
5
5
  "keywords": [
6
6
  "cli",
@@ -31,12 +31,10 @@
31
31
  "dist",
32
32
  "bin",
33
33
  "features.json",
34
- "templates",
35
34
  "README.md",
36
35
  "LICENSE"
37
36
  ],
38
37
  "scripts": {
39
- "prebuild": "node scripts/copy-templates.js",
40
38
  "build": "tsup src/index.ts --format cjs,esm --dts",
41
39
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
42
40
  "type-check": "tsc --noEmit",
@@ -1,2 +0,0 @@
1
- /// <reference types="next" />
2
- /// <reference types="next/image-types/global" />
@@ -1,15 +0,0 @@
1
- /** @type {import('next').NextConfig} */
2
- const nextConfig = {
3
- reactStrictMode: true,
4
- transpilePackages: ['@svton/hooks'],
5
- images: {
6
- remotePatterns: [
7
- {
8
- protocol: 'https',
9
- hostname: '**',
10
- },
11
- ],
12
- },
13
- };
14
-
15
- module.exports = nextConfig;
@@ -1,54 +0,0 @@
1
- {
2
- "name": "{{ORG_NAME}}/admin",
3
- "version": "1.0.0",
4
- "description": "{{PROJECT_NAME}} 管理后台",
5
- "scripts": {
6
- "dev": "next dev -p 3001",
7
- "build": "next build",
8
- "start": "next start -p 3001",
9
- "lint": "next lint",
10
- "type-check": "tsc --noEmit",
11
- "clean": "rm -rf .next"
12
- },
13
- "dependencies": {
14
- "@hookform/resolvers": "^3.3.3",
15
- "@radix-ui/react-avatar": "^1.0.4",
16
- "@radix-ui/react-dialog": "^1.0.5",
17
- "@radix-ui/react-dropdown-menu": "^2.0.6",
18
- "@radix-ui/react-label": "^2.0.2",
19
- "@radix-ui/react-select": "^2.0.0",
20
- "@radix-ui/react-separator": "^1.0.3",
21
- "@radix-ui/react-slot": "^1.0.2",
22
- "@radix-ui/react-switch": "^1.2.6",
23
- "@radix-ui/react-tabs": "^1.0.4",
24
- "@radix-ui/react-toast": "^1.1.5",
25
- "@svton/api-client": "^1.0.0",
26
- "@svton/hooks": "^1.0.0",
27
- "{{ORG_NAME}}/types": "workspace:*",
28
- "axios": "^1.7.9",
29
- "class-variance-authority": "^0.7.1",
30
- "clsx": "^2.1.1",
31
- "dayjs": "^1.11.13",
32
- "lucide-react": "^0.462.0",
33
- "next": "^15.0.0",
34
- "react": "^19.0.0",
35
- "react-dom": "^19.0.0",
36
- "react-hook-form": "^7.49.0",
37
- "swr": "^2.2.5",
38
- "tailwind-merge": "^3.0.0",
39
- "zod": "^3.22.4",
40
- "zustand": "^5.0.0"
41
- },
42
- "devDependencies": {
43
- "@types/node": "^22.0.0",
44
- "@types/react": "^19.0.0",
45
- "@types/react-dom": "^19.0.0",
46
- "autoprefixer": "^10.4.22",
47
- "eslint": "^9.0.0",
48
- "eslint-config-next": "^15.0.0",
49
- "postcss": "^8.5.0",
50
- "tailwindcss": "^3.4.0",
51
- "tailwindcss-animate": "^1.0.7",
52
- "typescript": "^5.7.0"
53
- }
54
- }
@@ -1,6 +0,0 @@
1
- module.exports = {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- };
@@ -1,37 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- @layer base {
6
- :root {
7
- --background: 0 0% 100%;
8
- --foreground: 222.2 84% 4.9%;
9
- --card: 0 0% 100%;
10
- --card-foreground: 222.2 84% 4.9%;
11
- --popover: 0 0% 100%;
12
- --popover-foreground: 222.2 84% 4.9%;
13
- --primary: 222.2 47.4% 11.2%;
14
- --primary-foreground: 210 40% 98%;
15
- --secondary: 210 40% 96.1%;
16
- --secondary-foreground: 222.2 47.4% 11.2%;
17
- --muted: 210 40% 96.1%;
18
- --muted-foreground: 215.4 16.3% 46.9%;
19
- --accent: 210 40% 96.1%;
20
- --accent-foreground: 222.2 47.4% 11.2%;
21
- --destructive: 0 84.2% 60.2%;
22
- --destructive-foreground: 210 40% 98%;
23
- --border: 214.3 31.8% 91.4%;
24
- --input: 214.3 31.8% 91.4%;
25
- --ring: 222.2 84% 4.9%;
26
- --radius: 0.5rem;
27
- }
28
- }
29
-
30
- @layer base {
31
- * {
32
- @apply border-border;
33
- }
34
- body {
35
- @apply bg-background text-foreground;
36
- }
37
- }
@@ -1,19 +0,0 @@
1
- import type { Metadata } from 'next';
2
- import './globals.css';
3
-
4
- export const metadata: Metadata = {
5
- title: '管理后台',
6
- description: '项目管理后台',
7
- };
8
-
9
- export default function RootLayout({
10
- children,
11
- }: {
12
- children: React.ReactNode;
13
- }) {
14
- return (
15
- <html lang="zh-CN">
16
- <body>{children}</body>
17
- </html>
18
- );
19
- }
@@ -1,96 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import { usePersistFn } from '@svton/hooks';
6
- import type { LoginDto } from '{{ORG_NAME}}/types';
7
-
8
- export default function LoginPage() {
9
- const router = useRouter();
10
- const [form, setForm] = useState<LoginDto>({ phone: '', password: '' });
11
- const [loading, setLoading] = useState(false);
12
- const [error, setError] = useState('');
13
-
14
- const handleSubmit = usePersistFn(async (e: React.FormEvent) => {
15
- e.preventDefault();
16
- setError('');
17
- setLoading(true);
18
-
19
- try {
20
- // 这里应该使用 @svton/api-client 的 API
21
- // const response = await apiClient.auth.login(form);
22
- // localStorage.setItem('token', response.data.accessToken);
23
-
24
- // 模拟登录成功
25
- await new Promise((resolve) => setTimeout(resolve, 1000));
26
- router.push('/users');
27
- } catch (err: any) {
28
- setError(err.message || '登录失败');
29
- } finally {
30
- setLoading(false);
31
- }
32
- });
33
-
34
- return (
35
- <div className="min-h-screen flex items-center justify-center bg-gray-50">
36
- <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
37
- <div>
38
- <h2 className="text-center text-3xl font-bold text-gray-900">
39
- 管理后台
40
- </h2>
41
- <p className="mt-2 text-center text-sm text-gray-600">
42
- 请登录您的账号
43
- </p>
44
- </div>
45
-
46
- <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
47
- {error && (
48
- <div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
49
- {error}
50
- </div>
51
- )}
52
-
53
- <div className="space-y-4">
54
- <div>
55
- <label htmlFor="phone" className="block text-sm font-medium text-gray-700">
56
- 手机号
57
- </label>
58
- <input
59
- id="phone"
60
- type="tel"
61
- required
62
- value={form.phone}
63
- onChange={(e) => setForm({ ...form, phone: e.target.value })}
64
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
65
- placeholder="请输入手机号"
66
- />
67
- </div>
68
-
69
- <div>
70
- <label htmlFor="password" className="block text-sm font-medium text-gray-700">
71
- 密码
72
- </label>
73
- <input
74
- id="password"
75
- type="password"
76
- required
77
- value={form.password}
78
- onChange={(e) => setForm({ ...form, password: e.target.value })}
79
- className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
80
- placeholder="请输入密码"
81
- />
82
- </div>
83
- </div>
84
-
85
- <button
86
- type="submit"
87
- disabled={loading}
88
- className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
89
- >
90
- {loading ? '登录中...' : '登录'}
91
- </button>
92
- </form>
93
- </div>
94
- </div>
95
- );
96
- }
@@ -1,8 +0,0 @@
1
- export default function Home() {
2
- return (
3
- <main className="flex min-h-screen flex-col items-center justify-center p-24">
4
- <h1 className="text-4xl font-bold">管理后台</h1>
5
- <p className="mt-4 text-gray-600">欢迎使用管理后台</p>
6
- </main>
7
- );
8
- }
@@ -1,165 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
- import { usePersistFn } from '@svton/hooks';
5
- import type { UserVo, UserListParams } from '{{ORG_NAME}}/types';
6
-
7
- export default function UsersPage() {
8
- const [users, setUsers] = useState<UserVo[]>([]);
9
- const [loading, setLoading] = useState(true);
10
- const [params, setParams] = useState<UserListParams>({
11
- page: 1,
12
- pageSize: 10,
13
- });
14
- const [total, setTotal] = useState(0);
15
-
16
- const fetchUsers = usePersistFn(async () => {
17
- setLoading(true);
18
- try {
19
- // 这里应该使用 @svton/api-client 的 API
20
- // const response = await apiClient.users.list(params);
21
- // setUsers(response.data.list);
22
- // setTotal(response.data.total);
23
-
24
- // 模拟数据
25
- await new Promise((resolve) => setTimeout(resolve, 500));
26
- setUsers([
27
- {
28
- id: 1,
29
- phone: '13800138000',
30
- nickname: '测试用户',
31
- avatar: '',
32
- role: 'user',
33
- status: 1,
34
- createdAt: new Date().toISOString(),
35
- updatedAt: new Date().toISOString(),
36
- },
37
- ]);
38
- setTotal(1);
39
- } catch (error) {
40
- console.error('获取用户列表失败', error);
41
- } finally {
42
- setLoading(false);
43
- }
44
- });
45
-
46
- useEffect(() => {
47
- fetchUsers();
48
- }, [params]);
49
-
50
- const handlePageChange = usePersistFn((page: number) => {
51
- setParams({ ...params, page });
52
- });
53
-
54
- return (
55
- <div className="min-h-screen bg-gray-50">
56
- <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
57
- <div className="px-4 py-6 sm:px-0">
58
- <div className="bg-white rounded-lg shadow">
59
- <div className="px-6 py-4 border-b border-gray-200">
60
- <h1 className="text-2xl font-bold text-gray-900">用户管理</h1>
61
- </div>
62
-
63
- {loading ? (
64
- <div className="p-8 text-center text-gray-500">加载中...</div>
65
- ) : (
66
- <>
67
- <div className="overflow-x-auto">
68
- <table className="min-w-full divide-y divide-gray-200">
69
- <thead className="bg-gray-50">
70
- <tr>
71
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
72
- ID
73
- </th>
74
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
75
- 手机号
76
- </th>
77
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
78
- 昵称
79
- </th>
80
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
81
- 角色
82
- </th>
83
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
84
- 状态
85
- </th>
86
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
87
- 创建时间
88
- </th>
89
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
90
- 操作
91
- </th>
92
- </tr>
93
- </thead>
94
- <tbody className="bg-white divide-y divide-gray-200">
95
- {users.map((user) => (
96
- <tr key={user.id}>
97
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
98
- {user.id}
99
- </td>
100
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
101
- {user.phone}
102
- </td>
103
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
104
- {user.nickname}
105
- </td>
106
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
107
- {user.role}
108
- </td>
109
- <td className="px-6 py-4 whitespace-nowrap">
110
- <span
111
- className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
112
- user.status === 1
113
- ? 'bg-green-100 text-green-800'
114
- : 'bg-red-100 text-red-800'
115
- }`}
116
- >
117
- {user.status === 1 ? '启用' : '禁用'}
118
- </span>
119
- </td>
120
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
121
- {new Date(user.createdAt).toLocaleDateString()}
122
- </td>
123
- <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
124
- <button className="text-blue-600 hover:text-blue-900 mr-4">
125
- 编辑
126
- </button>
127
- <button className="text-red-600 hover:text-red-900">
128
- 删除
129
- </button>
130
- </td>
131
- </tr>
132
- ))}
133
- </tbody>
134
- </table>
135
- </div>
136
-
137
- <div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
138
- <div className="text-sm text-gray-700">
139
- 共 {total} 条记录
140
- </div>
141
- <div className="flex gap-2">
142
- <button
143
- onClick={() => handlePageChange(params.page! - 1)}
144
- disabled={params.page === 1}
145
- className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
146
- >
147
- 上一页
148
- </button>
149
- <button
150
- onClick={() => handlePageChange(params.page! + 1)}
151
- disabled={params.page! * params.pageSize! >= total}
152
- className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
153
- >
154
- 下一页
155
- </button>
156
- </div>
157
- </div>
158
- </>
159
- )}
160
- </div>
161
- </div>
162
- </div>
163
- </div>
164
- );
165
- }
@@ -1,29 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as SwitchPrimitives from "@radix-ui/react-switch"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- const Switch = React.forwardRef<
9
- React.ElementRef<typeof SwitchPrimitives.Root>,
10
- React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11
- >(({ className, ...props }, ref) => (
12
- <SwitchPrimitives.Root
13
- className={cn(
14
- "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
15
- className
16
- )}
17
- {...props}
18
- ref={ref}
19
- >
20
- <SwitchPrimitives.Thumb
21
- className={cn(
22
- "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
23
- )}
24
- />
25
- </SwitchPrimitives.Root>
26
- ))
27
- Switch.displayName = SwitchPrimitives.Root.displayName
28
-
29
- export { Switch }
@@ -1,130 +0,0 @@
1
- /**
2
- * Admin 端 useAPI Hook
3
- * 基于 SWR 提供高级功能:
4
- * - 自动缓存和重新验证
5
- * - 乐观更新
6
- * - 依赖刷新
7
- * - 条件请求
8
- * - 分页支持
9
- */
10
-
11
- import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
12
- import useSWRInfinite, { type SWRInfiniteConfiguration } from 'swr/infinite';
13
- import useSWRMutation, { type SWRMutationConfiguration } from 'swr/mutation';
14
- import { apiAsync } from '@/lib/api-client';
15
- import type { ApiName, ApiParams, ApiResponse } from '@svton/api-client';
16
- // 引入类型定义以启用模块增强
17
- import '{{ORG_NAME}}/types';
18
-
19
- /**
20
- * 生成 SWR key
21
- */
22
- function generateKey<K extends ApiName>(apiName: K, params?: ApiParams<K>): string | null {
23
- if (params === undefined || params === null) {
24
- return null; // 条件请求:params 为空时不发起请求
25
- }
26
- return JSON.stringify([apiName, params]);
27
- }
28
-
29
- /**
30
- * SWR fetcher
31
- */
32
- async function fetcher<K extends ApiName>(key: string): Promise<ApiResponse<K>> {
33
- const [apiName, params] = JSON.parse(key);
34
- return (
35
- params !== undefined
36
- ? await (apiAsync as any)(apiName, params)
37
- : await (apiAsync as any)(apiName)
38
- ) as ApiResponse<K>;
39
- }
40
-
41
- /**
42
- * useQuery Hook - 用于数据获取(GET 请求)
43
- *
44
- * 特性:
45
- * - ✅ 自动缓存:相同请求自动复用缓存
46
- * - ✅ 自动重新验证:窗口聚焦、网络恢复时自动刷新
47
- * - ✅ 依赖刷新:params 变化时自动重新请求
48
- * - ✅ 条件请求:params 为 null 时不发起请求
49
- * - ✅ 乐观更新:支持 mutate 进行本地更新
50
- *
51
- * @example
52
- * ```tsx
53
- * function ContentList() {
54
- * const { data, error, isLoading, mutate } = useQuery('GET:/contents', {
55
- * page: 1,
56
- * pageSize: 20
57
- * });
58
- *
59
- * if (isLoading) return <div>加载中...</div>;
60
- * if (error) return <div>错误: {error.message}</div>;
61
- *
62
- * return <div>{data?.items.map(...)}</div>;
63
- * }
64
- * ```
65
- */
66
- export function useQuery<K extends ApiName>(
67
- apiName: K,
68
- params?: ApiParams<K> | null,
69
- config?: SWRConfiguration<ApiResponse<K>>,
70
- ): SWRResponse<ApiResponse<K>> {
71
- const key = generateKey(apiName, params as ApiParams<K>);
72
-
73
- return useSWR<ApiResponse<K>>(key, fetcher, {
74
- revalidateOnFocus: true, // 窗口聚焦时重新验证
75
- revalidateOnReconnect: true, // 网络恢复时重新验证
76
- dedupingInterval: 2000, // 2秒内去重
77
- ...config,
78
- });
79
- }
80
-
81
- /**
82
- * useMutation Hook - 用于数据提交(POST/PUT/DELETE 请求)
83
- *
84
- * @example
85
- * ```tsx
86
- * function CreateContent() {
87
- * const { trigger, isMutating } = useMutation('POST:/contents');
88
- *
89
- * const handleSubmit = async (formData) => {
90
- * try {
91
- * const result = await trigger({
92
- * title: formData.title,
93
- * body: formData.body,
94
- * });
95
- * console.log('Created:', result);
96
- * } catch (error) {
97
- * console.error('Failed:', error);
98
- * }
99
- * };
100
- *
101
- * return <Button onClick={handleSubmit} loading={isMutating}>提交</Button>;
102
- * }
103
- * ```
104
- */
105
- export function useMutation<K extends ApiName>(
106
- apiName: K,
107
- config?: SWRMutationConfiguration<ApiResponse<K>, Error, string, ApiParams<K>>,
108
- ) {
109
- const key = apiName; // mutation 使用 apiName 作为 key
110
-
111
- return useSWRMutation<ApiResponse<K>, Error, string, ApiParams<K>>(
112
- key,
113
- async (_key, { arg }) => {
114
- return (
115
- arg !== undefined ? await (apiAsync as any)(apiName, arg) : await (apiAsync as any)(apiName)
116
- ) as ApiResponse<K>;
117
- },
118
- config,
119
- );
120
- }
121
-
122
- /**
123
- * 手动刷新指定 key 的缓存
124
- */
125
- export { mutate } from 'swr';
126
-
127
- /**
128
- * 全局 SWR 配置
129
- */
130
- export type { SWRConfiguration };