first commit
16
.claude/settings.local.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(del .gitindex.lock)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(powershell -Command \"Remove-Item -Path ''.git\\\\index.lock'' -Force\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"C:\\Users\\admin\\Desktop\\filedata\\project\\泽林-frontend\\.git"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.cache/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
95
.umirc.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { defineConfig } from '@umijs/max';
|
||||
|
||||
export default defineConfig({
|
||||
antd: {},
|
||||
model: {},
|
||||
request: {},
|
||||
mock: {},
|
||||
|
||||
// 路由配置
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home',
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: '@/pages/Home',
|
||||
},
|
||||
{
|
||||
path: '/jobs',
|
||||
component: '@/pages/Jobs',
|
||||
},
|
||||
{
|
||||
path: '/manage',
|
||||
component: '@/layouts/BasicLayout',
|
||||
routes: [
|
||||
{
|
||||
path: '/manage/about',
|
||||
name: '关于',
|
||||
component: '@/pages/About',
|
||||
},
|
||||
{
|
||||
path: '/manage/user',
|
||||
name: '用户管理',
|
||||
component: '@/pages/User',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/appointment',
|
||||
component: '@/pages/Appointment',
|
||||
},
|
||||
{
|
||||
path: '/resume',
|
||||
component: '@/pages/Resume',
|
||||
},
|
||||
{
|
||||
path: '/resume/create',
|
||||
component: '@/pages/Resume/Create',
|
||||
},
|
||||
{
|
||||
path: '/resume/preview',
|
||||
component: '@/pages/Resume/Preview',
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: '@/pages/Admin',
|
||||
routes: [
|
||||
{ path: '/admin', redirect: '/admin/college' },
|
||||
{ path: '/admin/college', component: '@/pages/Admin/College' },
|
||||
{ path: '/admin/staff', component: '@/pages/Admin/Staff' },
|
||||
{ path: '/admin/student', component: '@/pages/Admin/Student' },
|
||||
{ path: '/admin/role', component: '@/pages/Admin/Role' },
|
||||
{ path: '/admin/overview', component: '@/pages/Statistics/Overview' },
|
||||
{ path: '/admin/appointment-list', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/appointment-users', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/task-list', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/banner', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/security', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/user-manage', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/menu-manage', component: '@/pages/Admin/Placeholder' },
|
||||
{ path: '/admin/operation-log', component: '@/pages/Admin/Placeholder' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: '/login',
|
||||
component: '@/pages/Login',
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: '@/pages/404',
|
||||
},
|
||||
],
|
||||
|
||||
// 全局反向代理配置
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.0.55:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
npmClient: 'npm',
|
||||
});
|
||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.autoClosingTags": false
|
||||
}
|
||||
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 泽林前端项目
|
||||
|
||||
基于 Umi4 + Ant Design 5 的企业级前端项目模板。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Umi 4** - 企业级前端开发框架
|
||||
- **Ant Design 5** - 企业级 UI 组件库
|
||||
- **React 18** - JavaScript 库
|
||||
- **TypeScript** - 类型安全
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 基础路由配置
|
||||
- ✅ Mock 数据模拟
|
||||
- ✅ 全局反向代理
|
||||
- ✅ 布局组件
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── mock/ # Mock 数据
|
||||
│ └── user.ts # 用户相关接口模拟
|
||||
├── src/
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ │ └── BasicLayout.tsx
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Home/
|
||||
│ │ ├── About/
|
||||
│ │ ├── User/
|
||||
│ │ ├── Login/
|
||||
│ │ └── 404/
|
||||
│ └── global.less # 全局样式
|
||||
├── .umirc.ts # Umi 配置文件
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 路由配置
|
||||
|
||||
路由在 `.umirc.ts` 中配置:
|
||||
|
||||
```ts
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: '@/layouts/BasicLayout',
|
||||
routes: [
|
||||
{ path: '/home', component: '@/pages/Home' },
|
||||
{ path: '/about', component: '@/pages/About' },
|
||||
{ path: '/user', component: '@/pages/User' },
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### Mock 配置
|
||||
|
||||
Mock 文件放在 `mock/` 目录下,Umi 会自动加载。
|
||||
|
||||
### 反向代理配置
|
||||
|
||||
在 `.umirc.ts` 中配置:
|
||||
|
||||
```ts
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/api': '' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 默认账号
|
||||
|
||||
- 用户名: admin
|
||||
- 密码: 123456
|
||||
174
mock/appointment.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
const allAppointments = [
|
||||
// 已预约
|
||||
{ id: 1, name: '能力提升面试', category: '模拟面试', time: '2025.08.10', session: '14:00:00-16:00:00', teacher: '李老师', location: '教室1', status: 'booked' },
|
||||
{ id: 2, name: '能力提升面试', category: '模拟面试', time: '2025.08.10', session: '14:00:00-16:00:00', teacher: '李老师', location: '教室1', status: 'booked' },
|
||||
{ id: 3, name: '能力提升面试', category: '模拟面试', time: '2025.08.10', session: '14:00:00-16:00:00', teacher: '李老师', location: '教室1', status: 'booked' },
|
||||
{ id: 4, name: '职业规划咨询', category: '职业咨询', time: '2025.08.12', session: '09:00:00-11:00:00', teacher: '王老师', location: '会议室A', status: 'booked' },
|
||||
{ id: 5, name: '简历修改指导', category: '简历指导', time: '2025.08.15', session: '10:00:00-12:00:00', teacher: '张老师', location: '教室3', status: 'booked' },
|
||||
// 预约待确认
|
||||
{ id: 6, name: '模拟群面练习', category: '模拟面试', time: '2025.08.18', session: '14:00:00-16:00:00', teacher: '赵老师', location: '教室2', status: 'pending' },
|
||||
{ id: 7, name: '行业分析讲座', category: '讲座', time: '2025.08.20', session: '09:00:00-11:00:00', teacher: '刘老师', location: '报告厅', status: 'pending' },
|
||||
{ id: 8, name: '职业规划咨询', category: '职业咨询', time: '2025.08.22', session: '14:00:00-16:00:00', teacher: '王老师', location: '会议室A', status: 'pending' },
|
||||
// 已完成
|
||||
{ id: 9, name: '能力提升面试', category: '模拟面试', time: '2025.07.20', session: '14:00:00-16:00:00', teacher: '李老师', location: '教室1', status: 'completed' },
|
||||
{ id: 10, name: '简历修改指导', category: '简历指导', time: '2025.07.15', session: '10:00:00-12:00:00', teacher: '张老师', location: '教室3', status: 'completed' },
|
||||
{ id: 11, name: '职业规划咨询', category: '职业咨询', time: '2025.07.10', session: '09:00:00-11:00:00', teacher: '王老师', location: '会议室A', status: 'completed' },
|
||||
{ id: 12, name: '模拟群面练习', category: '模拟面试', time: '2025.07.05', session: '14:00:00-16:00:00', teacher: '赵老师', location: '教室2', status: 'completed' },
|
||||
// 已取消
|
||||
{ id: 13, name: '行业分析讲座', category: '讲座', time: '2025.07.25', session: '09:00:00-11:00:00', teacher: '刘老师', location: '报告厅', status: 'cancelled' },
|
||||
{ id: 14, name: '能力提升面试', category: '模拟面试', time: '2025.07.22', session: '14:00:00-16:00:00', teacher: '李老师', location: '教室1', status: 'cancelled' },
|
||||
];
|
||||
|
||||
// 预约管理数据
|
||||
const specialManage = [
|
||||
{ id: 101, name: '能力提升面试', category: '模拟面试', startTime: '2025.09.01', endTime: '2026.10.01', remaining: 20, total: 300, sessions: 20, teacher: '李老师', status: 'ongoing' },
|
||||
{ id: 102, name: '能力提升面试', category: '模拟面试', startTime: '2025.09.01', endTime: '2026.10.01', remaining: 20, total: 300, sessions: 20, teacher: '李老师', status: 'ended' },
|
||||
{ id: 103, name: '能力提升面试', category: '模拟面试', startTime: '2025.09.01', endTime: '2026.10.01', remaining: 0, total: 300, sessions: 20, teacher: '李老师', status: 'ongoing' },
|
||||
];
|
||||
const categories = ['模拟面试', '职业咨询', '简历指导'];
|
||||
const teachers = ['李老师', '王老师', '张老师', '赵老师', '刘老师', '陈老师', '周老师', '吴老师'];
|
||||
const statuses = ['ongoing', 'notStarted', 'ongoing', 'ongoing', 'ongoing', 'ongoing', 'ended', 'ongoing'];
|
||||
const names = ['能力提升面试', '职业规划咨询', '简历修改指导', '模拟群面练习', '行业分析讲座', '求职技巧培训'];
|
||||
const manageAppointments = [
|
||||
...specialManage,
|
||||
...Array.from({ length: 97 }, (_, i) => ({
|
||||
id: 104 + i,
|
||||
name: names[i % names.length],
|
||||
category: categories[i % categories.length],
|
||||
startTime: '2025.09.01',
|
||||
endTime: '2026.10.01',
|
||||
remaining: 20 + (i % 50),
|
||||
total: 300,
|
||||
sessions: 10 + (i % 20),
|
||||
teacher: teachers[i % teachers.length],
|
||||
status: statuses[i % statuses.length],
|
||||
})),
|
||||
];
|
||||
|
||||
// 用户映射:userId=2 的用户没有任何预约数据
|
||||
const userAppointments: Record<number, number[]> = {
|
||||
1: allAppointments.map((a) => a.id), // 默认用户有所有数据
|
||||
2: [], // 空白用户
|
||||
};
|
||||
|
||||
export default {
|
||||
// 获取预约列表
|
||||
'GET /api/appointment/list': (req: any, res: any) => {
|
||||
const { status, name, student, date, page = 1, pageSize = 10, userId = 1 } = req.query;
|
||||
const uid = Number(userId);
|
||||
const userIds = userAppointments[uid] ?? userAppointments[1];
|
||||
|
||||
let filtered = allAppointments.filter((item) => userIds.includes(item.id));
|
||||
|
||||
// 按状态筛选
|
||||
if (status) {
|
||||
filtered = filtered.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
// 按预约名称模糊搜索
|
||||
if (name) {
|
||||
filtered = filtered.filter((item) => item.name.includes(name));
|
||||
}
|
||||
|
||||
// 按日期筛选
|
||||
if (date) {
|
||||
filtered = filtered.filter((item) => item.time === date);
|
||||
}
|
||||
|
||||
const total = filtered.length;
|
||||
const start = (Number(page) - 1) * Number(pageSize);
|
||||
const end = start + Number(pageSize);
|
||||
const list = filtered.slice(start, end);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
data: {
|
||||
list,
|
||||
total,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
},
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
|
||||
// 预约管理列表
|
||||
'GET /api/appointment/manage': (req: any, res: any) => {
|
||||
const { name, category, teacher, status, page = 1, pageSize = 10 } = req.query;
|
||||
|
||||
let filtered = [...manageAppointments];
|
||||
|
||||
if (name) {
|
||||
filtered = filtered.filter((item) => item.name.includes(name));
|
||||
}
|
||||
if (category) {
|
||||
filtered = filtered.filter((item) => item.category === category);
|
||||
}
|
||||
if (teacher) {
|
||||
filtered = filtered.filter((item) => item.teacher.includes(teacher));
|
||||
}
|
||||
if (status) {
|
||||
filtered = filtered.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
const total = filtered.length;
|
||||
const start = (Number(page) - 1) * Number(pageSize);
|
||||
const end = start + Number(pageSize);
|
||||
const list = filtered.slice(start, end);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
data: { list, total, page: Number(page), pageSize: Number(pageSize) },
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
|
||||
// 预约操作
|
||||
'POST /api/appointment/book': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const item = manageAppointments.find((a) => a.id === Number(id));
|
||||
|
||||
if (!item) {
|
||||
res.json({ code: 404, message: '预约项目不存在' });
|
||||
return;
|
||||
}
|
||||
if (item.status === 'ended') {
|
||||
res.json({ code: 400, message: '该项目已结束,不可预约' });
|
||||
return;
|
||||
}
|
||||
if (item.remaining <= 0) {
|
||||
res.json({ code: 400, message: '名额已满,不可预约' });
|
||||
return;
|
||||
}
|
||||
|
||||
item.remaining -= 1;
|
||||
res.json({ code: 0, message: '预约成功' });
|
||||
},
|
||||
|
||||
// 取消预约
|
||||
'POST /api/appointment/cancel': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const appointment = allAppointments.find((a) => a.id === Number(id));
|
||||
|
||||
if (!appointment) {
|
||||
res.json({ code: 404, message: '预约不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 已完成的不能取消
|
||||
if (appointment.status === 'completed') {
|
||||
res.json({ code: 400, message: '已结束的预约不能取消' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 已取消的不能重复取消
|
||||
if (appointment.status === 'cancelled') {
|
||||
res.json({ code: 400, message: '该预约已取消' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有 booked 和 pending 可以取消
|
||||
appointment.status = 'cancelled';
|
||||
res.json({ code: 0, message: '取消成功' });
|
||||
},
|
||||
};
|
||||
215
mock/college.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
// Mock data for college management
|
||||
const collegeTree = [
|
||||
{
|
||||
id: '1',
|
||||
name: '设计学院',
|
||||
depts: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: '视觉传达设计系',
|
||||
majors: [
|
||||
{
|
||||
id: '1-1-1',
|
||||
name: '平面设计',
|
||||
classes: [
|
||||
{ id: '1-1-1-1', name: '平面设计1班' },
|
||||
{ id: '1-1-1-2', name: '平面设计2班' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1-1-2',
|
||||
name: '数字媒体设计',
|
||||
classes: [
|
||||
{ id: '1-1-2-1', name: '数字媒体1班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1-2',
|
||||
name: '环境设计系',
|
||||
majors: [
|
||||
{
|
||||
id: '1-2-1',
|
||||
name: '室内设计',
|
||||
classes: [
|
||||
{ id: '1-2-1-1', name: '室内设计1班' },
|
||||
{ id: '1-2-1-2', name: '室内设计2班' },
|
||||
{ id: '1-2-1-3', name: '室内设计3班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '管理学院',
|
||||
depts: [
|
||||
{
|
||||
id: '2-1',
|
||||
name: '工商管理系',
|
||||
majors: [
|
||||
{
|
||||
id: '2-1-1',
|
||||
name: '工商管理',
|
||||
classes: [
|
||||
{ id: '2-1-1-1', name: '工商管理1班' },
|
||||
{ id: '2-1-1-2', name: '工商管理2班' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2-1-2',
|
||||
name: '市场营销',
|
||||
classes: [
|
||||
{ id: '2-1-2-1', name: '市场营销1班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2-2',
|
||||
name: '会计系',
|
||||
majors: [
|
||||
{
|
||||
id: '2-2-1',
|
||||
name: '财务管理',
|
||||
classes: [
|
||||
{ id: '2-2-1-1', name: '财务管理1班' },
|
||||
{ id: '2-2-1-2', name: '财务管理2班' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2-2-2',
|
||||
name: '审计学',
|
||||
classes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '艺术学院',
|
||||
depts: [
|
||||
{
|
||||
id: '3-1',
|
||||
name: '音乐系',
|
||||
majors: [
|
||||
{
|
||||
id: '3-1-1',
|
||||
name: '声乐表演',
|
||||
classes: [
|
||||
{ id: '3-1-1-1', name: '声乐表演1班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '信息工程学院',
|
||||
depts: [
|
||||
{
|
||||
id: '4-1',
|
||||
name: '计算机系',
|
||||
majors: [
|
||||
{
|
||||
id: '4-1-1',
|
||||
name: '软件工程',
|
||||
classes: [
|
||||
{ id: '4-1-1-1', name: '软件工程1班' },
|
||||
{ id: '4-1-1-2', name: '软件工程2班' },
|
||||
{ id: '4-1-1-3', name: '软件工程3班' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4-1-2',
|
||||
name: '计算机科学与技术',
|
||||
classes: [
|
||||
{ id: '4-1-2-1', name: '计科1班' },
|
||||
{ id: '4-1-2-2', name: '计科2班' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4-1-3',
|
||||
name: '人工智能',
|
||||
classes: [
|
||||
{ id: '4-1-3-1', name: 'AI1班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4-2',
|
||||
name: '电子信息系',
|
||||
majors: [
|
||||
{
|
||||
id: '4-2-1',
|
||||
name: '通信工程',
|
||||
classes: [
|
||||
{ id: '4-2-1-1', name: '通信工程1班' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '外国语学院',
|
||||
depts: [],
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/admin/college': (_req: any, res: any) => {
|
||||
res.json({
|
||||
code: 0,
|
||||
data: collegeTree,
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/admin/college': (req: any, res: any) => {
|
||||
const { data } = req.body;
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.json({ code: 40000, data: null, message: '参数错误' });
|
||||
}
|
||||
|
||||
// Validate: all names must be non-empty
|
||||
const validate = (items: any[], level: string): string | null => {
|
||||
for (const item of items) {
|
||||
if (!item.name || !item.name.trim()) {
|
||||
return `${level}名称不能为空`;
|
||||
}
|
||||
if (item.depts) {
|
||||
const err = validate(item.depts, '系');
|
||||
if (err) return err;
|
||||
}
|
||||
if (item.majors) {
|
||||
const err = validate(item.majors, '专业');
|
||||
if (err) return err;
|
||||
}
|
||||
if (item.classes) {
|
||||
const err = validate(item.classes, '班级');
|
||||
if (err) return err;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const err = validate(data, '学院');
|
||||
if (err) {
|
||||
return res.json({ code: 40001, data: null, message: err });
|
||||
}
|
||||
|
||||
console.log('[Mock] 保存学校管理数据, 学院数量:', data.length);
|
||||
res.json({
|
||||
code: 0,
|
||||
data: null,
|
||||
message: '保存成功',
|
||||
});
|
||||
},
|
||||
};
|
||||
42
mock/home.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export default {
|
||||
// 获取首页轮播 Banner 列表
|
||||
'GET /api/home/banners': {
|
||||
code: 0,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
title: '武汉高校校园招聘会',
|
||||
highlightText: '校园',
|
||||
subtitle: '科技无限 · 海量职位等你来',
|
||||
date: '活动时间:2026/01/01',
|
||||
imageUrl: 'https://test02-1302947942.cos.ap-shanghai.myqcloud.com/%E6%B3%BD%E6%9E%97-banner/banner.png',
|
||||
linkUrl: '/jobs?event=1',
|
||||
sort: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'AI助力职业发展',
|
||||
highlightText: '职业',
|
||||
subtitle: '智能匹配 · 精准推荐好工作',
|
||||
date: '平台持续更新中',
|
||||
imageUrl: 'https://test02-1302947942.cos.ap-shanghai.myqcloud.com/%E6%B3%BD%E6%9E%97-banner/banner.png',
|
||||
linkUrl: '/jobs',
|
||||
sort: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '春季校园专场招聘',
|
||||
highlightText: '专场',
|
||||
subtitle: '名企云集 · 优质岗位等你来',
|
||||
date: '活动时间:2026/03/15',
|
||||
imageUrl: 'https://test02-1302947942.cos.ap-shanghai.myqcloud.com/%E6%B3%BD%E6%9E%97-banner/banner.png',
|
||||
linkUrl: '/jobs?event=3',
|
||||
sort: 3,
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
message: 'ok',
|
||||
},
|
||||
};
|
||||
41
mock/overview.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
// 模拟数据总览数据
|
||||
const mockOverviewData = {
|
||||
aiInterviewUsage: 900,
|
||||
aiResumeUsage: 900,
|
||||
currentTasks: 0,
|
||||
completedPersons: 150,
|
||||
todayInterviewUsage: 10,
|
||||
todayResumeUsage: 8,
|
||||
};
|
||||
|
||||
export default {
|
||||
// 获取数据总览
|
||||
'GET /api/overview/data': (req: Request, res: Response) => {
|
||||
setTimeout(() => {
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: mockOverviewData,
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// 更新数据总览
|
||||
'POST /api/overview/update': (req: Request, res: Response) => {
|
||||
const { type, value } = req.body;
|
||||
|
||||
if (mockOverviewData.hasOwnProperty(type)) {
|
||||
(mockOverviewData as any)[type] = value;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: mockOverviewData,
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
};
|
||||
18
mock/resume.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 简历相关 Mock
|
||||
*
|
||||
* 以下接口已接入真实后端 192.168.0.55:8080,通过 proxy 转发:
|
||||
* - GET /api/resume/student/templates 模板列表
|
||||
* - POST /api/resume/student/ai/generate AI生成简历
|
||||
*
|
||||
* 以下仅保留尚未对接真实接口的 mock。
|
||||
*/
|
||||
|
||||
const generatedResumes: any[] = [];
|
||||
|
||||
export default {
|
||||
// 获取我的简历列表(暂无真实接口)
|
||||
'GET /api/resume/mine': (_req: any, res: any) => {
|
||||
res.json({ code: 0, data: generatedResumes, message: 'ok' });
|
||||
},
|
||||
};
|
||||
320
mock/role.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
// 模拟角色数据
|
||||
const roleList = [
|
||||
{
|
||||
id: '1',
|
||||
name: '超级管理员',
|
||||
description: '拥有系统所有权限',
|
||||
permissions: ['user_manage', 'role_manage', 'system_config', 'data_view'],
|
||||
status: 'active',
|
||||
createTime: '2024-01-01 10:00:00',
|
||||
updateTime: '2024-01-01 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '学院管理员',
|
||||
description: '管理学院相关事务',
|
||||
permissions: ['student_manage', 'staff_manage', 'data_view'],
|
||||
status: 'active',
|
||||
createTime: '2024-01-02 10:00:00',
|
||||
updateTime: '2024-01-02 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '教师',
|
||||
description: '教师角色,可查看学生信息',
|
||||
permissions: ['student_view', 'data_view'],
|
||||
status: 'active',
|
||||
createTime: '2024-01-03 10:00:00',
|
||||
updateTime: '2024-01-03 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '学生',
|
||||
description: '学生角色,基础权限',
|
||||
permissions: ['profile_view'],
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-04 10:00:00',
|
||||
updateTime: '2024-01-04 10:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
// 权限树数据
|
||||
const permissionTree = [
|
||||
{
|
||||
id: 'user_manage',
|
||||
name: '用户管理',
|
||||
code: 'user_manage',
|
||||
type: 'menu',
|
||||
children: [
|
||||
{
|
||||
id: 'student_manage',
|
||||
name: '学生管理',
|
||||
code: 'student_manage',
|
||||
type: 'menu',
|
||||
parentId: 'user_manage',
|
||||
},
|
||||
{
|
||||
id: 'staff_manage',
|
||||
name: '教职工管理',
|
||||
code: 'staff_manage',
|
||||
type: 'menu',
|
||||
parentId: 'user_manage',
|
||||
},
|
||||
{
|
||||
id: 'student_view',
|
||||
name: '学生查看',
|
||||
code: 'student_view',
|
||||
type: 'button',
|
||||
parentId: 'user_manage',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'role_manage',
|
||||
name: '角色管理',
|
||||
code: 'role_manage',
|
||||
type: 'menu',
|
||||
},
|
||||
{
|
||||
id: 'system_config',
|
||||
name: '系统配置',
|
||||
code: 'system_config',
|
||||
type: 'menu',
|
||||
children: [
|
||||
{
|
||||
id: 'banner_manage',
|
||||
name: '轮播图管理',
|
||||
code: 'banner_manage',
|
||||
type: 'menu',
|
||||
parentId: 'system_config',
|
||||
children: [
|
||||
{
|
||||
id: 'banner_add',
|
||||
name: '新增轮播图',
|
||||
code: 'banner_add',
|
||||
type: 'button',
|
||||
parentId: 'banner_manage',
|
||||
},
|
||||
{
|
||||
id: 'banner_edit',
|
||||
name: '编辑轮播图',
|
||||
code: 'banner_edit',
|
||||
type: 'button',
|
||||
parentId: 'banner_manage',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'security_config',
|
||||
name: '安全配置',
|
||||
code: 'security_config',
|
||||
type: 'menu',
|
||||
parentId: 'system_config',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'data_view',
|
||||
name: '数据查看',
|
||||
code: 'data_view',
|
||||
type: 'menu',
|
||||
},
|
||||
{
|
||||
id: 'profile_view',
|
||||
name: '个人信息查看',
|
||||
code: 'profile_view',
|
||||
type: 'button',
|
||||
},
|
||||
];
|
||||
|
||||
// 组织架构数据
|
||||
const organizationData = [
|
||||
{
|
||||
id: 'school_1',
|
||||
name: '学校管理',
|
||||
type: 'school',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'school_2',
|
||||
name: '学校管理',
|
||||
type: 'school',
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
id: 'dept_1',
|
||||
name: '编辑',
|
||||
type: 'department',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'major_1',
|
||||
name: '批量导入',
|
||||
type: 'major',
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 'class_1',
|
||||
name: '批量导入',
|
||||
type: 'class',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'major_2',
|
||||
name: '新增',
|
||||
type: 'major',
|
||||
expanded: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'school_3',
|
||||
name: '角色管理',
|
||||
type: 'school',
|
||||
expanded: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 获取角色列表
|
||||
export default {
|
||||
'GET /api/admin/role': (req: Request, res: Response) => {
|
||||
const { page = 1, pageSize = 10, name } = req.query;
|
||||
|
||||
let filteredList = [...roleList];
|
||||
|
||||
// 按名称筛选
|
||||
if (name) {
|
||||
filteredList = filteredList.filter(item =>
|
||||
item.name.includes(name as string)
|
||||
);
|
||||
}
|
||||
|
||||
const start = (Number(page) - 1) * Number(pageSize);
|
||||
const end = start + Number(pageSize);
|
||||
const list = filteredList.slice(start, end);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
list,
|
||||
total: filteredList.length,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 获取组织架构数据
|
||||
'GET /api/admin/organization': (req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: organizationData,
|
||||
});
|
||||
},
|
||||
|
||||
// 获取权限树
|
||||
'GET /api/admin/role/permissions': (req: Request, res: Response) => {
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: permissionTree,
|
||||
});
|
||||
},
|
||||
|
||||
// 创建角色
|
||||
'POST /api/admin/role/create': (req: Request, res: Response) => {
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const newRole = {
|
||||
id: String(Date.now()),
|
||||
name,
|
||||
description: description || '',
|
||||
permissions: permissions || [],
|
||||
status: 'active',
|
||||
createTime: new Date().toLocaleString('zh-CN'),
|
||||
updateTime: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
|
||||
roleList.push(newRole);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data: newRole,
|
||||
});
|
||||
},
|
||||
|
||||
// 更新角色
|
||||
'POST /api/admin/role/update': (req: Request, res: Response) => {
|
||||
const { id, name, description, permissions } = req.body;
|
||||
|
||||
const index = roleList.findIndex(item => item.id === id);
|
||||
if (index === -1) {
|
||||
return res.json({
|
||||
code: 1,
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
roleList[index] = {
|
||||
...roleList[index],
|
||||
name,
|
||||
description: description || '',
|
||||
permissions: permissions || [],
|
||||
updateTime: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data: roleList[index],
|
||||
});
|
||||
},
|
||||
|
||||
// 切换角色状态
|
||||
'POST /api/admin/role/toggle-status': (req: Request, res: Response) => {
|
||||
const { id } = req.body;
|
||||
|
||||
const role = roleList.find(item => item.id === id);
|
||||
if (!role) {
|
||||
return res.json({
|
||||
code: 1,
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
role.status = role.status === 'active' ? 'inactive' : 'active';
|
||||
role.updateTime = new Date().toLocaleString('zh-CN');
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '状态更新成功',
|
||||
data: role,
|
||||
});
|
||||
},
|
||||
|
||||
// 删除角色
|
||||
'POST /api/admin/role/delete': (req: Request, res: Response) => {
|
||||
const { id } = req.body;
|
||||
|
||||
const index = roleList.findIndex(item => item.id === id);
|
||||
if (index === -1) {
|
||||
return res.json({
|
||||
code: 1,
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
roleList.splice(index, 1);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
message: '删除成功',
|
||||
});
|
||||
},
|
||||
};
|
||||
150
mock/staff.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
const familyNames = ['赵', '冯', '钱', '刘', '孙', '李', '周', '吴', '郑', '王', '陈', '杨', '黄', '张', '林', '何', '高', '马', '罗', '梁'];
|
||||
const givenNames = ['玉岚', '艺莲', '若蓉', '思钰', '婷雅', '恩铭', '浩然', '子涵', '欣怡', '梓萱', '宇轩', '雨桐', '佳琪', '思远', '天乐', '晓峰', '文博', '嘉欣', '雅静', '明辉'];
|
||||
|
||||
const colleges = ['设计学院', '管理学院', '艺术学院'];
|
||||
const deptMap: Record<string, string[]> = {
|
||||
'设计学院': ['视觉传达设计系', '环境设计系'],
|
||||
'管理学院': ['工商管理系', '会计系', '人力资源系'],
|
||||
'艺术学院': ['音乐系', '美术系'],
|
||||
};
|
||||
|
||||
const classMap: Record<string, string[]> = {
|
||||
'工商管理系': ['工商管理1班', '工商管理2班'],
|
||||
'会计系': ['财务管理1班', '财务管理2班'],
|
||||
'人力资源系': ['人力资源1班'],
|
||||
'视觉传达设计系': ['平面设计1班', '平面设计2班'],
|
||||
'环境设计系': ['室内设计1班', '室内设计2班', '室内设计3班'],
|
||||
'音乐系': ['声乐表演1班'],
|
||||
'美术系': ['国画1班', '油画1班'],
|
||||
};
|
||||
|
||||
const statuses: Array<'normal' | 'disabled'> = ['normal', 'normal', 'normal', 'normal', 'disabled'];
|
||||
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function maskPhone(phone: string) {
|
||||
return phone.substring(0, 3) + '****' + phone.substring(7);
|
||||
}
|
||||
|
||||
// 生成100条教职工数据
|
||||
const staffList: any[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const college = randomPick(colleges);
|
||||
const depts = deptMap[college] || [];
|
||||
const dept = randomPick(depts);
|
||||
const classes = classMap[dept] || [];
|
||||
const cls = classes.length > 0 ? randomPick(classes) : '';
|
||||
const status = randomPick(statuses);
|
||||
const phone = `1${randomPick(['39', '56', '78', '38', '59'])}${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`;
|
||||
|
||||
staffList.push({
|
||||
id: String(i + 1),
|
||||
name: randomPick(familyNames) + randomPick(givenNames),
|
||||
phone,
|
||||
maskedPhone: maskPhone(phone),
|
||||
status,
|
||||
college,
|
||||
dept,
|
||||
className: cls,
|
||||
});
|
||||
}
|
||||
|
||||
// 学院树结构(用于左侧导航)
|
||||
const collegeTree = colleges.map((c) => ({
|
||||
id: c,
|
||||
name: c,
|
||||
children: (deptMap[c] || []).map((d) => ({
|
||||
id: d,
|
||||
name: d,
|
||||
children: (classMap[d] || []).map((cls) => ({
|
||||
id: cls,
|
||||
name: cls,
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
export default {
|
||||
'GET /api/admin/staff': (req: any, res: any) => {
|
||||
const { page = '1', pageSize = '10', name, phone, college, dept, className, status } = req.query;
|
||||
let filtered = [...staffList];
|
||||
if (name) filtered = filtered.filter((s) => s.name.includes(name));
|
||||
if (phone) filtered = filtered.filter((s) => s.phone.includes(phone));
|
||||
if (college) filtered = filtered.filter((s) => s.college === college);
|
||||
if (dept) filtered = filtered.filter((s) => s.dept === dept);
|
||||
if (className) filtered = filtered.filter((s) => s.className === className);
|
||||
if (status) filtered = filtered.filter((s) => s.status === status);
|
||||
|
||||
const p = Number(page);
|
||||
const ps = Number(pageSize);
|
||||
const start = (p - 1) * ps;
|
||||
res.json({ code: 0, data: { list: filtered.slice(start, start + ps), total: filtered.length, page: p, pageSize: ps }, message: 'ok' });
|
||||
},
|
||||
|
||||
'GET /api/admin/staff/college-tree': (_req: any, res: any) => {
|
||||
res.json({ code: 0, data: collegeTree, message: 'ok' });
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/add': (req: any, res: any) => {
|
||||
const { name, phone, college, dept, className } = req.body;
|
||||
if (!name || !phone) { res.json({ code: 40000, message: '请填写完整信息' }); return; }
|
||||
const newStaff = {
|
||||
id: String(staffList.length + 1000),
|
||||
name, phone, maskedPhone: maskPhone(phone), status: 'normal' as const,
|
||||
college: college || '', dept: dept || '', className: className || '',
|
||||
};
|
||||
staffList.unshift(newStaff);
|
||||
res.json({ code: 0, data: newStaff, message: '新增成功' });
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/edit': (req: any, res: any) => {
|
||||
const { id, name, phone, college, dept, className } = req.body;
|
||||
const staff = staffList.find((s) => s.id === id);
|
||||
if (!staff) { res.json({ code: 40000, message: '教职工不存在' }); return; }
|
||||
Object.assign(staff, { name, phone, maskedPhone: maskPhone(phone), college, dept, className });
|
||||
res.json({ code: 0, data: staff, message: '编辑成功' });
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/toggle-status': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const staff = staffList.find((s) => s.id === id);
|
||||
if (staff) { staff.status = staff.status === 'normal' ? 'disabled' : 'normal'; res.json({ code: 0, message: '操作成功' }); }
|
||||
else { res.json({ code: 40000, message: '教职工不存在' }); }
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/delete': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const idx = staffList.findIndex((s) => s.id === id);
|
||||
if (idx > -1) { staffList.splice(idx, 1); res.json({ code: 0, message: '删除成功' }); }
|
||||
else { res.json({ code: 40000, message: '教职工不存在' }); }
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/batch-delete': (req: any, res: any) => {
|
||||
const { ids } = req.body;
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) { res.json({ code: 40000, message: '请选择要删除的教职工' }); return; }
|
||||
let count = 0;
|
||||
for (let i = staffList.length - 1; i >= 0; i--) { if (ids.includes(staffList[i].id)) { staffList.splice(i, 1); count++; } }
|
||||
res.json({ code: 0, data: { count }, message: `成功删除${count}名教职工` });
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/batch-import': (_req: any, res: any) => {
|
||||
const imported: any[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const college = randomPick(colleges);
|
||||
const dept = randomPick(deptMap[college] || []);
|
||||
const phone = `1${randomPick(['39', '56', '78', '38', '59'])}${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`;
|
||||
const s = { id: String(Date.now() + i), name: randomPick(familyNames) + randomPick(givenNames), phone, maskedPhone: maskPhone(phone), status: 'normal' as const, college, dept, className: '' };
|
||||
staffList.unshift(s);
|
||||
imported.push(s);
|
||||
}
|
||||
res.json({ code: 0, data: { count: imported.length }, message: `成功导入${imported.length}名教职工` });
|
||||
},
|
||||
|
||||
'POST /api/admin/staff/reset-password': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const staff = staffList.find((s) => s.id === id);
|
||||
if (staff) { res.json({ code: 0, message: '密码已重置为 123456' }); }
|
||||
else { res.json({ code: 40000, message: '教职工不存在' }); }
|
||||
},
|
||||
};
|
||||
379
mock/student.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
const familyNames = ['赵', '冯', '钱', '刘', '孙', '李', '周', '吴', '郑', '王', '陈', '杨', '黄', '张', '林', '何', '高', '马', '罗', '梁'];
|
||||
const givenNames = ['玉岚', '艺莲', '若蓉', '思钰', '婷雅', '恩铭', '浩然', '子涵', '欣怡', '梓萱', '宇轩', '雨桐', '佳琪', '思远', '天乐', '晓峰', '文博', '嘉欣', '雅静', '明辉'];
|
||||
const colleges = ['电气化学院', '管理学院', '设计学院', '信息工程学院', '艺术学院', '外国语学院'];
|
||||
const deptMap: Record<string, string[]> = {
|
||||
'电气化学院': ['电力工程系', '电气工程系', '自动化系'],
|
||||
'管理学院': ['工商管理系', '会计系', '人力资源系'],
|
||||
'设计学院': ['视觉传达设计系', '环境设计系'],
|
||||
'信息工程学院': ['计算机系', '电子信息系'],
|
||||
'艺术学院': ['音乐系', '美术系'],
|
||||
'外国语学院': ['英语系', '日语系'],
|
||||
};
|
||||
const majorMap: Record<string, string[]> = {
|
||||
'电力工程系': ['电气化', '电力工程'],
|
||||
'电气工程系': ['电气工程', '智能电网'],
|
||||
'自动化系': ['自动化', '机器人工程'],
|
||||
'工商管理系': ['工商管理', '市场营销'],
|
||||
'会计系': ['财务管理', '审计学'],
|
||||
'人力资源系': ['人力资源管理'],
|
||||
'视觉传达设计系': ['平面设计', '数字媒体设计'],
|
||||
'环境设计系': ['室内设计', '景观设计'],
|
||||
'计算机系': ['软件工程', '计算机科学与技术', '人工智能'],
|
||||
'电子信息系': ['通信工程', '电子信息'],
|
||||
'音乐系': ['声乐表演', '器乐演奏'],
|
||||
'美术系': ['国画', '油画'],
|
||||
'英语系': ['英语', '翻译'],
|
||||
'日语系': ['日语'],
|
||||
};
|
||||
|
||||
const statuses: Array<'normal' | 'disabled'> = ['normal', 'normal', 'normal', 'normal', 'disabled'];
|
||||
const roles = ['学生'];
|
||||
const grades = ['2023级', '2024级', '2025级'];
|
||||
const classes = ['1班', '2班', '3班', '4班', '5班'];
|
||||
|
||||
// 年级管理数据
|
||||
const gradeList: any[] = [
|
||||
{ id: '1', name: '2025级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '2', name: '2026级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '3', name: '2024级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '4', name: '2023级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '5', name: '2022级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '6', name: '2021级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '7', name: '2020级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '8', name: '2025级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '9', name: '2025级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
{ id: '10', name: '2025级', createTime: '2025.09.01 16:01:30', graduated: false },
|
||||
];
|
||||
|
||||
// 设置一条数据为已毕业状态,用于"还原"操作演示
|
||||
gradeList[1].graduated = true;
|
||||
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function maskPhone(phone: string) {
|
||||
return phone.substring(0, 3) + '****' + phone.substring(7);
|
||||
}
|
||||
|
||||
// 生成100条学生数据
|
||||
const students: any[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const college = randomPick(colleges);
|
||||
const depts = deptMap[college] || [];
|
||||
const dept = randomPick(depts);
|
||||
const majors = majorMap[dept] || [];
|
||||
const major = randomPick(majors);
|
||||
const status = randomPick(statuses);
|
||||
const phone = `1${randomPick(['39', '56', '78', '38', '59'])}${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`;
|
||||
|
||||
students.push({
|
||||
id: String(i + 1),
|
||||
name: randomPick(familyNames) + randomPick(givenNames),
|
||||
studentNo: String(178000 + Math.floor(Math.random() * 1000)),
|
||||
college,
|
||||
dept,
|
||||
major,
|
||||
className: `${Math.ceil(Math.random() * 3)}班`,
|
||||
grade: randomPick(grades),
|
||||
phone,
|
||||
maskedPhone: maskPhone(phone),
|
||||
role: randomPick(roles),
|
||||
updateDate: '2025.09.01',
|
||||
status,
|
||||
graduated: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 生成100条已毕业学生数据
|
||||
const graduatedStudents: any[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const college = randomPick(colleges);
|
||||
const depts = deptMap[college] || [];
|
||||
const dept = randomPick(depts);
|
||||
const majors = majorMap[dept] || [];
|
||||
const major = randomPick(majors);
|
||||
const status = randomPick(statuses);
|
||||
const phone = `1${randomPick(['39', '56', '78', '38', '59'])}${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`;
|
||||
|
||||
graduatedStudents.push({
|
||||
id: String(i + 2000),
|
||||
name: randomPick(familyNames) + randomPick(givenNames),
|
||||
studentNo: String(178000 + Math.floor(Math.random() * 1000)),
|
||||
college,
|
||||
dept,
|
||||
major,
|
||||
className: `${Math.ceil(Math.random() * 3)}班`,
|
||||
grade: randomPick(grades),
|
||||
phone,
|
||||
maskedPhone: maskPhone(phone),
|
||||
role: randomPick(roles),
|
||||
updateDate: '2025.09.01',
|
||||
status,
|
||||
graduated: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
'GET /api/admin/student': (req: any, res: any) => {
|
||||
const {
|
||||
page = '1',
|
||||
pageSize = '10',
|
||||
graduated,
|
||||
studentNo,
|
||||
name,
|
||||
college,
|
||||
dept,
|
||||
major,
|
||||
className,
|
||||
status,
|
||||
} = req.query;
|
||||
|
||||
const source = graduated === 'true' ? graduatedStudents : students;
|
||||
let filtered = [...source];
|
||||
|
||||
if (studentNo) {
|
||||
filtered = filtered.filter((s) => s.studentNo.includes(studentNo));
|
||||
}
|
||||
if (name) {
|
||||
filtered = filtered.filter((s) => s.name.includes(name));
|
||||
}
|
||||
if (college) {
|
||||
filtered = filtered.filter((s) => s.college === college);
|
||||
}
|
||||
if (dept) {
|
||||
filtered = filtered.filter((s) => s.dept === dept);
|
||||
}
|
||||
if (major) {
|
||||
filtered = filtered.filter((s) => s.major === major);
|
||||
}
|
||||
if (className) {
|
||||
filtered = filtered.filter((s) => s.className === className);
|
||||
}
|
||||
if (status) {
|
||||
filtered = filtered.filter((s) => s.status === status);
|
||||
}
|
||||
|
||||
const p = Number(page);
|
||||
const ps = Number(pageSize);
|
||||
const start = (p - 1) * ps;
|
||||
const list = filtered.slice(start, start + ps);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
data: {
|
||||
list,
|
||||
total: filtered.length,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
},
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/admin/student/toggle-status': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const student = students.find((s) => s.id === id) || graduatedStudents.find((s) => s.id === id);
|
||||
if (student) {
|
||||
student.status = student.status === 'normal' ? 'disabled' : 'normal';
|
||||
res.json({ code: 0, data: null, message: '操作成功' });
|
||||
} else {
|
||||
res.json({ code: 40000, data: null, message: '学生不存在' });
|
||||
}
|
||||
},
|
||||
|
||||
'POST /api/admin/student/delete': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
let idx = students.findIndex((s) => s.id === id);
|
||||
if (idx > -1) {
|
||||
students.splice(idx, 1);
|
||||
res.json({ code: 0, data: null, message: '删除成功' });
|
||||
return;
|
||||
}
|
||||
idx = graduatedStudents.findIndex((s) => s.id === id);
|
||||
if (idx > -1) {
|
||||
graduatedStudents.splice(idx, 1);
|
||||
res.json({ code: 0, data: null, message: '删除成功' });
|
||||
return;
|
||||
}
|
||||
res.json({ code: 40000, data: null, message: '学生不存在' });
|
||||
},
|
||||
|
||||
'POST /api/admin/student/batch-delete': (req: any, res: any) => {
|
||||
const { ids, graduated } = req.body;
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
res.json({ code: 40000, data: null, message: '请选择要删除的学生' });
|
||||
return;
|
||||
}
|
||||
const source = graduated ? graduatedStudents : students;
|
||||
let count = 0;
|
||||
for (let i = source.length - 1; i >= 0; i--) {
|
||||
if (ids.includes(source[i].id)) {
|
||||
source.splice(i, 1);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
res.json({ code: 0, data: { count }, message: `成功删除${count}名学生` });
|
||||
},
|
||||
|
||||
'POST /api/admin/student/batch-import': (req: any, res: any) => {
|
||||
// 模拟批量导入5名学生
|
||||
const imported: any[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const college = randomPick(colleges);
|
||||
const depts = deptMap[college] || [];
|
||||
const dept = randomPick(depts);
|
||||
const majors = majorMap[dept] || [];
|
||||
const major = randomPick(majors);
|
||||
const phone = `1${randomPick(['39', '56', '78', '38', '59'])}${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`;
|
||||
const newStudent = {
|
||||
id: String(Date.now() + i),
|
||||
name: randomPick(familyNames) + randomPick(givenNames),
|
||||
studentNo: String(178000 + Math.floor(Math.random() * 1000)),
|
||||
college,
|
||||
dept,
|
||||
major,
|
||||
className: `${Math.ceil(Math.random() * 3)}班`,
|
||||
grade: randomPick(grades),
|
||||
phone,
|
||||
maskedPhone: maskPhone(phone),
|
||||
role: '学生',
|
||||
updateDate: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
|
||||
status: 'normal' as const,
|
||||
graduated: false,
|
||||
};
|
||||
students.unshift(newStudent);
|
||||
imported.push(newStudent);
|
||||
}
|
||||
res.json({ code: 0, data: { count: imported.length }, message: `成功导入${imported.length}名学生` });
|
||||
},
|
||||
|
||||
'POST /api/admin/student/reset-password': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const student = students.find((s) => s.id === id) || graduatedStudents.find((s) => s.id === id);
|
||||
if (student) {
|
||||
console.log(`[Mock] 重置学生 ${student.name} 密码`);
|
||||
res.json({ code: 0, data: null, message: '密码已重置为 123456' });
|
||||
} else {
|
||||
res.json({ code: 40000, data: null, message: '学生不存在' });
|
||||
}
|
||||
},
|
||||
|
||||
'POST /api/admin/student/edit': (req: any, res: any) => {
|
||||
const { id, name, studentNo, role, college, dept, major, className, grade, phone } = req.body;
|
||||
const student = students.find((s) => s.id === id) || graduatedStudents.find((s) => s.id === id);
|
||||
if (!student) {
|
||||
res.json({ code: 40000, data: null, message: '学生不存在' });
|
||||
return;
|
||||
}
|
||||
if (!name || !studentNo || !role || !college || !dept || !major || !className || !grade) {
|
||||
res.json({ code: 40000, data: null, message: '请填写完整信息' });
|
||||
return;
|
||||
}
|
||||
Object.assign(student, {
|
||||
name, studentNo, role, college, dept, major, className, grade,
|
||||
phone: phone || '',
|
||||
maskedPhone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
|
||||
updateDate: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
|
||||
});
|
||||
res.json({ code: 0, data: student, message: '编辑成功' });
|
||||
},
|
||||
|
||||
'POST /api/admin/student/add': (req: any, res: any) => {
|
||||
const { name, studentNo, role, college, dept, major, className, grade, phone } = req.body;
|
||||
if (!name || !studentNo || !role || !college || !dept || !major || !className || !grade) {
|
||||
res.json({ code: 40000, data: null, message: '请填写完整信息' });
|
||||
return;
|
||||
}
|
||||
const newStudent = {
|
||||
id: String(students.length + 1000),
|
||||
name,
|
||||
studentNo,
|
||||
college,
|
||||
dept,
|
||||
major,
|
||||
className,
|
||||
grade,
|
||||
phone: phone || '',
|
||||
maskedPhone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
|
||||
role,
|
||||
updateDate: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
|
||||
status: 'normal' as const,
|
||||
};
|
||||
students.unshift(newStudent);
|
||||
res.json({ code: 0, data: newStudent, message: '新增成功' });
|
||||
},
|
||||
|
||||
'GET /api/admin/student/colleges': (_req: any, res: any) => {
|
||||
res.json({
|
||||
code: 0,
|
||||
data: {
|
||||
colleges,
|
||||
deptMap,
|
||||
majorMap,
|
||||
classes,
|
||||
grades,
|
||||
roles,
|
||||
},
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
|
||||
// 年级管理接口
|
||||
'GET /api/admin/grade': (req: any, res: any) => {
|
||||
const { page = '1', pageSize = '10', name } = req.query;
|
||||
let filtered = [...gradeList];
|
||||
if (name) {
|
||||
filtered = filtered.filter((g: any) => g.name.includes(name));
|
||||
}
|
||||
const p = Number(page);
|
||||
const ps = Number(pageSize);
|
||||
const start = (p - 1) * ps;
|
||||
const list = filtered.slice(start, start + ps);
|
||||
res.json({ code: 0, data: { list, total: filtered.length, page: p, pageSize: ps }, message: 'ok' });
|
||||
},
|
||||
|
||||
'POST /api/admin/grade/add': (req: any, res: any) => {
|
||||
const { name } = req.body;
|
||||
const newGrade = {
|
||||
id: String(gradeList.length + 1),
|
||||
name,
|
||||
createTime: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
||||
};
|
||||
gradeList.unshift(newGrade);
|
||||
res.json({ code: 0, data: newGrade, message: '新增成功' });
|
||||
},
|
||||
|
||||
'POST /api/admin/grade/edit': (req: any, res: any) => {
|
||||
const { id, name } = req.body;
|
||||
const item = gradeList.find((g: any) => g.id === id);
|
||||
if (item) {
|
||||
item.name = name;
|
||||
res.json({ code: 0, data: item, message: '编辑成功' });
|
||||
} else {
|
||||
res.json({ code: 1, message: '未找到该年级' });
|
||||
}
|
||||
},
|
||||
|
||||
'POST /api/admin/grade/graduate': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const idx = gradeList.findIndex((g: any) => g.id === id);
|
||||
if (idx >= 0) {
|
||||
gradeList[idx].graduated = true;
|
||||
res.json({ code: 0, message: '转毕业成功' });
|
||||
} else {
|
||||
res.json({ code: 1, message: '未找到该年级' });
|
||||
}
|
||||
},
|
||||
|
||||
'POST /api/admin/grade/restore': (req: any, res: any) => {
|
||||
const { id } = req.body;
|
||||
const idx = gradeList.findIndex((g: any) => g.id === id);
|
||||
if (idx >= 0) {
|
||||
gradeList[idx].graduated = false;
|
||||
res.json({ code: 0, message: '还原成功' });
|
||||
} else {
|
||||
res.json({ code: 1, message: '未找到该年级' });
|
||||
}
|
||||
},
|
||||
};
|
||||
156
mock/user.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 用户相关 Mock
|
||||
*
|
||||
* 认证接口(登录、验证码登录、发送验证码)已接入真实后端 192.168.0.55:8080,
|
||||
* 通过 .umirc.ts 的 proxy 代理转发,不再 mock。
|
||||
*
|
||||
* 以下仅保留尚未对接真实接口的 mock。
|
||||
*/
|
||||
|
||||
const mockUsers = [
|
||||
{
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
realName: '管理员',
|
||||
phone: '13800138000',
|
||||
avatar: null,
|
||||
userType: 1,
|
||||
password: '123456',
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
username: 'teacher001',
|
||||
realName: '李老师',
|
||||
phone: '13800138002',
|
||||
avatar: null,
|
||||
userType: 2,
|
||||
password: '123456',
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
username: 'student001',
|
||||
realName: '张三',
|
||||
phone: '13800138001',
|
||||
avatar: null,
|
||||
userType: 3,
|
||||
password: '123456',
|
||||
},
|
||||
];
|
||||
|
||||
let nextUserId = 4;
|
||||
|
||||
const generateToken = () =>
|
||||
'eyJhbGciOiJIUzI1NiJ9.mock-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
||||
|
||||
const toUserData = (user: (typeof mockUsers)[0], token: string) => ({
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
realName: user.realName,
|
||||
phone: user.phone,
|
||||
avatar: user.avatar,
|
||||
userType: user.userType,
|
||||
token,
|
||||
});
|
||||
|
||||
export default {
|
||||
// 验证重置密码验证码(暂无真实接口)
|
||||
'POST /api/auth/verifyResetCode': (req: any, res: any) => {
|
||||
const { phone, code } = req.body;
|
||||
|
||||
if (!phone || !code) {
|
||||
return res.json({ code: 40000, data: null, message: '请求参数错误' });
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return res.json({ code: 40000, data: null, message: '手机号格式不正确' });
|
||||
}
|
||||
|
||||
const user = mockUsers.find((u) => u.phone === phone);
|
||||
if (!user) {
|
||||
return res.json({ code: 40000, data: null, message: '该手机号未注册' });
|
||||
}
|
||||
|
||||
if (code !== '123456') {
|
||||
return res.json({ code: 40000, data: null, message: '验证码错误或已过期' });
|
||||
}
|
||||
|
||||
console.log(`[Mock] 验证码验证通过: ${phone}`);
|
||||
res.json({ code: 0, data: true, message: 'ok' });
|
||||
},
|
||||
|
||||
// 重置密码(暂无真实接口)
|
||||
'POST /api/auth/resetPassword': (req: any, res: any) => {
|
||||
const { phone, code, newPassword } = req.body;
|
||||
|
||||
if (!phone || !code || !newPassword) {
|
||||
return res.json({ code: 40000, data: null, message: '请求参数错误' });
|
||||
}
|
||||
|
||||
if (code !== '123456') {
|
||||
return res.json({ code: 40000, data: null, message: '验证码错误或已过期' });
|
||||
}
|
||||
|
||||
const user = mockUsers.find((u) => u.phone === phone);
|
||||
if (!user) {
|
||||
return res.json({ code: 40000, data: null, message: '该手机号未注册' });
|
||||
}
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 20) {
|
||||
return res.json({ code: 40000, data: null, message: '密码长度需为6-20位' });
|
||||
}
|
||||
|
||||
user.password = newPassword;
|
||||
console.log(`[Mock] 用户 ${user.username} 密码已重置`);
|
||||
res.json({ code: 0, data: true, message: 'ok' });
|
||||
},
|
||||
|
||||
// 用户注册(暂无真实接口)
|
||||
'POST /api/auth/register': (req: any, res: any) => {
|
||||
const { username, password, confirmPassword, realName, phone, email, userType } = req.body;
|
||||
|
||||
if (!username || !password || !confirmPassword || !realName || !phone) {
|
||||
return res.json({ code: 40000, data: null, message: '请求参数错误' });
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]{4,20}$/.test(username)) {
|
||||
return res.json({ code: 40000, data: null, message: '用户名只能包含字母、数字、下划线' });
|
||||
}
|
||||
|
||||
if (password.length < 6 || password.length > 20) {
|
||||
return res.json({ code: 40000, data: null, message: '密码长度需为6-20位' });
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return res.json({ code: 40000, data: null, message: '两次密码不一致' });
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return res.json({ code: 40000, data: null, message: '手机号格式不正确' });
|
||||
}
|
||||
|
||||
if (mockUsers.find((u) => u.username === username)) {
|
||||
return res.json({ code: 40000, data: null, message: '用户名已存在' });
|
||||
}
|
||||
|
||||
if (mockUsers.find((u) => u.phone === phone)) {
|
||||
return res.json({ code: 40000, data: null, message: '手机号已注册' });
|
||||
}
|
||||
|
||||
const newUser = {
|
||||
userId: nextUserId++,
|
||||
username,
|
||||
realName,
|
||||
phone,
|
||||
avatar: null,
|
||||
userType: userType || 3,
|
||||
password,
|
||||
};
|
||||
mockUsers.push(newUser);
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
data: toUserData(newUser, generateToken()),
|
||||
message: 'ok',
|
||||
});
|
||||
},
|
||||
};
|
||||
21660
package-lock.json
generated
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "zelin-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "max dev",
|
||||
"build": "max build",
|
||||
"start": "npm run dev",
|
||||
"preview": "max preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/plots": "^2.6.8",
|
||||
"@umijs/max": "^4.1.0",
|
||||
"administrative-division-cn": "^0.2.1",
|
||||
"antd": "^5.12.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1519
pencli/data_count.pen
Normal file
7525
pencli/login.pen
Normal file
7511
pencli/user_manager.pen
Normal file
BIN
public/admin/background.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
3
public/assets/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# 请将以下文件放入此目录:
|
||||
# - logo.png (左上角logo)
|
||||
# - login-bg.jpg (登录页背景图)
|
||||
BIN
public/assets/guoping.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/loginbg.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/assets/logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/zw-logo.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/assets/任务.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/assets/岗位.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
public/assets/简历.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/assets/预约.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/data_count/AI简历记录.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/data_count/AI面试数据统计.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/data_count/学生就业能力信息.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/data_count/简历数据统计.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/data_count/面试任务统计.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/data_count/面试记录.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/data_count/预约面试统计.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/fonts/胡晓波男神体.otf
Normal file
BIN
public/fonts/胡晓波真帅体.otf
Normal file
BIN
public/fonts/胡晓波骚包体.otf
Normal file
3
public/icons/Address.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 15.8509V17.5175H2.5V15.8509H3.33333V6.14252C3.33467 5.81014 3.43537 5.48576 3.62247 5.21104C3.80957 4.93633 4.07454 4.72383 4.38333 4.60085L9.38333 2.60085C9.63894 2.49898 9.91579 2.46198 10.1892 2.49315C10.4626 2.52432 10.724 2.62269 10.9501 2.77949C11.1762 2.93628 11.36 3.14662 11.485 3.39173C11.6101 3.63684 11.6725 3.90909 11.6667 4.18418V15.8509H13.3333V8.19252C13.3332 8.12171 13.3511 8.05203 13.3854 7.99006C13.4196 7.92809 13.4691 7.87588 13.5292 7.83835C13.5892 7.80082 13.6578 7.77921 13.7286 7.77557C13.7993 7.77192 13.8697 7.78636 13.9333 7.81752L16.4333 9.06752C16.5029 9.10264 16.5614 9.15619 16.6025 9.22233C16.6437 9.28846 16.6659 9.36463 16.6667 9.44252V15.8509H17.5ZM10 11.6842H5V13.3509H10V11.6842ZM10 7.51752H5V9.18418H10V7.51752Z" fill="#D4E0E0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 888 B |
5
public/icons/手机.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="手机">
|
||||
<path id="Union" d="M11.3337 0.583008C12.3 0.583184 13.0837 1.36662 13.0837 2.33301V13.666C13.0837 14.6324 12.3 15.4158 11.3337 15.416H4.66669C3.70019 15.416 2.91669 14.6325 2.91669 13.666V2.33301C2.91669 1.36651 3.70019 0.583008 4.66669 0.583008H11.3337ZM4.66669 2.08301C4.52862 2.08301 4.41669 2.19494 4.41669 2.33301V13.666C4.41669 13.8041 4.52862 13.916 4.66669 13.916H11.3337C11.4716 13.9158 11.5837 13.804 11.5837 13.666V2.33301C11.5837 2.19504 11.4716 2.08318 11.3337 2.08301H4.66669ZM9.33368 11.916C9.74763 11.9162 10.0835 12.2521 10.0837 12.666C10.0837 13.0801 9.74774 13.4158 9.33368 13.416H6.66669C6.25247 13.416 5.91669 13.0802 5.91669 12.666C5.91687 12.252 6.25259 11.916 6.66669 11.916H9.33368ZM8.66669 2.58301C9.08073 2.58321 9.41669 2.91892 9.41669 3.33301C9.41669 3.7471 9.08073 4.08281 8.66669 4.08301H7.33368C6.91947 4.08301 6.58368 3.74722 6.58368 3.33301C6.58368 2.91879 6.91947 2.58301 7.33368 2.58301H8.66669Z" fill="#9EACC0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/user_manager/展开.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
public/user_manager/收起.png
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
public/user_manager/添加.png
Normal file
|
After Width: | Height: | Size: 568 B |
3776
src/.umi/appData.json
Normal file
9
src/.umi/core/EmptyRoute.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React from 'react';
|
||||
import { Outlet, useOutletContext } from 'umi';
|
||||
export default function EmptyRoute() {
|
||||
const context = useOutletContext();
|
||||
return <Outlet context={context} />;
|
||||
}
|
||||
18
src/.umi/core/defineApp.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import type { IRuntimeConfig as Plugin0 } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-antd/runtimeConfig.d'
|
||||
import type { IRuntimeConfig as Plugin1 } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-request/runtimeConfig.d'
|
||||
interface IDefaultRuntimeConfig {
|
||||
onRouteChange?: (props: { routes: any, clientRoutes: any, location: any, action: any, isFirst: boolean }) => void;
|
||||
patchRoutes?: (props: { routes: any }) => void;
|
||||
patchClientRoutes?: (props: { routes: any }) => void;
|
||||
render?: (oldRender: () => void) => void;
|
||||
rootContainer?: (lastRootContainer: JSX.Element, args?: any) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
export type RuntimeConfig = IDefaultRuntimeConfig & Plugin0 & Plugin1
|
||||
|
||||
export function defineApp(config: RuntimeConfig): RuntimeConfig {
|
||||
return config;
|
||||
}
|
||||
10
src/.umi/core/helmet.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React from 'react';
|
||||
import { HelmetProvider } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react';
|
||||
import { context } from './helmetContext';
|
||||
|
||||
export const innerProvider = (container) => {
|
||||
return React.createElement(HelmetProvider, { context }, container);
|
||||
}
|
||||
4
src/.umi/core/helmetContext.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
export const context = {};
|
||||
72
src/.umi/core/history.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import { createHashHistory, createMemoryHistory, createBrowserHistory } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react';
|
||||
import type { UmiHistory } from './historyIntelli';
|
||||
|
||||
let history: UmiHistory;
|
||||
let basename: string = '/';
|
||||
export function createHistory(opts: any) {
|
||||
let h;
|
||||
if (opts.type === 'hash') {
|
||||
h = createHashHistory();
|
||||
} else if (opts.type === 'memory') {
|
||||
h = createMemoryHistory(opts);
|
||||
} else {
|
||||
h = createBrowserHistory();
|
||||
}
|
||||
if (opts.basename) {
|
||||
basename = opts.basename;
|
||||
}
|
||||
|
||||
|
||||
history = {
|
||||
...h,
|
||||
push(to, state) {
|
||||
h.push(patchTo(to, h), state);
|
||||
},
|
||||
replace(to, state) {
|
||||
h.replace(patchTo(to, h), state);
|
||||
},
|
||||
get location() {
|
||||
return h.location;
|
||||
},
|
||||
get action() {
|
||||
return h.action;
|
||||
}
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
export function setHistory(h: UmiHistory) {
|
||||
if (h) {
|
||||
history = h;
|
||||
}
|
||||
}
|
||||
|
||||
// Patch `to` to support basename
|
||||
// Refs:
|
||||
// https://github.com/remix-run/history/blob/3e9dab4/packages/history/index.ts#L484
|
||||
// https://github.com/remix-run/history/blob/dev/docs/api-reference.md#to
|
||||
function patchTo(to: any, h: History) {
|
||||
if (typeof to === 'string') {
|
||||
return `${stripLastSlash(basename)}${to}`;
|
||||
} else if (typeof to === 'object') {
|
||||
|
||||
const currentPathname = h.location.pathname;
|
||||
|
||||
return {
|
||||
...to,
|
||||
pathname: to.pathname? `${stripLastSlash(basename)}${to.pathname}` : currentPathname,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unexpected to: ${to}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stripLastSlash(path) {
|
||||
return path.slice(-1) === '/' ? path.slice(0, -1) : path;
|
||||
}
|
||||
|
||||
export { history };
|
||||
132
src/.umi/core/historyIntelli.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import { getRoutes } from './route'
|
||||
import type { History } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react'
|
||||
|
||||
type Routes = Awaited<ReturnType<typeof getRoutes>>['routes']
|
||||
type AllRoute = Routes[keyof Routes]
|
||||
type IsRoot<T extends any> = 'parentId' extends keyof T ? false : true
|
||||
|
||||
// show `/` in not `layout / wrapper` only
|
||||
type GetAllRouteWithoutLayout<Item extends AllRoute> = Item extends any
|
||||
? 'isWrapper' extends keyof Item
|
||||
? never
|
||||
: 'isLayout' extends keyof Item
|
||||
? never
|
||||
: Item
|
||||
: never
|
||||
type AllRouteWithoutLayout = GetAllRouteWithoutLayout<AllRoute>
|
||||
type IndexRoutePathname = '/' extends AllRouteWithoutLayout['path']
|
||||
? '/'
|
||||
: never
|
||||
|
||||
type GetChildrens<T extends any> = T extends any
|
||||
? IsRoot<T> extends true
|
||||
? never
|
||||
: T
|
||||
: never
|
||||
type Childrens = GetChildrens<AllRoute>
|
||||
type Root = Exclude<AllRoute, Childrens>
|
||||
type AllIds = AllRoute['id']
|
||||
|
||||
type GetChildrensByParentId<
|
||||
Id extends AllIds,
|
||||
Item = AllRoute
|
||||
> = Item extends any
|
||||
? 'parentId' extends keyof Item
|
||||
? Item['parentId'] extends Id
|
||||
? Item
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
|
||||
type RouteObject<
|
||||
Id extends AllIds,
|
||||
Item = GetChildrensByParentId<Id>
|
||||
> = IsNever<Item> extends true
|
||||
? ''
|
||||
: Item extends AllRoute
|
||||
? {
|
||||
[Key in Item['path'] as TrimSlash<Key>]: UnionMerge<
|
||||
RouteObject<Item['id']>
|
||||
>
|
||||
}
|
||||
: never
|
||||
|
||||
type GetRootRouteObject<Item extends Root> = Item extends Root
|
||||
? {
|
||||
[K in Item['path'] as TrimSlash<K>]: UnionMerge<RouteObject<Item['id']>>
|
||||
}
|
||||
: never
|
||||
type MergedResult = UnionMerge<GetRootRouteObject<Root>>
|
||||
|
||||
// --- patch history types ---
|
||||
|
||||
type HistoryTo = Parameters<History['push']>['0']
|
||||
type HistoryPath = Exclude<HistoryTo, string>
|
||||
|
||||
type UmiPathname = Path<MergedResult> | (string & {})
|
||||
interface UmiPath extends HistoryPath {
|
||||
pathname: UmiPathname
|
||||
}
|
||||
type UmiTo = UmiPathname | UmiPath
|
||||
|
||||
type UmiPush = (to: UmiTo, state?: any) => void
|
||||
type UmiReplace = (to: UmiTo, state?: any) => void
|
||||
|
||||
|
||||
export interface UmiHistory extends History {
|
||||
push: UmiPush
|
||||
replace: UmiReplace
|
||||
}
|
||||
|
||||
// --- type utils ---
|
||||
type TrimLeftSlash<T extends string> = T extends `/${infer R}`
|
||||
? TrimLeftSlash<R>
|
||||
: T
|
||||
type TrimRightSlash<T extends string> = T extends `${infer R}/`
|
||||
? TrimRightSlash<R>
|
||||
: T
|
||||
type TrimSlash<T extends string> = TrimLeftSlash<TrimRightSlash<T>>
|
||||
|
||||
type IsNever<T> = [T] extends [never] ? true : false
|
||||
type IsEqual<A, B> = (<G>() => G extends A ? 1 : 2) extends <G>() => G extends B
|
||||
? 1
|
||||
: 2
|
||||
? true
|
||||
: false
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I
|
||||
) => void
|
||||
? I
|
||||
: never
|
||||
type UnionMerge<U> = UnionToIntersection<U> extends infer O
|
||||
? { [K in keyof O]: O[K] }
|
||||
: never
|
||||
|
||||
type ExcludeEmptyKey<T> = IsEqual<T, ''> extends true ? never : T
|
||||
|
||||
type PathConcat<
|
||||
TKey extends string,
|
||||
TValue,
|
||||
N = TrimSlash<TKey>
|
||||
> = TValue extends string
|
||||
? ExcludeEmptyKey<N>
|
||||
:
|
||||
| ExcludeEmptyKey<N>
|
||||
| `${N & string}${IsNever<ExcludeEmptyKey<N>> extends true
|
||||
? ''
|
||||
: '/'}${UnionPath<TValue>}`
|
||||
|
||||
type UnionPath<T> = {
|
||||
[K in keyof T]-?: PathConcat<K & string, T[K]>
|
||||
}[keyof T]
|
||||
|
||||
type MakeSureLeftSlash<T> = T extends any
|
||||
? `/${TrimRightSlash<T & string>}`
|
||||
: never
|
||||
|
||||
// exclude `/*`, because it always at the top of the IDE tip list
|
||||
type Path<T, K = UnionPath<T>> = Exclude<MakeSureLeftSlash<K>, '/*'> | IndexRoutePathname
|
||||
50
src/.umi/core/plugin.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import * as Plugin_0 from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/app.ts';
|
||||
import * as Plugin_1 from '@@/core/helmet.ts';
|
||||
import * as Plugin_2 from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-model/runtime.tsx';
|
||||
import { PluginManager } from 'umi';
|
||||
|
||||
function __defaultExport (obj) {
|
||||
if (obj.default) {
|
||||
return typeof obj.default === 'function' ? obj.default() : obj.default
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
export function getPlugins() {
|
||||
return [
|
||||
{
|
||||
apply: __defaultExport(Plugin_0),
|
||||
path: process.env.NODE_ENV === 'production' ? void 0 : 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/app.ts',
|
||||
},
|
||||
{
|
||||
apply: Plugin_1,
|
||||
path: process.env.NODE_ENV === 'production' ? void 0 : '@@/core/helmet.ts',
|
||||
},
|
||||
{
|
||||
apply: Plugin_2,
|
||||
path: process.env.NODE_ENV === 'production' ? void 0 : 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-model/runtime.tsx',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getValidKeys() {
|
||||
return ['patchRoutes','patchClientRoutes','modifyContextOpts','modifyClientRenderOpts','rootContainer','innerProvider','i18nProvider','accessProvider','dataflowProvider','outerProvider','render','onRouteChange','antd','qiankun','request',];
|
||||
}
|
||||
|
||||
let pluginManager = null;
|
||||
|
||||
export function createPluginManager() {
|
||||
pluginManager = PluginManager.create({
|
||||
plugins: getPlugins(),
|
||||
validKeys: getValidKeys(),
|
||||
});
|
||||
|
||||
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
export function getPluginManager() {
|
||||
return pluginManager;
|
||||
}
|
||||
406
src/.umi/core/pluginConfig.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import { IConfigFromPluginsJoi } from "./pluginConfigJoi.d";
|
||||
|
||||
interface IConfigTypes {
|
||||
codeSplitting: {
|
||||
jsStrategy: "bigVendors" | "depPerChunk" | "granularChunks";
|
||||
jsStrategyOptions?: ({
|
||||
|
||||
} | undefined);
|
||||
cssStrategy?: ("mergeAll" | undefined);
|
||||
cssStrategyOptions?: ({
|
||||
|
||||
} | undefined);
|
||||
};
|
||||
title: string;
|
||||
styles: Array<string | {
|
||||
src?: (string | undefined);
|
||||
} | {
|
||||
content?: (string | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
scripts: Array<string | {
|
||||
src?: (string | undefined);
|
||||
} | {
|
||||
content?: (string | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
routes: Array<{
|
||||
component?: (string | undefined);
|
||||
layout?: (false | undefined);
|
||||
path?: (string | undefined);
|
||||
redirect?: (string | undefined);
|
||||
routes?: IConfigTypes['routes'];
|
||||
wrappers?: (Array<string> | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
routeLoader: {
|
||||
moduleType: "esm" | "cjs";
|
||||
};
|
||||
reactRouter5Compat: boolean | {
|
||||
|
||||
};
|
||||
presets: Array<string>;
|
||||
plugins: Array<string>;
|
||||
npmClient: "pnpm" | "tnpm" | "cnpm" | "yarn" | "npm";
|
||||
mountElementId: string;
|
||||
metas: Array<{
|
||||
charset?: (string | undefined);
|
||||
content?: (string | undefined);
|
||||
"http-equiv"?: (string | undefined);
|
||||
name?: (string | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
links: Array<{
|
||||
crossorigin?: (string | undefined);
|
||||
href?: (string | undefined);
|
||||
hreflang?: (string | undefined);
|
||||
media?: (string | undefined);
|
||||
referrerpolicy?: (string | undefined);
|
||||
rel?: (string | undefined);
|
||||
sizes?: (any | undefined);
|
||||
title?: (any | undefined);
|
||||
type?: (any | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
historyWithQuery: {
|
||||
|
||||
};
|
||||
history: {
|
||||
type: "browser" | "hash" | "memory";
|
||||
};
|
||||
headScripts: Array<string | {
|
||||
src?: (string | undefined);
|
||||
} | {
|
||||
content?: (string | undefined);
|
||||
} | { [x: string]: any }>;
|
||||
esbuildMinifyIIFE: boolean;
|
||||
conventionRoutes: {
|
||||
base?: (string | undefined);
|
||||
exclude?: (Array<any> | undefined);
|
||||
};
|
||||
conventionLayout: boolean;
|
||||
base: string;
|
||||
analyze: {
|
||||
|
||||
};
|
||||
writeToDisk: boolean;
|
||||
transformRuntime: { [x: string]: any };
|
||||
theme: { [x: string]: any };
|
||||
targets: { [x: string]: any };
|
||||
svgr: { [x: string]: any };
|
||||
svgo: { [x: string]: any } | boolean;
|
||||
stylusLoader: { [x: string]: any };
|
||||
styleLoader: { [x: string]: any };
|
||||
srcTranspilerOptions: {
|
||||
esbuild?: ({ [x: string]: any } | undefined);
|
||||
swc?: ({ [x: string]: any } | undefined);
|
||||
};
|
||||
srcTranspiler: "babel" | "esbuild" | "swc";
|
||||
sassLoader: { [x: string]: any };
|
||||
runtimePublicPath: {
|
||||
|
||||
};
|
||||
purgeCSS: { [x: string]: any };
|
||||
publicPath: string;
|
||||
proxy: { [x: string]: any } | Array<any>;
|
||||
postcssLoader: { [x: string]: any };
|
||||
outputPath: string;
|
||||
normalCSSLoaderModules: { [x: string]: any };
|
||||
mfsu: {
|
||||
cacheDirectory?: (string | undefined);
|
||||
chainWebpack?: (((...args: any[]) => unknown) | undefined);
|
||||
esbuild?: (boolean | undefined);
|
||||
exclude?: (Array<string | any> | undefined);
|
||||
include?: (Array<string> | undefined);
|
||||
mfName?: (string | undefined);
|
||||
remoteAliases?: (Array<string> | undefined);
|
||||
remoteName?: (string | undefined);
|
||||
runtimePublicPath?: (boolean | undefined);
|
||||
shared?: ({ [x: string]: any } | undefined);
|
||||
strategy?: ("eager" | "normal" | undefined);
|
||||
} | boolean;
|
||||
mdx: {
|
||||
loader?: (string | undefined);
|
||||
loaderOptions?: ({ [x: string]: any } | undefined);
|
||||
};
|
||||
manifest: {
|
||||
basePath?: (string | undefined);
|
||||
fileName?: (string | undefined);
|
||||
};
|
||||
lessLoader: { [x: string]: any };
|
||||
jsMinifierOptions: { [x: string]: any };
|
||||
jsMinifier: "esbuild" | "swc" | "terser" | "uglifyJs" | "none";
|
||||
inlineLimit: number;
|
||||
ignoreMomentLocale: boolean;
|
||||
https: {
|
||||
cert?: (string | undefined);
|
||||
hosts?: (Array<string> | undefined);
|
||||
http2?: (boolean | undefined);
|
||||
key?: (string | undefined);
|
||||
};
|
||||
hash: boolean;
|
||||
forkTSChecker: { [x: string]: any };
|
||||
fastRefresh: boolean;
|
||||
extraPostCSSPlugins: Array<any>;
|
||||
extraBabelPresets: Array<string | Array<any>>;
|
||||
extraBabelPlugins: Array<string | Array<any>>;
|
||||
extraBabelIncludes: Array<string | any>;
|
||||
externals: { [x: string]: any } | string | ((...args: any[]) => unknown);
|
||||
esm: {
|
||||
|
||||
};
|
||||
devtool: "cheap-source-map" | "cheap-module-source-map" | "eval" | "eval-source-map" | "eval-cheap-source-map" | "eval-cheap-module-source-map" | "eval-nosources-cheap-source-map" | "eval-nosources-cheap-module-source-map" | "eval-nosources-source-map" | "source-map" | "hidden-source-map" | "hidden-nosources-cheap-source-map" | "hidden-nosources-cheap-module-source-map" | "hidden-nosources-source-map" | "hidden-cheap-source-map" | "hidden-cheap-module-source-map" | "inline-source-map" | "inline-cheap-source-map" | "inline-cheap-module-source-map" | "inline-nosources-cheap-source-map" | "inline-nosources-cheap-module-source-map" | "inline-nosources-source-map" | "nosources-source-map" | "nosources-cheap-source-map" | "nosources-cheap-module-source-map" | boolean;
|
||||
depTranspiler: "babel" | "esbuild" | "swc" | "none";
|
||||
define: { [x: string]: any };
|
||||
deadCode: {
|
||||
context?: (string | undefined);
|
||||
detectUnusedExport?: (boolean | undefined);
|
||||
detectUnusedFiles?: (boolean | undefined);
|
||||
exclude?: (Array<string> | undefined);
|
||||
failOnHint?: (boolean | undefined);
|
||||
patterns?: (Array<string> | undefined);
|
||||
};
|
||||
cssPublicPath: string;
|
||||
cssMinifierOptions: { [x: string]: any };
|
||||
cssMinifier: "cssnano" | "esbuild" | "parcelCSS" | "none";
|
||||
cssLoaderModules: { [x: string]: any };
|
||||
cssLoader: { [x: string]: any };
|
||||
copy: Array<{
|
||||
from: string;
|
||||
to: string;
|
||||
} | string>;
|
||||
checkDepCssModules?: boolean;
|
||||
cacheDirectoryPath: string;
|
||||
babelLoaderCustomize: string;
|
||||
autoprefixer: { [x: string]: any };
|
||||
autoCSSModules: boolean;
|
||||
alias: { [x: string]: any };
|
||||
crossorigin: boolean | {
|
||||
includes?: (Array<any> | undefined);
|
||||
};
|
||||
esmi: {
|
||||
cdnOrigin: string;
|
||||
shimUrl?: (string | undefined);
|
||||
};
|
||||
exportStatic: {
|
||||
extraRoutePaths?: (((...args: any[]) => unknown) | Array<string> | undefined);
|
||||
ignorePreRenderError?: (boolean | undefined);
|
||||
};
|
||||
favicons: Array<string>;
|
||||
helmet: boolean;
|
||||
icons: {
|
||||
autoInstall?: ({
|
||||
|
||||
} | undefined);
|
||||
defaultComponentConfig?: ({
|
||||
|
||||
} | undefined);
|
||||
alias?: ({
|
||||
|
||||
} | undefined);
|
||||
include?: (Array<string> | undefined);
|
||||
};
|
||||
mock: {
|
||||
exclude?: (Array<string> | undefined);
|
||||
include?: (Array<string> | undefined);
|
||||
};
|
||||
mpa: {
|
||||
template?: (string | undefined);
|
||||
layout?: (string | undefined);
|
||||
getConfigFromEntryFile?: (boolean | undefined);
|
||||
entry?: ({
|
||||
|
||||
} | undefined);
|
||||
};
|
||||
phantomDependency: {
|
||||
exclude?: (Array<string> | undefined);
|
||||
};
|
||||
polyfill: {
|
||||
imports?: (Array<string> | undefined);
|
||||
};
|
||||
routePrefetch: {
|
||||
defaultPrefetch?: ("none" | "intent" | "render" | "viewport" | undefined);
|
||||
defaultPrefetchTimeout?: (number | undefined);
|
||||
};
|
||||
terminal: {
|
||||
|
||||
};
|
||||
tmpFiles: boolean;
|
||||
clientLoader: {
|
||||
|
||||
};
|
||||
routeProps: {
|
||||
|
||||
};
|
||||
ssr: {
|
||||
serverBuildPath?: (string | undefined);
|
||||
serverBuildTarget?: ("express" | "worker" | undefined);
|
||||
platform?: (string | undefined);
|
||||
builder?: ("esbuild" | "webpack" | "mako" | undefined);
|
||||
__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: ({
|
||||
pureApp?: (boolean | undefined);
|
||||
pureHtml?: (boolean | undefined);
|
||||
} | undefined);
|
||||
useStream?: (boolean | undefined);
|
||||
};
|
||||
lowImport: {
|
||||
libs?: (Array<any> | undefined);
|
||||
css?: (string | undefined);
|
||||
};
|
||||
vite: {
|
||||
|
||||
};
|
||||
apiRoute: {
|
||||
platform?: (string | undefined);
|
||||
};
|
||||
monorepoRedirect: boolean | {
|
||||
srcDir?: (Array<string> | undefined);
|
||||
exclude?: (Array<any> | undefined);
|
||||
peerDeps?: (boolean | undefined);
|
||||
};
|
||||
test: {
|
||||
|
||||
};
|
||||
clickToComponent: {
|
||||
/** 默认情况下,点击将默认编辑器为vscode, 你可以设置编辑器 vscode 或者 vscode-insiders */
|
||||
editor?: (string | undefined);
|
||||
};
|
||||
legacy: {
|
||||
buildOnly?: (boolean | undefined);
|
||||
nodeModulesTransform?: (boolean | undefined);
|
||||
checkOutput?: (boolean | undefined);
|
||||
};
|
||||
/** 设置 babel class-properties 启用 loose
|
||||
@doc https://umijs.org/docs/api/config#classpropertiesloose */
|
||||
classPropertiesLoose: boolean | {
|
||||
|
||||
};
|
||||
ui: {
|
||||
|
||||
};
|
||||
mako: {
|
||||
plugins?: (Array<{
|
||||
load?: (((...args: any[]) => unknown) | undefined);
|
||||
generateEnd?: (((...args: any[]) => unknown) | undefined);
|
||||
}> | undefined);
|
||||
px2rem?: ({
|
||||
root?: (number | undefined);
|
||||
propBlackList?: (Array<string> | undefined);
|
||||
propWhiteList?: (Array<string> | undefined);
|
||||
selectorBlackList?: (Array<string> | undefined);
|
||||
selectorWhiteList?: (Array<string> | undefined);
|
||||
selectorDoubleList?: (Array<string> | undefined);
|
||||
} | undefined);
|
||||
experimental?: ({
|
||||
webpackSyntaxValidate?: (Array<string> | undefined);
|
||||
} | undefined);
|
||||
flexBugs?: (boolean | undefined);
|
||||
optimization?: ({
|
||||
skipModules?: (boolean | undefined);
|
||||
} | undefined);
|
||||
};
|
||||
utoopack: {
|
||||
|
||||
};
|
||||
hmrGuardian: boolean;
|
||||
forget: {
|
||||
ReactCompilerConfig?: ({
|
||||
|
||||
} | undefined);
|
||||
};
|
||||
verifyCommit: {
|
||||
scope?: (Array<string> | undefined);
|
||||
allowEmoji?: (boolean | undefined);
|
||||
};
|
||||
run: {
|
||||
globals?: (Array<string> | undefined);
|
||||
};
|
||||
access: { [x: string]: any };
|
||||
analytics: {
|
||||
baidu?: (string | undefined);
|
||||
ga?: (string | undefined);
|
||||
ga_v2?: (string | undefined);
|
||||
};
|
||||
antd: {
|
||||
dark?: (boolean | undefined);
|
||||
compact?: (boolean | undefined);
|
||||
import?: (boolean | undefined);
|
||||
style?: ("less" | "css" | undefined);
|
||||
theme?: ({
|
||||
components: { [x: string]: { [x: string]: any } };
|
||||
} | { [x: string]: any } | undefined);
|
||||
appConfig?: ({ [x: string]: any } | undefined);
|
||||
momentPicker?: (boolean | undefined);
|
||||
styleProvider?: ({ [x: string]: any } | undefined);
|
||||
configProvider?: ({
|
||||
theme: {
|
||||
components: { [x: string]: { [x: string]: any } };
|
||||
} | { [x: string]: any };
|
||||
} | { [x: string]: any } | undefined);
|
||||
};
|
||||
dva: {
|
||||
extraModels?: (Array<string> | undefined);
|
||||
immer?: ({ [x: string]: any } | undefined);
|
||||
skipModelValidate?: (boolean | undefined);
|
||||
};
|
||||
initialState: {
|
||||
loading?: (string | undefined);
|
||||
};
|
||||
layout: { [x: string]: any };
|
||||
locale: {
|
||||
default?: (string | undefined);
|
||||
useLocalStorage?: (boolean | undefined);
|
||||
baseNavigator?: (boolean | undefined);
|
||||
title?: (boolean | undefined);
|
||||
antd?: (boolean | undefined);
|
||||
baseSeparator?: (string | undefined);
|
||||
};
|
||||
mf: {
|
||||
name?: (string | undefined);
|
||||
remotes?: (Array<{
|
||||
aliasName?: (string | undefined);
|
||||
name: string;
|
||||
entry?: (string | undefined);
|
||||
entries?: ({
|
||||
|
||||
} | undefined);
|
||||
keyResolver?: (string | undefined);
|
||||
}> | undefined);
|
||||
shared?: ({ [x: string]: any } | undefined);
|
||||
library?: ({ [x: string]: any } | undefined);
|
||||
remoteHash?: (boolean | undefined);
|
||||
};
|
||||
model: {
|
||||
extraModels?: (Array<string> | undefined);
|
||||
sort?: ((((...args: any[]) => unknown) | undefined) | undefined);
|
||||
};
|
||||
moment2dayjs: {
|
||||
preset?: ("antd" | "antdv3" | "none" | undefined);
|
||||
plugins?: (Array<string> | undefined);
|
||||
};
|
||||
qiankun: {
|
||||
slave?: ({ [x: string]: any } | undefined);
|
||||
master?: ({ [x: string]: any } | undefined);
|
||||
externalQiankun?: (boolean | undefined);
|
||||
};
|
||||
reactQuery: {
|
||||
devtool?: ({ [x: string]: any } | boolean | undefined);
|
||||
queryClient?: ({ [x: string]: any } | boolean | undefined);
|
||||
};
|
||||
request: {
|
||||
dataField?: (string | undefined);
|
||||
};
|
||||
styledComponents: {
|
||||
babelPlugin?: ({ [x: string]: any } | undefined);
|
||||
};
|
||||
tailwindcss: { [x: string]: any };
|
||||
valtio: {
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
type PrettifyWithCloseable<T> = {
|
||||
[K in keyof T]: T[K] | false;
|
||||
} & {};
|
||||
|
||||
export type IConfigFromPlugins = PrettifyWithCloseable<
|
||||
IConfigFromPluginsJoi & Partial<IConfigTypes>
|
||||
>;
|
||||
7
src/.umi/core/pluginConfigJoi.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
// Created by Umi Plugin
|
||||
|
||||
export interface IConfigFromPluginsJoi {
|
||||
stagewise?: unknown
|
||||
}
|
||||
220
src/.umi/core/polyfill.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.error.cause.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.aggregate-error.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.aggregate-error.cause.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.at.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.find-last.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.find-last-index.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.push.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.reduce.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.reduce-right.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.to-reversed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.to-sorted.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.to-spliced.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.array.with.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.map.group-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.object.group-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.object.has-own.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.promise.any.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.promise.with-resolvers.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.reflect.to-string-tag.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.regexp.flags.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.string.at-alternative.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.string.is-well-formed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.string.replace-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.string.to-well-formed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.at.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.find-last.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.find-last-index.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.set.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.to-reversed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.to-sorted.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/es.typed-array.with.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.suppressed-error.constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.from-async.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.filter-out.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.filter-reject.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.group.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.group-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.group-by-to-map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.group-to-map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.is-template-object.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.last-index.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.last-item.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array.unique-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array-buffer.detached.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array-buffer.transfer.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.array-buffer.transfer-to-fixed-length.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-disposable-stack.constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.as-indexed-pairs.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.async-dispose.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.drop.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.every.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.filter.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.find.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.flat-map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.for-each.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.indexed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.reduce.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.some.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.take.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.async-iterator.to-array.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.bigint.range.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.composite-key.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.composite-symbol.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.data-view.get-float16.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.data-view.get-uint8-clamped.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.data-view.set-float16.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.data-view.set-uint8-clamped.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.disposable-stack.constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.function.demethodize.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.function.is-callable.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.function.is-constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.function.metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.function.un-this.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.constructor.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.as-indexed-pairs.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.dispose.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.drop.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.every.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.filter.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.find.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.flat-map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.for-each.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.indexed.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.range.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.reduce.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.some.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.take.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.to-array.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.iterator.to-async.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.json.is-raw-json.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.json.parse.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.json.raw-json.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.delete-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.emplace.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.every.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.filter.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.find.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.find-key.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.includes.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.key-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.key-of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.map-keys.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.map-values.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.merge.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.reduce.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.some.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.update.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.update-or-insert.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.map.upsert.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.clamp.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.deg-per-rad.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.degrees.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.fscale.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.f16round.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.iaddh.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.imulh.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.isubh.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.rad-per-deg.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.radians.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.scale.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.seeded-prng.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.signbit.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.math.umulh.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.number.from-string.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.number.range.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.object.iterate-entries.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.object.iterate-keys.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.object.iterate-values.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.observable.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.promise.try.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.define-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.delete-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.get-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.get-metadata-keys.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.get-own-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.get-own-metadata-keys.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.has-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.has-own-metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.reflect.metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.regexp.escape.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.add-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.delete-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.difference.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.difference.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.every.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.filter.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.find.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.intersection.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.intersection.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-disjoint-from.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-disjoint-from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-subset-of.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-subset-of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-superset-of.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.is-superset-of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.join.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.map.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.reduce.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.some.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.symmetric-difference.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.symmetric-difference.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.union.v2.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.set.union.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.string.at.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.string.cooked.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.string.code-points.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.string.dedent.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.async-dispose.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.dispose.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.is-registered-symbol.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.is-registered.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.is-well-known-symbol.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.is-well-known.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.matcher.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.metadata.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.metadata-key.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.observable.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.pattern-match.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.symbol.replace-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.from-async.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.filter-out.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.filter-reject.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.group-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.to-spliced.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.typed-array.unique-by.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.uint8-array.from-base64.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.uint8-array.from-hex.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.uint8-array.to-base64.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.uint8-array.to-hex.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-map.delete-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-map.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-map.of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-map.emplace.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-map.upsert.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-set.add-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-set.delete-all.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-set.from.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/esnext.weak-set.of.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.dom-exception.stack.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.immediate.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.self.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.structured-clone.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.url.can-parse.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.url-search-params.delete.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.url-search-params.has.js";
|
||||
import "C:/Users/admin/Desktop/filedata/project/\u6CFD\u6797-frontend/node_modules/core-js/modules/web.url-search-params.size.js";
|
||||
import 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/regenerator-runtime/runtime.js';
|
||||
export {};
|
||||
40
src/.umi/core/route.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React from 'react';
|
||||
|
||||
export async function getRoutes() {
|
||||
const routes = {"1":{"path":"/","redirect":"/home","id":"1"},"2":{"path":"/home","id":"2"},"3":{"path":"/jobs","id":"3"},"4":{"path":"/manage","id":"4"},"5":{"path":"/manage/about","name":"关于","parentId":"4","id":"5"},"6":{"path":"/manage/user","name":"用户管理","parentId":"4","id":"6"},"7":{"path":"/appointment","id":"7"},"8":{"path":"/resume","id":"8"},"9":{"path":"/resume/create","id":"9"},"10":{"path":"/resume/preview","id":"10"},"11":{"path":"/admin","id":"11"},"12":{"path":"/admin","redirect":"/admin/college","parentId":"11","id":"12"},"13":{"path":"/admin/college","parentId":"11","id":"13"},"14":{"path":"/admin/staff","parentId":"11","id":"14"},"15":{"path":"/admin/student","parentId":"11","id":"15"},"16":{"path":"/admin/role","parentId":"11","id":"16"},"17":{"path":"/admin/overview","parentId":"11","id":"17"},"18":{"path":"/admin/appointment-list","parentId":"11","id":"18"},"19":{"path":"/admin/appointment-users","parentId":"11","id":"19"},"20":{"path":"/admin/task-list","parentId":"11","id":"20"},"21":{"path":"/admin/banner","parentId":"11","id":"21"},"22":{"path":"/admin/security","parentId":"11","id":"22"},"23":{"path":"/admin/user-manage","parentId":"11","id":"23"},"24":{"path":"/admin/menu-manage","parentId":"11","id":"24"},"25":{"path":"/admin/operation-log","parentId":"11","id":"25"},"26":{"path":"/login","id":"26"},"27":{"path":"*","id":"27"}} as const;
|
||||
return {
|
||||
routes,
|
||||
routeComponents: {
|
||||
'1': React.lazy(() => import('./EmptyRoute')),
|
||||
'2': React.lazy(() => import(/* webpackChunkName: "p__Home__index" */'@/pages/Home/index.tsx')),
|
||||
'3': React.lazy(() => import(/* webpackChunkName: "p__Jobs__index" */'@/pages/Jobs/index.tsx')),
|
||||
'4': React.lazy(() => import(/* webpackChunkName: "layouts__BasicLayout" */'@/layouts/BasicLayout.tsx')),
|
||||
'5': React.lazy(() => import(/* webpackChunkName: "p__About__index" */'@/pages/About/index.tsx')),
|
||||
'6': React.lazy(() => import(/* webpackChunkName: "p__User__index" */'@/pages/User/index.tsx')),
|
||||
'7': React.lazy(() => import(/* webpackChunkName: "p__Appointment__index" */'@/pages/Appointment/index.tsx')),
|
||||
'8': React.lazy(() => import(/* webpackChunkName: "p__Resume__index" */'@/pages/Resume/index.tsx')),
|
||||
'9': React.lazy(() => import(/* webpackChunkName: "p__Resume__Create__index" */'@/pages/Resume/Create/index.tsx')),
|
||||
'10': React.lazy(() => import(/* webpackChunkName: "p__Resume__Preview__index" */'@/pages/Resume/Preview/index.tsx')),
|
||||
'11': React.lazy(() => import(/* webpackChunkName: "p__Admin__index" */'@/pages/Admin/index.tsx')),
|
||||
'12': React.lazy(() => import('./EmptyRoute')),
|
||||
'13': React.lazy(() => import(/* webpackChunkName: "p__Admin__College__index" */'@/pages/Admin/College/index.tsx')),
|
||||
'14': React.lazy(() => import(/* webpackChunkName: "p__Admin__Staff__index" */'@/pages/Admin/Staff/index.tsx')),
|
||||
'15': React.lazy(() => import(/* webpackChunkName: "p__Admin__Student__index" */'@/pages/Admin/Student/index.tsx')),
|
||||
'16': React.lazy(() => import(/* webpackChunkName: "p__Admin__Role__index" */'@/pages/Admin/Role/index.tsx')),
|
||||
'17': React.lazy(() => import(/* webpackChunkName: "p__Statistics__Overview__index" */'@/pages/Statistics/Overview/index.tsx')),
|
||||
'18': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'19': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'20': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'21': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'22': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'23': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'24': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'25': React.lazy(() => import(/* webpackChunkName: "p__Admin__Placeholder" */'@/pages/Admin/Placeholder.tsx')),
|
||||
'26': React.lazy(() => import(/* webpackChunkName: "p__Login__index" */'@/pages/Login/index.tsx')),
|
||||
'27': React.lazy(() => import(/* webpackChunkName: "p__404__index" */'@/pages/404/index.tsx')),
|
||||
},
|
||||
};
|
||||
}
|
||||
37
src/.umi/core/terminal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
let count = 0;
|
||||
let groupLevel = 0;
|
||||
function send(type: string, message?: string) {
|
||||
if(process.env.NODE_ENV==='production'){
|
||||
return;
|
||||
}else{
|
||||
const encodedMessage = message ? `&m=${encodeURI(message)}` : '';
|
||||
fetch(`/__umi/api/terminal?type=${type}&t=${Date.now()}&c=${count++}&g=${groupLevel}${encodedMessage}`, { mode: 'no-cors' })
|
||||
}
|
||||
}
|
||||
function prettyPrint(obj: any) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
function stringifyObjs(objs: any[]) {
|
||||
const obj = objs.length > 1 ? objs.map(stringify).join(' ') : objs[0];
|
||||
return typeof obj === 'object' ? `${prettyPrint(obj)}` : obj.toString();
|
||||
}
|
||||
function stringify(obj: any) {
|
||||
return typeof obj === 'object' ? `${JSON.stringify(obj)}` : obj.toString();
|
||||
}
|
||||
const terminal = {
|
||||
log(...objs: any[]) { send('log', stringifyObjs(objs)) },
|
||||
info(...objs: any[]) { send('info', stringifyObjs(objs)) },
|
||||
warn(...objs: any[]) { send('warn', stringifyObjs(objs)) },
|
||||
error(...objs: any[]) { send('error', stringifyObjs(objs)) },
|
||||
group() { groupLevel++ },
|
||||
groupCollapsed() { groupLevel++ },
|
||||
groupEnd() { groupLevel && --groupLevel },
|
||||
clear() { send('clear') },
|
||||
trace(...args: any[]) { console.trace(...args) },
|
||||
profile(...args: any[]) { console.profile(...args) },
|
||||
profileEnd(...args: any[]) { console.profileEnd(...args) },
|
||||
};
|
||||
export { terminal };
|
||||
23
src/.umi/exports.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
// defineApp
|
||||
export { defineApp } from './core/defineApp'
|
||||
export type { RuntimeConfig } from './core/defineApp'
|
||||
// plugins
|
||||
export { Provider, useModel } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-model';
|
||||
export { useRequest, UseRequestProvider, request, getRequestInstance } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-request';
|
||||
// plugins types.d.ts
|
||||
export * from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-antd/types.d';
|
||||
export * from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/.umi/plugin-request/types.d';
|
||||
// @umijs/renderer-*
|
||||
export { createBrowserHistory, createHashHistory, createMemoryHistory, Helmet, HelmetProvider, createSearchParams, generatePath, matchPath, matchRoutes, Navigate, NavLink, Outlet, resolvePath, useLocation, useMatch, useNavigate, useOutlet, useOutletContext, useParams, useResolvedPath, useRoutes, useSearchParams, useAppData, useClientLoaderData, useLoaderData, useRouteProps, useSelectedRoutes, useServerLoaderData, renderClient, __getRoot, Link, useRouteData, __useFetcher, withRouter } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react';
|
||||
export type { History, ClientLoader } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react'
|
||||
// umi/client/client/plugin
|
||||
export { ApplyPluginsType, PluginManager } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/umi/client/client/plugin.js';
|
||||
export { history, createHistory } from './core/history';
|
||||
export { terminal } from './core/terminal';
|
||||
// react ssr
|
||||
export const useServerInsertedHTML: Function = () => {};
|
||||
// test
|
||||
export { TestBrowser } from './testBrowser';
|
||||
53
src/.umi/plugin-antd/runtime.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React from 'react';
|
||||
import {
|
||||
ConfigProvider,
|
||||
} from 'antd';
|
||||
import { ApplyPluginsType } from 'umi';
|
||||
import { getPluginManager } from '../core/plugin';
|
||||
|
||||
let cacheAntdConfig = null;
|
||||
|
||||
const getAntdConfig = () => {
|
||||
if(!cacheAntdConfig){
|
||||
cacheAntdConfig = getPluginManager().applyPlugins({
|
||||
key: 'antd',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: {
|
||||
},
|
||||
});
|
||||
}
|
||||
return cacheAntdConfig;
|
||||
}
|
||||
|
||||
function AntdProvider({ children }) {
|
||||
let container = children;
|
||||
|
||||
const [antdConfig, _setAntdConfig] = React.useState(() => {
|
||||
const {
|
||||
appConfig: _,
|
||||
...finalConfigProvider
|
||||
} = getAntdConfig();
|
||||
return finalConfigProvider
|
||||
});
|
||||
const setAntdConfig: typeof _setAntdConfig = (newConfig) => {
|
||||
_setAntdConfig(prev => {
|
||||
return merge({}, prev, typeof newConfig === 'function' ? newConfig(prev) : newConfig)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export function rootContainer(children) {
|
||||
return (
|
||||
<AntdProvider>
|
||||
{children}
|
||||
</AntdProvider>
|
||||
);
|
||||
}
|
||||
6
src/.umi/plugin-antd/runtimeConfig.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import type { RuntimeAntdConfig } from './types.d';
|
||||
export type IRuntimeConfig = {
|
||||
antd?: RuntimeAntdConfig
|
||||
};
|
||||
12
src/.umi/plugin-antd/types.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
type AntdConfig = Prettify<{}
|
||||
|
||||
|
||||
>;
|
||||
|
||||
export type RuntimeAntdConfig = (memo: AntdConfig) => AntdConfig;
|
||||
183
src/.umi/plugin-model/index.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
// @ts-ignore
|
||||
import type { models as rawModels } from '@@/plugin-model/model';
|
||||
import isEqual from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/fast-deep-equal/index.js';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
type Models = typeof rawModels;
|
||||
|
||||
type GetNamespaces<M> = {
|
||||
[K in keyof M]: M[K] extends { namespace: string }
|
||||
? M[K]['namespace']
|
||||
: never;
|
||||
}[keyof M];
|
||||
|
||||
type Namespaces = GetNamespaces<Models>;
|
||||
|
||||
// @ts-ignore
|
||||
const Context = React.createContext<{ dispatcher: Dispatcher }>(null);
|
||||
|
||||
class Dispatcher {
|
||||
callbacks: Record<Namespaces, Set<Function>> = {};
|
||||
data: Record<Namespaces, unknown> = {};
|
||||
update = (namespace: Namespaces) => {
|
||||
if (this.callbacks[namespace]) {
|
||||
this.callbacks[namespace].forEach((cb) => {
|
||||
try {
|
||||
const data = this.data[namespace];
|
||||
cb(data);
|
||||
} catch (e) {
|
||||
cb(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface ExecutorProps {
|
||||
hook: () => any;
|
||||
onUpdate: (val: any) => void;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
function Executor(props: ExecutorProps) {
|
||||
const { hook, onUpdate, namespace } = props;
|
||||
|
||||
const updateRef = useRef(onUpdate);
|
||||
const initialLoad = useRef(false);
|
||||
|
||||
let data: any;
|
||||
try {
|
||||
data = hook();
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`plugin-model: Invoking '${namespace || 'unknown'}' model failed:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
// 首次执行时立刻返回初始值
|
||||
useMemo(() => {
|
||||
updateRef.current(data);
|
||||
}, []);
|
||||
|
||||
// React 16.13 后 update 函数用 useEffect 包裹
|
||||
useEffect(() => {
|
||||
if (initialLoad.current) {
|
||||
updateRef.current(data);
|
||||
} else {
|
||||
initialLoad.current = true;
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispatcher = new Dispatcher();
|
||||
|
||||
export function Provider(props: {
|
||||
models: Record<string, any>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Context.Provider value={{ dispatcher }}>
|
||||
{Object.keys(props.models).map((namespace) => {
|
||||
return (
|
||||
<Executor
|
||||
key={namespace}
|
||||
hook={props.models[namespace]}
|
||||
namespace={namespace}
|
||||
onUpdate={(val) => {
|
||||
dispatcher.data[namespace] = val;
|
||||
dispatcher.update(namespace);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type GetModelByNamespace<M, N> = {
|
||||
[K in keyof M]: M[K] extends { namespace: string; model: unknown }
|
||||
? M[K]['namespace'] extends N
|
||||
? M[K]['model'] extends (...args: any) => any
|
||||
? ReturnType<M[K]['model']>
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
}[keyof M];
|
||||
|
||||
type Model<N> = GetModelByNamespace<Models, N>;
|
||||
type Selector<N, S> = (model: Model<N>) => S;
|
||||
|
||||
type SelectedModel<N, T> = T extends (...args: any) => any
|
||||
? ReturnType<NonNullable<T>>
|
||||
: Model<N>;
|
||||
|
||||
export function useModel<N extends Namespaces>(namespace: N): Model<N>;
|
||||
|
||||
export function useModel<N extends Namespaces, S>(
|
||||
namespace: N,
|
||||
selector: Selector<N, S>,
|
||||
): SelectedModel<N, typeof selector>;
|
||||
|
||||
export function useModel<N extends Namespaces, S>(
|
||||
namespace: N,
|
||||
selector?: Selector<N, S>,
|
||||
): SelectedModel<N, typeof selector> {
|
||||
const { dispatcher } = useContext<{ dispatcher: Dispatcher }>(Context);
|
||||
const selectorRef = useRef(selector);
|
||||
selectorRef.current = selector;
|
||||
const [state, setState] = useState(() =>
|
||||
selectorRef.current
|
||||
? selectorRef.current(dispatcher.data[namespace])
|
||||
: dispatcher.data[namespace],
|
||||
);
|
||||
const stateRef = useRef<any>(state);
|
||||
stateRef.current = state;
|
||||
|
||||
const isMount = useRef(false);
|
||||
useEffect(() => {
|
||||
isMount.current = true;
|
||||
return () => {
|
||||
isMount.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (data: any) => {
|
||||
if (!isMount.current) {
|
||||
// 如果 handler 执行过程中,组件被卸载了,则强制更新全局 data
|
||||
// TODO: 需要加个 example 测试
|
||||
setTimeout(() => {
|
||||
dispatcher.data[namespace] = data;
|
||||
dispatcher.update(namespace);
|
||||
});
|
||||
} else {
|
||||
const currentState = selectorRef.current
|
||||
? selectorRef.current(data)
|
||||
: data;
|
||||
const previousState = stateRef.current;
|
||||
if (!isEqual(currentState, previousState)) {
|
||||
// 避免 currentState 拿到的数据是老的,从而导致 isEqual 比对逻辑有问题
|
||||
stateRef.current = currentState;
|
||||
setState(currentState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dispatcher.callbacks[namespace] ||= new Set() as any; // rawModels 是 umi 动态生成的文件,导致前面 callback[namespace] 的类型无法推导出来,所以用 as any 来忽略掉
|
||||
dispatcher.callbacks[namespace].add(handler);
|
||||
dispatcher.update(namespace);
|
||||
|
||||
return () => {
|
||||
dispatcher.callbacks[namespace].delete(handler);
|
||||
};
|
||||
}, [namespace]);
|
||||
|
||||
return state;
|
||||
}
|
||||
6
src/.umi/plugin-model/model.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
export const models = {
|
||||
|
||||
} as const
|
||||
20
src/.umi/plugin-model/runtime.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React from 'react';
|
||||
import { Provider } from './';
|
||||
import { models as rawModels } from './model';
|
||||
|
||||
function ProviderWrapper(props: any) {
|
||||
const models = React.useMemo(() => {
|
||||
return Object.keys(rawModels).reduce((memo, key) => {
|
||||
memo[rawModels[key].namespace] = rawModels[key].model;
|
||||
return memo;
|
||||
}, {});
|
||||
}, []);
|
||||
return <Provider models={models} {...props}>{ props.children }</Provider>
|
||||
}
|
||||
|
||||
export function dataflowProvider(container, opts) {
|
||||
return <ProviderWrapper {...opts}>{ container }</ProviderWrapper>;
|
||||
}
|
||||
9
src/.umi/plugin-request/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
export {
|
||||
useRequest,
|
||||
UseRequestProvider,
|
||||
request,
|
||||
getRequestInstance,
|
||||
} from './request';
|
||||
270
src/.umi/plugin-request/request.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import axios, {
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
type AxiosError,
|
||||
} from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/axios';
|
||||
import useUmiRequest, { UseRequestProvider } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/plugins/node_modules/@ahooksjs/use-request';
|
||||
import { ApplyPluginsType } from 'umi';
|
||||
import { getPluginManager } from '../core/plugin';
|
||||
|
||||
import {
|
||||
BaseOptions,
|
||||
BasePaginatedOptions,
|
||||
BaseResult,
|
||||
CombineService,
|
||||
LoadMoreFormatReturn,
|
||||
LoadMoreOptions,
|
||||
LoadMoreOptionsWithFormat,
|
||||
LoadMoreParams,
|
||||
LoadMoreResult,
|
||||
OptionsWithFormat,
|
||||
PaginatedFormatReturn,
|
||||
PaginatedOptionsWithFormat,
|
||||
PaginatedParams,
|
||||
PaginatedResult,
|
||||
} from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/plugins/node_modules/@ahooksjs/use-request/es/types';
|
||||
|
||||
type ResultWithData< T = any > = { data?: T; [key: string]: any };
|
||||
|
||||
function useRequest<
|
||||
R = any,
|
||||
P extends any[] = any,
|
||||
U = any,
|
||||
UU extends U = any,
|
||||
>(
|
||||
service: CombineService<R, P>,
|
||||
options: OptionsWithFormat<R, P, U, UU>,
|
||||
): BaseResult<U, P>;
|
||||
function useRequest<R extends ResultWithData = any, P extends any[] = any>(
|
||||
service: CombineService<R, P>,
|
||||
options?: BaseOptions<R['data'], P>,
|
||||
): BaseResult<R['data'], P>;
|
||||
function useRequest<R extends LoadMoreFormatReturn = any, RR = any>(
|
||||
service: CombineService<RR, LoadMoreParams<R>>,
|
||||
options: LoadMoreOptionsWithFormat<R, RR>,
|
||||
): LoadMoreResult<R>;
|
||||
function useRequest<
|
||||
R extends ResultWithData<LoadMoreFormatReturn | any> = any,
|
||||
RR extends R = any,
|
||||
>(
|
||||
service: CombineService<R, LoadMoreParams<R['data']>>,
|
||||
options: LoadMoreOptions<RR['data']>,
|
||||
): LoadMoreResult<R['data']>;
|
||||
|
||||
function useRequest<R = any, Item = any, U extends Item = any>(
|
||||
service: CombineService<R, PaginatedParams>,
|
||||
options: PaginatedOptionsWithFormat<R, Item, U>,
|
||||
): PaginatedResult<Item>;
|
||||
function useRequest<Item = any, U extends Item = any>(
|
||||
service: CombineService<
|
||||
ResultWithData<PaginatedFormatReturn<Item>>,
|
||||
PaginatedParams
|
||||
>,
|
||||
options: BasePaginatedOptions<U>,
|
||||
): PaginatedResult<Item>;
|
||||
function useRequest(service: any, options: any = {}) {
|
||||
return useUmiRequest(service, {
|
||||
formatResult: result => result?.data,
|
||||
requestMethod: (requestOptions: any) => {
|
||||
if (typeof requestOptions === 'string') {
|
||||
return request(requestOptions);
|
||||
}
|
||||
if (typeof requestOptions === 'object') {
|
||||
const { url, ...rest } = requestOptions;
|
||||
return request(url, rest);
|
||||
}
|
||||
throw new Error('request options error');
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// request 方法 opts 参数的接口
|
||||
interface IRequestOptions extends AxiosRequestConfig {
|
||||
skipErrorHandler?: boolean;
|
||||
requestInterceptors?: IRequestInterceptorTuple[];
|
||||
responseInterceptors?: IResponseInterceptorTuple[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface IRequestOptionsWithResponse extends IRequestOptions {
|
||||
getResponse: true;
|
||||
}
|
||||
|
||||
interface IRequestOptionsWithoutResponse extends IRequestOptions{
|
||||
getResponse: false;
|
||||
}
|
||||
|
||||
interface IRequest{
|
||||
<T = any>(url: string, opts: IRequestOptionsWithResponse): Promise<AxiosResponse<T>>;
|
||||
<T = any>(url: string, opts: IRequestOptionsWithoutResponse): Promise<T>;
|
||||
<T = any>(url: string, opts: IRequestOptions): Promise<T>; // getResponse 默认是 false, 因此不提供该参数时,只返回 data
|
||||
<T = any>(url: string): Promise<T>; // 不提供 opts 时,默认使用 'GET' method,并且默认返回 data
|
||||
}
|
||||
|
||||
type RequestError = AxiosError | Error
|
||||
|
||||
interface IErrorHandler {
|
||||
(error: RequestError, opts: IRequestOptions): void;
|
||||
}
|
||||
type WithPromise<T> = T | Promise<T>;
|
||||
type IRequestInterceptorAxios = (config: IRequestOptions) => WithPromise<IRequestOptions>;
|
||||
type IRequestInterceptorUmiRequest = (url: string, config : IRequestOptions) => WithPromise<{ url: string, options: IRequestOptions }>;
|
||||
type IRequestInterceptor = IRequestInterceptorAxios | IRequestInterceptorUmiRequest;
|
||||
type IErrorInterceptor = (error: Error) => Promise<Error>;
|
||||
type IResponseInterceptor = <T = any>(response : AxiosResponse<T>) => WithPromise<AxiosResponse<T>> ;
|
||||
type IRequestInterceptorTuple = [IRequestInterceptor , IErrorInterceptor] | [IRequestInterceptor] | IRequestInterceptor
|
||||
type IResponseInterceptorTuple = [IResponseInterceptor, IErrorInterceptor] | [IResponseInterceptor] | IResponseInterceptor
|
||||
|
||||
export interface RequestConfig<T = any> extends AxiosRequestConfig {
|
||||
errorConfig?: {
|
||||
errorHandler?: IErrorHandler;
|
||||
errorThrower?: ( res: T ) => void
|
||||
};
|
||||
requestInterceptors?: IRequestInterceptorTuple[];
|
||||
responseInterceptors?: IResponseInterceptorTuple[];
|
||||
}
|
||||
|
||||
let requestInstance: AxiosInstance;
|
||||
let config: RequestConfig;
|
||||
const getConfig = (): RequestConfig => {
|
||||
if (config) return config;
|
||||
config = getPluginManager().applyPlugins({
|
||||
key: 'request',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: {},
|
||||
});
|
||||
return config;
|
||||
};
|
||||
|
||||
const getRequestInstance = (): AxiosInstance => {
|
||||
if (requestInstance) return requestInstance;
|
||||
const config = getConfig();
|
||||
requestInstance = axios.create(config);
|
||||
|
||||
config?.requestInterceptors?.forEach((interceptor) => {
|
||||
if(interceptor instanceof Array){
|
||||
requestInstance.interceptors.request.use(async (config) => {
|
||||
const { url } = config;
|
||||
if(interceptor[0].length === 2){
|
||||
const { url: newUrl, options } = await interceptor[0](url, config);
|
||||
return { ...options, url: newUrl };
|
||||
}
|
||||
return interceptor[0](config);
|
||||
}, interceptor[1]);
|
||||
} else {
|
||||
requestInstance.interceptors.request.use(async (config) => {
|
||||
const { url } = config;
|
||||
if(interceptor.length === 2){
|
||||
const { url: newUrl, options } = await interceptor(url, config);
|
||||
return { ...options, url: newUrl };
|
||||
}
|
||||
return interceptor(config);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
config?.responseInterceptors?.forEach((interceptor) => {
|
||||
interceptor instanceof Array ?
|
||||
requestInstance.interceptors.response.use(interceptor[0], interceptor[1]):
|
||||
requestInstance.interceptors.response.use(interceptor);
|
||||
});
|
||||
|
||||
// 当响应的数据 success 是 false 的时候,抛出 error 以供 errorHandler 处理。
|
||||
requestInstance.interceptors.response.use((response) => {
|
||||
const { data } = response;
|
||||
if(data?.success === false && config?.errorConfig?.errorThrower){
|
||||
config.errorConfig.errorThrower(data);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
return requestInstance;
|
||||
};
|
||||
|
||||
const request: IRequest = (url: string, opts: any = { method: 'GET' }) => {
|
||||
const requestInstance = getRequestInstance();
|
||||
const config = getConfig();
|
||||
const { getResponse = false, requestInterceptors, responseInterceptors } = opts;
|
||||
const requestInterceptorsToEject = requestInterceptors?.map((interceptor) => {
|
||||
if(interceptor instanceof Array){
|
||||
return requestInstance.interceptors.request.use(async (config) => {
|
||||
const { url } = config;
|
||||
if(interceptor[0].length === 2){
|
||||
const { url: newUrl, options } = await interceptor[0](url, config);
|
||||
return { ...options, url: newUrl };
|
||||
}
|
||||
return interceptor[0](config);
|
||||
}, interceptor[1]);
|
||||
} else {
|
||||
return requestInstance.interceptors.request.use(async (config) => {
|
||||
const { url } = config;
|
||||
if(interceptor.length === 2){
|
||||
const { url: newUrl, options } = await interceptor(url, config);
|
||||
return { ...options, url: newUrl };
|
||||
}
|
||||
return interceptor(config);
|
||||
})
|
||||
}
|
||||
});
|
||||
const responseInterceptorsToEject = responseInterceptors?.map((interceptor) => {
|
||||
return interceptor instanceof Array ?
|
||||
requestInstance.interceptors.response.use(interceptor[0], interceptor[1]):
|
||||
requestInstance.interceptors.response.use(interceptor);
|
||||
});
|
||||
return new Promise((resolve, reject)=>{
|
||||
requestInstance
|
||||
.request({...opts, url})
|
||||
.then((res)=>{
|
||||
requestInterceptorsToEject?.forEach((interceptor) => {
|
||||
requestInstance.interceptors.request.eject(interceptor);
|
||||
});
|
||||
responseInterceptorsToEject?.forEach((interceptor) => {
|
||||
requestInstance.interceptors.response.eject(interceptor);
|
||||
});
|
||||
resolve(getResponse ? res : res.data);
|
||||
})
|
||||
.catch((error)=>{
|
||||
requestInterceptorsToEject?.forEach((interceptor) => {
|
||||
requestInstance.interceptors.request.eject(interceptor);
|
||||
});
|
||||
responseInterceptorsToEject?.forEach((interceptor) => {
|
||||
requestInstance.interceptors.response.eject(interceptor);
|
||||
});
|
||||
try {
|
||||
const handler =
|
||||
config?.errorConfig?.errorHandler;
|
||||
if(handler)
|
||||
handler(error, opts, config);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
useRequest,
|
||||
UseRequestProvider,
|
||||
request,
|
||||
getRequestInstance,
|
||||
};
|
||||
|
||||
export type {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosError,
|
||||
RequestError,
|
||||
IRequestInterceptorAxios as RequestInterceptorAxios,
|
||||
IRequestInterceptorUmiRequest as RequestInterceptorUmiRequest,
|
||||
IRequestInterceptor as RequestInterceptor,
|
||||
IErrorInterceptor as ErrorInterceptor,
|
||||
IResponseInterceptor as ResponseInterceptor,
|
||||
IRequestOptions as RequestOptions,
|
||||
IRequest as Request,
|
||||
};
|
||||
6
src/.umi/plugin-request/runtimeConfig.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import type { RequestConfig } from './types.d'
|
||||
export type IRuntimeConfig = {
|
||||
request?: RequestConfig
|
||||
};
|
||||
16
src/.umi/plugin-request/types.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
export type {
|
||||
RequestConfig,
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosError,
|
||||
RequestError,
|
||||
RequestInterceptorAxios,
|
||||
RequestInterceptorUmiRequest,
|
||||
RequestInterceptor,
|
||||
ErrorInterceptor,
|
||||
ResponseInterceptor,
|
||||
RequestOptions,
|
||||
Request } from './request';
|
||||
89
src/.umi/testBrowser.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ApplyPluginsType } from 'umi';
|
||||
import { renderClient, RenderClientOpts } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react';
|
||||
import { createHistory } from './core/history';
|
||||
import { createPluginManager } from './core/plugin';
|
||||
import { getRoutes } from './core/route';
|
||||
import type { Location } from 'history';
|
||||
|
||||
|
||||
import 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/global.less';
|
||||
import 'antd/dist/reset.css';
|
||||
const publicPath = '/';
|
||||
const runtimePublicPath = false;
|
||||
|
||||
type TestBrowserProps = {
|
||||
location?: Partial<Location>;
|
||||
historyRef?: React.MutableRefObject<Location>;
|
||||
};
|
||||
|
||||
export function TestBrowser(props: TestBrowserProps) {
|
||||
const pluginManager = createPluginManager();
|
||||
const [context, setContext] = useState<RenderClientOpts | undefined>(
|
||||
undefined
|
||||
);
|
||||
useEffect(() => {
|
||||
const genContext = async () => {
|
||||
const { routes, routeComponents } = await getRoutes(pluginManager);
|
||||
// allow user to extend routes
|
||||
await pluginManager.applyPlugins({
|
||||
key: 'patchRoutes',
|
||||
type: ApplyPluginsType.event,
|
||||
args: {
|
||||
routes,
|
||||
routeComponents,
|
||||
},
|
||||
});
|
||||
const contextOpts = pluginManager.applyPlugins({
|
||||
key: 'modifyContextOpts',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: {},
|
||||
});
|
||||
const basename = contextOpts.basename || '/';
|
||||
const history = createHistory({
|
||||
type: 'memory',
|
||||
basename,
|
||||
});
|
||||
const context = {
|
||||
routes,
|
||||
routeComponents,
|
||||
pluginManager,
|
||||
rootElement: contextOpts.rootElement || document.getElementById('root'),
|
||||
publicPath,
|
||||
runtimePublicPath,
|
||||
history,
|
||||
basename,
|
||||
components: true,
|
||||
};
|
||||
const modifiedContext = pluginManager.applyPlugins({
|
||||
key: 'modifyClientRenderOpts',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: context,
|
||||
});
|
||||
return modifiedContext;
|
||||
};
|
||||
genContext().then((context) => {
|
||||
setContext(context);
|
||||
if (props.location) {
|
||||
context?.history?.push(props.location);
|
||||
}
|
||||
if (props.historyRef) {
|
||||
props.historyRef.current = context?.history;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (context === undefined) {
|
||||
return <div id="loading" />;
|
||||
}
|
||||
|
||||
const Children = renderClient(context);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Children />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
44
src/.umi/tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "../../",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
"@@/*": [
|
||||
"src/.umi/*"
|
||||
],
|
||||
"@umijs/max": [
|
||||
"../../node_modules/umi"
|
||||
],
|
||||
"@umijs/max/typings": [
|
||||
"src/.umi/typings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../../.umirc.ts",
|
||||
"../../.umirc.*.ts",
|
||||
"../../**/*.d.ts",
|
||||
"../../**/*.ts",
|
||||
"../../**/*.tsx"
|
||||
]
|
||||
}
|
||||
136
src/.umi/typings.d.ts
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
type CSSModuleClasses = { readonly [key: string]: string }
|
||||
declare module '*.css' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
declare module '*.scss' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
declare module '*.sass' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
declare module '*.less' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
declare module '*.styl' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
declare module '*.stylus' {
|
||||
const classes: CSSModuleClasses
|
||||
export default classes
|
||||
}
|
||||
|
||||
// images
|
||||
declare module '*.jpg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.svg' {
|
||||
import * as React from 'react';
|
||||
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||
SVGSVGElement
|
||||
> & { title?: string }>;
|
||||
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.ico' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
// media
|
||||
declare module '*.mp4' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.webm' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.ogg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.wav' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.flac' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.aac' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
// fonts
|
||||
declare module '*.woff' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.woff2' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.eot' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.ttf' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.otf' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
// other
|
||||
declare module '*.wasm' {
|
||||
const initWasm: (options: WebAssembly.Imports) => Promise<WebAssembly.Exports>
|
||||
export default initWasm
|
||||
}
|
||||
declare module '*.webmanifest' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.pdf' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
82
src/.umi/umi.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import './core/polyfill';
|
||||
import 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/src/global.less';
|
||||
import 'antd/dist/reset.css';
|
||||
import { renderClient } from 'C:/Users/admin/Desktop/filedata/project/泽林-frontend/node_modules/@umijs/renderer-react';
|
||||
import { getRoutes } from './core/route';
|
||||
import { createPluginManager } from './core/plugin';
|
||||
import { createHistory } from './core/history';
|
||||
import { ApplyPluginsType } from 'umi';
|
||||
|
||||
|
||||
const publicPath = "/";
|
||||
const runtimePublicPath = false;
|
||||
|
||||
async function render() {
|
||||
const pluginManager = createPluginManager();
|
||||
const { routes, routeComponents } = await getRoutes(pluginManager);
|
||||
|
||||
// allow user to extend routes
|
||||
await pluginManager.applyPlugins({
|
||||
key: 'patchRoutes',
|
||||
type: ApplyPluginsType.event,
|
||||
args: {
|
||||
routes,
|
||||
routeComponents,
|
||||
},
|
||||
});
|
||||
|
||||
const contextOpts = pluginManager.applyPlugins({
|
||||
key: 'modifyContextOpts',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: {},
|
||||
});
|
||||
|
||||
const basename = contextOpts.basename || '/';
|
||||
const historyType = contextOpts.historyType || 'browser';
|
||||
|
||||
const history = createHistory({
|
||||
type: historyType,
|
||||
basename,
|
||||
...contextOpts.historyOpts,
|
||||
});
|
||||
|
||||
return (pluginManager.applyPlugins({
|
||||
key: 'render',
|
||||
type: ApplyPluginsType.compose,
|
||||
initialValue() {
|
||||
const context = {
|
||||
useStream: true,
|
||||
routes,
|
||||
routeComponents,
|
||||
pluginManager,
|
||||
mountElementId: 'root',
|
||||
rootElement: contextOpts.rootElement || document.getElementById('root'),
|
||||
publicPath,
|
||||
runtimePublicPath,
|
||||
history,
|
||||
historyType,
|
||||
basename,
|
||||
__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {"pureApp":false,"pureHtml":false},
|
||||
callback: contextOpts.callback,
|
||||
};
|
||||
const modifiedContext = pluginManager.applyPlugins({
|
||||
key: 'modifyClientRenderOpts',
|
||||
type: ApplyPluginsType.modify,
|
||||
initialValue: context,
|
||||
});
|
||||
return renderClient(modifiedContext);
|
||||
},
|
||||
}))();
|
||||
}
|
||||
|
||||
|
||||
render();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.g_umi = {
|
||||
version: '4.6.26',
|
||||
};
|
||||
}
|
||||
63
src/app.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { message } from 'antd';
|
||||
import type { RequestConfig } from '@umijs/max';
|
||||
import { history } from '@umijs/max';
|
||||
|
||||
const codeMessage: Record<number, string> = {
|
||||
40000: '请求参数错误',
|
||||
40100: '未登录',
|
||||
40101: '无权限',
|
||||
40102: 'Token已过期',
|
||||
40103: 'Token无效',
|
||||
40300: '禁止访问',
|
||||
40400: '请求数据不存在',
|
||||
50000: '系统内部异常',
|
||||
50001: '操作失败',
|
||||
};
|
||||
|
||||
export const request: RequestConfig = {
|
||||
timeout: 10000,
|
||||
|
||||
requestInterceptors: [
|
||||
(config: any) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
],
|
||||
|
||||
responseInterceptors: [
|
||||
(response: any) => {
|
||||
const data = response.data;
|
||||
|
||||
if (data?.code === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const code = data?.code;
|
||||
const msg = data?.message || codeMessage[code] || '请求失败';
|
||||
|
||||
// 未登录 / Token过期 / Token无效 → 跳转登录页
|
||||
if (code === 40100 || code === 40102 || code === 40103) {
|
||||
localStorage.removeItem('token');
|
||||
message.error(msg);
|
||||
history.push('/login');
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
|
||||
// 禁止访问
|
||||
if (code === 40300) {
|
||||
message.error(msg);
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
|
||||
// 其他业务错误码:不自动弹提示,交给页面自行处理
|
||||
// 页面可通过 res.code !== 0 判断并展示 res.message
|
||||
return response;
|
||||
},
|
||||
],
|
||||
};
|
||||
82
src/components/JobCard/index.less
Normal file
@@ -0,0 +1,82 @@
|
||||
.jobCard {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
transition: box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.jobCardBody {
|
||||
padding: 20px 20px 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.jobCardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.jobTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.jobSalary {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #E84C4C;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jobCompany {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.addressIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jobTags {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.jobBadgeBar {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: linear-gradient(91deg, #D7F0EF 0%, #FCFBFB 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.badgeImg {
|
||||
height: 24px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badgeText {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
63
src/components/JobCard/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
const SOURCE_LOGO_MAP: Record<string, string> = {
|
||||
'校方': '/assets/logo.png',
|
||||
'24365': '/assets/zw-logo.jpg',
|
||||
'国聘': '/assets/guoping.png',
|
||||
};
|
||||
|
||||
export interface JobItem {
|
||||
id: number;
|
||||
title: string;
|
||||
salary: string;
|
||||
company: string;
|
||||
experience: string;
|
||||
companySize: string;
|
||||
industry: string;
|
||||
source: string;
|
||||
sourceIcon: string;
|
||||
positionType: string;
|
||||
workLocation: string;
|
||||
educationRequirement: string;
|
||||
sourceUrl: string;
|
||||
publishDate: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface JobCardProps {
|
||||
job: JobItem;
|
||||
}
|
||||
|
||||
const JobCard: React.FC<JobCardProps> = ({ job }) => {
|
||||
const sourceLogo = SOURCE_LOGO_MAP[job.source] || job.sourceIcon || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.jobCard}
|
||||
onClick={() => job.sourceUrl && window.open(job.sourceUrl, '_blank')}
|
||||
>
|
||||
<div className={styles.jobCardBody}>
|
||||
<div className={styles.jobCardHeader}>
|
||||
<span className={styles.jobTitle}>{job.title}</span>
|
||||
<span className={styles.jobSalary}>{job.salary}</span>
|
||||
</div>
|
||||
<div className={styles.jobCompany}>
|
||||
<img src="/icons/Address.svg" alt="地址" className={styles.addressIcon} />
|
||||
{job.company}
|
||||
</div>
|
||||
<div className={styles.jobTags}>
|
||||
{[job.experience, job.companySize, job.industry].filter(Boolean).join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.jobBadgeBar}>
|
||||
{sourceLogo && (
|
||||
<img src={sourceLogo} alt={job.source} className={styles.badgeImg} />
|
||||
)}
|
||||
{job.source && <span className={styles.badgeText}>{job.source}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobCard;
|
||||
14
src/config/login.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 登录页面配置 - 可自行修改logo和背景图
|
||||
export const loginConfig = {
|
||||
// 系统标题
|
||||
title: 'AI就业平台',
|
||||
|
||||
// Logo图片路径 - 替换为您的logo路径
|
||||
logo: '/assets/logo.png',
|
||||
|
||||
// Logo旁的文字
|
||||
logoText: '',
|
||||
|
||||
// 背景图片路径 - 替换为您的背景图路径
|
||||
backgroundImage: '/assets/loginbg.png',
|
||||
};
|
||||
72
src/global.less
Normal file
@@ -0,0 +1,72 @@
|
||||
@font-face {
|
||||
font-family: 'HuXiaoBo-NanShen';
|
||||
src: url('/fonts/胡晓波男神体.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'HuXiaoBo-ZhenShuai';
|
||||
src: url('/fonts/胡晓波真帅体.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'HuXiaoBo-SaoBao';
|
||||
src: url('/fonts/胡晓波骚包体.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
/* ===== 全局确认弹窗样式 ===== */
|
||||
.custom-confirm-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-body {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
14
src/layouts/BasicLayout.less
Normal file
@@ -0,0 +1,14 @@
|
||||
// 去掉 Sider 的默认背景色
|
||||
:global {
|
||||
.ant-layout-sider {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-layout-sider-children {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-layout-sider-trigger {
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
}
|
||||
166
src/layouts/BasicLayout.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from '@umijs/max';
|
||||
import { Layout, Menu } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
BankOutlined,
|
||||
BarChartOutlined,
|
||||
CalendarOutlined,
|
||||
TrophyOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import './BasicLayout.less';
|
||||
|
||||
const { Header, Content, Sider } = Layout;
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '/admin',
|
||||
icon: <BankOutlined />,
|
||||
label: '学校管理',
|
||||
children: [
|
||||
{
|
||||
key: '/admin/college',
|
||||
label: '学院管理',
|
||||
},
|
||||
{
|
||||
key: '/admin/staff',
|
||||
label: '教职工管理',
|
||||
},
|
||||
{
|
||||
key: '/admin/student',
|
||||
label: '学生管理',
|
||||
},
|
||||
{
|
||||
key: '/admin/position',
|
||||
label: '角色管理',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '/admin/overview',
|
||||
icon: <BarChartOutlined />,
|
||||
label: '数据总览',
|
||||
},
|
||||
{
|
||||
key: '/appointment',
|
||||
icon: <CalendarOutlined />,
|
||||
label: '预约管理',
|
||||
},
|
||||
{
|
||||
key: '/career',
|
||||
icon: <TrophyOutlined />,
|
||||
label: '就业能力提升',
|
||||
},
|
||||
{
|
||||
key: '/system',
|
||||
icon: <SettingOutlined />,
|
||||
label: '系统配置',
|
||||
},
|
||||
];
|
||||
|
||||
const BasicLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
width={240}
|
||||
theme="light"
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundImage: 'url(/admin/background.png)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 2, height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
height: 64,
|
||||
margin: '16px 16px 24px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#1890ff',
|
||||
fontSize: collapsed ? 16 : 20,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{collapsed ? 'AI' : 'AI就业平台'}
|
||||
</div>
|
||||
<Menu
|
||||
theme="light"
|
||||
selectedKeys={[location.pathname]}
|
||||
defaultOpenKeys={['/admin']}
|
||||
mode="inline"
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#52c41a',
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<UserOutlined style={{ marginRight: 8 }} />
|
||||
{!collapsed && '管理员'}
|
||||
</div>
|
||||
</div>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header
|
||||
style={{
|
||||
padding: '0 24px',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 18, fontWeight: 'bold' }}>泽林前端</span>
|
||||
</Header>
|
||||
<Content style={{ margin: '24px 16px', padding: 24, background: '#fff' }}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicLayout;
|
||||
22
src/pages/404/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Button, Result } from 'antd';
|
||||
import { useNavigate } from '@umijs/max';
|
||||
|
||||
const NotFound: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="抱歉,您访问的页面不存在。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate('/home')}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
37
src/pages/About/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Card, Typography } from 'antd';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const About: React.FC = () => {
|
||||
return (
|
||||
<Card>
|
||||
<Typography>
|
||||
<Title level={3}>关于我们</Title>
|
||||
<Paragraph>
|
||||
泽林管理系统是一个基于 Umi4 + Ant Design 构建的现代化前端项目模板。
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<strong>技术栈:</strong>
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>Umi 4 - 企业级前端框架</li>
|
||||
<li>Ant Design 5 - 企业级 UI 组件库</li>
|
||||
<li>React 18 - JavaScript 库</li>
|
||||
<li>TypeScript - 类型安全</li>
|
||||
</ul>
|
||||
<Paragraph>
|
||||
<strong>功能特性:</strong>
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>路由配置</li>
|
||||
<li>Mock 数据模拟</li>
|
||||
<li>全局反向代理</li>
|
||||
<li>布局组件</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
240
src/pages/Admin/College/index.less
Normal file
@@ -0,0 +1,240 @@
|
||||
.page {
|
||||
background: #fff;
|
||||
border-radius: 0 12px 12px 12px;
|
||||
min-height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loadingWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.scrollArea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.addCollegeBtn {
|
||||
color: #00A196 !important;
|
||||
border-color: #00A196 !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 4px 20px !important;
|
||||
height: 36px !important;
|
||||
font-size: 14px !important;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 161, 150, 0.06) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.treeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.treeNode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.treeChild {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #00A196;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.arrowPlaceholder {
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.labelCollege {
|
||||
color: #e84393;
|
||||
background: #ffeef8;
|
||||
}
|
||||
|
||||
.labelDept {
|
||||
color: #00A196;
|
||||
background: #e6f7f5;
|
||||
}
|
||||
|
||||
.labelMajor {
|
||||
color: #00A196;
|
||||
background: #e6f7f5;
|
||||
}
|
||||
|
||||
.labelClass {
|
||||
color: #8e7cc3;
|
||||
background: #f0ecfa;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
.nameInput {
|
||||
flex: 1;
|
||||
max-width: 360px;
|
||||
height: 38px;
|
||||
border-radius: 20px !important;
|
||||
border: 1px solid #e0e0e0 !important;
|
||||
padding: 0 16px !important;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: #00A196 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btnAdd {
|
||||
height: 34px;
|
||||
border-radius: 6px !important;
|
||||
background: #00A196 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 16px !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: #00887e !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDelete {
|
||||
height: 34px;
|
||||
border-radius: 6px !important;
|
||||
background: #e74c3c !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-size: 13px !important;
|
||||
padding: 0 16px !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: #c0392b !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark toast notification */
|
||||
:global {
|
||||
.darkToast {
|
||||
.ant-message-notice-content {
|
||||
background: rgba(26, 26, 26, 0.6) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 8px 20px !important;
|
||||
box-shadow: none !important;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.darkToastContent {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Error state for empty inputs */
|
||||
.nameInputError {
|
||||
border-color: #ff4d4f !important;
|
||||
background: #fff2f0 !important;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bottom save/cancel bar */
|
||||
.bottomBar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 12px 32px;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #eee;
|
||||
border-radius: 0 0 12px 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btnSave {
|
||||
height: 36px;
|
||||
border-radius: 6px !important;
|
||||
background: #00A196 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
font-size: 14px !important;
|
||||
padding: 0 28px !important;
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
background: #00887e !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btnCancel {
|
||||
height: 36px;
|
||||
border-radius: 6px !important;
|
||||
background: #f5f5f5 !important;
|
||||
color: #666 !important;
|
||||
border: 1px solid #d9d9d9 !important;
|
||||
font-size: 14px !important;
|
||||
padding: 0 28px !important;
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
color: #333 !important;
|
||||
border-color: #bbb !important;
|
||||
}
|
||||
}
|
||||
760
src/pages/Admin/College/index.tsx
Normal file
@@ -0,0 +1,760 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from '@umijs/max';
|
||||
import { Input, Button, message, Spin } from 'antd';
|
||||
import { CaretRightOutlined, CaretDownOutlined, ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import styles from './index.less';
|
||||
|
||||
interface ClassItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface MajorItem {
|
||||
id: string;
|
||||
name: string;
|
||||
expanded: boolean;
|
||||
classes: ClassItem[];
|
||||
}
|
||||
|
||||
interface DeptItem {
|
||||
id: string;
|
||||
name: string;
|
||||
expanded: boolean;
|
||||
majors: MajorItem[];
|
||||
}
|
||||
|
||||
interface CollegeItem {
|
||||
id: string;
|
||||
name: string;
|
||||
expanded: boolean;
|
||||
depts: DeptItem[];
|
||||
}
|
||||
|
||||
let idCounter = 1000;
|
||||
const genId = () => `new_${++idCounter}`;
|
||||
|
||||
const CollegeManage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<CollegeItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [errorIds, setErrorIds] = useState<Set<string>>(new Set());
|
||||
const treeRef = useRef<HTMLDivElement>(null);
|
||||
const focusIdRef = useRef<string | null>(null);
|
||||
|
||||
// Fetch data from API
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request('/api/admin/college');
|
||||
if (res.code === 0 && Array.isArray(res.data)) {
|
||||
const mapTree = (items: any[]): CollegeItem[] =>
|
||||
items.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
expanded: false,
|
||||
depts: (item.depts || []).map((dept: any) => ({
|
||||
id: dept.id,
|
||||
name: dept.name,
|
||||
expanded: false,
|
||||
majors: (dept.majors || []).map((major: any) => ({
|
||||
id: major.id,
|
||||
name: major.name,
|
||||
expanded: false,
|
||||
classes: (major.classes || []).map((cls: any) => ({
|
||||
id: cls.id,
|
||||
name: cls.name,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
setData(mapTree(res.data));
|
||||
}
|
||||
} catch {
|
||||
message.error('获取数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// Auto-focus newly added input
|
||||
useEffect(() => {
|
||||
if (focusIdRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
const el = document.querySelector(
|
||||
`[data-id="${focusIdRef.current}"] input`,
|
||||
) as HTMLInputElement;
|
||||
if (el) {
|
||||
el.focus();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
focusIdRef.current = null;
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
const updateData = (updater: (d: CollegeItem[]) => CollegeItem[]) => {
|
||||
setData((prev) => updater([...prev]));
|
||||
setErrorIds(new Set());
|
||||
};
|
||||
|
||||
// ========== Validation ==========
|
||||
const validate = (): boolean => {
|
||||
const ids = new Set<string>();
|
||||
let firstEmptyId: string | null = null;
|
||||
|
||||
const checkItems = (
|
||||
colleges: CollegeItem[],
|
||||
) => {
|
||||
for (const c of colleges) {
|
||||
if (!c.name.trim()) {
|
||||
ids.add(c.id);
|
||||
if (!firstEmptyId) firstEmptyId = c.id;
|
||||
}
|
||||
for (const dept of c.depts) {
|
||||
if (!dept.name.trim()) {
|
||||
ids.add(dept.id);
|
||||
if (!firstEmptyId) firstEmptyId = dept.id;
|
||||
}
|
||||
for (const major of dept.majors) {
|
||||
if (!major.name.trim()) {
|
||||
ids.add(major.id);
|
||||
if (!firstEmptyId) firstEmptyId = major.id;
|
||||
}
|
||||
for (const cls of major.classes) {
|
||||
if (!cls.name.trim()) {
|
||||
ids.add(cls.id);
|
||||
if (!firstEmptyId) firstEmptyId = cls.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkItems(data);
|
||||
|
||||
if (ids.size > 0) {
|
||||
setErrorIds(ids);
|
||||
message.open({
|
||||
content: (
|
||||
<span className={styles.darkToastContent}>
|
||||
<ExclamationCircleFilled style={{ marginRight: 6 }} />
|
||||
请输入名称
|
||||
</span>
|
||||
),
|
||||
className: 'darkToast',
|
||||
duration: 2,
|
||||
});
|
||||
// Expand parents of first empty and scroll to it
|
||||
if (firstEmptyId) {
|
||||
expandParentsOf(firstEmptyId);
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector(
|
||||
`[data-id="${firstEmptyId}"] input`,
|
||||
) as HTMLInputElement;
|
||||
if (el) {
|
||||
el.focus();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const expandParentsOf = (targetId: string) => {
|
||||
setData((prev) =>
|
||||
prev.map((c) => {
|
||||
let collegeNeedsExpand = false;
|
||||
const newDepts = c.depts.map((dept) => {
|
||||
let deptNeedsExpand = false;
|
||||
const newMajors = dept.majors.map((major) => {
|
||||
const clsFound = major.classes.some((cls) => cls.id === targetId);
|
||||
if (clsFound || major.id === targetId) {
|
||||
deptNeedsExpand = true;
|
||||
collegeNeedsExpand = true;
|
||||
return { ...major, expanded: true };
|
||||
}
|
||||
return major;
|
||||
});
|
||||
if (dept.id === targetId) {
|
||||
collegeNeedsExpand = true;
|
||||
}
|
||||
if (deptNeedsExpand || dept.id === targetId) {
|
||||
return { ...dept, expanded: true, majors: newMajors };
|
||||
}
|
||||
return { ...dept, majors: newMajors };
|
||||
});
|
||||
if (collegeNeedsExpand || c.id === targetId) {
|
||||
return { ...c, expanded: true, depts: newDepts };
|
||||
}
|
||||
return { ...c, depts: newDepts };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// ========== Save ==========
|
||||
const handleSave = async () => {
|
||||
if (!validate()) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = data.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
depts: c.depts.map((dept) => ({
|
||||
id: dept.id,
|
||||
name: dept.name,
|
||||
majors: dept.majors.map((major) => ({
|
||||
id: major.id,
|
||||
name: major.name,
|
||||
classes: major.classes.map((cls) => ({
|
||||
id: cls.id,
|
||||
name: cls.name,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const res = await request('/api/admin/college', {
|
||||
method: 'POST',
|
||||
data: { data: payload },
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
message.success('保存成功');
|
||||
navigate('/admin/college');
|
||||
} else {
|
||||
message.error(res.message || '保存失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
fetchData();
|
||||
message.info('已取消修改');
|
||||
};
|
||||
|
||||
// ========== College ==========
|
||||
const addCollege = () => {
|
||||
const newId = genId();
|
||||
updateData((d) => [
|
||||
...d,
|
||||
{ id: newId, name: '', expanded: false, depts: [] },
|
||||
]);
|
||||
focusIdRef.current = newId;
|
||||
// Scroll to bottom
|
||||
setTimeout(() => {
|
||||
treeRef.current?.scrollTo({
|
||||
top: treeRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const updateCollegeName = (id: string, name: string) => {
|
||||
updateData((d) => d.map((c) => (c.id === id ? { ...c, name } : c)));
|
||||
};
|
||||
|
||||
const toggleCollege = (id: string) => {
|
||||
setData((prev) =>
|
||||
prev.map((c) => (c.id === id ? { ...c, expanded: !c.expanded } : c)),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteCollege = (id: string) => {
|
||||
updateData((d) => d.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
const addDept = (collegeId: string) => {
|
||||
const newId = genId();
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
expanded: true,
|
||||
depts: [
|
||||
...c.depts,
|
||||
{ id: newId, name: '', expanded: false, majors: [] },
|
||||
],
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
focusIdRef.current = newId;
|
||||
};
|
||||
|
||||
// ========== Department ==========
|
||||
const updateDeptName = (collegeId: string, deptId: string, name: string) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId ? { ...dept, name } : dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleDept = (collegeId: string, deptId: string) => {
|
||||
setData((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId ? { ...dept, expanded: !dept.expanded } : dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteDept = (collegeId: string, deptId: string) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? { ...c, depts: c.depts.filter((dept) => dept.id !== deptId) }
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const addMajor = (collegeId: string, deptId: string) => {
|
||||
const newId = genId();
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
expanded: true,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
expanded: true,
|
||||
majors: [
|
||||
...dept.majors,
|
||||
{ id: newId, name: '', expanded: false, classes: [] },
|
||||
],
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
focusIdRef.current = newId;
|
||||
};
|
||||
|
||||
// ========== Major ==========
|
||||
const updateMajorName = (
|
||||
collegeId: string,
|
||||
deptId: string,
|
||||
majorId: string,
|
||||
name: string,
|
||||
) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
majors: dept.majors.map((m) =>
|
||||
m.id === majorId ? { ...m, name } : m,
|
||||
),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleMajor = (collegeId: string, deptId: string, majorId: string) => {
|
||||
setData((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
majors: dept.majors.map((m) =>
|
||||
m.id === majorId ? { ...m, expanded: !m.expanded } : m,
|
||||
),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteMajor = (
|
||||
collegeId: string,
|
||||
deptId: string,
|
||||
majorId: string,
|
||||
) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
majors: dept.majors.filter((m) => m.id !== majorId),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const addClass = (
|
||||
collegeId: string,
|
||||
deptId: string,
|
||||
majorId: string,
|
||||
) => {
|
||||
const newId = genId();
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
expanded: true,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
expanded: true,
|
||||
majors: dept.majors.map((m) =>
|
||||
m.id === majorId
|
||||
? {
|
||||
...m,
|
||||
expanded: true,
|
||||
classes: [
|
||||
...m.classes,
|
||||
{ id: newId, name: '' },
|
||||
],
|
||||
}
|
||||
: m,
|
||||
),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
focusIdRef.current = newId;
|
||||
};
|
||||
|
||||
// ========== Class ==========
|
||||
const updateClassName = (
|
||||
collegeId: string,
|
||||
deptId: string,
|
||||
majorId: string,
|
||||
classId: string,
|
||||
name: string,
|
||||
) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
majors: dept.majors.map((m) =>
|
||||
m.id === majorId
|
||||
? {
|
||||
...m,
|
||||
classes: m.classes.map((cls) =>
|
||||
cls.id === classId ? { ...cls, name } : cls,
|
||||
),
|
||||
}
|
||||
: m,
|
||||
),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteClass = (
|
||||
collegeId: string,
|
||||
deptId: string,
|
||||
majorId: string,
|
||||
classId: string,
|
||||
) => {
|
||||
updateData((d) =>
|
||||
d.map((c) =>
|
||||
c.id === collegeId
|
||||
? {
|
||||
...c,
|
||||
depts: c.depts.map((dept) =>
|
||||
dept.id === deptId
|
||||
? {
|
||||
...dept,
|
||||
majors: dept.majors.map((m) =>
|
||||
m.id === majorId
|
||||
? {
|
||||
...m,
|
||||
classes: m.classes.filter(
|
||||
(cls) => cls.id !== classId,
|
||||
),
|
||||
}
|
||||
: m,
|
||||
),
|
||||
}
|
||||
: dept,
|
||||
),
|
||||
}
|
||||
: c,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.loadingWrap}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.scrollArea} ref={treeRef}>
|
||||
<h2 className={styles.title}>学校管理</h2>
|
||||
|
||||
<Button className={styles.addCollegeBtn} onClick={addCollege}>
|
||||
新增学院
|
||||
</Button>
|
||||
|
||||
<div className={styles.treeContainer}>
|
||||
{data.map((college) => (
|
||||
<div key={college.id} className={styles.treeNode}>
|
||||
<div className={styles.row} data-id={college.id}>
|
||||
<span
|
||||
className={styles.arrow}
|
||||
onClick={() => toggleCollege(college.id)}
|
||||
>
|
||||
{college.expanded ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
||||
</span>
|
||||
<span className={`${styles.label} ${styles.labelCollege}`}>
|
||||
学校名称
|
||||
</span>
|
||||
<Input
|
||||
className={`${styles.nameInput} ${errorIds.has(college.id) ? styles.nameInputError : ''}`}
|
||||
value={college.name}
|
||||
placeholder="请输入"
|
||||
onChange={(e) => updateCollegeName(college.id, e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className={styles.btnAdd}
|
||||
onClick={() => addDept(college.id)}
|
||||
>
|
||||
新增系
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.btnDelete}
|
||||
onClick={() => deleteCollege(college.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{college.expanded &&
|
||||
college.depts.map((dept) => (
|
||||
<div key={dept.id} className={styles.treeChild}>
|
||||
<div className={styles.row} data-id={dept.id}>
|
||||
<span
|
||||
className={styles.arrow}
|
||||
onClick={() => toggleDept(college.id, dept.id)}
|
||||
>
|
||||
{dept.expanded ? (
|
||||
<CaretDownOutlined />
|
||||
) : (
|
||||
<CaretRightOutlined />
|
||||
)}
|
||||
</span>
|
||||
<span className={`${styles.label} ${styles.labelDept}`}>
|
||||
系名称
|
||||
</span>
|
||||
<Input
|
||||
className={`${styles.nameInput} ${errorIds.has(dept.id) ? styles.nameInputError : ''}`}
|
||||
value={dept.name}
|
||||
placeholder="请输入"
|
||||
onChange={(e) =>
|
||||
updateDeptName(college.id, dept.id, e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className={styles.btnAdd}
|
||||
onClick={() => addMajor(college.id, dept.id)}
|
||||
>
|
||||
新增专业
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.btnDelete}
|
||||
onClick={() => deleteDept(college.id, dept.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{dept.expanded &&
|
||||
dept.majors.map((major) => (
|
||||
<div key={major.id} className={styles.treeChild}>
|
||||
<div className={styles.row} data-id={major.id}>
|
||||
<span
|
||||
className={styles.arrow}
|
||||
onClick={() =>
|
||||
toggleMajor(college.id, dept.id, major.id)
|
||||
}
|
||||
>
|
||||
{major.expanded ? (
|
||||
<CaretDownOutlined />
|
||||
) : (
|
||||
<CaretRightOutlined />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.label} ${styles.labelMajor}`}
|
||||
>
|
||||
专业名称
|
||||
</span>
|
||||
<Input
|
||||
className={`${styles.nameInput} ${errorIds.has(major.id) ? styles.nameInputError : ''}`}
|
||||
value={major.name}
|
||||
placeholder="请输入"
|
||||
onChange={(e) =>
|
||||
updateMajorName(
|
||||
college.id,
|
||||
dept.id,
|
||||
major.id,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className={styles.btnAdd}
|
||||
onClick={() =>
|
||||
addClass(college.id, dept.id, major.id)
|
||||
}
|
||||
>
|
||||
新增班级
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.btnDelete}
|
||||
onClick={() =>
|
||||
deleteMajor(college.id, dept.id, major.id)
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{major.expanded &&
|
||||
major.classes.map((cls) => (
|
||||
<div key={cls.id} className={styles.treeChild}>
|
||||
<div className={styles.row} data-id={cls.id}>
|
||||
<span className={styles.arrowPlaceholder} />
|
||||
<span
|
||||
className={`${styles.label} ${styles.labelClass}`}
|
||||
>
|
||||
班级名称
|
||||
</span>
|
||||
<Input
|
||||
className={`${styles.nameInput} ${errorIds.has(cls.id) ? styles.nameInputError : ''}`}
|
||||
value={cls.name}
|
||||
placeholder="请输入"
|
||||
onChange={(e) =>
|
||||
updateClassName(
|
||||
college.id,
|
||||
dept.id,
|
||||
major.id,
|
||||
cls.id,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className={styles.btnDelete}
|
||||
onClick={() =>
|
||||
deleteClass(
|
||||
college.id,
|
||||
dept.id,
|
||||
major.id,
|
||||
cls.id,
|
||||
)
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save / Cancel bar */}
|
||||
<div className={styles.bottomBar}>
|
||||
<Button
|
||||
className={styles.btnSave}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button className={styles.btnCancel} onClick={handleCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollegeManage;
|
||||
37
src/pages/Admin/Placeholder.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { useLocation } from '@umijs/max';
|
||||
|
||||
const pageTitleMap: Record<string, string> = {
|
||||
'/admin/college': '学院管理',
|
||||
'/admin/staff': '教职工管理',
|
||||
'/admin/student': '学生管理',
|
||||
'/admin/role': '角色管理',
|
||||
'/admin/overview': '数据总览',
|
||||
'/admin/appointment-list': '预约列表',
|
||||
'/admin/appointment-users': '预约人员列表',
|
||||
'/admin/task-list': '任务管理列表',
|
||||
'/admin/banner': '首页轮播图',
|
||||
'/admin/security': '安全配置页',
|
||||
'/admin/user-manage': '用户管理',
|
||||
'/admin/menu-manage': '菜单管理',
|
||||
'/admin/operation-log': '操作日志',
|
||||
};
|
||||
|
||||
const Placeholder: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const title = pageTitleMap[location.pathname] || '管理页面';
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderRadius: '0 12px 12px 12px',
|
||||
padding: 32,
|
||||
minHeight: 'calc(100vh - 48px)',
|
||||
}}>
|
||||
<h2 style={{ fontSize: 20, color: '#2A3F54', marginBottom: 16 }}>{title}</h2>
|
||||
<p style={{ color: '#999', fontSize: 14 }}>该页面正在开发中…</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Placeholder;
|
||||
131
src/pages/Admin/Role/components/AddRoleModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Tree, message } from 'antd';
|
||||
import { request } from '@umijs/max';
|
||||
import type { PermissionItem } from '../types';
|
||||
|
||||
interface AddRoleModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const AddRoleModal: React.FC<AddRoleModalProps> = ({ visible, onClose, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
|
||||
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
|
||||
|
||||
// 获取权限树
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchPermissions();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const fetchPermissions = async () => {
|
||||
try {
|
||||
const res = await request('/api/admin/role/permissions');
|
||||
if (res.code === 0) {
|
||||
setPermissions(res.data);
|
||||
}
|
||||
} catch {
|
||||
message.error('获取权限列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
const res = await request('/api/admin/role/create', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
...values,
|
||||
permissions: checkedKeys,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
message.success('创建成功');
|
||||
onSuccess();
|
||||
handleClose();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch {
|
||||
// 表单验证失败
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setCheckedKeys([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 转换权限数据为树形结构
|
||||
const convertToTreeData = (items: PermissionItem[]) => {
|
||||
return items.map(item => ({
|
||||
title: item.name,
|
||||
key: item.id,
|
||||
children: item.children ? convertToTreeData(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="新增角色"
|
||||
open={visible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleClose}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
okButtonProps={{
|
||||
style: { background: '#00A196', borderColor: '#00A196' }
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="角色名称"
|
||||
rules={[{ required: true, message: '请输入角色名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="角色描述"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入角色描述"
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="权限配置"
|
||||
>
|
||||
<Tree
|
||||
checkable
|
||||
checkedKeys={checkedKeys}
|
||||
onCheck={(keys) => setCheckedKeys(keys as string[])}
|
||||
treeData={convertToTreeData(permissions)}
|
||||
height={300}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddRoleModal;
|
||||
267
src/pages/Admin/Role/components/EditRoleModal.less
Normal file
@@ -0,0 +1,267 @@
|
||||
.editModal {
|
||||
:global {
|
||||
.ant-modal-content {
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 6px 26.25px 5px rgba(0, 0, 0, 0.05),
|
||||
0 16px 21px 2px rgba(0, 0, 0, 0.04),
|
||||
0 8px 8.75px -5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
.ant-modal-mask {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
height: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e1e7ef;
|
||||
background: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #141e35;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formSection {
|
||||
padding: 32px 100px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 14px;
|
||||
color: #141e35;
|
||||
line-height: 1.57;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.formInput {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #9eacc0;
|
||||
|
||||
&::placeholder {
|
||||
color: #9eacc0;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: #00A196;
|
||||
}
|
||||
}
|
||||
|
||||
.permissionSection {
|
||||
flex: 1;
|
||||
padding: 16px 100px 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permissionList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permissionNode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.permissionNodeChecked {
|
||||
background: #f2fdf5;
|
||||
|
||||
&:hover {
|
||||
background: #e6faf0;
|
||||
}
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.expandIconImg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.iconPlaceholder {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #9eacc0;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
border-color: #00A196;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxChecked {
|
||||
background: #00A196;
|
||||
border-color: #00A196;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 1px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: 2px solid #fff;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.permissionText {
|
||||
font-size: 14px;
|
||||
color: #141e35;
|
||||
line-height: 1.57;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid #e1e7ef;
|
||||
background: #fff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.confirmBtn {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
background: #00a196;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #00b8a9;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #008a7b;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelBtn {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #9eacc0;
|
||||
border-radius: 4px;
|
||||
color: #141e35;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #00a196;
|
||||
color: #00a196;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: #008a7b;
|
||||
color: #008a7b;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.permissionSection::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.permissionSection::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.permissionSection::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.permissionSection::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
194
src/pages/Admin/Role/components/EditRoleModal.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, message } from 'antd';
|
||||
import { request } from '@umijs/max';
|
||||
import styles from './EditRoleModal.less';
|
||||
|
||||
interface Permission {
|
||||
id: string;
|
||||
name: string;
|
||||
checked?: boolean;
|
||||
children?: Permission[];
|
||||
}
|
||||
|
||||
interface EditRoleModalProps {
|
||||
visible: boolean;
|
||||
roleKey: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditRoleModal: React.FC<EditRoleModalProps> = ({
|
||||
visible,
|
||||
roleKey,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(['user_manage', 'system_config']);
|
||||
const [checkedKeys, setCheckedKeys] = useState<string[]>(['system_config']);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchPermissions();
|
||||
fetchRoleDetail();
|
||||
}
|
||||
}, [visible, roleKey]);
|
||||
|
||||
const fetchPermissions = async () => {
|
||||
try {
|
||||
const res = await request('/api/admin/role/permissions');
|
||||
if (res.code === 0) {
|
||||
setPermissions(res.data);
|
||||
}
|
||||
} catch {
|
||||
// 使用模拟数据
|
||||
setPermissions([
|
||||
{ id: 'user_manage', name: '权限名称一二三', children: [
|
||||
{ id: 'student_manage', name: '权限名称一二三' },
|
||||
{ id: 'staff_manage', name: '权限名称一二三', children: [
|
||||
{ id: 'staff_add', name: '权限名称一二三' },
|
||||
{ id: 'staff_edit', name: '权限名称一二三' },
|
||||
]},
|
||||
]},
|
||||
{ id: 'role_manage', name: '权限名称一二三' },
|
||||
{ id: 'system_config', name: '权限名称一二三', children: [
|
||||
{ id: 'banner_manage', name: '权限名称一二三' },
|
||||
{ id: 'security_config', name: '权限名称一二三' },
|
||||
]},
|
||||
{ id: 'data_view', name: '权限名称一二三' },
|
||||
{ id: 'profile_view', name: '权限名称一二三' },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRoleDetail = async () => {
|
||||
form.setFieldsValue({
|
||||
name: roleKey === 'super_admin' ? '超级管理员' : roleKey,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpand = (id: string) => {
|
||||
setExpandedKeys((prev) =>
|
||||
prev.includes(id) ? prev.filter((k) => k !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleCheckPermission = (id: string) => {
|
||||
setCheckedKeys((prev) =>
|
||||
prev.includes(id) ? prev.filter((k) => k !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const renderPermissionNode = (node: Permission, level: number = 0) => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isExpanded = expandedKeys.includes(node.id);
|
||||
const isChecked = checkedKeys.includes(node.id);
|
||||
|
||||
const renderLevelIcons = () => {
|
||||
const icons = [];
|
||||
for (let i = 0; i < level; i++) {
|
||||
icons.push(<div key={`placeholder-${i}`} className={styles.iconPlaceholder} />);
|
||||
}
|
||||
return icons;
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div
|
||||
className={`${styles.permissionNode} ${isChecked ? styles.permissionNodeChecked : ''}`}
|
||||
>
|
||||
{renderLevelIcons()}
|
||||
{hasChildren ? (
|
||||
<div className={styles.expandIcon} onClick={() => handleExpand(node.id)}>
|
||||
<img
|
||||
src={isExpanded ? '/user_manager/收起.png' : '/user_manager/展开.png'}
|
||||
alt={isExpanded ? '收起' : '展开'}
|
||||
className={styles.expandIconImg}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.iconPlaceholder} />
|
||||
)}
|
||||
<div
|
||||
className={`${styles.checkbox} ${isChecked ? styles.checkboxChecked : ''}`}
|
||||
onClick={() => handleCheckPermission(node.id)}
|
||||
/>
|
||||
<span className={styles.permissionText}>{node.name}</span>
|
||||
</div>
|
||||
{hasChildren && isExpanded && (
|
||||
<div>
|
||||
{node.children!.map((child) => renderPermissionNode(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
message.success('编辑成功');
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch {
|
||||
message.error('请完善表单信息');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
height={800}
|
||||
className={styles.editModal}
|
||||
centered
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className={styles.modalContainer}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 className={styles.modalTitle}>编辑</h3>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.formSection}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label={
|
||||
<div className={styles.formLabel}>
|
||||
<span className={styles.required}>*</span>
|
||||
<span>教职工姓名</span>
|
||||
</div>
|
||||
}
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入' }]}
|
||||
>
|
||||
<Input placeholder="请输入" className={styles.formInput} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<div className={styles.permissionSection}>
|
||||
<div className={styles.permissionList}>
|
||||
{permissions.map((node) => renderPermissionNode(node))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<button className={styles.confirmBtn} onClick={handleSubmit} disabled={loading}>
|
||||
确定
|
||||
</button>
|
||||
<button className={styles.cancelBtn} onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditRoleModal;
|
||||
282
src/pages/Admin/Role/index.less
Normal file
@@ -0,0 +1,282 @@
|
||||
.page {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid #e1e7ef;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #141e35;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.addBtn {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
background: #00A196;
|
||||
border-color: #00A196;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.67;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #00B8A9 !important;
|
||||
border-color: #00B8A9 !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #008A7B !important;
|
||||
border-color: #008A7B !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: #00A196 !important;
|
||||
border-color: #00A196 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.addIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 0 8px 8px 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 左侧角色面板 */
|
||||
.leftPanel {
|
||||
width: 284px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 16px 0 0;
|
||||
border-right: 1px solid #e1e7ef;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.roleList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.roleItemActive {
|
||||
background: #f2fdf5;
|
||||
|
||||
&:hover {
|
||||
background: #e6faf0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9f7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.roleText {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 1.57;
|
||||
transition: color 0.2s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.editBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
color: #2463eb;
|
||||
cursor: pointer;
|
||||
line-height: 1.57;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
color: #dc2828;
|
||||
cursor: pointer;
|
||||
line-height: 1.57;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #9eacc0;
|
||||
font-size: 14px;
|
||||
line-height: 1.57;
|
||||
}
|
||||
|
||||
/* 右侧组织架构面板 */
|
||||
.rightPanel {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.organizationTree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.orgNode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.orgNodeHighlighted {
|
||||
background: #f2fdf5;
|
||||
|
||||
&:hover {
|
||||
background: #e6faf0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9f7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.expandIconImg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.expandIconPlaceholder {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orgTag {
|
||||
margin: 0;
|
||||
padding: 3px 8px;
|
||||
font-size: 10px;
|
||||
line-height: 1.6;
|
||||
border-radius: 47px;
|
||||
border: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orgNodeText {
|
||||
font-size: 14px;
|
||||
color: #141e35;
|
||||
line-height: 1.57;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orgChildren {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
290
src/pages/Admin/Role/index.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Input, Tree, Tag, message, Modal } from 'antd';
|
||||
import { SearchOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import type { TreeNode, OrganizationNode } from './types';
|
||||
import EditRoleModal from './components/EditRoleModal';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const RoleManage: React.FC = () => {
|
||||
const [selectedRole, setSelectedRole] = useState<string>('super_admin');
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(['school_1']);
|
||||
const [organizationData, setOrganizationData] = useState<OrganizationNode[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<string>('');
|
||||
|
||||
// 角色列表
|
||||
const roles: TreeNode[] = [
|
||||
{ key: 'super_admin', title: '超级管理员', color: '#141e35' },
|
||||
{ key: 'teacher', title: '教师', color: '#141e35' },
|
||||
{ key: 'student', title: '学生', color: '#141e35' },
|
||||
];
|
||||
|
||||
// 获取组织架构数据
|
||||
useEffect(() => {
|
||||
fetchOrganizationData();
|
||||
}, []);
|
||||
|
||||
const fetchOrganizationData = async () => {
|
||||
try {
|
||||
const res = await request('/api/admin/organization');
|
||||
if (res.code === 0) {
|
||||
setOrganizationData(res.data);
|
||||
}
|
||||
} catch {
|
||||
// 使用模拟数据
|
||||
setOrganizationData([
|
||||
{
|
||||
id: 'school_1',
|
||||
name: '学校管理',
|
||||
type: 'school',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'school_2',
|
||||
name: '学校管理',
|
||||
type: 'school',
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
id: 'dept_1',
|
||||
name: '编辑',
|
||||
type: 'department',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'major_1',
|
||||
name: '批量导入',
|
||||
type: 'major',
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 'class_1',
|
||||
name: '批量导入',
|
||||
type: 'class',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'major_2',
|
||||
name: '新增',
|
||||
type: 'major',
|
||||
expanded: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'school_3',
|
||||
name: '角色管理',
|
||||
type: 'school',
|
||||
expanded: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleSelect = (roleKey: string) => {
|
||||
setSelectedRole(roleKey);
|
||||
};
|
||||
|
||||
const handleEdit = (e: React.MouseEvent, roleKey: string) => {
|
||||
e.stopPropagation();
|
||||
setEditingRole(roleKey);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (e: React.MouseEvent, roleKey: string) => {
|
||||
e.stopPropagation();
|
||||
|
||||
Modal.confirm({
|
||||
title: '确定删除该角色吗?',
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okButtonProps: {
|
||||
style: { background: '#13b580', borderColor: '#13b580' },
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 模拟检查是否有用户关联
|
||||
const hasUsers = Math.random() > 0.5;
|
||||
|
||||
if (hasUsers) {
|
||||
message.error({
|
||||
content: '删除失败,已有用户关联该角色',
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
||||
style: {
|
||||
background: 'rgba(26, 26, 26, 0.6)',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
message.success('删除成功');
|
||||
// TODO: 刷新列表
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpand = (nodeId: string) => {
|
||||
setExpandedKeys((prev) =>
|
||||
prev.includes(nodeId) ? prev.filter((k) => k !== nodeId) : [...prev, nodeId]
|
||||
);
|
||||
};
|
||||
|
||||
const getTagColor = (type: string) => {
|
||||
const colors: Record<string, { bg: string; text: string }> = {
|
||||
school: { bg: '#fff0f1', text: '#e21d48' },
|
||||
department: { bg: '#fefce7', text: '#ca8511' },
|
||||
major: { bg: '#f0f9ff', text: '#0284c5' },
|
||||
class: { bg: '#fdf5ff', text: '#bf27d3' },
|
||||
};
|
||||
return colors[type] || { bg: '#f0f9ff', text: '#0284c5' };
|
||||
};
|
||||
|
||||
const getTagLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
school: '学校名称',
|
||||
department: '系名称',
|
||||
major: '专业名称',
|
||||
class: '班级名称',
|
||||
};
|
||||
return labels[type] || '标签';
|
||||
};
|
||||
|
||||
const renderOrganizationNode = (node: OrganizationNode, level: number = 0) => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isExpanded = expandedKeys.includes(node.id);
|
||||
const tagColor = getTagColor(node.type);
|
||||
const isHighlighted = node.id === 'major_1';
|
||||
|
||||
// 渲染多个层级的展开图标占位
|
||||
const renderLevelIcons = () => {
|
||||
const icons = [];
|
||||
for (let i = 0; i < level; i++) {
|
||||
icons.push(
|
||||
<div key={`placeholder-${i}`} className={styles.expandIconPlaceholder} />
|
||||
);
|
||||
}
|
||||
return icons;
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div
|
||||
className={`${styles.orgNode} ${isHighlighted ? styles.orgNodeHighlighted : ''}`}
|
||||
>
|
||||
{renderLevelIcons()}
|
||||
{hasChildren ? (
|
||||
<div className={styles.expandIcon} onClick={() => handleExpand(node.id)}>
|
||||
<img
|
||||
src={isExpanded ? '/user_manager/收起.png' : '/user_manager/展开.png'}
|
||||
alt={isExpanded ? '收起' : '展开'}
|
||||
className={styles.expandIconImg}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.expandIconPlaceholder} />
|
||||
)}
|
||||
<Tag
|
||||
className={styles.orgTag}
|
||||
style={{ background: tagColor.bg, color: tagColor.text, borderColor: tagColor.bg }}
|
||||
>
|
||||
{getTagLabel(node.type)}
|
||||
</Tag>
|
||||
<span className={styles.orgNodeText}>{node.name}</span>
|
||||
</div>
|
||||
{hasChildren && isExpanded && (
|
||||
<div className={styles.orgChildren}>
|
||||
{node.children!.map((child) => renderOrganizationNode(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
{/* 标题栏 */}
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>角色管理</h2>
|
||||
<div className={styles.headerActions}>
|
||||
<Button type="primary" className={styles.addBtn}>
|
||||
<img src="/user_manager/添加.png" alt="添加" className={styles.addIcon} />
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div className={styles.content}>
|
||||
{/* 左侧角色树 */}
|
||||
<div className={styles.leftPanel}>
|
||||
<div className={styles.roleList}>
|
||||
{roles.map((role) => (
|
||||
<div
|
||||
key={role.key}
|
||||
className={`${styles.roleItem} ${
|
||||
selectedRole === role.key ? styles.roleItemActive : ''
|
||||
}`}
|
||||
onClick={() => handleRoleSelect(role.key)}
|
||||
>
|
||||
<span
|
||||
className={styles.roleText}
|
||||
style={{
|
||||
color: selectedRole === role.key ? '#00a196' : role.color
|
||||
}}
|
||||
>
|
||||
{role.title}
|
||||
</span>
|
||||
{selectedRole === role.key && (
|
||||
<div className={styles.roleActions}>
|
||||
<button
|
||||
className={styles.editBtn}
|
||||
onClick={(e) => handleEdit(e, role.key)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<span className={styles.divider}>|</span>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
onClick={(e) => handleDelete(e, role.key)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧组织架构树 */}
|
||||
<div className={styles.rightPanel}>
|
||||
<div className={styles.organizationTree}>
|
||||
{organizationData.map((node) => renderOrganizationNode(node))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditRoleModal
|
||||
visible={editModalVisible}
|
||||
roleKey={editingRole}
|
||||
onClose={() => setEditModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
message.success('编辑成功');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleManage;
|
||||
32
src/pages/Admin/Role/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface RoleRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: string[];
|
||||
status: 'active' | 'inactive';
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface PermissionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: 'menu' | 'button' | 'api';
|
||||
parentId?: string;
|
||||
children?: PermissionItem[];
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
key: string;
|
||||
title: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface OrganizationNode {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'school' | 'department' | 'major' | 'class';
|
||||
expanded?: boolean;
|
||||
children?: OrganizationNode[];
|
||||
}
|
||||
63
src/pages/Admin/Staff/components/AddStaffModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Input, Button, message } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import type { StaffFormData } from '../types';
|
||||
import { initStaffForm } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const AddStaffModal: React.FC<Props> = ({ visible, onClose, onSuccess }) => {
|
||||
const [form, setForm] = useState<StaffFormData>(initStaffForm);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tipVisible, setTipVisible] = useState(true);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name || !form.phone) {
|
||||
message.warning('请填写完整信息');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request('/api/admin/staff/add', { method: 'POST', data: form });
|
||||
if (res.code === 0) { message.success('新增成功'); handleClose(); onSuccess(); }
|
||||
else { message.error(res.message); }
|
||||
} catch { message.error('新增失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleClose = () => { setForm(initStaffForm); setTipVisible(true); onClose(); };
|
||||
|
||||
return (
|
||||
<Modal open={visible} title="新增教职工" onCancel={handleClose} footer={null} width={600} centered className={styles.addModal} destroyOnClose>
|
||||
{tipVisible && (
|
||||
<div className={styles.tipBanner}>
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23d4622b'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Ctext x='12' y='16' text-anchor='middle' fill='white' font-size='14' font-weight='bold'%3E!%3C/text%3E%3C/svg%3E" alt="tip" className={styles.tipIcon} />
|
||||
<span className={styles.tipText}>创建成功后账号和密码均为手机号</span>
|
||||
<CloseOutlined className={styles.tipClose} onClick={() => setTipVisible(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.addModalBody}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>教职工姓名</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>手机号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.phone} onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.addModalFooter}>
|
||||
<Button className={styles.confirmBtn} onClick={handleSubmit} loading={loading}>确定</Button>
|
||||
<Button className={styles.cancelBtn} onClick={handleClose}>取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddStaffModal;
|
||||
49
src/pages/Admin/Staff/components/DetailModal.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import type { StaffRecord } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
record: StaffRecord | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DetailModal: React.FC<Props> = ({ visible, record, onClose }) => {
|
||||
return (
|
||||
<Modal open={visible} title="详情" onCancel={onClose} footer={null} width={500} centered className={styles.detailModal} destroyOnClose>
|
||||
{record && (
|
||||
<div className={styles.detailBody}>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>教职工姓名</span>
|
||||
<span className={styles.detailValue}>{record.name}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>手机号</span>
|
||||
<span className={styles.detailValue}>{record.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>学院</span>
|
||||
<span className={styles.detailValue}>{record.college || '-'}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>系</span>
|
||||
<span className={styles.detailValue}>{record.dept || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>状态</span>
|
||||
<span className={styles.detailValue}>{record.status === 'normal' ? '正常' : '禁用'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailModal;
|
||||
58
src/pages/Admin/Staff/components/EditStaffModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Input, Button, message } from 'antd';
|
||||
import { request } from '@umijs/max';
|
||||
import type { StaffRecord, StaffFormData } from '../types';
|
||||
import { initStaffForm } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
record: StaffRecord | null;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditStaffModal: React.FC<Props> = ({ visible, record, onClose, onSuccess }) => {
|
||||
const [form, setForm] = useState<StaffFormData>(initStaffForm);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (record) {
|
||||
setForm({ name: record.name, phone: record.phone, college: record.college, dept: record.dept, className: record.className });
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name || !form.phone) { message.warning('请填写完整信息'); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request('/api/admin/staff/edit', { method: 'POST', data: { id: record?.id, ...form } });
|
||||
if (res.code === 0) { message.success('编辑成功'); onClose(); onSuccess(); }
|
||||
else { message.error(res.message); }
|
||||
} catch { message.error('编辑失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={visible} title="编辑" onCancel={onClose} footer={null} width={600} centered className={styles.addModal} destroyOnClose>
|
||||
{record && (
|
||||
<div className={styles.addModalBody} style={{ marginTop: 16 }}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>教职工姓名</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} allowClear />
|
||||
</div>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>手机号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.phone} onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))} allowClear />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.addModalFooter}>
|
||||
<Button className={styles.confirmBtn} onClick={handleSubmit} loading={loading}>确定</Button>
|
||||
<Button className={styles.cancelBtn} onClick={onClose}>取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditStaffModal;
|
||||
338
src/pages/Admin/Staff/index.less
Normal file
@@ -0,0 +1,338 @@
|
||||
.page {
|
||||
background: #fff;
|
||||
border-radius: 0 12px 12px 12px;
|
||||
min-height: calc(100vh - 60px);
|
||||
|
||||
:global {
|
||||
.ant-input-affix-wrapper:hover,
|
||||
.ant-input:hover { border-color: #00A196; }
|
||||
.ant-input-affix-wrapper-focused,
|
||||
.ant-input:focus,
|
||||
.ant-input-affix-wrapper:focus { border-color: #00A196 !important; box-shadow: 0 0 0 2px rgba(0, 161, 150, 0.1) !important; }
|
||||
.ant-input { caret-color: #00A196; }
|
||||
.ant-select:hover .ant-select-selector { border-color: #00A196 !important; }
|
||||
.ant-select-focused .ant-select-selector { border-color: #00A196 !important; box-shadow: 0 0 0 2px rgba(0, 161, 150, 0.1) !important; }
|
||||
|
||||
.ant-pagination {
|
||||
.ant-pagination-item-active { background: #00A196 !important; border-color: #00A196 !important; a { color: #fff !important; } }
|
||||
.ant-pagination-item:hover:not(.ant-pagination-item-active) { background: #F2F7FF !important; border-color: #00A196; a { color: #00A196; } }
|
||||
.ant-pagination-prev:hover .ant-pagination-item-link,
|
||||
.ant-pagination-next:hover .ant-pagination-item-link { background: #F2F7FF !important; color: #00A196; border-color: #00A196; }
|
||||
.ant-pagination-options .ant-select-selector:hover { border-color: #00A196 !important; }
|
||||
.ant-pagination-options .ant-select-focused .ant-select-selector { border-color: #00A196 !important; box-shadow: 0 0 0 2px rgba(0, 161, 150, 0.1) !important; }
|
||||
.ant-pagination-options-quick-jumper input:hover { border-color: #00A196; }
|
||||
.ant-pagination-options-quick-jumper input:focus { border-color: #00A196; box-shadow: 0 0 0 2px rgba(0, 161, 150, 0.1); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 布局:左侧树 + 右侧内容 ===== */
|
||||
.staffLayout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.sideTree {
|
||||
width: 180px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #eef2f6;
|
||||
padding: 20px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sideTreeTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.sideTreeContent {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.treeItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
transition: all 0.15s;
|
||||
padding-right: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover { color: #00A196; background: rgba(0, 161, 150, 0.03); }
|
||||
}
|
||||
|
||||
.treeItemActive {
|
||||
color: #00A196 !important;
|
||||
background: rgba(0, 161, 150, 0.06) !important;
|
||||
}
|
||||
|
||||
.treeItemParentActive {
|
||||
color: #00A196 !important;
|
||||
}
|
||||
|
||||
.treeArrow {
|
||||
width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.treeArrowPlaceholder {
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.treeLabel {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ===== 右侧主区域 ===== */
|
||||
.mainArea {
|
||||
flex: 1;
|
||||
padding: 24px 32px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== 筛选区域 ===== */
|
||||
.filterRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filterItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
width: 180px;
|
||||
height: 32px;
|
||||
caret-color: #00A196;
|
||||
&:hover { border-color: #00A196 !important; }
|
||||
}
|
||||
|
||||
.filterBtns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.searchBtn {
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
background: #00A196 !important;
|
||||
border-color: #00A196 !important;
|
||||
border-radius: 4px;
|
||||
color: #fff !important;
|
||||
&:hover, &:focus, &:active { background: #009185 !important; border-color: #009185 !important; color: #fff !important; }
|
||||
}
|
||||
|
||||
.resetBtn {
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
border-radius: 4px;
|
||||
background: #E1E7EF !important;
|
||||
border-color: #E1E7EF !important;
|
||||
color: #333 !important;
|
||||
&:hover, &:focus, &:active { background: #d1d9e3 !important; border-color: #d1d9e3 !important; color: #333 !important; }
|
||||
}
|
||||
|
||||
/* ===== 分割线 ===== */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e8e8e8;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
/* ===== 操作按钮栏 ===== */
|
||||
.actionBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.actionBarLeft { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.actionBarRight {
|
||||
display: flex; align-items: center; gap: 4px; color: #666; font-size: 13px; cursor: pointer;
|
||||
&:hover { color: #00A196; }
|
||||
}
|
||||
|
||||
.settingIcon { font-size: 14px; }
|
||||
.settingText { font-size: 13px; }
|
||||
|
||||
.primaryOutlineBtn {
|
||||
height: 32px; border-radius: 4px !important; background: #00A196 !important; color: #fff !important;
|
||||
border-color: #00A196 !important; font-size: 13px !important; padding: 0 14px !important;
|
||||
&:hover { background: #009185 !important; border-color: #009185 !important; }
|
||||
}
|
||||
|
||||
.defaultBtn {
|
||||
height: 32px; border-radius: 4px !important; color: #333 !important; background: #fff !important;
|
||||
border-color: #d9d9d9 !important; font-size: 13px !important; padding: 0 14px !important;
|
||||
&:hover { color: #00A196 !important; border-color: #00A196 !important; }
|
||||
}
|
||||
|
||||
.dangerBtn {
|
||||
height: 32px; border-radius: 4px !important; background: #e74c3c !important; color: #fff !important;
|
||||
border-color: #e74c3c !important; font-size: 13px !important; padding: 0 14px !important;
|
||||
&:hover { background: #c0392b !important; border-color: #c0392b !important; color: #fff !important; }
|
||||
}
|
||||
|
||||
.selectedCount {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ===== 表格 ===== */
|
||||
.dataTable {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
:global {
|
||||
.ant-table-wrapper { width: 100%; }
|
||||
.ant-table { border-radius: 0 !important; table-layout: fixed !important; }
|
||||
.ant-table-container { border-radius: 0 !important; border-left: 1px solid #E1E7EF; border-top: 1px solid #E1E7EF; }
|
||||
.ant-table-thead > tr > th {
|
||||
height: 46px; background: #F1F5F9 !important; border-right: 1px solid #E1E7EF !important;
|
||||
border-bottom: 1px solid #E1E7EF !important; border-radius: 0 !important; font-weight: 500; color: #333; font-size: 14px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
font-size: 14px; color: #333; border-right: 1px solid #E1E7EF; border-bottom: 1px solid #E1E7EF;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.ant-table-selection-column {
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
}
|
||||
.ant-table-tbody > tr:hover > td { background: rgba(0, 161, 150, 0.05) !important; }
|
||||
.ant-table-cell { border-radius: 0 !important; }
|
||||
.ant-pagination { margin-top: 16px; }
|
||||
|
||||
/* checkbox 主题色 */
|
||||
.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: #00A196 !important;
|
||||
border-color: #00A196 !important;
|
||||
}
|
||||
.ant-checkbox-indeterminate .ant-checkbox-inner::after {
|
||||
background-color: #00A196 !important;
|
||||
}
|
||||
.ant-checkbox-wrapper:hover .ant-checkbox-inner,
|
||||
.ant-checkbox:hover .ant-checkbox-inner {
|
||||
border-color: #00A196 !important;
|
||||
}
|
||||
.ant-checkbox-checked::after {
|
||||
border-color: #00A196 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 状态 ===== */
|
||||
.statusCell { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; }
|
||||
.statusDot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
|
||||
|
||||
/* ===== 操作链接 ===== */
|
||||
.actionCell { white-space: nowrap; }
|
||||
.actionLink { color: #00A196; font-size: 13px; cursor: pointer; &:hover { color: #009185; } }
|
||||
.actionLinkWarn { color: #fa8c16; font-size: 13px; cursor: pointer; &:hover { color: #d87a10; } }
|
||||
.actionLinkDanger { color: #e74c3c; font-size: 13px; cursor: pointer; &:hover { color: #c0392b; } }
|
||||
.actionDivider { margin: 0 4px; color: #ddd; font-size: 12px; }
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
.addModal {
|
||||
:global {
|
||||
.ant-modal-content { border-radius: 12px; padding: 0; overflow: hidden; }
|
||||
.ant-modal-header { text-align: center; border-bottom: 1px solid #f0f0f0; padding: 14px 24px; margin: 0; }
|
||||
.ant-modal-title { font-size: 16px; font-weight: 600; color: #333; }
|
||||
.ant-modal-close { color: #999; }
|
||||
.ant-modal-body { padding: 0; }
|
||||
.ant-input:hover, .ant-input-affix-wrapper:hover { border-color: #00A196; }
|
||||
.ant-input:focus, .ant-input-affix-wrapper-focused { border-color: #00A196 !important; box-shadow: 0 0 0 2px rgba(0, 161, 150, 0.1) !important; }
|
||||
.ant-input { caret-color: #00A196; }
|
||||
}
|
||||
}
|
||||
|
||||
.addModalBody { width: 400px; margin: 0 auto; padding: 32px 0 20px; }
|
||||
|
||||
.tipBanner {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: linear-gradient(135deg, #FFF3E0, #FDE8D0); padding: 10px 24px; margin: 0;
|
||||
}
|
||||
.tipIcon { width: 22px; height: 22px; flex-shrink: 0; }
|
||||
.tipText { flex: 1; font-size: 13px; color: #333; }
|
||||
.tipClose { font-size: 12px; color: #999; cursor: pointer; &:hover { color: #666; } }
|
||||
|
||||
.formField { margin-bottom: 10px; }
|
||||
.formLabel { display: block; font-size: 14px; font-weight: 500; color: #333; margin-bottom: 6px; }
|
||||
.formRequired { color: #e74c3c; font-size: 14px; margin-right: 2px; }
|
||||
.formInput { width: 100%; height: 40px; border-radius: 8px !important; border: 1px solid #E1E7EF !important; }
|
||||
|
||||
.addModalFooter {
|
||||
display: flex; justify-content: center; gap: 16px; padding: 16px 24px; border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.confirmBtn {
|
||||
min-width: 80px; height: 36px; background: #00A196 !important; border-color: #00A196 !important;
|
||||
border-radius: 4px; color: #fff !important; font-size: 14px;
|
||||
&:hover, &:focus, &:active { background: #009185 !important; border-color: #009185 !important; color: #fff !important; }
|
||||
}
|
||||
|
||||
.cancelBtn {
|
||||
min-width: 80px; height: 36px; border-radius: 4px; border-color: #d9d9d9 !important; color: #333 !important; font-size: 14px;
|
||||
&:hover, &:focus, &:active { border-color: #bbb !important; color: #333 !important; background: #fafafa !important; }
|
||||
}
|
||||
|
||||
/* ===== 详情弹窗 ===== */
|
||||
.detailModal {
|
||||
:global {
|
||||
.ant-modal-content { border-radius: 12px; padding: 0; }
|
||||
.ant-modal-header { text-align: center; border-bottom: 1px solid #f0f0f0; padding: 14px 24px; margin: 0; }
|
||||
.ant-modal-title { font-size: 16px; font-weight: 600; color: #333; }
|
||||
.ant-modal-close { color: #999; }
|
||||
.ant-modal-body { padding: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
.detailBody { padding: 24px 32px; }
|
||||
.detailRow { display: flex; margin-bottom: 20px; }
|
||||
.detailItem { flex: 1; display: flex; gap: 8px; }
|
||||
.detailLabel { font-size: 14px; color: #999; white-space: nowrap; flex-shrink: 0; }
|
||||
.detailValue { font-size: 14px; color: #333; font-weight: 500; }
|
||||
|
||||
/* ===== 列设置 ===== */
|
||||
.columnSettingPanel {
|
||||
min-width: 140px;
|
||||
:global {
|
||||
.ant-checkbox-wrapper { font-size: 14px; color: #333; }
|
||||
.ant-checkbox-checked .ant-checkbox-inner { background-color: #00A196; border-color: #00A196; }
|
||||
.ant-checkbox-indeterminate .ant-checkbox-inner::after { background-color: #00A196; }
|
||||
.ant-checkbox-wrapper:hover .ant-checkbox-inner { border-color: #00A196; }
|
||||
}
|
||||
}
|
||||
.columnSettingDivider { height: 1px; background: #f0f0f0; margin: 8px 0; }
|
||||
.columnSettingItem { padding: 4px 0; }
|
||||
366
src/pages/Admin/Staff/index.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Input, Button, Table, message, Modal, ConfigProvider, Upload, Popover, Checkbox } from 'antd';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, PlusOutlined, UploadOutlined, CaretRightOutlined, CaretDownOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import type { StaffRecord, CollegeTreeItem } from './types';
|
||||
import { statusMap } from './types';
|
||||
import AddStaffModal from './components/AddStaffModal';
|
||||
import EditStaffModal from './components/EditStaffModal';
|
||||
import DetailModal from './components/DetailModal';
|
||||
import styles from './index.less';
|
||||
|
||||
const StaffManage: React.FC = () => {
|
||||
const [filterName, setFilterName] = useState('');
|
||||
const [filterPhone, setFilterPhone] = useState('');
|
||||
// 实际用于请求的搜索参数(点查询后才更新)
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const [searchPhone, setSearchPhone] = useState('');
|
||||
|
||||
// 学院树
|
||||
const [collegeTree, setCollegeTree] = useState<CollegeTreeItem[]>([]);
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
const [selectedTreeKey, setSelectedTreeKey] = useState<string>('');
|
||||
const [selectedTreeType, setSelectedTreeType] = useState<'college' | 'dept' | 'class' | ''>('');
|
||||
|
||||
// 表格状态
|
||||
const [list, setList] = useState<StaffRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 弹窗
|
||||
const [addModalVisible, setAddModalVisible] = useState(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<StaffRecord | null>(null);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [detailRecord, setDetailRecord] = useState<StaffRecord | null>(null);
|
||||
|
||||
// 批量操作
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchMode, setBatchMode] = useState(false);
|
||||
|
||||
// 列设置
|
||||
const allColumnKeys = ['name', 'maskedPhone', 'status'];
|
||||
const [visibleColumnKeys, setVisibleColumnKeys] = useState<string[]>(allColumnKeys);
|
||||
const [columnSettingOpen, setColumnSettingOpen] = useState(false);
|
||||
|
||||
// 防抖 timer
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// 获取学院树
|
||||
useEffect(() => {
|
||||
request('/api/admin/staff/college-tree').then((res) => {
|
||||
if (res.code === 0) setCollegeTree(res.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 获取教职工列表(只依赖实际搜索参数)
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, any> = { page, pageSize };
|
||||
if (searchName) params.name = searchName;
|
||||
if (searchPhone) params.phone = searchPhone;
|
||||
if (selectedTreeKey && selectedTreeType) {
|
||||
if (selectedTreeType === 'college') params.college = selectedTreeKey;
|
||||
else if (selectedTreeType === 'dept') params.dept = selectedTreeKey;
|
||||
else if (selectedTreeType === 'class') params.className = selectedTreeKey;
|
||||
}
|
||||
const res = await request('/api/admin/staff', { params });
|
||||
if (res.code === 0) { setList(res.data.list); setTotal(res.data.total); }
|
||||
} catch { message.error('获取数据失败'); }
|
||||
finally { setLoading(false); }
|
||||
}, [page, pageSize, searchName, searchPhone, selectedTreeKey, selectedTreeType]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleSearch = () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setSearchName(filterName);
|
||||
setSearchPhone(filterPhone);
|
||||
setPage(1);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFilterName('');
|
||||
setFilterPhone('');
|
||||
setSearchName('');
|
||||
setSearchPhone('');
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
// 树操作
|
||||
const toggleExpand = (key: string) => {
|
||||
setExpandedKeys((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]);
|
||||
};
|
||||
|
||||
const handleTreeSelect = (key: string, type: 'college' | 'dept' | 'class') => {
|
||||
if (selectedTreeKey === key) { setSelectedTreeKey(''); setSelectedTreeType(''); }
|
||||
else { setSelectedTreeKey(key); setSelectedTreeType(type); }
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const confirmProps = {
|
||||
okButtonProps: { style: { background: '#00A196', borderColor: '#00A196' } },
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#e74c3c' }} />,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
width: 400,
|
||||
className: 'custom-confirm-modal',
|
||||
};
|
||||
|
||||
// 操作
|
||||
const handleToggleStatus = (record: StaffRecord) => {
|
||||
const action = record.status === 'normal' ? '禁用' : '启用';
|
||||
Modal.confirm({
|
||||
...confirmProps,
|
||||
title: '提示',
|
||||
content: `确定要${action}教职工 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await request('/api/admin/staff/toggle-status', { method: 'POST', data: { id: record.id } });
|
||||
if (res.code === 0) { message.success(`${action}成功`); fetchList(); } else { message.error(res.message); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: StaffRecord) => {
|
||||
Modal.confirm({
|
||||
...confirmProps,
|
||||
title: '提示',
|
||||
content: `确定要删除教职工 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await request('/api/admin/staff/delete', { method: 'POST', data: { id: record.id } });
|
||||
if (res.code === 0) { message.success('删除成功'); fetchList(); } else { message.error(res.message); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetPassword = (record: StaffRecord) => {
|
||||
Modal.confirm({
|
||||
...confirmProps,
|
||||
title: '提示',
|
||||
content: `确定要重置教职工 ${record.name} 的密码吗?`,
|
||||
onOk: async () => {
|
||||
const res = await request('/api/admin/staff/reset-password', { method: 'POST', data: { id: record.id } });
|
||||
if (res.code === 0) { message.success(res.message); } else { message.error(res.message); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!batchMode) { setBatchMode(true); return; }
|
||||
if (selectedRowKeys.length === 0) { message.warning('请先选择要删除的教职工'); return; }
|
||||
Modal.confirm({
|
||||
...confirmProps,
|
||||
title: '提示',
|
||||
content: '是否删除已勾选数据?',
|
||||
onOk: async () => {
|
||||
const res = await request('/api/admin/staff/batch-delete', { method: 'POST', data: { ids: selectedRowKeys } });
|
||||
if (res.code === 0) { message.success(res.message); setSelectedRowKeys([]); fetchList(); } else { message.error(res.message); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchImport = async () => {
|
||||
const res = await request('/api/admin/staff/batch-import', { method: 'POST', data: {} });
|
||||
if (res.code === 0) { message.success(res.message); fetchList(); } else { message.error(res.message); }
|
||||
};
|
||||
|
||||
const baseColumns: ColumnsType<StaffRecord> = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: '25%', ellipsis: true },
|
||||
{ title: '账号', dataIndex: 'maskedPhone', key: 'maskedPhone', width: '25%', ellipsis: true },
|
||||
{
|
||||
title: '状态', key: 'status', width: '25%', ellipsis: true,
|
||||
render: (_, record) => {
|
||||
const s = statusMap[record.status];
|
||||
return (<span className={styles.statusCell}><span className={styles.statusDot} style={{ background: s.color }} />{s.label}</span>);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const actionCol: ColumnsType<StaffRecord>[0] = {
|
||||
title: '操作', key: 'action', width: '25%',
|
||||
render: (_, record) => (
|
||||
<span className={styles.actionCell}>
|
||||
<a className={styles.actionLink} onClick={() => { setEditRecord(record); setEditModalVisible(true); }}>编辑</a>
|
||||
<span className={styles.actionDivider}>|</span>
|
||||
<a className={styles.actionLink} onClick={() => { setDetailRecord(record); setDetailVisible(true); }}>详情</a>
|
||||
<span className={styles.actionDivider}>|</span>
|
||||
<a className={styles.actionLink} onClick={() => handleResetPassword(record)}>重置密码</a>
|
||||
<span className={styles.actionDivider}>|</span>
|
||||
<a className={record.status === 'normal' ? styles.actionLinkWarn : styles.actionLink} onClick={() => handleToggleStatus(record)}>
|
||||
{record.status === 'normal' ? '禁用' : '启用'}
|
||||
</a>
|
||||
<span className={styles.actionDivider}>|</span>
|
||||
<a className={styles.actionLinkDanger} onClick={() => handleDelete(record)}>删除</a>
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
const filteredBaseColumns = baseColumns.filter((col) => {
|
||||
const key = (col as any).key || (col as any).dataIndex;
|
||||
return visibleColumnKeys.includes(key);
|
||||
});
|
||||
const columns = [...filteredBaseColumns, actionCol];
|
||||
|
||||
// 判断某个节点是否是选中节点的祖先
|
||||
const isAncestorOfSelected = useCallback((item: CollegeTreeItem): boolean => {
|
||||
if (!selectedTreeKey || !item.children) return false;
|
||||
for (const child of item.children) {
|
||||
if (child.id === selectedTreeKey) return true;
|
||||
if (child.children && isAncestorOfSelected(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}, [selectedTreeKey]);
|
||||
|
||||
// 渲染学院树
|
||||
const renderTree = (items: CollegeTreeItem[], level: 'college' | 'dept' | 'class') => {
|
||||
return items.map((item) => {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedKeys.includes(item.id);
|
||||
const isSelected = selectedTreeKey === item.id;
|
||||
const isParentActive = isAncestorOfSelected(item);
|
||||
const nextLevel = level === 'college' ? 'dept' : 'class';
|
||||
|
||||
let itemClass = styles.treeItem;
|
||||
if (isSelected) itemClass += ` ${styles.treeItemActive}`;
|
||||
else if (isParentActive) itemClass += ` ${styles.treeItemParentActive}`;
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<div
|
||||
className={itemClass}
|
||||
style={{ paddingLeft: level === 'college' ? 12 : level === 'dept' ? 28 : 44 }}
|
||||
onClick={() => handleTreeSelect(item.id, level)}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<span className={styles.treeArrow} onClick={(e) => { e.stopPropagation(); toggleExpand(item.id); }}>
|
||||
{isExpanded ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.treeArrowPlaceholder} />
|
||||
)}
|
||||
<span className={styles.treeLabel}>{item.name}</span>
|
||||
</div>
|
||||
{hasChildren && isExpanded && renderTree(item.children!, nextLevel as any)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<div className={styles.page}>
|
||||
<div className={styles.staffLayout}>
|
||||
{/* 左侧学院树 */}
|
||||
<div className={styles.sideTree}>
|
||||
<div className={styles.sideTreeTitle}>教职工管理</div>
|
||||
<div className={styles.sideTreeContent}>
|
||||
{renderTree(collegeTree, 'college')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<div className={styles.mainArea}>
|
||||
{/* 筛选区域 */}
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterItem}>
|
||||
<span className={styles.filterLabel}>教职工姓名</span>
|
||||
<Input className={styles.filterInput} placeholder="请输入" value={filterName} onChange={(e) => setFilterName(e.target.value)} onPressEnter={handleSearch} allowClear autoComplete="off" />
|
||||
</div>
|
||||
<div className={styles.filterItem}>
|
||||
<span className={styles.filterLabel}>教职工账号</span>
|
||||
<Input className={styles.filterInput} placeholder="请输入" value={filterPhone} onChange={(e) => setFilterPhone(e.target.value)} onPressEnter={handleSearch} allowClear autoComplete="off" />
|
||||
</div>
|
||||
<div className={styles.filterBtns}>
|
||||
<Button className={styles.searchBtn} onClick={handleSearch}>查询</Button>
|
||||
<Button className={styles.resetBtn} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className={styles.divider} />
|
||||
|
||||
{/* 操作按钮栏 */}
|
||||
<div className={styles.actionBar}>
|
||||
<div className={styles.actionBarLeft}>
|
||||
{!batchMode && <Button className={styles.primaryOutlineBtn} icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>新增教职工</Button>}
|
||||
{!batchMode && (
|
||||
<Upload showUploadList={false} beforeUpload={() => { handleBatchImport(); return false; }}>
|
||||
<Button className={styles.defaultBtn} icon={<UploadOutlined />}>批量导入</Button>
|
||||
</Upload>
|
||||
)}
|
||||
{batchMode && <span className={styles.selectedCount}>已选 {selectedRowKeys.length} 个</span>}
|
||||
<Button className={batchMode ? styles.dangerBtn : styles.defaultBtn} onClick={handleBatchDelete}>批量删除</Button>
|
||||
{batchMode && <Button className={styles.defaultBtn} onClick={() => { setBatchMode(false); setSelectedRowKeys([]); }}>取消</Button>}
|
||||
</div>
|
||||
<div className={styles.actionBarRight}>
|
||||
<Popover
|
||||
open={columnSettingOpen} onOpenChange={setColumnSettingOpen} trigger="click" placement="bottomRight"
|
||||
content={
|
||||
<div className={styles.columnSettingPanel}>
|
||||
<Checkbox
|
||||
indeterminate={visibleColumnKeys.length > 0 && visibleColumnKeys.length < allColumnKeys.length}
|
||||
checked={visibleColumnKeys.length === allColumnKeys.length}
|
||||
onChange={(e) => setVisibleColumnKeys(e.target.checked ? [...allColumnKeys] : [])}
|
||||
>全选</Checkbox>
|
||||
<div className={styles.columnSettingDivider} />
|
||||
{baseColumns.map((col) => {
|
||||
const key = (col as any).key || (col as any).dataIndex;
|
||||
const title = col.title as string;
|
||||
return (
|
||||
<div key={key} className={styles.columnSettingItem}>
|
||||
<Checkbox checked={visibleColumnKeys.includes(key)} onChange={(e) => {
|
||||
setVisibleColumnKeys((prev) => e.target.checked ? [...prev, key] : prev.filter((k) => k !== key));
|
||||
}}>{title}</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={styles.actionBarRight} style={{ cursor: 'pointer' }}>
|
||||
<SettingOutlined className={styles.settingIcon} />
|
||||
<span className={styles.settingText}>列表设置</span>
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className={styles.dataTable}>
|
||||
<Table<StaffRecord>
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
rowSelection={batchMode ? { selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys) } : undefined}
|
||||
pagination={{
|
||||
current: page, pageSize, total,
|
||||
showTotal: (t) => `共${t}条`,
|
||||
showSizeChanger: true, showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
|
||||
}}
|
||||
scroll={{ x: '100%' }}
|
||||
size="middle"
|
||||
tableLayout="fixed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddStaffModal visible={addModalVisible} onClose={() => setAddModalVisible(false)} onSuccess={fetchList} />
|
||||
<EditStaffModal visible={editModalVisible} record={editRecord} onClose={() => setEditModalVisible(false)} onSuccess={fetchList} />
|
||||
<DetailModal visible={detailVisible} record={detailRecord} onClose={() => setDetailVisible(false)} />
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaffManage;
|
||||
37
src/pages/Admin/Staff/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface StaffRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
maskedPhone: string;
|
||||
status: 'normal' | 'disabled';
|
||||
college?: string;
|
||||
dept?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface StaffFormData {
|
||||
name: string;
|
||||
phone: string;
|
||||
college: string | undefined;
|
||||
dept: string | undefined;
|
||||
className: string | undefined;
|
||||
}
|
||||
|
||||
export const initStaffForm: StaffFormData = {
|
||||
name: '',
|
||||
phone: '',
|
||||
college: undefined,
|
||||
dept: undefined,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export const statusMap: Record<string, { label: string; color: string }> = {
|
||||
normal: { label: '正常', color: '#00A196' },
|
||||
disabled: { label: '禁用', color: '#E1E7EF' },
|
||||
};
|
||||
|
||||
export interface CollegeTreeItem {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: CollegeTreeItem[];
|
||||
}
|
||||
142
src/pages/Admin/Student/components/AddStudentModal.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Input, Select, Button, message } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import type { CollegeData, StudentFormData } from '../types';
|
||||
import { initStudentForm } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface AddStudentModalProps {
|
||||
visible: boolean;
|
||||
collegeData: CollegeData;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const AddStudentModal: React.FC<AddStudentModalProps> = ({ visible, collegeData, onClose, onSuccess }) => {
|
||||
const [form, setForm] = useState<StudentFormData>(initStudentForm);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tipVisible, setTipVisible] = useState(true);
|
||||
|
||||
const updateForm = (key: string, val: any) => {
|
||||
setForm((prev) => ({ ...prev, [key]: val }));
|
||||
};
|
||||
|
||||
const handleCollegeChange = (val: string | undefined) => {
|
||||
setForm((prev) => ({ ...prev, college: val, dept: undefined, major: undefined }));
|
||||
};
|
||||
|
||||
const handleDeptChange = (val: string | undefined) => {
|
||||
setForm((prev) => ({ ...prev, dept: val, major: undefined }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { name, studentNo, role, college, dept, major, className, grade, phone } = form;
|
||||
if (!name || !studentNo || !role || !college || !dept || !major || !className || !grade || !phone) {
|
||||
message.warning('请填写完整信息');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request('/api/admin/student/add', {
|
||||
method: 'POST',
|
||||
data: form,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('新增成功');
|
||||
handleClose();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch {
|
||||
message.error('新增失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setForm(initStudentForm);
|
||||
setTipVisible(true);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="新增学生"
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
centered
|
||||
className={styles.addModal}
|
||||
destroyOnClose
|
||||
>
|
||||
{/* 提示横幅 - 全宽 */}
|
||||
{tipVisible && (
|
||||
<div className={styles.tipBanner}>
|
||||
<img
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23d4622b'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Ctext x='12' y='16' text-anchor='middle' fill='white' font-size='14' font-weight='bold'%3E!%3C/text%3E%3C/svg%3E"
|
||||
alt="tip"
|
||||
className={styles.tipIcon}
|
||||
/>
|
||||
<span className={styles.tipText}>创建成功后账号和密码均为学号</span>
|
||||
<CloseOutlined className={styles.tipClose} onClick={() => setTipVisible(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.addModalBody}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学生姓名</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.name} onChange={(e) => updateForm('name', e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.studentNo} onChange={(e) => updateForm('studentNo', e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>角色</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.role} onChange={(val) => updateForm('role', val)} options={collegeData.roles.map((r) => ({ label: r, value: r }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学院</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.college} onChange={handleCollegeChange} options={collegeData.colleges.map((c) => ({ label: c, value: c }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>系</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.dept} onChange={handleDeptChange} options={form.college ? (collegeData.deptMap[form.college] || []).map((d) => ({ label: d, value: d })) : []} disabled={!form.college} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>专业</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.major} onChange={(val) => updateForm('major', val)} options={form.dept ? (collegeData.majorMap[form.dept] || []).map((m) => ({ label: m, value: m })) : []} disabled={!form.dept} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>班级</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.className} onChange={(val) => updateForm('className', val)} options={collegeData.classes.map((c) => ({ label: c, value: c }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>年级</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.grade} onChange={(val) => updateForm('grade', val)} options={collegeData.grades.map((g) => ({ label: g, value: g }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}>手机号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.phone} onChange={(e) => updateForm('phone', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.addModalFooter}>
|
||||
<Button className={styles.confirmBtn} onClick={handleSubmit} loading={loading}>确定</Button>
|
||||
<Button className={styles.cancelBtn} onClick={handleClose}>取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddStudentModal;
|
||||
78
src/pages/Admin/Student/components/DetailModal.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import type { StudentRecord } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface DetailModalProps {
|
||||
visible: boolean;
|
||||
record: StudentRecord | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DetailModal: React.FC<DetailModalProps> = ({ visible, record, onClose }) => {
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="详情"
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={500}
|
||||
centered
|
||||
className={styles.detailModal}
|
||||
destroyOnClose
|
||||
>
|
||||
{record && (
|
||||
<div className={styles.detailBody}>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>学生姓名</span>
|
||||
<span className={styles.detailValue}>{record.name}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>学号</span>
|
||||
<span className={styles.detailValue}>{record.studentNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>角色</span>
|
||||
<span className={styles.detailValue}>{record.role}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>学院</span>
|
||||
<span className={styles.detailValue}>{record.college}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>系</span>
|
||||
<span className={styles.detailValue}>{record.dept}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>专业</span>
|
||||
<span className={styles.detailValue}>{record.major}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>班级</span>
|
||||
<span className={styles.detailValue}>{record.className}</span>
|
||||
</div>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>年级</span>
|
||||
<span className={styles.detailValue}>{record.grade}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailRow}>
|
||||
<div className={styles.detailItem}>
|
||||
<span className={styles.detailLabel}>手机号</span>
|
||||
<span className={styles.detailValue}>{record.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailModal;
|
||||
141
src/pages/Admin/Student/components/EditStudentModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Input, Select, Button, message } from 'antd';
|
||||
import { request } from '@umijs/max';
|
||||
import type { CollegeData, StudentRecord, StudentFormData } from '../types';
|
||||
import { initStudentForm } from '../types';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface EditStudentModalProps {
|
||||
visible: boolean;
|
||||
record: StudentRecord | null;
|
||||
collegeData: CollegeData;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditStudentModal: React.FC<EditStudentModalProps> = ({ visible, record, collegeData, onClose, onSuccess }) => {
|
||||
const [form, setForm] = useState<StudentFormData>(initStudentForm);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (record) {
|
||||
setForm({
|
||||
name: record.name,
|
||||
studentNo: record.studentNo,
|
||||
role: record.role,
|
||||
college: record.college,
|
||||
dept: record.dept,
|
||||
major: record.major,
|
||||
className: record.className,
|
||||
grade: record.grade,
|
||||
phone: record.phone,
|
||||
});
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
const updateForm = (key: string, val: any) => {
|
||||
setForm((prev) => ({ ...prev, [key]: val }));
|
||||
};
|
||||
|
||||
const handleCollegeChange = (val: string | undefined) => {
|
||||
setForm((prev) => ({ ...prev, college: val, dept: undefined, major: undefined }));
|
||||
};
|
||||
|
||||
const handleDeptChange = (val: string | undefined) => {
|
||||
setForm((prev) => ({ ...prev, dept: val, major: undefined }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { name, studentNo, role, college, dept, major, className, grade } = form;
|
||||
if (!name || !studentNo || !role || !college || !dept || !major || !className || !grade) {
|
||||
message.warning('请填写完整信息');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request('/api/admin/student/edit', {
|
||||
method: 'POST',
|
||||
data: { id: record?.id, ...form },
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('编辑成功');
|
||||
onClose();
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch {
|
||||
message.error('编辑失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="编辑"
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
centered
|
||||
className={styles.addModal}
|
||||
destroyOnClose
|
||||
>
|
||||
{record && (
|
||||
<div className={styles.addModalBody}>
|
||||
<div className={styles.formField} style={{ marginTop: 16 }}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学生姓名</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.name} onChange={(e) => updateForm('name', e.target.value)} allowClear />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.studentNo} onChange={(e) => updateForm('studentNo', e.target.value)} allowClear />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>角色</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.role} onChange={(val) => updateForm('role', val)} options={collegeData.roles.map((r) => ({ label: r, value: r }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>学院</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.college} onChange={handleCollegeChange} options={collegeData.colleges.map((c) => ({ label: c, value: c }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>系</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.dept} onChange={handleDeptChange} options={form.college ? (collegeData.deptMap[form.college] || []).map((d) => ({ label: d, value: d })) : []} disabled={!form.college} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>专业</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.major} onChange={(val) => updateForm('major', val)} options={form.dept ? (collegeData.majorMap[form.dept] || []).map((m) => ({ label: m, value: m })) : []} disabled={!form.dept} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>班级</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.className} onChange={(val) => updateForm('className', val)} options={collegeData.classes.map((c) => ({ label: c, value: c }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>年级</label>
|
||||
<Select className={styles.formSelect} placeholder="请选择" value={form.grade} onChange={(val) => updateForm('grade', val)} options={collegeData.grades.map((g) => ({ label: g, value: g }))} popupClassName={styles.filterDropdown} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}>手机号</label>
|
||||
<Input className={styles.formInput} placeholder="请输入" value={form.phone} onChange={(e) => updateForm('phone', e.target.value)} allowClear />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.addModalFooter}>
|
||||
<Button className={styles.confirmBtn} onClick={handleSubmit} loading={loading}>确定</Button>
|
||||
<Button className={styles.cancelBtn} onClick={onClose}>取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditStudentModal;
|
||||
260
src/pages/Admin/Student/components/GradeManage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Input, Button, Table, message, Modal, Popconfirm } from 'antd';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import styles from '../index.less';
|
||||
|
||||
interface GradeRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
createTime: string;
|
||||
graduated?: boolean;
|
||||
}
|
||||
|
||||
interface GradeManageProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const GradeManage: React.FC<GradeManageProps> = ({ onBack }) => {
|
||||
const [filterName, setFilterName] = useState('');
|
||||
const [list, setList] = useState<GradeRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 新增/编辑弹窗
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalLoading, setModalLoading] = useState(false);
|
||||
const [editingRecord, setEditingRecord] = useState<GradeRecord | null>(null);
|
||||
const [gradeName, setGradeName] = useState('');
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, any> = { page, pageSize };
|
||||
if (filterName) params.name = filterName;
|
||||
const res = await request('/api/admin/grade', { params });
|
||||
if (res.code === 0) {
|
||||
setList(res.data.list);
|
||||
setTotal(res.data.total);
|
||||
}
|
||||
} catch {
|
||||
message.error('获取数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, pageSize, filterName]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setPage(1);
|
||||
fetchList();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFilterName('');
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleOpenAdd = () => {
|
||||
setEditingRecord(null);
|
||||
setGradeName('');
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleOpenEdit = (record: GradeRecord) => {
|
||||
setEditingRecord(record);
|
||||
setGradeName(record.name);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalSubmit = async () => {
|
||||
if (!gradeName.trim()) {
|
||||
message.warning('请输入年级名称');
|
||||
return;
|
||||
}
|
||||
setModalLoading(true);
|
||||
try {
|
||||
const url = editingRecord ? '/api/admin/grade/edit' : '/api/admin/grade/add';
|
||||
const data = editingRecord ? { id: editingRecord.id, name: gradeName } : { name: gradeName };
|
||||
const res = await request(url, { method: 'POST', data });
|
||||
if (res.code === 0) {
|
||||
message.success(editingRecord ? '编辑成功' : '新增成功');
|
||||
setModalVisible(false);
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
setModalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGraduateConfirm = async (record: GradeRecord) => {
|
||||
const res = await request('/api/admin/grade/graduate', {
|
||||
method: 'POST',
|
||||
data: { id: record.id },
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('转毕业成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreConfirm = async (record: GradeRecord) => {
|
||||
const res = await request('/api/admin/grade/restore', {
|
||||
method: 'POST',
|
||||
data: { id: record.id },
|
||||
});
|
||||
if (res.code === 0) {
|
||||
message.success('还原成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<GradeRecord> = [
|
||||
{ title: '年级名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<span className={styles.actionCell}>
|
||||
<a className={styles.actionLink} onClick={() => handleOpenEdit(record)}>编辑</a>
|
||||
<span className={styles.actionDivider}>|</span>
|
||||
{record.graduated ? (
|
||||
<Popconfirm
|
||||
title="还原"
|
||||
description="确定要还原该年级吗?"
|
||||
onConfirm={() => handleRestoreConfirm(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
overlayStyle={{ maxWidth: 200 }}
|
||||
>
|
||||
<a className={styles.actionLink}>还原</a>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Popconfirm
|
||||
title="转毕业"
|
||||
description="转毕业后此年级下的所有学生将不再计入数据统计。"
|
||||
onConfirm={() => handleGraduateConfirm(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
icon={<ExclamationCircleOutlined style={{ color: '#faad14' }} />}
|
||||
overlayStyle={{ maxWidth: 200 }}
|
||||
>
|
||||
<a className={styles.actionLink}>转毕业</a>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h2 className={styles.title}>年级管理</h2>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterItem}>
|
||||
<span className={styles.filterLabel}>年级</span>
|
||||
<Input
|
||||
className={styles.filterInput}
|
||||
placeholder="请输入"
|
||||
value={filterName}
|
||||
onChange={(e) => setFilterName(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterBtns}>
|
||||
<Button className={styles.searchBtn} onClick={handleSearch}>查询</Button>
|
||||
<Button className={styles.resetBtn} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className={styles.divider} />
|
||||
|
||||
{/* 操作按钮栏 */}
|
||||
<div className={styles.actionBar}>
|
||||
<div className={styles.actionBarLeft}>
|
||||
<Button className={styles.primaryOutlineBtn} icon={<PlusOutlined />} onClick={handleOpenAdd}>新增年级</Button>
|
||||
</div>
|
||||
<div className={styles.actionBarRight}>
|
||||
<Button className={styles.defaultBtn} onClick={onBack} style={{ marginRight: 12 }}>返回</Button>
|
||||
<SettingOutlined className={styles.settingIcon} />
|
||||
<span className={styles.settingText}>列表设置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className={styles.dataTable}>
|
||||
<Table<GradeRecord>
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共${t}条`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 新增/编辑年级弹窗 */}
|
||||
<Modal
|
||||
open={modalVisible}
|
||||
title={editingRecord ? '编辑年级' : '新增年级'}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
width={400}
|
||||
centered
|
||||
className={styles.addModal}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className={styles.addModalBody} style={{ padding: '24px 0' }}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.formLabel}><span className={styles.formRequired}>*</span>年级名称</label>
|
||||
<Input
|
||||
className={styles.formInput}
|
||||
placeholder="请输入年级名称"
|
||||
value={gradeName}
|
||||
onChange={(e) => setGradeName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.addModalFooter}>
|
||||
<Button className={styles.confirmBtn} onClick={handleModalSubmit} loading={modalLoading}>确定</Button>
|
||||
<Button className={styles.cancelBtn} onClick={() => setModalVisible(false)}>取消</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GradeManage;
|
||||