zt-admin-template 1.0.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 (80) hide show
  1. package/package.json +11 -0
  2. package/template/.env.development +2 -0
  3. package/template/.env.production +2 -0
  4. package/template/.env.test +2 -0
  5. package/template/.kiro/specs/course-backend-integration/.config.kiro +1 -0
  6. package/template/.kiro/specs/course-backend-integration/design.md +234 -0
  7. package/template/.kiro/specs/course-backend-integration/requirements.md +116 -0
  8. package/template/.kiro/specs/course-backend-integration/tasks.md +0 -0
  9. package/template/COMPLETION_CHECKLIST.md +305 -0
  10. package/template/DEPLOYMENT_GUIDE.md +391 -0
  11. package/template/FINAL_SUMMARY.md +428 -0
  12. package/template/IMPLEMENTATION_SUMMARY.md +382 -0
  13. package/template/INTEGRATION_GUIDE.md +458 -0
  14. package/template/PROJECT_OVERVIEW.md +343 -0
  15. package/template/QUICK_START.md +273 -0
  16. package/template/RBAC_Tutorial.md +424 -0
  17. package/template/README.md +16 -0
  18. package/template/React_Antd_TS_Tutorial.md +279 -0
  19. package/template/START_ALL.md +163 -0
  20. package/template/SYSTEM_MANAGEMENT.md +247 -0
  21. package/template/eslint.config.js +29 -0
  22. package/template/index.html +13 -0
  23. package/template/koa-server/README.md +65 -0
  24. package/template/koa-server/app.js +625 -0
  25. package/template/koa-server/package-lock.json +1547 -0
  26. package/template/koa-server/package.json +26 -0
  27. package/template/koa-server/public/assets/index-B1Cj4mG9.css +1 -0
  28. package/template/koa-server/public/assets/index-Mgxg-xqT.js +503 -0
  29. package/template/koa-server/public/favicon.svg +1 -0
  30. package/template/koa-server/public/icons.svg +24 -0
  31. package/template/koa-server/public/index.html +14 -0
  32. package/template/koa-server/uploads/1774265088480-962006467.png +0 -0
  33. package/template/koa-server/uploads/file-1774346891704-610962013.png +0 -0
  34. package/template/koa-server/uploads/file-1774346898887-58636533.png +0 -0
  35. package/template/koa-server/uploads/file-1774346912676-771862547.png +0 -0
  36. package/template/koa-server/uploads/file-1774347025308-130037894.png +0 -0
  37. package/template/koa-server/uploads/file-1774347031104-766499773.png +0 -0
  38. package/template/koa-server/uploads/file-1774347094969-731402203.png +0 -0
  39. package/template/koa-server/uploads/file-1774347101948-330296656.png +0 -0
  40. package/template/koa-server/uploads/file-1774351682377-932868720.png +0 -0
  41. package/template/koa-server/uploads/file-1774352037654-877426905.png +0 -0
  42. package/template/koa-server/uploads/file-1774352175463-386248997.png +0 -0
  43. package/template/koa-server/uploads/file-1774361446433-405859961.png +0 -0
  44. package/template/koa-server/uploads/file-1774361512207-465806267.png +0 -0
  45. package/template/lianxi.html +15 -0
  46. package/template/package-lock.json +6307 -0
  47. package/template/package.json +36 -0
  48. package/template/public/favicon.svg +1 -0
  49. package/template/public/icons.svg +24 -0
  50. package/template/src/App.css +184 -0
  51. package/template/src/App.tsx +44 -0
  52. package/template/src/api/course.ts +86 -0
  53. package/template/src/api/menu.ts +55 -0
  54. package/template/src/api/role.ts +58 -0
  55. package/template/src/api/user.ts +58 -0
  56. package/template/src/assets/hero.png +0 -0
  57. package/template/src/assets/react.svg +1 -0
  58. package/template/src/assets/vite.svg +1 -0
  59. package/template/src/components/Child.tsx +10 -0
  60. package/template/src/components/MainLayout.tsx +169 -0
  61. package/template/src/components/SunZi.tsx +13 -0
  62. package/template/src/contexts/ThemeContext.tsx +33 -0
  63. package/template/src/hooks/usePermission.tsx +62 -0
  64. package/template/src/index.css +111 -0
  65. package/template/src/main.tsx +13 -0
  66. package/template/src/pages/Dashboard.tsx +39 -0
  67. package/template/src/pages/Users.tsx +95 -0
  68. package/template/src/pages/banner/BannerList.tsx +182 -0
  69. package/template/src/pages/course/Course.tsx +586 -0
  70. package/template/src/pages/course/CourseList.tsx +168 -0
  71. package/template/src/pages/system/menu/Menu.tsx +501 -0
  72. package/template/src/pages/system/role/Role.tsx +458 -0
  73. package/template/src/pages/system/user/User.tsx +364 -0
  74. package/template/src/types/permission.ts +21 -0
  75. package/template/src/utils/request.tsx +94 -0
  76. package/template/src/vite-env.d.ts +1 -0
  77. package/template/tsconfig.app.json +32 -0
  78. package/template/tsconfig.json +7 -0
  79. package/template/tsconfig.node.json +13 -0
  80. package/template/vite.config.ts +30 -0
