Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
09fbd8e3a5 | ||
|
022e230419 | ||
|
12daa31ab9 | ||
|
c206fa4bf2 | ||
|
b9f600d3bc | ||
|
05bad03d69 | ||
|
d25308beee | ||
|
590642320a | ||
|
370cc361f6 | ||
|
b4647458ba | ||
|
1c2ed1e4df | ||
|
5fa072592a | ||
|
0a5286d026 | ||
|
bac04d164f | ||
|
6541c3a191 | ||
|
92a9f85171 | ||
|
c9e1894f17 | ||
|
fbc61f9eac | ||
|
b5fe6ecd4d | ||
|
fe47657213 | ||
|
5eb3675921 | ||
|
37fccc1f40 | ||
|
f5047edd91 | ||
|
e97f0318e6 | ||
|
5b027dfa23 | ||
|
6776fecfe9 | ||
|
e13ff1ae25 | ||
|
7c84e185a6 | ||
|
4d527cff77 | ||
|
5d1a6109c2 | ||
|
eb02def070 | ||
|
296aa71d51 | ||
|
b96c609eef | ||
|
8beae72101 | ||
|
269c366d6b | ||
|
afc82856f6 | ||
|
e215fbb59c | ||
|
1e92f19923 | ||
|
8d03678e71 | ||
|
ff9e212366 | ||
|
c3b3e06a07 | ||
|
8e60deb3bc | ||
|
03403d2c33 | ||
|
893ab33811 | ||
|
5e4c35f9bf | ||
|
877aec3b01 | ||
|
e8399362ea | ||
|
5dcde4f911 | ||
|
12c4b810c2 | ||
|
3ea07739d1 | ||
|
9f27aaac42 | ||
|
be6264dcd3 | ||
|
3c9b354aea | ||
|
040dcdfaed | ||
|
8c905c6552 | ||
|
8f27bb9fda | ||
|
23ff7068f7 | ||
|
82c53ed87f | ||
|
acb8b79edd | ||
|
21c016af62 | ||
|
f0f316c504 | ||
|
ee1f009966 | ||
|
179a7d5f62 |
31
.cursor/rules/01-project-overview.mdc
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu API Project Overview
|
||||
|
||||
PlayEdu is an online training solution developed by Baishu Technology. The API is built with Java + Spring Boot 3, using a modular approach.
|
||||
|
||||
## Project Structure
|
||||
- [playedu-api](mdc:playedu-api) - Java backend API project
|
||||
- [playedu-admin](mdc:playedu-admin) - Admin frontend
|
||||
- [playedu-pc](mdc:playedu-pc) - PC web interface
|
||||
- [playedu-h5](mdc:playedu-h5) - Mobile web interface
|
||||
|
||||
## API Key Modules
|
||||
- [playedu-api/PlayeduApiApplication.java](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application entry point
|
||||
- [playedu-api](mdc:playedu-api/playedu-api) - API module containing controllers and API-specific logic
|
||||
- [playedu-common](mdc:playedu-api/playedu-common) - Common utilities and shared code
|
||||
- [playedu-resource](mdc:playedu-api/playedu-resource) - Resource management module
|
||||
- [playedu-course](mdc:playedu-api/playedu-course) - Course-related functionality
|
||||
- [playedu-system](mdc:playedu-api/playedu-system) - System management functionality
|
||||
|
||||
## Backend vs Frontend Controllers
|
||||
- [Backend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend) - Admin-facing API endpoints
|
||||
- [Frontend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend) - Student-facing API endpoints
|
||||
|
||||
## Development and Deployment
|
||||
- [pom.xml](mdc:playedu-api/pom.xml) - Main Maven configuration file
|
||||
- [Dockerfile](mdc:playedu-api/Dockerfile) - Docker build configuration
|
||||
- [compose.yml](mdc:compose.yml) - Docker Compose configuration
|
32
.cursor/rules/02-api-structure.mdc
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu API Structure
|
||||
|
||||
The API module follows a standard Spring Boot structure with controllers, services, and supporting components.
|
||||
|
||||
## Controller Layout
|
||||
The API endpoints are divided into backend (admin) and frontend (student) controllers:
|
||||
|
||||
### Backend Controllers
|
||||
- [Backend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend) - Admin management interfaces
|
||||
- [AdminUserController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/AdminUserController.java) - Administrator management
|
||||
- [CourseController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/CourseController.java) - Course management
|
||||
- [DepartmentController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java) - Department management
|
||||
- [ResourceController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/ResourceController.java) - Resource management
|
||||
- [UserController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/UserController.java) - User management
|
||||
|
||||
### Frontend Controllers
|
||||
- [Frontend Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend) - Student-facing endpoints
|
||||
- [LoginController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java) - Student login
|
||||
- [CourseController](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/CourseController.java) - Course access
|
||||
|
||||
## API Application Components
|
||||
- [PlayeduApiApplication](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application entry point
|
||||
- [Request DTOs](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/request) - Data transfer objects for API requests
|
||||
- [Response Format](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/util/JsonResponse.java) - Standard JSON response format
|
||||
- [Event Handlers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/event) - Event-driven components
|
||||
- [Scheduled Tasks](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/schedule) - Scheduled/recurring tasks
|
||||
- [Interceptors](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor) - HTTP request interceptors
|
32
.cursor/rules/03-configuration.mdc
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu Configuration Guide
|
||||
|
||||
The PlayEdu application uses standard Spring Boot configuration with YAML files.
|
||||
|
||||
## Application Configuration
|
||||
- [application.yml](mdc:playedu-api/playedu-api/src/main/resources/application.yml) - Main application configuration
|
||||
- [application-dev.yml](mdc:playedu-api/playedu-api/src/main/resources/application-dev.yml) - Development environment overrides
|
||||
|
||||
## Key Configuration Properties
|
||||
- Database connection settings
|
||||
- Redis cache configuration
|
||||
- File storage configuration
|
||||
- Security settings
|
||||
- Cors configuration
|
||||
|
||||
## Build Configuration
|
||||
- [pom.xml](mdc:playedu-api/pom.xml) - Main project Maven POM file
|
||||
- [playedu-api/pom.xml](mdc:playedu-api/playedu-api/pom.xml) - API module POM file
|
||||
- [playedu-common/pom.xml](mdc:playedu-api/playedu-common/pom.xml) - Common module POM file
|
||||
- [playedu-course/pom.xml](mdc:playedu-api/playedu-course/pom.xml) - Course module POM file
|
||||
- [playedu-resource/pom.xml](mdc:playedu-api/playedu-resource/pom.xml) - Resource module POM file
|
||||
- [playedu-system/pom.xml](mdc:playedu-api/playedu-system/pom.xml) - System module POM file
|
||||
|
||||
## Docker Configuration
|
||||
- [Dockerfile](mdc:playedu-api/Dockerfile) - Docker image definition
|
||||
- [Dockerfile.local](mdc:playedu-api/Dockerfile.local) - Local development Docker configuration
|
||||
- [compose.yml](mdc:compose.yml) - Docker Compose service definitions
|
46
.cursor/rules/04-module-structure.mdc
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu Module Structure
|
||||
|
||||
PlayEdu follows a modular architecture with separate modules for different concerns:
|
||||
|
||||
## Module Organization
|
||||
Each module follows a similar structure with domain models, services, and mappers:
|
||||
|
||||
- **playedu-api**: Main API controllers and application entry point
|
||||
- [Controllers](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/controller) - API endpoints
|
||||
- [Request DTOs](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/request) - Request data objects
|
||||
- [Configuration](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/config) - Module-specific configuration
|
||||
|
||||
- **playedu-common**: Shared utilities, base classes, and common functionality
|
||||
- [Constants](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/constant) - System constants
|
||||
- [Exceptions](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/exception) - Custom exceptions
|
||||
- [Utilities](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/util) - Common utility classes
|
||||
- [Base Models](mdc:playedu-api/playedu-common/src/main/java/xyz/playedu/common/bus) - Base model classes
|
||||
|
||||
- **playedu-resource**: Resource management (files, media, etc.)
|
||||
- [Domain Models](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/domain) - Entity classes
|
||||
- [Services](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/service) - Business logic
|
||||
- [Mappers](mdc:playedu-api/playedu-resource/src/main/java/xyz/playedu/resource/mapper) - Database access layer
|
||||
|
||||
- **playedu-course**: Course management functionality
|
||||
- [Domain Models](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/domain) - Course entities
|
||||
- [Services](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/service) - Course business logic
|
||||
- [Mappers](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/mapper) - Course data access
|
||||
|
||||
- **playedu-system**: System administration functionality
|
||||
- [Domain Models](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/domain) - System entities
|
||||
- [Services](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/service) - System business logic
|
||||
- [Mappers](mdc:playedu-api/playedu-system/src/main/java/xyz/playedu/system/mapper) - System data access
|
||||
|
||||
## Domain-Driven Design
|
||||
The codebase follows a layered architecture with:
|
||||
- Controllers: Handle API requests and responses
|
||||
- Services: Implement business logic
|
||||
- Mappers: Data access layer (using MyBatis)
|
||||
- Domain models: Entity classes representing business objects
|
||||
|
||||
This modular approach allows for separation of concerns and easier maintainability.
|
45
.cursor/rules/05-development-workflow.mdc
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu Development Workflow
|
||||
|
||||
This guide outlines the workflow for developing and running the PlayEdu API.
|
||||
|
||||
## Local Development Setup
|
||||
1. Clone the repository
|
||||
2. Use Docker Compose to run the application: `docker-compose up -d`
|
||||
3. Access points:
|
||||
- API: `http://localhost:9700`
|
||||
- Admin backend: `http://localhost:9900` (default credentials: `admin@playedu.xyz / playedu`)
|
||||
- PC web interface: `http://localhost:9800`
|
||||
- H5 mobile interface: `http://localhost:9801`
|
||||
|
||||
## Main Entry Points
|
||||
- [PlayeduApiApplication.java](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java) - Main application class
|
||||
- [application.yml](mdc:playedu-api/playedu-api/src/main/resources/application.yml) - Configuration
|
||||
|
||||
## Tech Stack
|
||||
- Java with Spring Boot 3
|
||||
- MySQL database
|
||||
- Redis for caching
|
||||
- MyBatis for data access
|
||||
- Docker for containerization
|
||||
|
||||
## Development Best Practices
|
||||
- Follow existing code structure when adding new features
|
||||
- Add unit tests for new functionality
|
||||
- Maintain module separation of concerns
|
||||
- Use existing utility classes from `playedu-common`
|
||||
|
||||
## Build Process
|
||||
To build the application:
|
||||
1. Use Maven: `mvn clean package`
|
||||
2. Build Docker image: `docker build -t playedu-api .`
|
||||
3. Run in development mode: `docker-compose up -d`
|
||||
|
||||
## Version Control
|
||||
- Follow standard Git workflow with feature branches
|
||||
- Create pull requests for significant changes
|
||||
- Update CHANGELOG.md for version releases
|
33
.cursor/rules/06-security-model.mdc
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu Security Model
|
||||
|
||||
This guide outlines the security model of the PlayEdu application.
|
||||
|
||||
## Authentication
|
||||
- [BackendAuthInterceptor](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor/BackendAuthInterceptor.java) - Backend authentication interceptor
|
||||
- [FrontendAuthInterceptor](mdc:playedu-api/playedu-api/src/main/java/xyz/playedu/api/interceptor/FrontendAuthInterceptor.java) - Frontend authentication interceptor
|
||||
- JWT-based authentication for both frontend and backend users
|
||||
|
||||
## Authorization
|
||||
- Role-based access control for backend users
|
||||
- Department-based content access for frontend users
|
||||
- Course permission enforcement
|
||||
|
||||
## Security Configuration
|
||||
- CORS configuration to prevent cross-site request forgery
|
||||
- Password encryption using BCrypt
|
||||
- Input validation and sanitization
|
||||
|
||||
## Resource Security
|
||||
- Private video storage and delivery
|
||||
- URL-based token authentication for media access
|
||||
- Anti-leech protection for media files
|
||||
|
||||
## Sensitive Data Protection
|
||||
- PII (Personally Identifiable Information) protection
|
||||
- Logging sanitization for sensitive data
|
||||
- Database encryption for critical fields
|
40
.cursor/rules/07-database-structure.mdc
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# PlayEdu Database Structure
|
||||
|
||||
This guide outlines the database structure of the PlayEdu application.
|
||||
|
||||
## Database Technology
|
||||
- MySQL database for persistent storage
|
||||
- Redis for caching and session management
|
||||
- MyBatis as the ORM framework
|
||||
|
||||
## Core Tables
|
||||
- **admin_users** - Administrator user accounts
|
||||
- **admin_roles** - Administrator roles for RBAC
|
||||
- **departments** - Organizational departments
|
||||
- **users** - Student/learner accounts
|
||||
- **courses** - Course information
|
||||
- **resources** - Media and document resources
|
||||
- **course_chapters** - Course chapter organization
|
||||
- **course_hour_records** - Learning progress tracking
|
||||
|
||||
## Entity Relationships
|
||||
- Departments have many Users (many-to-many)
|
||||
- Courses have many Chapters (one-to-many)
|
||||
- Courses have many Resources (many-to-many)
|
||||
- Users have progress records for Courses (many-to-many)
|
||||
|
||||
## Database Access
|
||||
- Data access through MyBatis Mappers
|
||||
- [Example Mapper](mdc:playedu-api/playedu-course/src/main/java/xyz/playedu/course/mapper/CourseMapper.java)
|
||||
- XML query definitions in resource XML files
|
||||
- [Example XML](mdc:playedu-api/playedu-course/src/main/resources/mapper/CourseMapper.xml)
|
||||
|
||||
## Data Migration
|
||||
- Managed through SQL scripts
|
||||
- Version controlled database changes
|
||||
- Backup procedures for data safety
|
20
.dockerignore
Normal file
@ -0,0 +1,20 @@
|
||||
/playedu-pc/node_modules/
|
||||
/playedu-pc/dist/
|
||||
/playedu-pc/.env.local/
|
||||
/playedu-pc/.env.development/
|
||||
/playedu-pc/.env.production/
|
||||
/playedu-pc/build/
|
||||
|
||||
/playedu-h5/node_modules/
|
||||
/playedu-h5/dist/
|
||||
/playedu-h5/.env.local/
|
||||
/playedu-h5/.env.development/
|
||||
/playedu-h5/.env.production/
|
||||
/playedu-h5/build/
|
||||
|
||||
/playedu-admin/node_modules/
|
||||
/playedu-admin/dist/
|
||||
/playedu-admin/.env.local/
|
||||
/playedu-admin/.env.development/
|
||||
/playedu-admin/.env.production/
|
||||
/playedu-admin/build/
|
9
.env.example
Normal file
@ -0,0 +1,9 @@
|
||||
################# PlayEdu ###################
|
||||
PLAYEDU_API_PORT=9700
|
||||
PLAYEDU_PC_PORT=9800
|
||||
PLAYEDU_H5_PORT=9801
|
||||
PLAYEDU_ADMIN_PORT=9900
|
||||
PLAYEDU_JWT_KEY=playeduxyz
|
||||
|
||||
################# MySQL ###################
|
||||
MYSQL_PORT=23307
|
36
.github/workflows/build.yml
vendored
@ -1,36 +0,0 @@
|
||||
name: EstablishDockerImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- 'feat/**'
|
||||
|
||||
env:
|
||||
IMAGE_FQDN: registry.cn-hangzhou.aliyuncs.com/playedu/api
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.cn-hangzhou.aliyuncs.com
|
||||
username: ${{ secrets.ALI_REGISTRY_EMAIL }}
|
||||
password: ${{ secrets.ALI_REGISTRY_PASS }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_FQDN }}:1.4
|
40
.gitignore
vendored
@ -1,37 +1,3 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
/src/main/resources/application-dev.yml
|
||||
/playedu-api/src/main/resources/application-dev.yml
|
||||
/logs
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
2
.mvn/wrapper/maven-wrapper.properties
vendored
@ -1,2 +0,0 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
|
168
CHANGELOG.md
Normal file
@ -0,0 +1,168 @@
|
||||
## 2.0
|
||||
|
||||
- 新增:`LDAP`同步数据统计
|
||||
- 新增:`LDAP`同步的详细记录
|
||||
- 新增:`LDAP`同步的数据下载
|
||||
- 修复:学员详情接口报错的Bug
|
||||
- 修复:限流缓存无过期时间的Bug
|
||||
- 优化:存储桶由`public`改为`private`
|
||||
- 优化:资源访问URL动态生成
|
||||
- 优化:数据表结构[注意:1.x版本无法直接升级2.x版本]
|
||||
- 优化:移除`Redis`运行依赖改为使用内存缓存
|
||||
- 优化:移除本地存储方案`MinIO`的支持改为支持阿里云OSS和腾讯云COS
|
||||
|
||||
## 1.8
|
||||
|
||||
- 优化:[API]日志输出
|
||||
- 优化:[API]LDAP的部门同步逻辑
|
||||
- 优化:[API]LDAP的用户同步逻辑
|
||||
- 优化:[其它]`docker`镜像更换为阿里云加速
|
||||
|
||||
## 1.7
|
||||
|
||||
- 新增:[API]MinIO配置信息增加环境变量的读取
|
||||
- 新增:[API]学员学习权限优化
|
||||
- 新增:[后台]后台首页增加课件数量的显示
|
||||
- 新增:[后台]线上课列表增加创建人字段
|
||||
- 优化:[后台]学员部门包含子部门所有学员数量
|
||||
- 优化:[API]根据分类ID获取所有子分类的课程
|
||||
- 优化:[API]根据部门ID获取所有父级部门的课程
|
||||
- 优化:[后台]部门指派器
|
||||
- 优化:[PC]视频播放器去除右键点击
|
||||
- 优化:[PC]首页学习时长去掉秒
|
||||
- 优化:[H5]首页tab切换优化
|
||||
|
||||
## 1.6
|
||||
|
||||
- 优化:删除学员身份证号字段并移除身份证号查看权限
|
||||
- 修复:LDAP用户读取。现已支持分页读取数据
|
||||
- 修复:学员批量导入
|
||||
- 优化:[PC]线上课课程详情tab交互
|
||||
- 优化:[PC]首页学习时间不显示天数
|
||||
- 优化:[PC]删除无用的js依赖
|
||||
- 优化:[PC]安全退出登录
|
||||
- 优化:[PC]系统错误提示
|
||||
- 优化:[PC]部门切换缓存
|
||||
- 修复:[PC]pnpm build 和 dev 报错
|
||||
- 优化:[后台]视频上传
|
||||
- 优化:[后台]删除无用的依赖
|
||||
- 优化:[后台]部门页面组件交互增加loading
|
||||
- 优化:[后台]删除资源选择上传资源的按钮
|
||||
- 优化:[后台]学员添加编辑
|
||||
- 修复:[后台]pnpm build 和 dev 报错
|
||||
- 优化:[H5]删除无用依赖
|
||||
- 优化:[H5]下拉刷新
|
||||
- 修复:[H5]pnpm build 和 dev 报错
|
||||
- 修复:[H5]底部导航栏悬浮bug
|
||||
|
||||
## 1.5
|
||||
|
||||
- 新增:线上课新增创建人字段
|
||||
- 新增:视频资源增加视频名搜索
|
||||
- 新增:LDAP部门+学员自动同步(每小时同步一次)
|
||||
- 优化:前台学习总时长统计最大单位由“天”改为“小时”
|
||||
- 优化:更换 minio 底层的SDK
|
||||
- 修复:修复角色权限配置参数错误的bug
|
||||
- 修复:修复学员开始学习时间错误bug
|
||||
- 修复:线上课编辑章节名排序bug
|
||||
- 修复:修复课时拖拽排序父子集交互
|
||||
- 修复:MySQL8版本无法连接bug
|
||||
|
||||
## 1.4
|
||||
|
||||
- 新增Window AD域登录支持
|
||||
- 新增一键同步 LDAP 的部门组织架构
|
||||
- 优化docker镜像内的时区
|
||||
- 优化后台的全局配置接口
|
||||
- 新增后台资源菜单权限
|
||||
- 新增后台分类菜单权限
|
||||
- 新增文件上传权限
|
||||
|
||||
## 1.3
|
||||
|
||||
- 新增 `LDAP` 登录
|
||||
- 数据库表在运行时自动同步到数据库,以后安装/升级程序无需手动将 `sql` 文件导入到数据库
|
||||
- 线上课新增 `published_at` 字段,用于控制课程的排序
|
||||
- `API` 程序模块化
|
||||
- 移除 `minio` 上传的限流控制
|
||||
- 图片上传最大尺寸调整为 `10mb`
|
||||
- 优化跨域配置
|
||||
- 新增视频上传失败的重新上传
|
||||
|
||||
## 1.2
|
||||
|
||||
- 课程附件
|
||||
- 管理员日志
|
||||
|
||||
## 1.1
|
||||
|
||||
+ 移除图形验证码
|
||||
+ 优化dockerfile
|
||||
+ 修复前后台的账号冲突
|
||||
+ 新增API限流
|
||||
+ 新增账户限流
|
||||
|
||||
## 1.0-beta7
|
||||
|
||||
+ 新增:后台|系统配置增加 API 地址配置
|
||||
+ 新增:后台|已上传视频预览播放
|
||||
+ 新增:后台|视频批量删除
|
||||
+ 新增:后台|视频、图片的分类修改
|
||||
+ 优化:后台|系统配置部分敏感配置 * 号代替显示
|
||||
+ 优化:后台|分类删除交互优化
|
||||
+ 优化:后台|跑马灯交互优化
|
||||
+ 优化:后台|超级管理员不显示权限配置按钮
|
||||
+ 修复:后台|未分类下上传资源的资源分类关联 bug
|
||||
+ 优化:API|重构用户的 JWT 底层服务
|
||||
+ 优化:PC学员端|全屏播放播放结束的下节课时显示
|
||||
|
||||
## 1.0-beta6
|
||||
|
||||
+ 优化MinIO配置
|
||||
+ 优化图形验证码
|
||||
+ 优化学员删除的关联数据删除
|
||||
+ 优化dockerfile
|
||||
+ 新增:禁止拖拽播放配置
|
||||
|
||||
## 1.0-beta5
|
||||
|
||||
+ 学员注册增加课程分类筛选
|
||||
+ 线上课-学员列表增加学员所属部门、邮箱字段的显示
|
||||
+ 线上课-学员列表显示可以观看该线上课的所有学员
|
||||
+ 修复学员的多部门查询bug
|
||||
+ 优化学员学习进度的展示(前后台保持统一)
|
||||
+ 修复后台部门的文案显示溢出
|
||||
+ 优化视频播放页面样式和交互
|
||||
|
||||
## 1.0-beta4
|
||||
|
||||
+ 新增学员线上课详细学习进度api
|
||||
+ 新增课时详情api
|
||||
+ 后台部门学员的学习进度api增加更多过滤参数
|
||||
+ 修复图形验证码的大写校验bug
|
||||
+ 提升系统稳定性
|
||||
|
||||
## 1.0-beta3
|
||||
|
||||
+ 后台的学员列表左侧部门列表显示每个部门的学员人数
|
||||
+ 后台学员列表在选择部门的情况下可直接查看该部门下的学员学习进度
|
||||
+ 后台学员列表学员增加『学习』按钮,点击可查看该学员的最近30天每日学习时长、该学员的所有课时、线上课的学习进度
|
||||
+ 优化 Docker 编译。现在可直接编译镜像无需手动执行命令安装依赖
|
||||
+ 修复后台学员默认头像的bug
|
||||
+ 修复部门的排序和父子级变更的权限控制Bug
|
||||
+ 优化已知的null错误,提升系统稳定性
|
||||
+ 修复PC端口的部门切换无法持久化保存的bug
|
||||
+ 修复PC端口的页面切换滚动条位置bug
|
||||
+ 优化PC端口的学员退出的逻辑
|
||||
|
||||
## 1.0-beta2
|
||||
|
||||
+ 后台线上课部门选择增加学员数量的显示
|
||||
+ 优化学员的默认头像
|
||||
+ 优化线上课的默认三张封面
|
||||
+ 优化学员导入
|
||||
+ 修复已知bug
|
||||
|
||||
## 1.0-beta1
|
||||
|
||||
- 一款开源的培训系统,您可以使用它快速搭建私有化内部培训平台
|
40
Dockerfile
@ -1,25 +1,39 @@
|
||||
FROM eclipse-temurin:17 as builder
|
||||
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/node:20-alpine AS node-builder
|
||||
|
||||
COPY . /app
|
||||
COPY playedu-admin /app/admin
|
||||
COPY playedu-pc /app/pc
|
||||
COPY playedu-h5 /app/h5
|
||||
|
||||
WORKDIR /app/admin
|
||||
RUN pnpm i && VITE_APP_URL=/api/ pnpm build
|
||||
|
||||
WORKDIR /app/pc
|
||||
RUN pnpm i && VITE_APP_URL=/api/ pnpm build
|
||||
|
||||
WORKDIR /app/h5
|
||||
RUN pnpm i && VITE_APP_URL=/api/ pnpm build
|
||||
|
||||
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/eclipse-temurin:17 AS java-builder
|
||||
|
||||
COPY playedu-api /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN /app/mvnw -Dmaven.test.skip=true clean package
|
||||
|
||||
FROM eclipse-temurin:17
|
||||
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/eclipse-temurin:17 AS base
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=java-builder /app/playedu-api/target/playedu-api.jar /app/api/app.jar
|
||||
|
||||
# 使用东八区时间环境
|
||||
RUN echo "Asia/Shanghai" > /etc/timezone
|
||||
COPY --from=node-builder /app/admin/dist /app/admin
|
||||
COPY --from=node-builder /app/pc/dist /app/pc
|
||||
COPY --from=node-builder /app/h5/dist /app/h5
|
||||
|
||||
# 将指定目录下的jar包复制到docker容器的/目录下
|
||||
COPY --from=builder /app/playedu-api/target/playedu-api.jar /app/app.jar
|
||||
COPY docker/nginx/conf/nginx.conf /etc/nginx/sites-enabled/default
|
||||
|
||||
RUN chmod +x /app/app.jar
|
||||
|
||||
# 声明服务运行在8080端口
|
||||
EXPOSE 9898
|
||||
EXPOSE 9800
|
||||
EXPOSE 9801
|
||||
EXPOSE 9900
|
||||
|
||||
# 指定docker容器启动时运行jar包
|
||||
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
||||
CMD nginx; echo "Waiting for MySQL to start..."; sleep 15; java -jar /app/api/app.jar --spring.profiles.active=prod --spring.datasource.url="jdbc:mysql://${DB_HOST}:${DB_PORT:-3306}/${DB_NAME}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true" --spring.datasource.username=${DB_USER} --spring.datasource.password=${DB_PASS} --sa-token.is-concurrent=${SA_TOKEN_IS_CONCURRENT:-false} --sa-token.jwt-secret-key=${SA_TOKEN_JWT_SECRET_KEY}
|
56
README.md
@ -1,42 +1,50 @@
|
||||
<p align="center">
|
||||
<img src="https://meedu.cloud.oss.meedu.vip/playedu/%E5%A4%B4%E5%9B%BE.jpg"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://playedu.xyz">官网</a> | <a href="https://playedu.xyz/docs/docs/intro/">文档</a> | <a href="https://playedu.xyz/docs/docs/function">功能列表</a> | <a href="https://playedu.xyz/docs/docs/install/quick">快速上手</a>
|
||||
</p>
|
||||
|
||||
### 系统介绍
|
||||
<h4 align="center">
|
||||
<a href="http://www.playeduos.com">PlayEdu官网</a> |
|
||||
<a href="https://www.playeduos.com/function.html">PlayEdu商业版</a> |
|
||||
<a href="https://faq.playeduos.com/opensource-maintenance-handbook/article/t08o2iHfLR">部署文档</a> |
|
||||
<a href="https://www.playeduos.com/demo.html">开源版演示站</a>
|
||||
</h4>
|
||||
|
||||
PlayEdu 是由白书科技团队经营多年线上教育系统打造出的一款全新的企业培训方案,致力于为更多企业机构搭建私有化内部培训平台。PlayEdu 基于 Java + MySQL 开发,采用前后端分离模式,前台采用 React18 为核心框架,后台采用 SpringBoot3 为核心框架。
|
||||
PlayEdu 是由白书科技团队经营多年线上教培领域打造出的一款业内领先的线上培训解决方案。PlayEdu 基于 Java + MySQL 开发,采用前后端分离模式,前端核心框架为 React18,后端核心框架为 SpringBoot3。开源版本提供部门管理、学员管理、在线视频学习、学员进度追踪、视频私有化存储等基础培训功能。
|
||||
**针对企业级培训场景,我们精心打造了“功能更多、响应更快、并发更强”的企业版本,满足企业多样化的培训需求。企业版本支持音视频学习、文档在线预览、线上考试、学习任务等多种学习方式,并提供多重安全防护,如视频转码加密、防盗链、学习防快进、防挂机等。同时,我们集成了企业微信、钉钉、飞书等主流办公系统,帮助企业快速部署专属培训平台!**
|
||||
|
||||
### 系统演示
|
||||
## 🚀 快速上手
|
||||
|
||||
| - | 站点 | 账号 | 密码 |
|
||||
| ------------ | ------------------------------------------------------ | ------------------- | -------- |
|
||||
| 学员端口 | [https://demo.playedu.xyz](https://demo.playedu.xyz) | `1@playedu.xyz` | `123123` |
|
||||
| 后台管理端口 | [https://admin.playedu.xyz](https://admin.playedu.xyz) | `admin@playedu.xyz` | `123123` |
|
||||
拉取代码:
|
||||
|
||||
### 依赖项目
|
||||
```
|
||||
git clone --branch main https://gitee.com/playeduxyz/playedu.git playedu
|
||||
```
|
||||
|
||||
- [PC 界面程序](https://github.com/PlayEdu/frontend)
|
||||
- [后台界面程序](https://github.com/PlayEdu/backend)
|
||||
构建镜像:
|
||||
|
||||
### 官方交流群
|
||||
```
|
||||
cd playedu && docker-compose up -d
|
||||
```
|
||||
|
||||
<p><img src="https://meedu.cloud.oss.meedu.vip/playedu/PlayEduk%E5%AE%A2%E6%9C%8D-zhu.png" width="200" /></p>
|
||||
命令执行完成以后,打开您的浏览器,输入 `http://localhost:9900` 即可访问后台管理界面,默认管理员账号和密码 `admin@playedu.xyz / playedu` 。
|
||||
|
||||
### 界面预览
|
||||
- PC 端口 `http://localhost:9800`
|
||||
- H5 端口 `http://localhost:9801`
|
||||
- API 端口 `http://localhost:9700`
|
||||
|
||||
## 🔰️ 软件安全
|
||||
|
||||
安全问题应该通过邮件私下报告给 tengyongzhi@meedu.vip。 您将在 24 小时内收到回复,如果因为某些原因您没有收到回复,请通过回复原始邮件的方式跟进,以确保我们收到了您的原始邮件。
|
||||
|
||||
## 👁 界面预览
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 使用协议
|
||||
## 📃 使用须知
|
||||
|
||||
● 要求
|
||||
- 保留页脚处版权信息。
|
||||
- 保留源代码中的协议。
|
||||
- 如果修改了代码,则必须在文件中进行说明。
|
||||
|
||||
● 允许
|
||||
- 私用、商用、修改。
|
||||
- **1.版权归属**: 杭州白书科技有限公司对 PlayEdu 开源版拥有完整版权,所有使用权保留。
|
||||
- **2.代码修改**: 在遵守相关开源协议的严格前提下,允许对 PlayEdu 开源版代码进行修改。修改时,必须在代码中加入明确备注,详细记录每一处修改的具体内容。
|
||||
- **3.版权保护**: 严令禁止删除、修改或篡改源代码中的版权信息及开源说明文件,侵犯版权的行为将面临法律追究。
|
||||
- 在任何使用场景下,必须严格保留 PlayEdu 开源版页面及代码中的原有版权信息,包括不限于 “Designed By PlayEdu” 页面版权标识、官网链接以及代码中的开源说明等,一旦出现侵犯版权的行为,将承担相应法律责任。
|
||||
|
50
compose.yml
Normal file
@ -0,0 +1,50 @@
|
||||
x-logging: &default-logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "10"
|
||||
|
||||
networks:
|
||||
playedu:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
|
||||
services:
|
||||
playedu:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/playedu/light:2.0
|
||||
restart: always
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=playedu
|
||||
- DB_USER=root
|
||||
- DB_PASS=playeduxyz
|
||||
- SA_TOKEN_IS_CONCURRENT=false
|
||||
- SA_TOKEN_JWT_SECRET_KEY=${PLAYEDU_JWT_KEY:-playeduxyz}
|
||||
ports:
|
||||
- "${PLAYEDU_API_PORT:-9700}:9898"
|
||||
- "${PLAYEDU_PC_PORT:-9800}:9800"
|
||||
- "${PLAYEDU_H5_PORT:-9801}:9801"
|
||||
- "${PLAYEDU_ADMIN_PORT:-9900}:9900"
|
||||
networks:
|
||||
- playedu
|
||||
depends_on:
|
||||
- mysql
|
||||
logging: *default-logging
|
||||
|
||||
mysql:
|
||||
build: ./docker/mysql
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_DATABASE=playedu
|
||||
- MYSQL_ROOT_PASSWORD=playeduxyz
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
ports:
|
||||
- "${MYSQL_PORT:-23307}:3306"
|
||||
networks:
|
||||
- playedu
|
||||
logging: *default-logging
|
2
docker/data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
13
docker/maven/settings.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>nexus-aliyun</id>
|
||||
<mirrorOf>*</mirrorOf>
|
||||
<name>Nexus aliyun</name>
|
||||
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
</settings>
|
5
docker/mysql/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
||||
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/mysql:8.1
|
||||
|
||||
COPY my.cnf /etc/mysql/conf.d/my.cnf
|
||||
|
||||
RUN chmod 0444 /etc/mysql/conf.d/my.cnf
|
12
docker/mysql/my.cnf
Normal file
@ -0,0 +1,12 @@
|
||||
# The MySQL Client configuration file.
|
||||
#
|
||||
# For explanations see
|
||||
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
|
||||
|
||||
[mysql]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysqld]
|
||||
sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
|
||||
character-set-server=utf8mb4
|
||||
default-authentication-plugin=mysql_native_password
|
76
docker/nginx/conf/nginx.conf
Normal file
@ -0,0 +1,76 @@
|
||||
client_max_body_size 500m;
|
||||
|
||||
server {
|
||||
listen 9800;
|
||||
server_name _;
|
||||
root /app/pc;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 5;
|
||||
gzip_types text/plain application/javascript text/css application/xml text/javascript;
|
||||
gzip_vary on;
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://127.0.0.1:9898/;
|
||||
}
|
||||
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9801;
|
||||
server_name _;
|
||||
root /app/h5;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 5;
|
||||
gzip_types text/plain application/javascript text/css application/xml text/javascript;
|
||||
gzip_vary on;
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://127.0.0.1:9898/;
|
||||
}
|
||||
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9900;
|
||||
server_name _;
|
||||
root /app/admin;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 5;
|
||||
gzip_types text/plain application/javascript text/css application/xml text/javascript;
|
||||
gzip_vary on;
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://127.0.0.1:9898/;
|
||||
}
|
||||
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
1
playedu-admin/.env.example
Normal file
@ -0,0 +1 @@
|
||||
VITE_APP_URL=
|
35
playedu-admin/.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
deploy-test.sh
|
||||
deploy-prod.sh
|
||||
deploy-demo.sh
|
||||
|
||||
dist/
|
22
playedu-admin/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
<p align="center">
|
||||
<img src="https://playedu.xyz/images/index/logo-big.png?v=2023032901" width="150"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">后台界面程序 - PlayEdu开源培训系统</h1>
|
||||
<p align="center">一款开源的培训系统,您可以使用它快速搭建私有化内部培训平台</p>
|
||||
|
||||
### 常用链接
|
||||
|
||||
+ [官网](https://playedu.xyz)
|
||||
+ [快速上手](https://playedu.xyz/docs/docs/category/%E5%90%8E%E5%8F%B0%E7%95%8C%E9%9D%A2%E7%A8%8B%E5%BA%8F%E5%AE%89%E8%A3%85)
|
||||
|
||||
### 开发团队
|
||||
|
||||
杭州白书科技有限公司
|
||||
|
||||
### 使用协议
|
||||
|
||||
欢迎使用杭州白书科技有限公司提供的开源培训解决方案!请您仔细阅读以下条款。通过使用 PlayEdu ,您表示同意接受以下所有条款。
|
||||
|
||||
+ 本开源项目中所有代码基于 Apache-2.0 许可协议,您默认遵守许可协议中约定的义务。
|
||||
+ 您默认授权我们将您使用 PlayEdu 所在业务的 Logo 放置在本官网展示。
|
17
playedu-admin/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title>管理后台</title>
|
||||
<script src="/js/DPlayer.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
39
playedu-admin/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "playedu-admin-interface",
|
||||
"private": false,
|
||||
"version": "1.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.3",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"ahooks": "^3.7.6",
|
||||
"antd": "^5.3.2",
|
||||
"axios": "^1.3.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.2",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"redux": "^4.2.1",
|
||||
"sort-by": "^1.2.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"less": "^4.1.3",
|
||||
"rollup-plugin-gzip": "^3.1.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
2126
playedu-admin/pnpm-lock.yaml
generated
Normal file
BIN
playedu-admin/public/avatar/avatar.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
playedu-admin/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
playedu-admin/public/js/DPlayer.min.js
vendored
Normal file
3
playedu-admin/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
BIN
playedu-admin/public/template/学员批量导入模板.xlsx
Normal file
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
217
playedu-admin/scripts/build.js
Normal file
@ -0,0 +1,217 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const fs = require('fs-extra');
|
||||
const bfj = require('bfj');
|
||||
const webpack = require('webpack');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
'\nSearch for the ' +
|
||||
chalk.underline(chalk.yellow('keywords')) +
|
||||
' to learn more about each warning.'
|
||||
);
|
||||
console.log(
|
||||
'To ignore, add ' +
|
||||
chalk.cyan('// eslint-disable-next-line') +
|
||||
' to the line before.\n'
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrlOrPath;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
if (tscCompileOnError) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
||||
)
|
||||
);
|
||||
printBuildError(err);
|
||||
} else {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let errMessage = err.message;
|
||||
|
||||
// Add additional information for postcss errors
|
||||
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||
errMessage +=
|
||||
'\nCompileError: Begins at CSS selector ' +
|
||||
err['postcssNode'].selector;
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [errMessage],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
// Ignore sourcemap warnings in CI builds. See #8227 for more info.
|
||||
const filteredWarnings = messages.warnings.filter(
|
||||
w => !/Failed to parse source map/.test(w)
|
||||
);
|
||||
if (filteredWarnings.length) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(filteredWarnings.join('\n\n')));
|
||||
}
|
||||
}
|
||||
|
||||
const resolveArgs = {
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
};
|
||||
|
||||
if (writeStatsJson) {
|
||||
return bfj
|
||||
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||
.then(() => resolve(resolveArgs))
|
||||
.catch(error => reject(new Error(error)));
|
||||
}
|
||||
|
||||
return resolve(resolveArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml,
|
||||
});
|
||||
}
|
154
playedu-admin/scripts/start.js
Normal file
@ -0,0 +1,154 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const semver = require('semver');
|
||||
const paths = require('../config/paths');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
const getClientEnvironment = require('../config/env');
|
||||
const react = require(require.resolve('react', { paths: [paths.appPath] }));
|
||||
|
||||
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const urls = prepareUrls(
|
||||
protocol,
|
||||
HOST,
|
||||
port,
|
||||
paths.publicUrlOrPath.slice(0, -1)
|
||||
);
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(
|
||||
proxySetting,
|
||||
paths.appPublic,
|
||||
paths.publicUrlOrPath
|
||||
);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = {
|
||||
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
|
||||
host: HOST,
|
||||
port,
|
||||
};
|
||||
const devServer = new WebpackDevServer(serverConfig, compiler);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.startCallback(() => {
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(function (sig) {
|
||||
process.on(sig, function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.CI !== 'true') {
|
||||
// Gracefully exit when stdin ends
|
||||
process.stdin.on('end', function () {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
52
playedu-admin/scripts/test.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PUBLIC_URL = '';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const jest = require('jest');
|
||||
const execSync = require('child_process').execSync;
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
function isInGitRepository() {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInMercurialRepository() {
|
||||
try {
|
||||
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch unless on CI or explicitly running all tests
|
||||
if (
|
||||
!process.env.CI &&
|
||||
argv.indexOf('--watchAll') === -1 &&
|
||||
argv.indexOf('--watchAll=false') === -1
|
||||
) {
|
||||
// https://github.com/facebook/create-react-app/issues/5210
|
||||
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
3
playedu-admin/src/App.module.less
Normal file
@ -0,0 +1,3 @@
|
||||
.App {
|
||||
background-color: #f6f6f6;
|
||||
}
|
19
playedu-admin/src/App.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Suspense } from "react";
|
||||
import styles from "./App.module.less";
|
||||
import { useRoutes } from "react-router-dom";
|
||||
import routes from "./routes";
|
||||
import LoadingPage from "./pages/loading";
|
||||
|
||||
function App() {
|
||||
const Views = () => useRoutes(routes);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<div className={styles.App}>
|
||||
<Views />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
13
playedu-admin/src/AutoTop.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const AutoScorllTop: React.FC<{ children: any }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.scrollTo(0, 0);
|
||||
}, [location.pathname]);
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AutoScorllTop;
|
25
playedu-admin/src/api/admin-log.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function adminLogList(
|
||||
page: number,
|
||||
size: number,
|
||||
admin_name: string,
|
||||
title: string,
|
||||
opt: string,
|
||||
start_time: string,
|
||||
end_time: string
|
||||
) {
|
||||
return client.get("/backend/v1/admin/log/index", {
|
||||
page: page,
|
||||
size: size,
|
||||
admin_name: admin_name,
|
||||
title: title,
|
||||
opt: opt,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
});
|
||||
}
|
||||
|
||||
export function adminLogDetail(id: number) {
|
||||
return client.get(`/backend/v1/admin/log/detail/${id}`, {});
|
||||
}
|
35
playedu-admin/src/api/admin-role.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function adminRoleList() {
|
||||
return client.get("/backend/v1/admin-role/index", {});
|
||||
}
|
||||
|
||||
export function createAdminRole() {
|
||||
return client.get("/backend/v1/admin-role/create", {});
|
||||
}
|
||||
|
||||
export function storeAdminRole(name: string, permissionIds: number[]) {
|
||||
return client.post("/backend/v1/admin-role/create", {
|
||||
name: name,
|
||||
permission_ids: permissionIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function adminRole(id: number) {
|
||||
return client.get(`/backend/v1/admin-role/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateAdminRole(
|
||||
id: number,
|
||||
name: string,
|
||||
permissionIds: number[]
|
||||
) {
|
||||
return client.put(`/backend/v1/admin-role/${id}`, {
|
||||
name: name,
|
||||
permission_ids: permissionIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyAdminRole(id: number) {
|
||||
return client.destroy(`/backend/v1/admin-role/${id}`);
|
||||
}
|
60
playedu-admin/src/api/admin-user.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function adminUserList(
|
||||
page: number,
|
||||
size: number,
|
||||
name: string,
|
||||
roleId: number
|
||||
) {
|
||||
return client.get("/backend/v1/admin-user/index", {
|
||||
page: page,
|
||||
size: size,
|
||||
name: name,
|
||||
role_id: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
export function createAdminUser() {
|
||||
return client.get("/backend/v1/admin-user/create", {});
|
||||
}
|
||||
|
||||
export function storeAdminUser(
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
isBanLogin: number,
|
||||
roleIds: number[]
|
||||
) {
|
||||
return client.post("/backend/v1/admin-user/create", {
|
||||
name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
is_ban_login: isBanLogin,
|
||||
role_ids: roleIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function AdminUser(id: number) {
|
||||
return client.get(`/backend/v1/admin-user/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateAdminUser(
|
||||
id: number,
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
isBanLogin: number,
|
||||
roleIds: number[]
|
||||
) {
|
||||
return client.put(`/backend/v1/admin-user/${id}`, {
|
||||
name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
is_ban_login: isBanLogin,
|
||||
role_ids: roleIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyAdminUser(id: number) {
|
||||
return client.destroy(`/backend/v1/admin-user/${id}`);
|
||||
}
|
9
playedu-admin/src/api/app-config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function appConfig() {
|
||||
return client.get("/backend/v1/app-config", {});
|
||||
}
|
||||
|
||||
export function saveAppConfig(data: any) {
|
||||
return client.put(`/backend/v1/app-config`, { data: data });
|
||||
}
|
20
playedu-admin/src/api/course-attachment.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function storeCourseAttachmentMulti(
|
||||
courseId: number,
|
||||
attachments: number[]
|
||||
) {
|
||||
return client.post(`/backend/v1/course/${courseId}/attachment/create-batch`, {
|
||||
attachments: attachments,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyAttachment(courseId: number, id: number) {
|
||||
return client.destroy(`/backend/v1/course/${courseId}/attachment/${id}`);
|
||||
}
|
||||
|
||||
export function transCourseAttachment(courseId: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/course/${courseId}/attachment/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
42
playedu-admin/src/api/course-category.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function courseCategoryList() {
|
||||
return client.get("/backend/v1/course-category/index", {});
|
||||
}
|
||||
|
||||
export function createCourseCategory() {
|
||||
return client.get("/backend/v1/course-category/create", {});
|
||||
}
|
||||
|
||||
export function storeCourseCategory(
|
||||
name: string,
|
||||
parentId: number,
|
||||
sort: number
|
||||
) {
|
||||
return client.post("/backend/v1/course-category/create", {
|
||||
name: name,
|
||||
parent_id: parentId,
|
||||
sort: sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function courseCategory(id: number) {
|
||||
return client.get(`/backend/v1/course-category/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateCourseCategory(
|
||||
id: number,
|
||||
name: string,
|
||||
parentId: number,
|
||||
sort: number
|
||||
) {
|
||||
return client.post(`/backend/v1/course-category/${id}`, {
|
||||
name: name,
|
||||
parent_id: parentId,
|
||||
sort: sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyCourseCategory(id: number) {
|
||||
return client.destroy(`/backend/v1/course-category/${id}`);
|
||||
}
|
46
playedu-admin/src/api/course-chapter.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function courseChapterList(courseId: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/chapter/index`, {});
|
||||
}
|
||||
|
||||
export function createCourseChapter(courseId: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/chapter/create`, {});
|
||||
}
|
||||
|
||||
export function storeCourseChapter(
|
||||
courseId: number,
|
||||
name: string,
|
||||
sort: number
|
||||
) {
|
||||
return client.post(`/backend/v1/course/${courseId}/chapter/create`, {
|
||||
name: name,
|
||||
sort: sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function courseChapter(courseId: number, id: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/course-chapter/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateCourseChapter(
|
||||
courseId: number,
|
||||
id: number,
|
||||
name: string,
|
||||
sort: number
|
||||
) {
|
||||
return client.put(`/backend/v1/course/${courseId}/chapter/${id}`, {
|
||||
name: name,
|
||||
sort: sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyCourseChapter(courseId: number, id: number) {
|
||||
return client.destroy(`/backend/v1/course/${courseId}/chapter/${id}`);
|
||||
}
|
||||
|
||||
export function transCourseChapter(courseId: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/course/${courseId}/chapter/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
66
playedu-admin/src/api/course-hour.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function courseHourList(courseId: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/hour/index`, {});
|
||||
}
|
||||
|
||||
export function createCourseHour(courseId: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/hour/create`, {});
|
||||
}
|
||||
|
||||
export function storeCourseHour(
|
||||
courseId: number,
|
||||
chapterId: number,
|
||||
title: string,
|
||||
type: string,
|
||||
druation: number,
|
||||
rid: number
|
||||
) {
|
||||
return client.post(`/backend/v1/course/${courseId}/hour/create`, {
|
||||
chapter_id: chapterId,
|
||||
title,
|
||||
type,
|
||||
druation,
|
||||
sort: 0,
|
||||
rid,
|
||||
});
|
||||
}
|
||||
|
||||
export function storeCourseHourMulti(courseId: number, hours: number[]) {
|
||||
return client.post(`/backend/v1/course/${courseId}/hour/create-batch`, {
|
||||
hours: hours,
|
||||
});
|
||||
}
|
||||
|
||||
export function courseHour(courseId: number, id: number) {
|
||||
return client.get(`/backend/v1/course/${courseId}/hour/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateCourseHour(
|
||||
courseId: number,
|
||||
id: number,
|
||||
chapterId: number,
|
||||
title: string,
|
||||
type: string,
|
||||
druation: number,
|
||||
rid: number
|
||||
) {
|
||||
return client.put(`/backend/v1/course/${courseId}/hour/${id}`, {
|
||||
chapter_id: chapterId,
|
||||
title,
|
||||
type,
|
||||
druation,
|
||||
sort: 0,
|
||||
rid,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyCourseHour(courseId: number, id: number) {
|
||||
return client.destroy(`/backend/v1/course/${courseId}/hour/${id}`);
|
||||
}
|
||||
|
||||
export function transCourseHour(courseId: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/course/${courseId}/hour/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
117
playedu-admin/src/api/course.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function courseList(
|
||||
page: number,
|
||||
size: number,
|
||||
sortField: string,
|
||||
sortAlgo: string,
|
||||
title: string,
|
||||
depIds: string,
|
||||
categoryIds: string
|
||||
) {
|
||||
return client.get("/backend/v1/course/index", {
|
||||
page: page,
|
||||
size: size,
|
||||
sort_field: sortField,
|
||||
sort_algo: sortAlgo,
|
||||
title: title,
|
||||
dep_ids: depIds,
|
||||
category_ids: categoryIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function createCourse() {
|
||||
return client.get("/backend/v1/course/create", {});
|
||||
}
|
||||
|
||||
// depIds => 部门id数组,请用英文逗号连接
|
||||
// categoryIds => 所属分类数组,请用英文逗号连接
|
||||
export function storeCourse(
|
||||
title: string,
|
||||
thumb: string,
|
||||
shortDesc: string,
|
||||
isShow: number,
|
||||
isRequired: number,
|
||||
depIds: number[],
|
||||
categoryIds: number[],
|
||||
chapters: any[],
|
||||
hours: any[],
|
||||
attachments: any[]
|
||||
) {
|
||||
return client.post("/backend/v1/course/create", {
|
||||
title: title,
|
||||
thumb: thumb,
|
||||
short_desc: shortDesc,
|
||||
is_show: isShow,
|
||||
is_required: isRequired,
|
||||
dep_ids: depIds,
|
||||
category_ids: categoryIds,
|
||||
chapters: chapters,
|
||||
hours: hours,
|
||||
attachments: attachments,
|
||||
});
|
||||
}
|
||||
|
||||
export function course(id: number) {
|
||||
return client.get(`/backend/v1/course/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateCourse(
|
||||
id: number,
|
||||
title: string,
|
||||
thumb: string,
|
||||
shortDesc: string,
|
||||
isShow: number,
|
||||
isRequired: number,
|
||||
depIds: number[],
|
||||
categoryIds: number[],
|
||||
chapters: number[],
|
||||
hours: number[],
|
||||
publishedAt: string
|
||||
) {
|
||||
return client.put(`/backend/v1/course/${id}`, {
|
||||
title: title,
|
||||
thumb: thumb,
|
||||
short_desc: shortDesc,
|
||||
is_show: isShow,
|
||||
is_required: isRequired,
|
||||
dep_ids: depIds,
|
||||
category_ids: categoryIds,
|
||||
chapters: chapters,
|
||||
hours: hours,
|
||||
sort_at: publishedAt,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyCourse(id: number) {
|
||||
return client.destroy(`/backend/v1/course/${id}`);
|
||||
}
|
||||
|
||||
//学员列表
|
||||
export function courseUser(
|
||||
courseId: number,
|
||||
page: number,
|
||||
size: number,
|
||||
sortField: string,
|
||||
sortAlgo: string,
|
||||
name: string,
|
||||
email: string,
|
||||
idCard: string
|
||||
) {
|
||||
return client.get(`/backend/v1/course/${courseId}/user/index`, {
|
||||
page: page,
|
||||
size: size,
|
||||
sort_field: sortField,
|
||||
sort_algo: sortAlgo,
|
||||
name: name,
|
||||
email: email,
|
||||
id_card: idCard,
|
||||
});
|
||||
}
|
||||
|
||||
//删除学员
|
||||
export function destroyCourseUser(courseId: number, ids: number[]) {
|
||||
return client.post(`/backend/v1/course/${courseId}/user/destroy`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
5
playedu-admin/src/api/dashboard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function dashboardList() {
|
||||
return client.get("/backend/v1/dashboard/index", {});
|
||||
}
|
60
playedu-admin/src/api/department.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function departmentList(params: any) {
|
||||
return client.get("/backend/v1/department/index", params);
|
||||
}
|
||||
|
||||
export function createDepartment() {
|
||||
return client.get("/backend/v1/department/create", {});
|
||||
}
|
||||
|
||||
export function storeDepartment(name: string, parentId: number, sort: number) {
|
||||
return client.post("/backend/v1/department/create", {
|
||||
name,
|
||||
parent_id: parentId,
|
||||
sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function department(id: number) {
|
||||
return client.get(`/backend/v1/department/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateDepartment(
|
||||
id: number,
|
||||
name: string,
|
||||
parentId: number,
|
||||
sort: number
|
||||
) {
|
||||
return client.put(`/backend/v1/department/${id}`, {
|
||||
name,
|
||||
parent_id: parentId,
|
||||
sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyDepartment(id: number) {
|
||||
return client.destroy(`/backend/v1/department/${id}`);
|
||||
}
|
||||
|
||||
export function dropSameClass(ids: number[]) {
|
||||
return client.put(`/backend/v1/department/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
||||
|
||||
export function dropDiffClass(id: number, parent_id: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/department/update/parent`, {
|
||||
id: id,
|
||||
parent_id: parent_id,
|
||||
ids: ids,
|
||||
});
|
||||
}
|
||||
|
||||
export function checkDestroy(id: number) {
|
||||
return client.get(`/backend/v1/department/${id}/destroy`, {});
|
||||
}
|
||||
|
||||
export function ldapSync() {
|
||||
return client.post(`/backend/v1/department/ldap-sync`, {});
|
||||
}
|
18
playedu-admin/src/api/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export * as login from "./login";
|
||||
export * as system from "./system";
|
||||
export * as adminRole from "./admin-role";
|
||||
export * as adminUser from "./admin-user";
|
||||
export * as courseCategory from "./course-category";
|
||||
export * as courseChapter from "./course-chapter";
|
||||
export * as course from "./course";
|
||||
export * as courseHour from "./course-hour";
|
||||
export * as courseAttachment from "./course-attachment";
|
||||
export * as department from "./department";
|
||||
export * as resourceCategory from "./resource-category";
|
||||
export * as resource from "./resource";
|
||||
export * as upload from "./upload";
|
||||
export * as user from "./user";
|
||||
export * as appConfig from "./app-config";
|
||||
export * as dashboard from "./dashboard";
|
||||
export * as adminLog from "./admin-log";
|
||||
export * as ldap from "./ldap";
|
166
playedu-admin/src/api/internal/httpClient.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import axios, { Axios, AxiosResponse } from "axios";
|
||||
import { message } from "antd";
|
||||
import { getToken, clearToken } from "../../utils/index";
|
||||
|
||||
const GoLogin = () => {
|
||||
clearToken();
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
const GoError = (code: number) => {
|
||||
// window.location.href = "/error?code=" + code;
|
||||
};
|
||||
|
||||
export class HttpClient {
|
||||
axios: Axios;
|
||||
|
||||
constructor(url: string) {
|
||||
this.axios = axios.create({
|
||||
baseURL: url,
|
||||
timeout: 15000,
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
//拦截器注册
|
||||
this.axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = getToken();
|
||||
token && (config.headers.Authorization = "Bearer " + token);
|
||||
return config;
|
||||
},
|
||||
(err) => {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
this.axios.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
let code = response.data.code; //业务返回代码
|
||||
let msg = response.data.msg; //错误消息
|
||||
|
||||
if (code === 0) {
|
||||
return Promise.resolve(response);
|
||||
} else if (code === 404) {
|
||||
message.error(msg);
|
||||
// 跳转到404页面
|
||||
GoError(404);
|
||||
} else if (code === 403) {
|
||||
message.error(msg);
|
||||
// 跳转到无权限页面
|
||||
GoError(403);
|
||||
} else if (code === 429) {
|
||||
message.error(msg);
|
||||
// 跳转到429页面
|
||||
GoError(429);
|
||||
} else if (code === 500) {
|
||||
message.error(msg);
|
||||
// 跳转到500异常页面
|
||||
GoError(500);
|
||||
} else {
|
||||
GoError(code);
|
||||
message.error(msg);
|
||||
}
|
||||
return Promise.reject(response);
|
||||
},
|
||||
// 当http的状态码非0
|
||||
(error) => {
|
||||
let status = error.response.status;
|
||||
if (status === 401) {
|
||||
message.error("请重新登录");
|
||||
GoLogin();
|
||||
} else if (status === 404) {
|
||||
// 跳转到404页面
|
||||
GoError(404);
|
||||
} else if (status === 403) {
|
||||
// 跳转到无权限页面
|
||||
GoError(403);
|
||||
} else if (status === 429) {
|
||||
// 跳转到429页面
|
||||
GoError(429);
|
||||
} else if (status === 500) {
|
||||
// 跳转到500异常页面
|
||||
GoError(500);
|
||||
} else {
|
||||
GoError(status);
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.get(url, {
|
||||
params: params,
|
||||
})
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.delete(url)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
post(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.post(url, params)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
put(url: string, params: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.put(url, params)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
request(config: object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios
|
||||
.request(config)
|
||||
.then((res) => {
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const APP_URL = import.meta.env.VITE_APP_URL || "";
|
||||
|
||||
const client = new HttpClient(APP_URL);
|
||||
|
||||
export default client;
|
26
playedu-admin/src/api/ldap.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
// 获取同步记录列表
|
||||
export function getSyncRecords(params: { page?: number; size?: number }) {
|
||||
return client.get("/backend/v1/ldap/sync-records", params);
|
||||
}
|
||||
|
||||
// 获取单条同步记录详情
|
||||
export function getSyncRecordDetail(id: number) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}`, {});
|
||||
}
|
||||
|
||||
// 获取同步记录的详细项目
|
||||
export function getSyncRecordDetails(id: number, params: {
|
||||
type: 'department' | 'user';
|
||||
action?: number;
|
||||
page?: number;
|
||||
size?: number
|
||||
}) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}/details`, params);
|
||||
}
|
||||
|
||||
// 下载同步记录数据
|
||||
export function downloadSyncRecord(id: number) {
|
||||
return client.get(`/backend/v1/ldap/sync-records/${id}/download`, {});
|
||||
}
|
23
playedu-admin/src/api/login.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function login(email: string, password: string) {
|
||||
return client.post("/backend/v1/auth/login", {
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return client.post("/backend/v1/auth/logout", {});
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
return client.get("/backend/v1/auth/detail", {});
|
||||
}
|
||||
|
||||
export function passwordChange(oldPassword: string, newPassword: string) {
|
||||
return client.put("/backend/v1/auth/password", {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
}
|
60
playedu-admin/src/api/resource-category.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function resourceCategoryList() {
|
||||
return client.get("/backend/v1/resource-category/index", {});
|
||||
}
|
||||
|
||||
export function createResourceCategory() {
|
||||
return client.get("/backend/v1/resource-category/create", {});
|
||||
}
|
||||
|
||||
export function storeResourceCategory(
|
||||
name: string,
|
||||
parentId: number,
|
||||
sort: number
|
||||
) {
|
||||
return client.post("/backend/v1/resource-category/create", {
|
||||
name,
|
||||
parent_id: parentId,
|
||||
sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function resourceCategory(id: number) {
|
||||
return client.get(`/backend/v1/resource-category/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateResourceCategory(
|
||||
id: number,
|
||||
name: string,
|
||||
parentId: number,
|
||||
sort: number
|
||||
) {
|
||||
return client.put(`/backend/v1/resource-category/${id}`, {
|
||||
name,
|
||||
parent_id: parentId,
|
||||
sort,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyResourceCategory(id: number) {
|
||||
return client.destroy(`/backend/v1/resource-category/${id}`);
|
||||
}
|
||||
|
||||
export function dropSameClass(ids: number[]) {
|
||||
return client.put(`/backend/v1/resource-category/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
||||
|
||||
export function dropDiffClass(id: number, parent_id: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/resource-category/update/parent`, {
|
||||
id: id,
|
||||
parent_id: parent_id,
|
||||
ids: ids,
|
||||
});
|
||||
}
|
||||
|
||||
export function checkDestroy(id: number) {
|
||||
return client.get(`/backend/v1/resource-category/${id}/destroy`, {});
|
||||
}
|
71
playedu-admin/src/api/resource.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function resourceList(
|
||||
page: number,
|
||||
size: number,
|
||||
sortField: string,
|
||||
sortAlgo: string,
|
||||
name: string,
|
||||
type: string,
|
||||
categoryIds: string
|
||||
) {
|
||||
return client.get("/backend/v1/resource/index", {
|
||||
page,
|
||||
size,
|
||||
sort_field: sortField,
|
||||
sort_algo: sortAlgo,
|
||||
name,
|
||||
type,
|
||||
category_ids: categoryIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function createResource(type: string) {
|
||||
return client.get("/backend/v1/resource/create", { type });
|
||||
}
|
||||
|
||||
export function storeResource(
|
||||
categoryId: number,
|
||||
name: string,
|
||||
extension: string,
|
||||
size: number,
|
||||
disk: string,
|
||||
fileId: string,
|
||||
path: string,
|
||||
url: string,
|
||||
extra: object
|
||||
) {
|
||||
let data = Object.assign(
|
||||
{},
|
||||
{
|
||||
category_id: categoryId,
|
||||
name,
|
||||
extension,
|
||||
size,
|
||||
disk,
|
||||
file_id: fileId,
|
||||
path,
|
||||
url,
|
||||
},
|
||||
extra
|
||||
);
|
||||
return client.post("/backend/v1/resource/create", data);
|
||||
}
|
||||
|
||||
export function destroyResource(id: number) {
|
||||
return client.destroy(`/backend/v1/resource/${id}`);
|
||||
}
|
||||
|
||||
export function destroyResourceMulti(ids: number[]) {
|
||||
return client.post(`/backend/v1/resource/destroy-multi`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
||||
|
||||
export function videoDetail(id: number) {
|
||||
return client.get(`/backend/v1/resource/${id}`, {});
|
||||
}
|
||||
|
||||
export function videoUpdate(id: number, params: any) {
|
||||
return client.put(`/backend/v1/resource/${id}`, params);
|
||||
}
|
9
playedu-admin/src/api/system.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function getImageCaptcha() {
|
||||
return client.get("/backend/v1/system/image-captcha", {});
|
||||
}
|
||||
|
||||
export function getSystemConfig() {
|
||||
return client.get("/backend/v1/system/config", {});
|
||||
}
|
40
playedu-admin/src/api/upload.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function minioUploadId(extension: string) {
|
||||
return client.get("/backend/v1/upload/minio/upload-id", {
|
||||
extension,
|
||||
});
|
||||
}
|
||||
export function minioPreSignUrl(
|
||||
uploadId: string,
|
||||
filename: string,
|
||||
partNumber: number
|
||||
) {
|
||||
return client.get("/backend/v1/upload/minio/pre-sign-url", {
|
||||
upload_id: uploadId,
|
||||
filename,
|
||||
part_number: partNumber,
|
||||
});
|
||||
}
|
||||
|
||||
export function minioMergeVideo(
|
||||
filename: string,
|
||||
uploadId: string,
|
||||
categoryIds: string,
|
||||
originalFilename: string,
|
||||
extension: string,
|
||||
size: number,
|
||||
duration: number,
|
||||
poster: string
|
||||
) {
|
||||
return client.post("/backend/v1/upload/minio/merge-file", {
|
||||
filename,
|
||||
upload_id: uploadId,
|
||||
original_filename: originalFilename,
|
||||
category_ids: categoryIds,
|
||||
size,
|
||||
duration,
|
||||
extension,
|
||||
poster,
|
||||
});
|
||||
}
|
149
playedu-admin/src/api/user.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
//params可选值如下:
|
||||
// name - 姓名
|
||||
// nickname - 昵称
|
||||
// email - 邮箱
|
||||
// id_card - 身份证号
|
||||
// is_active - 是否激活[1:是,0:否]
|
||||
// is_lock - 是否锁定[1:是,0:否]
|
||||
// is_verify - 是否完成实名认证[1:是,0:否]
|
||||
// is_set_password - 是否设置密码[1:是,0:否]
|
||||
// created_at - 注册时间区间过滤 - 格式(字符串): "开始时间,结束时间"
|
||||
// dep_ids - 部门id字符串 - 格式(字符串): 1,2,3
|
||||
// sort_field - 排序字段(默认值:id) 可选值:id,created_at
|
||||
// sort_algo - 排序算法(默认值:desc) 可选值:asc,desc
|
||||
export function userList(page: number, size: number, params: object) {
|
||||
return client.get("/backend/v1/user/index", {
|
||||
page,
|
||||
size,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
export function createUser() {
|
||||
return client.get("/backend/v1/user/create", {});
|
||||
}
|
||||
|
||||
export function storeUser(
|
||||
email: string,
|
||||
name: string,
|
||||
avatar: string,
|
||||
password: string,
|
||||
idCard: string,
|
||||
depIds: number[]
|
||||
) {
|
||||
return client.post("/backend/v1/user/create", {
|
||||
email,
|
||||
name,
|
||||
avatar,
|
||||
password,
|
||||
id_card: idCard,
|
||||
dep_ids: depIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function user(id: number) {
|
||||
return client.get(`/backend/v1/user/${id}`, {});
|
||||
}
|
||||
|
||||
export function updateUser(
|
||||
id: number,
|
||||
email: string,
|
||||
name: string,
|
||||
avatar: string,
|
||||
password: string,
|
||||
idCard: string,
|
||||
depIds: number[]
|
||||
) {
|
||||
return client.put(`/backend/v1/user/${id}`, {
|
||||
email,
|
||||
name,
|
||||
avatar,
|
||||
password,
|
||||
id_card: idCard,
|
||||
dep_ids: depIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyUser(id: number) {
|
||||
return client.destroy(`/backend/v1/user/${id}`);
|
||||
}
|
||||
|
||||
//startline是表格真是数据的起始行号-用于提示哪一行数据存在问题
|
||||
//users是一个二维字符串数组,每个数组的元素如下:[部门ids字符串,邮箱,昵称,密码,姓名,身份证]
|
||||
export function storeBatch(startLine: number, users: string[][]) {
|
||||
return client.post("/backend/v1/user/store-batch", {
|
||||
start_line: startLine,
|
||||
users: users,
|
||||
});
|
||||
}
|
||||
|
||||
export function learnStats(id: number) {
|
||||
return client.get(`/backend/v1/user/${id}/learn-stats`, {});
|
||||
}
|
||||
|
||||
export function learnHours(
|
||||
id: number,
|
||||
page: number,
|
||||
size: number,
|
||||
params: object
|
||||
) {
|
||||
return client.get(`/backend/v1/user/${id}/learn-hours`, {
|
||||
page,
|
||||
size,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
export function learnCourses(
|
||||
id: number,
|
||||
page: number,
|
||||
size: number,
|
||||
params: object
|
||||
) {
|
||||
return client.get(`/backend/v1/user/${id}/learn-courses`, {
|
||||
page,
|
||||
size,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
export function learnAllCourses(id: number) {
|
||||
return client.get(`/backend/v1/user/${id}/all-courses`, {});
|
||||
}
|
||||
|
||||
export function departmentProgress(
|
||||
id: number,
|
||||
page: number,
|
||||
size: number,
|
||||
params: object
|
||||
) {
|
||||
return client.get(`/backend/v1/department/${id}/users`, {
|
||||
page,
|
||||
size,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
export function learnCoursesProgress(
|
||||
id: number,
|
||||
courseId: number,
|
||||
params: any
|
||||
) {
|
||||
return client.get(`/backend/v1/user/${id}/learn-course/${courseId} `, params);
|
||||
}
|
||||
|
||||
export function destroyAllUserLearned(id: number, courseId: number) {
|
||||
return client.destroy(`/backend/v1/user/${id}/learn-course/${courseId}`);
|
||||
}
|
||||
|
||||
export function destroyUserLearned(
|
||||
id: number,
|
||||
courseId: number,
|
||||
hourId: number
|
||||
) {
|
||||
return client.destroy(
|
||||
`/backend/v1/user/${id}/learn-course/${courseId}/hour/${hourId}`
|
||||
);
|
||||
}
|
313
playedu-admin/src/assets/iconfont/iconfont.css
Normal file
BIN
playedu-admin/src/assets/iconfont/iconfont.ttf
Normal file
BIN
playedu-admin/src/assets/iconfont/iconfont.woff
Normal file
BIN
playedu-admin/src/assets/iconfont/iconfont.woff2
Normal file
BIN
playedu-admin/src/assets/images/commen/avatar.png
Normal file
After Width: | Height: | Size: 836 B |
BIN
playedu-admin/src/assets/images/commen/close.png
Normal file
After Width: | Height: | Size: 736 B |
BIN
playedu-admin/src/assets/images/commen/fanghu.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
playedu-admin/src/assets/images/commen/icon-video.png
Normal file
After Width: | Height: | Size: 243 B |
BIN
playedu-admin/src/assets/images/commen/icon5.png
Normal file
After Width: | Height: | Size: 706 B |
BIN
playedu-admin/src/assets/images/commen/icon6.png
Normal file
After Width: | Height: | Size: 716 B |
BIN
playedu-admin/src/assets/images/commen/upload.png
Normal file
After Width: | Height: | Size: 919 B |
BIN
playedu-admin/src/assets/images/dashboard/icon-more.png
Normal file
After Width: | Height: | Size: 527 B |
BIN
playedu-admin/src/assets/images/dashboard/icon-n1.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
playedu-admin/src/assets/images/dashboard/icon-n2.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
playedu-admin/src/assets/images/dashboard/icon-n3.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
playedu-admin/src/assets/images/dashboard/img-a1.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
playedu-admin/src/assets/images/login/banner.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
playedu-admin/src/assets/images/login/icon.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
playedu-admin/src/assets/logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
playedu-admin/src/assets/thumb/avatar.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
playedu-admin/src/assets/thumb/thumb1.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
playedu-admin/src/assets/thumb/thumb2.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
playedu-admin/src/assets/thumb/thumb3.png
Normal file
After Width: | Height: | Size: 12 KiB |
20
playedu-admin/src/compenents/back-bar/index.module.less
Normal file
@ -0,0 +1,20 @@
|
||||
.back-bar-box {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.line {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background-color: #d8d8d8;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
29
playedu-admin/src/compenents/back-bar/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Button } from "antd";
|
||||
import { useState } from "react";
|
||||
import styles from "./index.module.less";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LeftOutlined } from "@ant-design/icons";
|
||||
|
||||
interface PropInterface {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const BackBartment = (props: PropInterface) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className={styles["back-bar-box"]}>
|
||||
<Button
|
||||
style={{ paddingLeft: 0 }}
|
||||
icon={<LeftOutlined />}
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<div className={styles["line"]}></div>
|
||||
<div className={styles["name"]}>{props.title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
64
playedu-admin/src/compenents/create-rs-category/index.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Button, Input, message, Modal } from "antd";
|
||||
import { useState } from "react";
|
||||
import { resourceCategory } from "../../api";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
interface PropInterface {
|
||||
type: string;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const CreateResourceCategory = (props: PropInterface) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
|
||||
const confirm = () => {
|
||||
if (name.length == 0) {
|
||||
message.error("请输入分类名");
|
||||
return;
|
||||
}
|
||||
resourceCategory
|
||||
.storeResourceCategory(name, 0, 0)
|
||||
.then(() => {
|
||||
setName("");
|
||||
message.success("分类添加成功");
|
||||
setShowModal(false);
|
||||
props.onUpdate();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("错误", err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
shape="circle"
|
||||
icon={<PlusOutlined />}
|
||||
/>
|
||||
{showModal ? (
|
||||
<Modal
|
||||
onCancel={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
onOk={confirm}
|
||||
open={true}
|
||||
title="创建分类"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入分类名"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
32
playedu-admin/src/compenents/duration-text/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface PropInterface {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export const DurationText = (props: PropInterface) => {
|
||||
const [hour, setHour] = useState(0);
|
||||
const [minute, setMinute] = useState(0);
|
||||
const [second, setSecond] = useState(0);
|
||||
const duration = props.duration;
|
||||
|
||||
useEffect(() => {
|
||||
let h = Math.trunc(duration / 3600);
|
||||
let m = Math.trunc((duration % 3600) / 60);
|
||||
let s = Math.trunc((duration % 3600) % 60);
|
||||
|
||||
setHour(h);
|
||||
setMinute(m);
|
||||
setSecond(s);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
{hour === 0 ? null : hour + ":"}
|
||||
{minute >= 10 ? minute : "0" + minute}:
|
||||
{second >= 10 ? second : "0" + second}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
44
playedu-admin/src/compenents/footer/index.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { Layout } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface PropInterface {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export const Footer: React.FC<PropInterface> = ({ type }) => {
|
||||
return (
|
||||
<Layout.Footer
|
||||
style={{
|
||||
width: "100%",
|
||||
background: type === "none" ? "none" : "#F6F6F6",
|
||||
height: 166,
|
||||
paddingTop: 80,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to="https://www.playeduos.com/"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
target="blank"
|
||||
>
|
||||
{/* 此处为版权标识,严禁删改 */}
|
||||
<i
|
||||
style={{ fontSize: 30, color: "#cccccc" }}
|
||||
className="iconfont icon-waterprint footer-icon"
|
||||
onClick={() => {}}
|
||||
></i>
|
||||
<span
|
||||
className="ml-5"
|
||||
style={{ color: "#D7D7D7", fontSize: 12, marginTop: -5 }}
|
||||
>
|
||||
Version 2.0
|
||||
</span>
|
||||
</Link>
|
||||
</Layout.Footer>
|
||||
);
|
||||
};
|
31
playedu-admin/src/compenents/header/index.module.less
Normal file
@ -0,0 +1,31 @@
|
||||
.app-header {
|
||||
width: 100%;
|
||||
background-color: white !important;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
/* Firefox */
|
||||
-webkit-box-sizing: border-box;
|
||||
/* Safari */
|
||||
padding: 0px 24px;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
width: 100%;
|
||||
background-color: white !important;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
width: 151px;
|
||||
height: 40px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.top-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
64
playedu-admin/src/compenents/header/index.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import styles from "./index.module.less";
|
||||
import { Button, Dropdown, MenuProps } from "antd";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import avatar from "../../assets/images/commen/avatar.png";
|
||||
import { logoutAction } from "../../store/user/loginUserSlice";
|
||||
import { clearToken } from "../../utils/index";
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelector((state: any) => state.loginUser.value.user);
|
||||
|
||||
const onClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "login_out") {
|
||||
clearToken();
|
||||
dispatch(logoutAction());
|
||||
navigate("/login");
|
||||
} else if (key === "change_password") {
|
||||
navigate("/change-password");
|
||||
}
|
||||
};
|
||||
|
||||
const items: MenuProps["items"] = [
|
||||
{
|
||||
label: "修改密码",
|
||||
key: "change_password",
|
||||
icon: (
|
||||
<i
|
||||
className="iconfont icon-icon-password c-red"
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "退出登录",
|
||||
key: "login_out",
|
||||
icon: (
|
||||
<i
|
||||
className="iconfont icon-a-icon-logout c-red"
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className={styles["app-header"]}>
|
||||
<div className={styles["main-header"]}>
|
||||
<div></div>
|
||||
<Button.Group className={styles["button-group"]}>
|
||||
<Dropdown menu={{ items, onClick }} placement="bottomRight">
|
||||
<div className="d-flex">
|
||||
{user.name && (
|
||||
<img style={{ width: 30, height: 30 }} src={avatar} />
|
||||
)}
|
||||
<span className="ml-8 c-admin">{user.name}</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
16
playedu-admin/src/compenents/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export * from "./footer";
|
||||
export * from "./header";
|
||||
export * from "./left-menu";
|
||||
export * from "./upload-image-button";
|
||||
export * from "./tree-department";
|
||||
export * from "./back-bar";
|
||||
export * from "./permission-button";
|
||||
export * from "./tree-category";
|
||||
export * from ".//tree-adminroles";
|
||||
export * from "./duration-text";
|
||||
export * from "./upload-video-sub";
|
||||
export * from "./select-resource";
|
||||
export * from "./upload-courseware-button";
|
||||
export * from "./upload-courseware-sub";
|
||||
export * from "./select-attachment";
|
||||
export * from "./select-range";
|
29
playedu-admin/src/compenents/keep-alive/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useUpdate } from "ahooks";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useLocation, useOutlet } from "react-router-dom";
|
||||
|
||||
function KeepAlive() {
|
||||
const componentList = useRef(new Map());
|
||||
const outLet = useOutlet();
|
||||
const { pathname } = useLocation();
|
||||
const forceUpdate = useUpdate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!componentList.current.has(pathname)) {
|
||||
componentList.current.set(pathname, outLet);
|
||||
}
|
||||
forceUpdate();
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Array.from(componentList.current).map(([key, component]) => (
|
||||
<div key={key} style={{ display: pathname === key ? "block" : "none" }}>
|
||||
{component}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeepAlive;
|
20
playedu-admin/src/compenents/left-menu/index.module.less
Normal file
@ -0,0 +1,20 @@
|
||||
.left-menu {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
|
||||
.App-logo {
|
||||
width: 124px;
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
margin-left: 38px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.menu-box {
|
||||
width: 200px;
|
||||
height: calc(100% - 74px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
261
playedu-admin/src/compenents/left-menu/index.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Menu } from "antd";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import styles from "./index.module.less";
|
||||
import logo from "../../assets/logo.png";
|
||||
|
||||
function getItem(
|
||||
label: any,
|
||||
key: any,
|
||||
icon: any,
|
||||
children: any,
|
||||
type: any,
|
||||
permission: any
|
||||
) {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
permission,
|
||||
};
|
||||
}
|
||||
const items = [
|
||||
getItem(
|
||||
"首页概览",
|
||||
"/",
|
||||
<i className={`iconfont icon-icon-home`} />,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
),
|
||||
getItem(
|
||||
"分类管理",
|
||||
"/resource-category",
|
||||
<i className="iconfont icon-icon-category" />,
|
||||
null,
|
||||
null,
|
||||
"resource-category-menu"
|
||||
),
|
||||
getItem(
|
||||
"资源管理",
|
||||
"resource",
|
||||
<i className="iconfont icon-icon-file" />,
|
||||
[
|
||||
getItem("视频", "/videos", null, null, null, "resource-menu"),
|
||||
getItem("图片", "/images", null, null, null, "resource-menu"),
|
||||
getItem("课件", "/courseware", null, null, null, "resource-menu"),
|
||||
],
|
||||
null,
|
||||
null
|
||||
),
|
||||
getItem(
|
||||
"课程中心",
|
||||
"courses",
|
||||
<i className="iconfont icon-icon-study" />,
|
||||
[getItem("线上课", "/course", null, null, null, "course")],
|
||||
null,
|
||||
null
|
||||
),
|
||||
getItem(
|
||||
"学员管理",
|
||||
"user",
|
||||
<i className="iconfont icon-icon-user" />,
|
||||
[
|
||||
getItem("学员", "/member/index", null, null, null, "user-index"),
|
||||
getItem("部门", "/department", null, null, null, "department-cud"),
|
||||
],
|
||||
null,
|
||||
null
|
||||
),
|
||||
getItem(
|
||||
"系统设置",
|
||||
"system",
|
||||
<i className="iconfont icon-icon-setting" />,
|
||||
[
|
||||
getItem(
|
||||
"系统配置",
|
||||
"/system/config/index",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"system-config"
|
||||
),
|
||||
getItem(
|
||||
"管理人员",
|
||||
"/system/administrator",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"admin-user-index"
|
||||
),
|
||||
getItem("管理日志", "/system/adminlog", null, null, null, "admin-log"),
|
||||
],
|
||||
null,
|
||||
null
|
||||
),
|
||||
getItem(
|
||||
"使用许可",
|
||||
"/licensing",
|
||||
<i className="iconfont icon-xuke" />,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
];
|
||||
|
||||
export const LeftMenu: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const children2Parent: any = {
|
||||
"^/video": ["resource"],
|
||||
"^/image": ["resource"],
|
||||
"^/courseware": ["resource"],
|
||||
"^/member": ["user"],
|
||||
"^/department": ["user"],
|
||||
"^/course": ["courses"],
|
||||
"^/system": ["system"],
|
||||
};
|
||||
|
||||
const hit = (pathname: string): string[] => {
|
||||
for (let p in children2Parent) {
|
||||
if (pathname.search(p) >= 0) {
|
||||
return children2Parent[p];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const openKeyMerge = (pathname: string): string[] => {
|
||||
let newOpenKeys = hit(pathname);
|
||||
for (let i = 0; i < openKeys.length; i++) {
|
||||
let isIn = false;
|
||||
for (let j = 0; j < newOpenKeys.length; j++) {
|
||||
if (newOpenKeys[j] === openKeys[i]) {
|
||||
isIn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isIn) {
|
||||
continue;
|
||||
}
|
||||
newOpenKeys.push(openKeys[i]);
|
||||
}
|
||||
return newOpenKeys;
|
||||
};
|
||||
|
||||
// 选中的菜单
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([
|
||||
location.pathname,
|
||||
]);
|
||||
// 展开菜单
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(hit(location.pathname));
|
||||
const permissions = useSelector(
|
||||
(state: any) => state.loginUser.value.permissions
|
||||
);
|
||||
const [activeMenus, setActiveMenus] = useState<any>([]);
|
||||
|
||||
const onClick = (e: any) => {
|
||||
navigate(e.key);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkMenuPermissions(items, permissions);
|
||||
}, [items, permissions]);
|
||||
|
||||
const checkMenuPermissions = (items: any, permissions: any) => {
|
||||
let menus: any = [];
|
||||
if (permissions.length === 0) {
|
||||
setActiveMenus(menus);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in items) {
|
||||
let menuItem = items[i];
|
||||
// 一级菜单=>没有子菜单&配置了权限
|
||||
if (menuItem.children === null) {
|
||||
if (
|
||||
menuItem.permission !== null &&
|
||||
typeof permissions[menuItem.permission] === "undefined"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
menus.push(menuItem);
|
||||
continue;
|
||||
}
|
||||
let children = [];
|
||||
|
||||
for (let j in menuItem.children) {
|
||||
let childrenItem = menuItem.children[j];
|
||||
|
||||
if (
|
||||
typeof permissions[childrenItem.permission] !== "undefined" ||
|
||||
!childrenItem.permission
|
||||
) {
|
||||
// 存在权限
|
||||
children.push(childrenItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (children.length > 0) {
|
||||
menus.push(Object.assign({}, menuItem, { children: children }));
|
||||
}
|
||||
}
|
||||
setActiveMenus(menus);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname.indexOf("/course/user") !== -1) {
|
||||
setSelectedKeys(["/course"]);
|
||||
setOpenKeys(openKeyMerge("/course"));
|
||||
} else if (location.pathname.indexOf("/member/learn") !== -1) {
|
||||
setSelectedKeys(["/member/index"]);
|
||||
setOpenKeys(openKeyMerge("/member/index"));
|
||||
} else {
|
||||
setSelectedKeys([location.pathname]);
|
||||
setOpenKeys(openKeyMerge(location.pathname));
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className={styles["left-menu"]}>
|
||||
<div
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
background: "#fff",
|
||||
}}
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
>
|
||||
{/* 此处为版权标识,严禁删改 */}
|
||||
<img src={logo} className={styles["App-logo"]} />
|
||||
</div>
|
||||
<div className={styles["menu-box"]}>
|
||||
<Menu
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: 200,
|
||||
background: "#ffffff",
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
openKeys={openKeys}
|
||||
mode="inline"
|
||||
items={activeMenus}
|
||||
onSelect={(data: any) => {
|
||||
setSelectedKeys(data.selectedKeys);
|
||||
}}
|
||||
onOpenChange={(keys: any) => {
|
||||
setOpenKeys(keys);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
55
playedu-admin/src/compenents/permission-button/index.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { Button } from "antd";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface PropInterface {
|
||||
type: "link" | "text" | "primary" | "default";
|
||||
text: string;
|
||||
p: string;
|
||||
class: string;
|
||||
icon?: any;
|
||||
onClick?: () => void;
|
||||
disabled: any;
|
||||
}
|
||||
|
||||
export const PerButton = (props: PropInterface) => {
|
||||
const permissions = useSelector(
|
||||
(state: any) => state.loginUser.value.permissions
|
||||
);
|
||||
const isThrough = () => {
|
||||
if (!permissions) {
|
||||
return false;
|
||||
}
|
||||
return typeof permissions[props.p] !== "undefined";
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isThrough() && props.type === "link" && (
|
||||
<Button
|
||||
className={props.class}
|
||||
type="link"
|
||||
danger
|
||||
icon={props.icon}
|
||||
onClick={() => {
|
||||
props.onClick && props.onClick();
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{props.text}
|
||||
</Button>
|
||||
)}
|
||||
{isThrough() && props.type !== "link" && (
|
||||
<Button
|
||||
className={props.class}
|
||||
type={props.type}
|
||||
icon={props.icon}
|
||||
onClick={() => {
|
||||
props.onClick && props.onClick();
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{props.text}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|