Compare commits
18 Commits
8beae72101
...
92a9f85171
Author | SHA1 | Date | |
---|---|---|---|
|
92a9f85171 | ||
|
c9e1894f17 | ||
|
fbc61f9eac | ||
|
b5fe6ecd4d | ||
|
fe47657213 | ||
|
5eb3675921 | ||
|
37fccc1f40 | ||
|
f5047edd91 | ||
|
e97f0318e6 | ||
|
5b027dfa23 | ||
|
6776fecfe9 | ||
|
e13ff1ae25 | ||
|
7c84e185a6 | ||
|
4d527cff77 | ||
|
5d1a6109c2 | ||
|
eb02def070 | ||
|
296aa71d51 | ||
|
b96c609eef |
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
################# PlayEdu ###################
|
||||
PLAYEDU_API_PORT=9700
|
||||
PLAYEDU_PC_PORT=9800
|
||||
PLAYEDU_H5_PORT=9801
|
||||
PLAYEDU_ADMIN_PORT=9900
|
||||
PLAYEDU_JWT_KEY=playeduxyz
|
||||
|
||||
################# MySQL ###################
|
||||
MYSQL_PORT=3306
|
||||
|
||||
################# Redis ###################
|
||||
REDIS_PORT=6379
|
||||
|
||||
################# MINIO ###################
|
||||
MINIO_ROOT_USERNAME=username
|
||||
MINIO_ROOT_PASSWORD=password
|
||||
MINIO_PORT=9002
|
||||
MINIO_CONSOLE_PORT=50002
|
10
.github/workflows/build.yml
vendored
@ -2,16 +2,12 @@ name: EstablishDockerImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- 'feat/**'
|
||||
tags:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
env:
|
||||
IMAGE_FQDN: registry.cn-hangzhou.aliyuncs.com/playedu/api
|
||||
IMAGE_TAG: ''
|
||||
IMAGE_FQDN: registry.cn-hangzhou.aliyuncs.com/playedu/light
|
||||
IMAGE_TAG: ""
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
|
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
|
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
||||
## v1.7 版本更新
|
||||
|
||||
- 新增:[API]MinIO配置信息增加环境变量的读取
|
||||
- 新增:[API]学员学习权限优化
|
||||
- 新增:[后台]后台首页增加课件数量的显示
|
||||
- 新增:[后台]线上课列表增加创建人字段
|
||||
- 优化:[后台]学员部门包含子部门所有学员数量
|
||||
- 优化:[API]根据分类ID获取所有子分类的课程
|
||||
- 优化:[API]根据部门ID获取所有父级部门的课程
|
||||
- 优化:[后台]部门指派器
|
||||
- 优化:[PC]视频播放器去除右键点击
|
||||
- 优化:[PC]首页学习时长去掉秒
|
||||
- 优化:[H5]首页tab切换优化
|
43
Dockerfile
@ -1,6 +1,27 @@
|
||||
FROM eclipse-temurin:17 as builder
|
||||
FROM node:20-alpine as NodeBuilder
|
||||
|
||||
COPY . /app
|
||||
RUN yarn config set registry https://registry.npmmirror.com &&\
|
||||
yarn global add pnpm &&\
|
||||
pnpm config set registry https://registry.npmmirror.com
|
||||
|
||||
COPY playedu-admin /app/admin
|
||||
COPY playedu-pc /app/pc
|
||||
COPY playedu-h5 /app/h5
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
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 eclipse-temurin:17 as javaBuilder
|
||||
|
||||
COPY playedu-api /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -8,18 +29,18 @@ RUN /app/mvnw -Dmaven.test.skip=true clean package
|
||||
|
||||
FROM eclipse-temurin:17
|
||||
|
||||
WORKDIR /app
|
||||
LABEL maintainer="滕勇志 <tengyongzhi@meedu.vip>"
|
||||
|
||||
# 使用东八区时间环境
|
||||
RUN echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 将指定目录下的jar包复制到docker容器的/目录下
|
||||
COPY --from=builder /app/playedu-api/target/playedu-api.jar /app/app.jar
|
||||
RUN apt update && apt install -y nginx
|
||||
|
||||
RUN chmod +x /app/app.jar
|
||||
COPY --from=javaBuilder /app/playedu-api/target/playedu-api.jar /app/api/app.jar
|
||||
|
||||
# 声明服务运行在8080端口
|
||||
EXPOSE 9898
|
||||
COPY --from=NodeBuilder /app/admin/dist /app/admin
|
||||
COPY --from=NodeBuilder /app/pc/dist /app/pc
|
||||
COPY --from=NodeBuilder /app/h5/dist /app/h5
|
||||
|
||||
# 指定docker容器启动时运行jar包
|
||||
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
||||
COPY docker/nginx/conf/nginx.conf /etc/nginx/sites-enabled/default
|
||||
|
||||
CMD nginx; echo "Waiting for MySQL/Redis/MinIO 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} --spring.data.redis.host=${REDIS_HOST} --spring.data.redis.port=${REDIS_PORT:-6379} --spring.data.redis.password=${REDIS_PASS} --spring.data.redis.database=${REDIS_DB:-0} --sa-token.is-concurrent=${SA_TOKEN_IS_CONCURRENT:-false} --sa-token.jwt-secret-key=${SA_TOKEN_JWT_SECRET_KEY}
|
56
README.md
@ -2,36 +2,50 @@
|
||||
<img src="https://meedu.cloud.oss.meedu.vip/playedu/%E5%A4%B4%E5%9B%BE.jpg"/>
|
||||
</p>
|
||||
|
||||
## 系统介绍
|
||||
<h4 align="center">
|
||||
<a href="http://www.playeduos.com">官网</a> |
|
||||
<a href="https://www.playeduos.com/function.html">商业版</a> |
|
||||
<a href="https://faq.playeduos.com/">部署文档</a> |
|
||||
<a href="https://www.playeduos.com/demo.html">演示站</a> |
|
||||
<a href="https://faq.playeduos.com/qa">开源社区</a>
|
||||
</h4>
|
||||
|
||||
**PlayEdu** 是由白书科技团队经营多年线上教育系统打造出的一款全新的企业培训解决方案,致力于为更多企业机构搭建私有化内部培训平台。**PlayEdu** 基于 Java + MySQL 开发;采用前后端分离模式;前端采用 React18 为核心框架,后端采用 SpringBoot3 为核心框架。提供部门管理、学员管理、在线视频学习、学员进度追踪、视频私有化存储等培训主要功能。**与此同时,我们在开源版本的基础上还提供了功能更加丰富的企业版本。企业版本在开源功能的基础上提供了包括视频云端存储、视频加密、音频学习、文档(PDF|WORD|PPT)在线学习、在线考试、学习计划培训等功能。企业版本更多信息请点击下方的企业版链接查看。**
|
||||
PlayEdu 是由白书科技团队经营多年线上教培领域打造出的一款业内领先的线上培训解决方案。PlayEdu 基于 Java + MySQL 开发,采用前后端分离模式,前端核心框架为 React18,后端核心框架为 SpringBoot3。开源版本提供部门管理、学员管理、在线视频学习、学员进度追踪、视频私有化存储等基础培训功能。
|
||||
**针对企业级培训场景,我们精心打造了“功能更多、响应更快、并发更强”的企业版本,满足企业多样化的培训需求。企业版本支持音视频学习、文档在线预览、线上考试、学习任务等多种学习方式,并提供多重安全防护,如视频转码加密、防盗链、学习防快进、防挂机等。同时,我们集成了企业微信、钉钉、飞书等主流办公系统,帮助企业快速部署专属培训平台!**
|
||||
|
||||
## 常用链接
|
||||
## 🚀 快速上手
|
||||
|
||||
| 站点 | 链接 |
|
||||
| ---------- | ---------------------------------------------------------------------------- |
|
||||
| 官网 | [http://www.playedu.xyz](http://www.playedu.xyz) |
|
||||
| **企业版** | [https://www.playedu.xyz/commercial](https://www.playedu.xyz/commercial) |
|
||||
| 部署文档 | [https://www.playedu.xyz/book](https://www.playedu.xyz/book) |
|
||||
| 系统演示 | [https://www.playedu.xyz/demo](https://www.playedu.xyz/demo) |
|
||||
| 问答社区 | [https://www.playedu.xyz/qa](https://www.playedu.xyz/qa) |
|
||||
拉取代码:
|
||||
|
||||
## 依赖前端项目
|
||||
```
|
||||
git clone --branch main https://gitee.com/playeduxyz/playedu.git playedu
|
||||
```
|
||||
|
||||
- [后台界面程序](https://github.com/PlayEdu/backend)
|
||||
- [PC 界面程序](https://github.com/PlayEdu/frontend)
|
||||
- [H5 界面程序](https://github.com/PlayEdu/h5)
|
||||
构建镜像:
|
||||
|
||||
## 界面预览
|
||||
```
|
||||
cd playedu && docker-compose up -d
|
||||
```
|
||||
|
||||
命令执行完成以后,打开您的浏览器,输入 `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.版权保护**: 严禁删除、修改或篡改源代码中的版权信息及开源说明文件。
|
||||
- **4.商业化行为**: 在遵循开源协议的前提下,可以进行商用。但请注意,任何商业化行为都应明确标注 PlayEdu 开源版的版权信息。
|
||||
|
91
compose.yml
Normal file
@ -0,0 +1,91 @@
|
||||
x-logging: &default-logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "10"
|
||||
|
||||
networks:
|
||||
playedu:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.10.10.0/24
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
mysql-conf:
|
||||
redis-data:
|
||||
minio-data:
|
||||
|
||||
services:
|
||||
playedu:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/playedu/light:1.7
|
||||
restart: always
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=playedu
|
||||
- DB_USER=root
|
||||
- DB_PASS=playeduxyz
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASS=playeduxyz
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_DB=2
|
||||
- SA_TOKEN_IS_CONCURRENT=false
|
||||
- SA_TOKEN_JWT_SECRET_KEY=${PLAYEDU_JWT_KEY:-playeduxyz}
|
||||
ports:
|
||||
- "${PLAYEDU_API_PORT:-9700}:9700"
|
||||
- "${PLAYEDU_PC_PORT:-9800}:9800"
|
||||
- "${PLAYEDU_H5_PORT:-9801}:9801"
|
||||
- "${PLAYEDU_ADMIN_PORT:-9900}:9900"
|
||||
networks:
|
||||
- playedu
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
- minio
|
||||
logging: *default-logging
|
||||
|
||||
redis:
|
||||
build: ./docker/redis
|
||||
restart: always
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
# ports:
|
||||
# - "${REDIS_PORT:-6379}:6379"
|
||||
networks:
|
||||
- playedu
|
||||
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
|
||||
- mysql-conf:/etc/mysql/conf.d
|
||||
# ports:
|
||||
# - "${MYSQL_PORT:-3306}:3306"
|
||||
networks:
|
||||
- playedu
|
||||
logging: *default-logging
|
||||
|
||||
minio:
|
||||
image: bitnami/minio:latest
|
||||
restart: always
|
||||
environment:
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USERNAME:-username}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-password}
|
||||
- MINIO_DEFAULT_BUCKETS=playedu:public
|
||||
volumes:
|
||||
- minio-data:/bitnami/minio/data
|
||||
ports:
|
||||
- "${MINIO_PORT:-9002}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-50002}:9001"
|
||||
networks:
|
||||
- playedu
|
||||
logging: *default-logging
|
18
docker/mysql/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM mysql:8.1
|
||||
|
||||
# 时区配置
|
||||
ARG TZ=UTC
|
||||
ENV TZ ${TIMEZONE}
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone && chown -R mysql:root /var/lib/mysql/
|
||||
|
||||
# 复制自定义的my.cnf
|
||||
COPY my.cnf /etc/mysql/conf.d/my.cnf
|
||||
|
||||
# my.cnf只读权限
|
||||
RUN chmod 0444 /etc/mysql/conf.d/my.cnf
|
||||
|
||||
# 暴露3306端口
|
||||
EXPOSE 3306
|
||||
|
||||
# 启动mysql
|
||||
CMD ["mysqld"]
|
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
|
131
docker/nginx/conf/nginx.conf
Normal file
@ -0,0 +1,131 @@
|
||||
client_max_body_size 50m;
|
||||
|
||||
server {
|
||||
# 监听端口号9800
|
||||
listen 9800;
|
||||
# 禁用绝对重定向
|
||||
# 如果没有设置该配置项,当Nginx重定向时,它将使用绝对路径,这意味着当用户访问一个虚拟主机时
|
||||
# Nginx可能会将他们重定向到另一个主机的URL,这可能会导致安全问题和用户体验问题。因此,通过
|
||||
# 设置 absolute_redirect off ,Nginx将使用相对路径进行重定向,以确保用户在同一主机上进
|
||||
# 行重定向,而不会遇到上述问题。
|
||||
absolute_redirect off;
|
||||
# 匹配任何主机名
|
||||
server_name _;
|
||||
# 指定Web应用程序的根目录
|
||||
root /app/pc;
|
||||
# 指定默认的索引文件为index.html
|
||||
index index.html;
|
||||
# 启用gzip压缩
|
||||
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;
|
||||
|
||||
# 将所有以/api/开头的请求代理到本地地址为127.0.0.1:9898的Web应用程序后端服务器
|
||||
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/;
|
||||
}
|
||||
|
||||
# 处理所有不以/api/开头的请求,尝试查找匹配的文件,如果没有找到则返回index.html
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
# 监听端口号9801
|
||||
listen 9801;
|
||||
# 禁用绝对重定向
|
||||
# 如果没有设置该配置项,当Nginx重定向时,它将使用绝对路径,这意味着当用户访问一个虚拟主机时
|
||||
# Nginx可能会将他们重定向到另一个主机的URL,这可能会导致安全问题和用户体验问题。因此,通过
|
||||
# 设置 absolute_redirect off ,Nginx将使用相对路径进行重定向,以确保用户在同一主机上进
|
||||
# 行重定向,而不会遇到上述问题。
|
||||
absolute_redirect off;
|
||||
# 匹配任何主机名
|
||||
server_name _;
|
||||
# 指定Web应用程序的根目录
|
||||
root /app/h5;
|
||||
# 指定默认的索引文件为index.html
|
||||
index index.html;
|
||||
# 启用gzip压缩
|
||||
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;
|
||||
|
||||
# 将所有以/api/开头的请求代理到本地地址为127.0.0.1:9898的Web应用程序后端服务器
|
||||
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/;
|
||||
}
|
||||
|
||||
# 处理所有不以/api/开头的请求,尝试查找匹配的文件,如果没有找到则返回index.html
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
# 监听端口号9900
|
||||
listen 9900;
|
||||
# 禁用绝对重定向
|
||||
# 如果没有设置该配置项,当Nginx重定向时,它将使用绝对路径,这意味着当用户访问一个虚拟主机时
|
||||
# Nginx可能会将他们重定向到另一个主机的URL,这可能会导致安全问题和用户体验问题。因此,通过
|
||||
# 设置 absolute_redirect off ,Nginx将使用相对路径进行重定向,以确保用户在同一主机上进
|
||||
# 行重定向,而不会遇到上述问题。
|
||||
absolute_redirect off;
|
||||
# 匹配任何主机名
|
||||
server_name _;
|
||||
# 指定Web应用程序的根目录
|
||||
root /app/admin;
|
||||
# 指定默认的索引文件为index.html
|
||||
index index.html;
|
||||
# 启用gzip压缩
|
||||
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;
|
||||
|
||||
# 将所有以/api/开头的请求代理到本地地址为127.0.0.1:9898的Web应用程序后端服务器
|
||||
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/;
|
||||
}
|
||||
|
||||
# 处理所有不以/api/开头的请求,尝试查找匹配的文件,如果没有找到则返回index.html
|
||||
location ~* ^/(?![api].*) {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
# 监听端口号80
|
||||
listen 9700;
|
||||
# 匹配任何主机名
|
||||
server_name _;
|
||||
# 指定Web应用程序的根目录
|
||||
root /app/pc;
|
||||
# 指定默认的索引文件为index.html
|
||||
index index.html;
|
||||
|
||||
# 将所有请求代理到本地地址为127.0.0.1:9898的Web应用程序后端服务器
|
||||
location / {
|
||||
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/;
|
||||
}
|
||||
}
|
9
docker/redis/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM redis:7.0.12
|
||||
|
||||
COPY redis.conf /usr/local/etc/redis/redis.conf
|
||||
|
||||
VOLUME /data
|
||||
|
||||
EXPOSE 6379
|
||||
|
||||
CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
1377
docker/redis/redis.conf
Normal file
3
playedu-admin/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
/build
|
||||
/dist
|
1
playedu-admin/.env.example
Normal file
@ -0,0 +1 @@
|
||||
VITE_APP_URL=
|
37
playedu-admin/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# 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/
|
||||
|
||||
pnpm-lock.yaml
|
13
playedu-admin/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM node:lts-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN yarn config set registry https://registry.npm.taobao.org && yarn && yarn build
|
||||
|
||||
FROM nginx:1.23.4-alpine-slim
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/docker/nginx.conf /etc/nginx/nginx.conf
|
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 放置在本官网展示。
|
83
playedu-admin/docker/nginx.conf
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 32000;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#开启gzip
|
||||
gzip on;
|
||||
#低于1kb的资源不压缩
|
||||
gzip_min_length 1k;
|
||||
#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。
|
||||
gzip_comp_level 5;
|
||||
#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
|
||||
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
|
||||
#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
#是否添加“Vary: Accept-Encoding”响应头
|
||||
gzip_vary on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
#禁止访问的文件或目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
|
||||
#一键申请SSL证书验证目录相关设置
|
||||
location ~ \.well-known{
|
||||
allow all;
|
||||
}
|
||||
|
||||
#禁止在证书验证目录放入敏感文件
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
|
||||
{
|
||||
expires 30d;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
location ~ .*\.(js|css)?$
|
||||
{
|
||||
expires 12h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
}
|
||||
}
|
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"
|
||||
}
|
||||
}
|
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,
|
||||
published_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`, {});
|
||||
}
|
17
playedu-admin/src/api/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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";
|
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;
|
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}`
|
||||
);
|
||||
}
|
143
playedu-admin/src/assets/iconfont/iconfont.css
Normal file
@ -0,0 +1,143 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3943555 */
|
||||
src: url('iconfont.woff2?t=1690600882833') format('woff2'),
|
||||
url('iconfont.woff?t=1690600882833') format('woff'),
|
||||
url('iconfont.ttf?t=1690600882833') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-playedu:before {
|
||||
content: "\e756";
|
||||
}
|
||||
|
||||
.icon-icon-xuexi:before {
|
||||
content: "\e753";
|
||||
}
|
||||
|
||||
.icon-icon-wode:before {
|
||||
content: "\e754";
|
||||
}
|
||||
|
||||
.icon-icon-shouye:before {
|
||||
content: "\e755";
|
||||
}
|
||||
|
||||
.icon-icon-xiala:before {
|
||||
content: "\e752";
|
||||
}
|
||||
|
||||
.icon-close:before {
|
||||
content: "\e751";
|
||||
}
|
||||
|
||||
.icon-fullscreen:before {
|
||||
content: "\e74b";
|
||||
}
|
||||
|
||||
.icon-speed:before {
|
||||
content: "\e74c";
|
||||
}
|
||||
|
||||
.icon-mute:before {
|
||||
content: "\e74d";
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: "\e74e";
|
||||
}
|
||||
|
||||
.icon-pause:before {
|
||||
content: "\e74f";
|
||||
}
|
||||
|
||||
.icon-unmute:before {
|
||||
content: "\e750";
|
||||
}
|
||||
|
||||
.icon-icon-tips:before {
|
||||
content: "\e74a";
|
||||
}
|
||||
|
||||
.icon-icon-fold:before {
|
||||
content: "\e749";
|
||||
}
|
||||
|
||||
.icon-icon-12:before {
|
||||
content: "\e748";
|
||||
}
|
||||
|
||||
.icon-waterprint:before {
|
||||
content: "\e747";
|
||||
}
|
||||
|
||||
.icon-adduser:before {
|
||||
content: "\e743";
|
||||
}
|
||||
|
||||
.icon-upvideo:before {
|
||||
content: "\e744";
|
||||
}
|
||||
|
||||
.icon-onlinelesson:before {
|
||||
content: "\e745";
|
||||
}
|
||||
|
||||
.icon-department:before {
|
||||
content: "\e746";
|
||||
}
|
||||
|
||||
.icon-icon-drag:before {
|
||||
content: "\e740";
|
||||
}
|
||||
|
||||
.icon-icon-edit:before {
|
||||
content: "\e741";
|
||||
}
|
||||
|
||||
.icon-icon-delete:before {
|
||||
content: "\e742";
|
||||
}
|
||||
|
||||
.icon-icon-video:before {
|
||||
content: "\e73f";
|
||||
}
|
||||
|
||||
.icon-icon-home:before {
|
||||
content: "\e737";
|
||||
}
|
||||
|
||||
.icon-icon-category:before {
|
||||
content: "\e738";
|
||||
}
|
||||
|
||||
.icon-icon-file:before {
|
||||
content: "\e739";
|
||||
}
|
||||
|
||||
.icon-icon-study:before {
|
||||
content: "\e73a";
|
||||
}
|
||||
|
||||
.icon-icon-user:before {
|
||||
content: "\e73b";
|
||||
}
|
||||
|
||||
.icon-icon-setting:before {
|
||||
content: "\e73c";
|
||||
}
|
||||
|
||||
.icon-icon-password:before {
|
||||
content: "\e73d";
|
||||
}
|
||||
|
||||
.icon-a-icon-logout:before {
|
||||
content: "\e73e";
|
||||
}
|
||||
|
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/icon-video.png
Normal file
After Width: | Height: | Size: 243 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 |
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 1.7
|
||||
</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;
|
||||
}
|
||||
}
|
253
playedu-admin/src/compenents/left-menu/index.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
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
|
||||
),
|
||||
];
|
||||
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
12
playedu-admin/src/compenents/private-route/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { getToken } from "../../utils/index";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
interface PropInterface {
|
||||
Component: any;
|
||||
}
|
||||
|
||||
const PrivateRoute: React.FC<PropInterface> = ({ Component }) => {
|
||||
return getToken() ? Component : <Navigate to="/login" replace={true} />;
|
||||
};
|
||||
export default PrivateRoute;
|
81
playedu-admin/src/compenents/select-attachment/index.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Modal, Tabs } from "antd";
|
||||
import styles from "./index.module.less";
|
||||
import { UploadCoursewareSub } from "../../compenents";
|
||||
import type { TabsProps } from "antd";
|
||||
|
||||
interface PropsInterface {
|
||||
defaultKeys: any[];
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: any[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type selAttachmentModel = {
|
||||
name: string;
|
||||
rid: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export const SelectAttachment = (props: PropsInterface) => {
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
const [tabKey, setTabKey] = useState(1);
|
||||
const [selectKeys, setSelectKeys] = useState<number[]>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<selAttachmentModel[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(!refresh);
|
||||
}, [props.open]);
|
||||
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
key: "1",
|
||||
label: `课件`,
|
||||
children: (
|
||||
<UploadCoursewareSub
|
||||
label="课件"
|
||||
defaultCheckedList={props.defaultKeys}
|
||||
open={refresh}
|
||||
onSelected={(arr: any[], videos: any[]) => {
|
||||
setSelectKeys(arr);
|
||||
setSelectVideos(videos);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = (key: string) => {
|
||||
setTabKey(Number(key));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.open ? (
|
||||
<Modal
|
||||
title="资源素材库"
|
||||
centered
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
props.onCancel();
|
||||
}}
|
||||
open={true}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
props.onSelected(selectKeys, selectVideos);
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Tabs defaultActiveKey="1" items={items} onChange={onChange} />
|
||||
</Row>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
54
playedu-admin/src/compenents/select-range/index.module.less
Normal file
@ -0,0 +1,54 @@
|
||||
.user-content {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 12px;
|
||||
width: 200px;
|
||||
height: 514px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 16px;
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22px;
|
||||
.tit {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 40px;
|
||||
}
|
||||
.link {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-item {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
.user-name {
|
||||
width: 140px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 40px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
113
playedu-admin/src/compenents/select-range/index.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal, Tabs, message } from "antd";
|
||||
import {} from "../../compenents";
|
||||
import styles from "./index.module.less";
|
||||
import type { TabsProps } from "antd";
|
||||
import { SelectDepsSub } from "./select-deps-sub";
|
||||
import { CloseOutlined } from "@ant-design/icons";
|
||||
|
||||
interface PropsInterface {
|
||||
defaultDepIds: any[];
|
||||
defaultDeps: any[];
|
||||
open: boolean;
|
||||
onSelected: (selDepIds: any[], selDeps: any[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type selVideosModel = {
|
||||
name: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export const SelectRange = (props: PropsInterface) => {
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
const [selectKeys, setSelectKeys] = useState<number[]>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<selVideosModel[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(!refresh);
|
||||
}, [props.open]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectKeys(props.defaultDepIds);
|
||||
setSelectVideos(props.defaultDeps);
|
||||
}, [props.defaultDepIds, props.defaultDeps, refresh]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.open ? (
|
||||
<Modal
|
||||
title="选择部门"
|
||||
centered
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
props.onCancel();
|
||||
}}
|
||||
okText="确定"
|
||||
open={true}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
if (selectKeys.length === 0) {
|
||||
message.error("请选择至少一个部门对象");
|
||||
return;
|
||||
}
|
||||
props.onSelected(selectKeys, selectVideos);
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", display: "flex" }}>
|
||||
<div style={{ width: 528 }} className="select-range-modal">
|
||||
<SelectDepsSub
|
||||
defaultkeys={selectKeys}
|
||||
open={refresh}
|
||||
onSelected={(arr: any[], videos: any[]) => {
|
||||
if (arr && videos) {
|
||||
setSelectKeys(arr);
|
||||
setSelectVideos(videos);
|
||||
}
|
||||
}}
|
||||
></SelectDepsSub>
|
||||
</div>
|
||||
<div className={styles["user-content"]}>
|
||||
<div className={styles["title"]}>
|
||||
<div className={styles["tit"]}>已选择:</div>
|
||||
<div
|
||||
className={styles["link"]}
|
||||
onClick={() => {
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</div>
|
||||
</div>
|
||||
{selectVideos.length > 0 &&
|
||||
selectVideos.map((item: any, index: number) => (
|
||||
<div key={"dep" + index} className={styles["user-item"]}>
|
||||
<div className={styles["user-name"]}>
|
||||
{item.title.props.children}
|
||||
</div>
|
||||
<CloseOutlined
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: "rgba(0,0,0,0.45)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
let arr = [...selectKeys];
|
||||
let arr2 = [...selectVideos];
|
||||
arr.splice(index, 1);
|
||||
arr2.splice(index, 1);
|
||||
setSelectKeys(arr);
|
||||
setSelectVideos(arr2);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { useState } from "react";
|
||||
import { TreeDeps } from "../../tree-deps";
|
||||
|
||||
interface PropsInterface {
|
||||
defaultkeys: any[];
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: any[]) => void;
|
||||
}
|
||||
|
||||
export const SelectDepsSub = (props: PropsInterface) => {
|
||||
const [init, setInit] = useState(true);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 528,
|
||||
height: 458,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<TreeDeps
|
||||
selected={props.defaultkeys}
|
||||
refresh={props.open}
|
||||
showNum={true}
|
||||
type=""
|
||||
onUpdate={(keys: any, nodes: any) => {
|
||||
props.onSelected(keys, nodes);
|
||||
}}
|
||||
></TreeDeps>
|
||||
</div>
|
||||
);
|
||||
};
|
67
playedu-admin/src/compenents/select-resource/index.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Modal, } from "antd";
|
||||
import styles from "./index.module.less";
|
||||
import { UploadVideoSub } from "../../compenents";
|
||||
|
||||
interface PropsInterface {
|
||||
defaultKeys: any[];
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: any[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type selVideosModel = {
|
||||
name: string;
|
||||
rid: number;
|
||||
type: string;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export const SelectResource = (props: PropsInterface) => {
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
const [selectKeys, setSelectKeys] = useState<number[]>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<selVideosModel[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(!refresh);
|
||||
}, [props.open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.open ? (
|
||||
<Modal
|
||||
title="视频库"
|
||||
centered
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
props.onCancel();
|
||||
}}
|
||||
open={true}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
props.onSelected(selectKeys, selectVideos);
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<div className="float-left mt-24">
|
||||
<UploadVideoSub
|
||||
label="视频"
|
||||
defaultCheckedList={props.defaultKeys}
|
||||
open={refresh}
|
||||
onSelected={(arr: any[], videos: any[]) => {
|
||||
setSelectKeys(arr);
|
||||
setSelectVideos(videos);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
88
playedu-admin/src/compenents/tree-adminroles/index.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { Tree } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import { adminRole } from "../../api/index";
|
||||
|
||||
interface Option {
|
||||
key: string | number;
|
||||
title: any;
|
||||
children: any[];
|
||||
}
|
||||
|
||||
interface PropInterface {
|
||||
refresh: boolean;
|
||||
roleDelSuccess: boolean;
|
||||
type: string;
|
||||
text: string;
|
||||
onUpdate: (keys: any, title: any, isSuper: boolean) => void;
|
||||
}
|
||||
|
||||
export const TreeAdminroles = (props: PropInterface) => {
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectKey, setSelectKey] = useState<any>([]);
|
||||
const [superId, setSuperId] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
onSelect([], "");
|
||||
}, [props.roleDelSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
adminRole.adminRoleList().then((res: any) => {
|
||||
let adminrole = res.data;
|
||||
let superId = 0;
|
||||
if (adminrole.length > 0) {
|
||||
const new_arr: Option[] = [];
|
||||
for (let i = 0; i < adminrole.length; i++) {
|
||||
new_arr.push({
|
||||
title: adminrole[i].name,
|
||||
key: adminrole[i].id,
|
||||
children: [],
|
||||
});
|
||||
if (adminrole[i].slug === "super-role") {
|
||||
superId = adminrole[i].id;
|
||||
}
|
||||
}
|
||||
setTreeData(new_arr);
|
||||
}
|
||||
setSuperId(superId);
|
||||
});
|
||||
}, [props.refresh]);
|
||||
|
||||
const onSelect = (selectedKeys: any, info: any) => {
|
||||
let label = "全部" + props.text;
|
||||
if (info) {
|
||||
label = info.node.title;
|
||||
}
|
||||
let isSuper = false;
|
||||
if (selectedKeys[0] === superId && superId !== 0) {
|
||||
isSuper = true;
|
||||
}
|
||||
props.onUpdate(selectedKeys, label, isSuper);
|
||||
setSelectKey(selectedKeys);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="playedu-tree">
|
||||
<div
|
||||
className={
|
||||
selectKey.length === 0
|
||||
? "mb-8 category-label active"
|
||||
: "mb-8 category-label"
|
||||
}
|
||||
onClick={() => {
|
||||
onSelect([], "");
|
||||
}}
|
||||
>
|
||||
<div className="j-b-flex">
|
||||
<span>全部{props.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tree
|
||||
onSelect={onSelect}
|
||||
selectedKeys={selectKey}
|
||||
treeData={treeData}
|
||||
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
109
playedu-admin/src/compenents/tree-category/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { Tree } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface Option {
|
||||
key: string | number;
|
||||
title: any;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
interface PropInterface {
|
||||
type: string;
|
||||
text: string;
|
||||
selected: any;
|
||||
onUpdate: (keys: any, title: any) => void;
|
||||
}
|
||||
|
||||
export const TreeCategory = (props: PropInterface) => {
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [selectKey, setSelectKey] = useState<number[]>([]);
|
||||
const resourceCategories = useSelector(
|
||||
(state: any) => state.systemConfig.value.resourceCategories
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selected && props.selected.length > 0) {
|
||||
setSelectKey(props.selected);
|
||||
}
|
||||
}, [props.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(resourceCategories) !== "{}") {
|
||||
const new_arr: Option[] = checkArr(resourceCategories, 0);
|
||||
if (props.type === "no-cate") {
|
||||
new_arr.unshift({
|
||||
key: 0,
|
||||
title: <span className="tree-title-elli">未分类</span>,
|
||||
});
|
||||
}
|
||||
setTreeData(new_arr);
|
||||
}
|
||||
}, [resourceCategories]);
|
||||
|
||||
const checkArr = (categories: CategoriesBoxModel, id: number) => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < categories[id].length; i++) {
|
||||
if (!categories[categories[id][i].id]) {
|
||||
let name = (
|
||||
<span className="tree-title-elli">{categories[id][i].name}</span>
|
||||
);
|
||||
arr.push({
|
||||
title: name,
|
||||
key: categories[id][i].id,
|
||||
});
|
||||
} else {
|
||||
let name = (
|
||||
<span className="tree-title-elli">{categories[id][i].name}</span>
|
||||
);
|
||||
const new_arr: Option[] = checkArr(categories, categories[id][i].id);
|
||||
arr.push({
|
||||
title: name,
|
||||
key: categories[id][i].id,
|
||||
children: new_arr,
|
||||
});
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const onSelect = (selectedKeys: any, info: any) => {
|
||||
let label = "全部" + props.text;
|
||||
if (info) {
|
||||
label = info.node.title.props.children;
|
||||
}
|
||||
props.onUpdate(selectedKeys, label);
|
||||
setSelectKey(selectedKeys);
|
||||
};
|
||||
|
||||
const onExpand = (selectedKeys: any, info: any) => {};
|
||||
|
||||
return (
|
||||
<div className="playedu-tree">
|
||||
<div
|
||||
className={
|
||||
selectKey.length === 0
|
||||
? "mb-8 category-label active"
|
||||
: "mb-8 category-label"
|
||||
}
|
||||
onClick={() => {
|
||||
onSelect([], "");
|
||||
}}
|
||||
>
|
||||
<div className="j-b-flex">
|
||||
<span>全部{props.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
{treeData.length > 0 && (
|
||||
<Tree
|
||||
onSelect={onSelect}
|
||||
selectedKeys={selectKey}
|
||||
onExpand={onExpand}
|
||||
treeData={treeData}
|
||||
// defaultExpandAll={true}
|
||||
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
177
playedu-admin/src/compenents/tree-department/index.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { Tree } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import { department } from "../../api/index";
|
||||
|
||||
interface Option {
|
||||
key: string | number;
|
||||
title: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
interface PropInterface {
|
||||
text: string;
|
||||
showNum: boolean;
|
||||
selected: any;
|
||||
refresh: boolean;
|
||||
onUpdate: (keys: any, title: any) => void;
|
||||
}
|
||||
|
||||
export const TreeDepartment = (props: PropInterface) => {
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectKey, setSelectKey] = useState<number[]>([]);
|
||||
const [userTotal, setUserTotal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selected && props.selected.length > 0) {
|
||||
setSelectKey(props.selected);
|
||||
}
|
||||
}, [props.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
department.departmentList({}).then((res: any) => {
|
||||
const departments: DepartmentsBoxModel = res.data.departments;
|
||||
const departCount: DepIdsModel = res.data.dep_user_count;
|
||||
setUserTotal(res.data.user_total);
|
||||
if (JSON.stringify(departments) !== "{}") {
|
||||
if (props.showNum) {
|
||||
const new_arr: any[] = checkNewArr(departments, 0, departCount);
|
||||
setTreeData(new_arr);
|
||||
} else {
|
||||
const new_arr: any[] = checkArr(departments, 0);
|
||||
setTreeData(new_arr);
|
||||
}
|
||||
} else {
|
||||
const new_arr: Option[] = [
|
||||
{
|
||||
key: "",
|
||||
title: "全部",
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
setTreeData(new_arr);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, [props.refresh]);
|
||||
|
||||
const checkNewArr = (
|
||||
departments: DepartmentsBoxModel,
|
||||
id: number,
|
||||
counts: any
|
||||
) => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < departments[id].length; i++) {
|
||||
if (!departments[departments[id][i].id]) {
|
||||
arr.push({
|
||||
title: getNewTitle(
|
||||
departments[id][i].name,
|
||||
departments[id][i].id,
|
||||
counts
|
||||
),
|
||||
key: departments[id][i].id,
|
||||
});
|
||||
} else {
|
||||
const new_arr: any = checkNewArr(
|
||||
departments,
|
||||
departments[id][i].id,
|
||||
counts
|
||||
);
|
||||
arr.push({
|
||||
title: getNewTitle(
|
||||
departments[id][i].name,
|
||||
departments[id][i].id,
|
||||
counts
|
||||
),
|
||||
key: departments[id][i].id,
|
||||
children: new_arr,
|
||||
});
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const checkArr = (departments: DepartmentsBoxModel, id: number) => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < departments[id].length; i++) {
|
||||
if (!departments[departments[id][i].id]) {
|
||||
arr.push({
|
||||
title: (
|
||||
<span className="tree-title-elli">{departments[id][i].name}</span>
|
||||
),
|
||||
key: departments[id][i].id,
|
||||
});
|
||||
} else {
|
||||
const new_arr: any[] = checkArr(departments, departments[id][i].id);
|
||||
arr.push({
|
||||
title: (
|
||||
<span className="tree-title-elli">{departments[id][i].name}</span>
|
||||
),
|
||||
key: departments[id][i].id,
|
||||
children: new_arr,
|
||||
});
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const getNewTitle = (title: any, id: number, counts: any) => {
|
||||
if (counts) {
|
||||
let value = counts[id] || 0;
|
||||
return (
|
||||
<span className="tree-title-elli">{title + "(" + value + ")"}</span>
|
||||
);
|
||||
} else {
|
||||
return <span className="tree-title-elli">{title}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = (selectedKeys: any, info: any) => {
|
||||
let label = "全部" + props.text;
|
||||
if (info) {
|
||||
label = info.node.title.props.children;
|
||||
}
|
||||
if (selectedKeys.length <= 1) {
|
||||
props.onUpdate(selectedKeys, label);
|
||||
setSelectKey(selectedKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const onExpand = (selectedKeys: any, info: any) => {
|
||||
let label = "全部" + props.text;
|
||||
if (info) {
|
||||
label = info.node.title.props.children;
|
||||
}
|
||||
if (selectedKeys.length <= 1) {
|
||||
props.onUpdate(selectedKeys, label);
|
||||
setSelectKey(selectedKeys);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="playedu-tree">
|
||||
<div
|
||||
className={
|
||||
selectKey.length === 0
|
||||
? "mb-8 category-label active"
|
||||
: "mb-8 category-label"
|
||||
}
|
||||
onClick={() => onSelect([], "")}
|
||||
>
|
||||
全部{props.text}
|
||||
{props.showNum && userTotal ? "(" + userTotal + ")" : ""}
|
||||
</div>
|
||||
{treeData.length > 0 && (
|
||||
<Tree
|
||||
selectedKeys={selectKey}
|
||||
onSelect={onSelect}
|
||||
onExpand={onExpand}
|
||||
treeData={treeData}
|
||||
// defaultExpandAll={true}
|
||||
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|