@@ -0,0 +1,364 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Table,
4
+ Button,
5
+ Modal,
6
+ Form,
7
+ Input,
8
+ Select,
9
+ Switch,
10
+ Space,
11
+ Popconfirm,
12
+ message,
13
+ Card,
14
+ Row,
15
+ Col,
16
+ Tooltip,
17
+ } from 'antd';
18
+ import {
19
+ PlusOutlined,
20
+ EditOutlined,
21
+ DeleteOutlined,
22
+ SearchOutlined,
23
+ ReloadOutlined,
24
+ DownloadOutlined,
25
+ } from '@ant-design/icons';
26
+ import type { TableColumnsType } from 'antd';
27
+ import dayjs from 'dayjs';
28
+ import { getUserList, addUser, updateUser, deleteUser, type UserRecord } from '@/api/user';
29
+
30
+ const User = () => {
31
+ const [form] = Form.useForm();
32
+ const [searchForm] = Form.useForm();
33
+ const [users, setUsers] = useState<UserRecord[]>([]);
34
+ const [isModalVisible, setIsModalVisible] = useState(false);
35
+ const [editingUser, setEditingUser] = useState<UserRecord | null>(null);
36
+ const [loading, setLoading] = useState(false);
37
+ const [tableLoading, setTableLoading] = useState(false);
38
+ const [pagination, setPagination] = useState({ pageNum: 1, pageSize: 10, total: 0 });
39
+ const [searchParams, setSearchParams] = useState<{ username?: string; phonenumber?: string; status?: number }>({});
40
+
41
+ // 获取用户列表
42
+ const fetchUsers = async (pageNum = 1, pageSize = 10, params?: { username?: string; phonenumber?: string; status?: number }) => {
43
+ try {
44
+ setTableLoading(true);
45
+ const searchValues = params || searchParams;
46
+ const response = await getUserList({ pageNum, pageSize, ...searchValues });
47
+ setUsers(response.rows);
48
+ setPagination({ pageNum, pageSize, total: response.total });
49
+ } catch (error) {
50
+ console.error('获取用户列表失败:', error);
51
+ } finally {
52
+ setTableLoading(false);
53
+ }
54
+ };
55
+
56
+ // 初始化加载
57
+ useEffect(() => {
58
+ fetchUsers();
59
+ }, []);
60
+
61
+ const columns: TableColumnsType<UserRecord> = [
62
+ {
63
+ title: '用户编号',
64
+ dataIndex: 'id',
65
+ key: 'id',
66
+ width: 100,
67
+ },
68
+ {
69
+ title: '用户名称',
70
+ dataIndex: 'username',
71
+ key: 'username',
72
+ width: 120,
73
+ },
74
+ {
75
+ title: '用户昵称',
76
+ dataIndex: 'nickName',
77
+ key: 'nickName',
78
+ width: 120,
79
+ },
80
+ {
81
+ title: '部门',
82
+ dataIndex: 'dept',
83
+ key: 'dept',
84
+ width: 120,
85
+ },
86
+ {
87
+ title: '手机号码',
88
+ dataIndex: 'phonenumber',
89
+ key: 'phonenumber',
90
+ width: 130,
91
+ },
92
+ {
93
+ title: '状态',
94
+ dataIndex: 'status',
95
+ key: 'status',
96
+ width: 80,
97
+ render: (status: number) => (
98
+ <Switch checked={status === 0} disabled />
99
+ ),
100
+ },
101
+ {
102
+ title: '创建时间',
103
+ dataIndex: 'createTime',
104
+ key: 'createTime',
105
+ width: 180,
106
+ },
107
+ {
108
+ title: '操作',
109
+ key: 'action',
110
+ width: 200,
111
+ fixed: 'right',
112
+ render: (_, record) => (
113
+ <Space size="small">
114
+ <Tooltip title="编辑">
115
+ <Button
116
+ type="primary"
117
+ size="small"
118
+ icon={<EditOutlined />}
119
+ onClick={() => handleEdit(record)}
120
+ />
121
+ </Tooltip>
122
+ <Tooltip title="删除">
123
+ <Popconfirm
124
+ title="删除用户"
125
+ description="确定删除该用户吗?"
126
+ onConfirm={() => handleDelete(record.id)}
127
+ okText="确定"
128
+ cancelText="取消"
129
+ >
130
+ <Button danger size="small" icon={<DeleteOutlined />} />
131
+ </Popconfirm>
132
+ </Tooltip>
133
+ </Space>
134
+ ),
135
+ },
136
+ ];
137
+
138
+ const handleAdd = () => {
139
+ setEditingUser(null);
140
+ form.resetFields();
141
+ setIsModalVisible(true);
142
+ };
143
+
144
+ const handleEdit = (record: UserRecord) => {
145
+ setEditingUser(record);
146
+ form.setFieldsValue({
147
+ username: record.username,
148
+ nickName: record.nickName,
149
+ email: record.email,
150
+ phonenumber: record.phonenumber,
151
+ sex: record.sex,
152
+ status: record.status === 0,
153
+ dept: record.dept,
154
+ remark: record.remark,
155
+ });
156
+ setIsModalVisible(true);
157
+ };
158
+
159
+ const handleDelete = async (id: string) => {
160
+ try {
161
+ await deleteUser(id);
162
+ message.success('删除成功');
163
+ fetchUsers(pagination.pageNum, pagination.pageSize);
164
+ } catch (error) {
165
+ console.error('删除失败:', error);
166
+ }
167
+ };
168
+
169
+ const handleModalOk = async () => {
170
+ try {
171
+ const values = await form.validateFields();
172
+ setLoading(true);
173
+
174
+ if (editingUser) {
175
+ await updateUser({
176
+ ...editingUser,
177
+ ...values,
178
+ status: values.status ? 0 : 1,
179
+ });
180
+ message.success('编辑成功');
181
+ } else {
182
+ await addUser({
183
+ ...values,
184
+ status: values.status ? 0 : 1,
185
+ });
186
+ message.success('添加成功');
187
+ }
188
+
189
+ setIsModalVisible(false);
190
+ form.resetFields();
191
+ fetchUsers(pagination.pageNum, pagination.pageSize);
192
+ } catch (error) {
193
+ console.error('操作失败:', error);
194
+ } finally {
195
+ setLoading(false);
196
+ }
197
+ };
198
+
199
+ const handleSearch = (values: any) => {
200
+ const { username, phonenumber, status } = values;
201
+ const params = { username, phonenumber, status };
202
+ setSearchParams(params);
203
+ fetchUsers(1, pagination.pageSize, params);
204
+ };
205
+
206
+ const handleReset = () => {
207
+ searchForm.resetFields();
208
+ setSearchParams({});
209
+ fetchUsers(1, pagination.pageSize, {});
210
+ };
211
+
212
+ const handleExport = () => {
213
+ message.success('导出成功');
214
+ };
215
+
216
+ return (
217
+ <div style={{ padding: '24px' }}>
218
+ <Card style={{ marginBottom: '24px' }}>
219
+ <Form
220
+ form={searchForm}
221
+ layout="inline"
222
+ onFinish={handleSearch}
223
+ style={{ marginBottom: '16px' }}
224
+ >
225
+ <Form.Item name="username" label="用户名称">
226
+ <Input placeholder="请输入用户名称" />
227
+ </Form.Item>
228
+ <Form.Item name="phonenumber" label="手机号码">
229
+ <Input placeholder="请输入手机号码" />
230
+ </Form.Item>
231
+ <Form.Item name="status" label="状态">
232
+ <Select placeholder="请选择状态" style={{ width: 120 }} allowClear>
233
+ <Select.Option value={0}>正常</Select.Option>
234
+ <Select.Option value={1}>停用</Select.Option>
235
+ </Select>
236
+ </Form.Item>
237
+ <Form.Item>
238
+ <Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
239
+ 搜索
240
+ </Button>
241
+ </Form.Item>
242
+ <Form.Item>
243
+ <Button onClick={handleReset}>重置</Button>
244
+ </Form.Item>
245
+ </Form>
246
+ </Card>
247
+
248
+ <Card>
249
+ <Row gutter={[16, 16]} style={{ marginBottom: '16px' }}>
250
+ <Col>
251
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
252
+ 新增
253
+ </Button>
254
+ </Col>
255
+ <Col>
256
+ <Button icon={<ReloadOutlined />} onClick={() => fetchUsers(pagination.pageNum, pagination.pageSize)}>
257
+ 刷新
258
+ </Button>
259
+ </Col>
260
+ <Col>
261
+ <Button icon={<DownloadOutlined />} onClick={handleExport}>
262
+ 导出
263
+ </Button>
264
+ </Col>
265
+ </Row>
266
+
267
+ <Table
268
+ columns={columns}
269
+ dataSource={users}
270
+ rowKey="id"
271
+ loading={tableLoading}
272
+ pagination={{
273
+ current: pagination.pageNum,
274
+ pageSize: pagination.pageSize,
275
+ total: pagination.total,
276
+ showSizeChanger: true,
277
+ showQuickJumper: true,
278
+ showTotal: (total: number) => `共 ${total} 条`,
279
+ onChange: (page, pageSize) => fetchUsers(page, pageSize, searchParams),
280
+ }}
281
+ scroll={{ x: 1200 }}
282
+ />
283
+ </Card>
284
+
285
+ <Modal
286
+ title={editingUser ? '编辑用户' : '新增用户'}
287
+ open={isModalVisible}
288
+ onOk={handleModalOk}
289
+ onCancel={() => {
290
+ setIsModalVisible(false);
291
+ form.resetFields();
292
+ }}
293
+ width={600}
294
+ confirmLoading={loading}
295
+ >
296
+ <Form form={form} layout="vertical" autoComplete="off">
297
+ <Form.Item
298
+ name="username"
299
+ label="用户名称"
300
+ rules={[{ required: true, message: '请输入用户名称' }]}
301
+ >
302
+ <Input placeholder="请输入用户名称" disabled={!!editingUser} />
303
+ </Form.Item>
304
+
305
+ <Form.Item
306
+ name="nickName"
307
+ label="用户昵称"
308
+ rules={[{ required: true, message: '请输入用户昵称' }]}
309
+ >
310
+ <Input placeholder="请输入用户昵称" />
311
+ </Form.Item>
312
+
313
+ <Row gutter={16}>
314
+ <Col span={12}>
315
+ <Form.Item name="email" label="邮箱" rules={[{ type: 'email', message: '邮箱格式不正确' }]}>
316
+ <Input placeholder="请输入邮箱" />
317
+ </Form.Item>
318
+ </Col>
319
+ <Col span={12}>
320
+ <Form.Item
321
+ name="phonenumber"
322
+ label="手机号码"
323
+ rules={[{ pattern: /^1[3-9]\d{9}$/, message: '手机号码格式不正确' }]}
324
+ >
325
+ <Input placeholder="请输入手机号码" />
326
+ </Form.Item>
327
+ </Col>
328
+ </Row>
329
+
330
+ <Row gutter={16}>
331
+ <Col span={12}>
332
+ <Form.Item name="sex" label="性别">
333
+ <Select placeholder="请选择性别">
334
+ <Select.Option value="0">男</Select.Option>
335
+ <Select.Option value="1">女</Select.Option>
336
+ <Select.Option value="2">未知</Select.Option>
337
+ </Select>
338
+ </Form.Item>
339
+ </Col>
340
+ <Col span={12}>
341
+ <Form.Item name="dept" label="部门">
342
+ <Select placeholder="请选择部门">
343
+ <Select.Option value="研发部门">研发部门</Select.Option>
344
+ <Select.Option value="测试部门">测试部门</Select.Option>
345
+ <Select.Option value="运维部门">运维部门</Select.Option>
346
+ </Select>
347
+ </Form.Item>
348
+ </Col>
349
+ </Row>
350
+
351
+ <Form.Item name="status" label="状态" valuePropName="checked">
352
+ <Switch checkedChildren="正常" unCheckedChildren="停用" />
353
+ </Form.Item>
354
+
355
+ <Form.Item name="remark" label="备注">
356
+ <Input.TextArea placeholder="请输入备注" rows={3} />
357
+ </Form.Item>
358
+ </Form>
359
+ </Modal>
360
+ </div>
361
+ );
362
+ };
363
+
364
+ export default User;
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+
3
+ // 该文件用来定义权限相关的类型
4
+ export type PermissionCode = string;
5
+
6
+ // 定义用户拥有的权限信息 类型
7
+ export interface UserPermission{
8
+ permissions:PermissionCode[], // 用户的权限数组
9
+ roles:string[] // 用户拥有的角色列表
10
+ }
11
+
12
+ // 路由权限信息
13
+ export interface AuthRouteConfig{
14
+ path:string,
15
+ element:React.ReactNode,
16
+ title?:string,
17
+ icon?:string,
18
+ permission?:PermissionCode,// 访问该路由需要具备的权限码
19
+ children?:AuthRouteConfig[],
20
+ hidden?:boolean
21
+ }
@@ -0,0 +1,94 @@
1
+ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
2
+ import { message } from 'antd';
3
+
4
+ // 获取 API 地址
5
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';
6
+
7
+ // 创建 axios 实例
8
+ const instance: AxiosInstance = axios.create({
9
+ baseURL: API_URL,
10
+ timeout: 10000,
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ });
15
+
16
+ // 请求拦截器
17
+ instance.interceptors.request.use(
18
+ (config) => {
19
+ const token = localStorage.getItem('token');
20
+ if (token) {
21
+ config.headers.Authorization = `Bearer ${token}`;
22
+ }
23
+ return config;
24
+ },
25
+ (error) => {
26
+ return Promise.reject(error);
27
+ }
28
+ );
29
+
30
+ // 响应拦截器 —— 直接返回 data,绕过 AxiosResponse 包装
31
+ instance.interceptors.response.use(
32
+ (response: AxiosResponse) => {
33
+ const { code, message: msg, data } = response.data;
34
+
35
+ if (code === 200 || code === 0) {
36
+ return data as any;
37
+ }
38
+
39
+ message.error(msg || '请求失败');
40
+ return Promise.reject(new Error(msg || '请求失败'));
41
+ },
42
+ (error) => {
43
+ if (error.response) {
44
+ const { status, data } = error.response;
45
+ const msg = data?.message || '请求失败';
46
+
47
+ switch (status) {
48
+ case 401:
49
+ message.error('登录已过期,请重新登录');
50
+ localStorage.removeItem('token');
51
+ window.location.href = '/login';
52
+ break;
53
+ case 403:
54
+ message.error('没有权限访问');
55
+ break;
56
+ case 404:
57
+ message.error('请求的资源不存在');
58
+ break;
59
+ case 500:
60
+ message.error('服务器内部错误');
61
+ break;
62
+ default:
63
+ message.error(msg);
64
+ }
65
+ } else if (error.request) {
66
+ message.error('网络错误,请检查网络连接');
67
+ } else {
68
+ message.error('请求配置错误');
69
+ }
70
+
71
+ return Promise.reject(error);
72
+ }
73
+ );
74
+
75
+ // 用类型包装,让 get/post 等方法返回 Promise<T> 而非 Promise<AxiosResponse<T>>
76
+ export const request = {
77
+ get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
78
+ return instance.get(url, config) as unknown as Promise<T>;
79
+ },
80
+ post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
81
+ return instance.post(url, data, config) as unknown as Promise<T>;
82
+ },
83
+ put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
84
+ return instance.put(url, data, config) as unknown as Promise<T>;
85
+ },
86
+ delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
87
+ return instance.delete(url, config) as unknown as Promise<T>;
88
+ },
89
+ patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
90
+ return instance.patch(url, data, config) as unknown as Promise<T>;
91
+ },
92
+ };
93
+
94
+ export default instance;
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "target": "ES2020",
6
+ "useDefineForClassFields": true,
7
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
8
+ "module": "ESNext",
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "moduleDetection": "force",
17
+ "noEmit": true,
18
+ "jsx": "react-jsx",
19
+ "jsxImportSource": "react",
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "@/*": ["src/*"]
23
+ },
24
+
25
+ /* Linting */
26
+ "strict": false,
27
+ "noUnusedLocals": false,
28
+ "noUnusedParameters": false,
29
+ "noFallthroughCasesInSwitch": true
30
+ },
31
+ "include": ["src"]
32
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5
+ "skipLibCheck": true,
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "allowSyntheticDefaultImports": true,
9
+ "strict": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["vite.config.ts"]
13
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'path'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ build: {
9
+ outDir: 'koa-server/public',
10
+ emptyOutDir: true,
11
+ },
12
+ resolve: {
13
+ alias: {
14
+ '@': path.resolve(__dirname, './src'),
15
+ },
16
+ },
17
+ server: {
18
+ proxy: {
19
+ '/api': {
20
+ target: 'http://localhost:8080',
21
+ changeOrigin: true,
22
+ rewrite: (path) => path.replace(/^\/api/, ''),
23
+ },
24
+ '/upload': {
25
+ target: 'http://localhost:8080',
26
+ changeOrigin: true,
27
+ },
28
+ },
29
+ },
30
+ })