引入前端界面
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
|
||||
202
playedu-admin/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 杭州白书科技有限公司
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
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
BIN
playedu-admin/public/thumb/thumb1.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
playedu-admin/public/thumb/thumb2.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
playedu-admin/public/thumb/thumb3.png
Normal file
|
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>
|
||||
);
|
||||
};
|
||||
250
playedu-admin/src/compenents/tree-deps/index.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Spin, Tree, Checkbox } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import { department } from "../../api/index";
|
||||
import type { TreeProps } from "antd/es/tree";
|
||||
import type { CheckboxChangeEvent } from "antd/es/checkbox";
|
||||
|
||||
interface Option {
|
||||
key: string | number;
|
||||
title: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
interface PropInterface {
|
||||
type: string;
|
||||
refresh: boolean;
|
||||
showNum: boolean;
|
||||
selected: any;
|
||||
onUpdate: (keys: any, nodes: any) => void;
|
||||
}
|
||||
|
||||
export const TreeDeps = (props: PropInterface) => {
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectKeys, setSelectKeys] = useState<number[]>([]);
|
||||
const [checkKeys, setCheckKeys] = useState<number[]>([]);
|
||||
const [userTotal, setUserTotal] = useState(0);
|
||||
const [showLeafIcon, setShowLeafIcon] = useState<boolean | React.ReactNode>(
|
||||
true
|
||||
);
|
||||
const [expandKeys, setExpandKeys] = useState<number[]>([]);
|
||||
const [totalKeys, setTotalKeys] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectKeys(props.selected);
|
||||
setCheckKeys(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);
|
||||
const topLevelParentKeyList = new_arr.map((node: any) => node.key);
|
||||
setTotalKeys(topLevelParentKeyList);
|
||||
} else {
|
||||
const new_arr: any[] = checkArr(departments, 0);
|
||||
setTreeData(new_arr);
|
||||
const topLevelParentKeyList = new_arr.map((node: any) => node.key);
|
||||
setTotalKeys(topLevelParentKeyList);
|
||||
}
|
||||
} else {
|
||||
const new_arr: Option[] = [];
|
||||
setTreeData(new_arr);
|
||||
setTotalKeys([]);
|
||||
}
|
||||
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: TreeProps["onSelect"] = (selectedKeys: any, info: any) => {
|
||||
console.log("onCheck", selectedKeys, info);
|
||||
let nodes = [];
|
||||
if (info) {
|
||||
nodes = info.selectedNodes;
|
||||
}
|
||||
props.onUpdate(selectedKeys, nodes);
|
||||
setSelectKeys(selectedKeys);
|
||||
};
|
||||
|
||||
const onExpand = (expandedKeys: any, info: any) => {
|
||||
console.log("onExpand", expandedKeys, info);
|
||||
if (checkKeys.includes(info.node.key)) {
|
||||
// 关闭该节点的展开
|
||||
return;
|
||||
}
|
||||
setExpandKeys(expandedKeys);
|
||||
};
|
||||
|
||||
const onCheck: TreeProps["onCheck"] = (checkedKeys: any, info: any) => {
|
||||
console.log("onCheck", checkedKeys, info);
|
||||
let nodes = [];
|
||||
if (info) {
|
||||
nodes = info.checkedNodes;
|
||||
}
|
||||
props.onUpdate(checkedKeys.checked, nodes);
|
||||
setCheckKeys(checkedKeys.checked);
|
||||
if (info.checked && info.node.children) {
|
||||
setExpandKeys(expandKeys.filter((key) => key !== info.node.key)); // 关闭父节点的展开
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e: CheckboxChangeEvent) => {
|
||||
console.log(`checked = ${e.target.checked}`);
|
||||
if (e.target.checked) {
|
||||
const topLevelParentKeyList = treeData.map((node: any) => node.key);
|
||||
const topLevelParentNodes = treeData.map((node: any) => node);
|
||||
// 设置最外层父级节点为勾选状态
|
||||
if (props.type === "single") {
|
||||
console.log("全选");
|
||||
setSelectKeys([]);
|
||||
setExpandKeys([]);
|
||||
} else {
|
||||
setCheckKeys(topLevelParentKeyList);
|
||||
setExpandKeys([]);
|
||||
}
|
||||
props.onUpdate(topLevelParentKeyList, topLevelParentNodes);
|
||||
} else {
|
||||
setSelectKeys([]);
|
||||
setExpandKeys([]);
|
||||
setCheckKeys([]);
|
||||
props.onUpdate([], []);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<div className="text-center mt-30">
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: loading ? "none" : "block" }}>
|
||||
{treeData.length > 0 && (
|
||||
<>
|
||||
{props.type === "single" ? (
|
||||
<div className="playedu-old-tree">
|
||||
<div style={{ height: 40 }} className="d-flex">
|
||||
<Checkbox onChange={onChange}>全选</Checkbox>
|
||||
</div>
|
||||
<Tree
|
||||
onSelect={onSelect}
|
||||
treeData={treeData}
|
||||
onExpand={(expandedKeys: any, info: any) => {
|
||||
setExpandKeys(expandedKeys);
|
||||
}}
|
||||
selectedKeys={selectKeys}
|
||||
expandedKeys={expandKeys}
|
||||
showLine={{ showLeafIcon }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="playedu-old-tree">
|
||||
<div style={{ height: 40 }} className="d-flex">
|
||||
<Checkbox
|
||||
checked={
|
||||
checkKeys.toString() === totalKeys.toString()
|
||||
? true
|
||||
: false
|
||||
}
|
||||
onChange={onChange}
|
||||
>
|
||||
全选
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Tree
|
||||
selectable={false}
|
||||
checkable
|
||||
checkStrictly={true}
|
||||
checkedKeys={checkKeys}
|
||||
multiple={true}
|
||||
onExpand={onExpand}
|
||||
expandedKeys={expandKeys}
|
||||
showLine={{ showLeafIcon }}
|
||||
onCheck={onCheck}
|
||||
treeData={treeData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
267
playedu-admin/src/compenents/upload-courseware-button/index.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Row,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { generateUUID } from "../../utils";
|
||||
import { minioMergeVideo, minioUploadId } from "../../api/upload";
|
||||
import { UploadChunk } from "../../js/minio-upload-chunk";
|
||||
|
||||
interface PropsInterface {
|
||||
categoryIds: number[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const UploadCoursewareButton = (props: PropsInterface) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const localFileList = useRef<FileItem[]>([]);
|
||||
const intervalId = useRef<number>();
|
||||
const [fileList, setFileList] = useState<FileItem[]>([]);
|
||||
|
||||
const getMinioUploadId = async (extension: string) => {
|
||||
let resp: any = await minioUploadId(extension);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
intervalId.current = setInterval(() => {
|
||||
if (localFileList.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < localFileList.current.length; i++) {
|
||||
if (localFileList.current[i].upload.status === 0) {
|
||||
localFileList.current[i].upload.handler.start();
|
||||
break;
|
||||
}
|
||||
if (localFileList.current[i].upload.status === 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
console.log("定时器已创建", intervalId.current);
|
||||
} else {
|
||||
window.clearInterval(intervalId.current);
|
||||
console.log("定时器已销毁");
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const uploadProps = {
|
||||
multiple: true,
|
||||
beforeUpload: async (file: File) => {
|
||||
if (file.size === 0) {
|
||||
message.error(`文件 ${file.name} 为空文件`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
let extension: any = file.name.split(".");
|
||||
extension = extension[extension.length - 1];
|
||||
if (
|
||||
extension === "rar" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
|
||||
file.type === "application/msword" ||
|
||||
file.type === "application/vnd.ms-word.document.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-word.template.macroEnabled.12" ||
|
||||
file.type === "text/plain" ||
|
||||
file.type === "application/pdf" ||
|
||||
file.type === "application/x-zip-compressed" ||
|
||||
file.type === "application/octet-stream" ||
|
||||
file.type === "application/zip" ||
|
||||
file.type === "application/x-rar" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
|
||||
file.type === "application/vnd.ms-excel" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template" ||
|
||||
file.type === "application/vnd.ms-excel.sheet.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.template.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.addin.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.sheet.binary.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-powerpoint" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow" ||
|
||||
file.type === "application/vnd.ms-powerpoint.addin.macroEnabled.12" ||
|
||||
file.type ===
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"
|
||||
) {
|
||||
// 添加到本地待上传
|
||||
let data = await getMinioUploadId(extension);
|
||||
let run = new UploadChunk(file, data["upload_id"], data["filename"]);
|
||||
let item: FileItem = {
|
||||
id: generateUUID(),
|
||||
file: file,
|
||||
upload: {
|
||||
handler: run,
|
||||
progress: 0,
|
||||
status: 0,
|
||||
remark: "",
|
||||
},
|
||||
};
|
||||
item.upload.handler.on("success", () => {
|
||||
minioMergeVideo(
|
||||
data["filename"],
|
||||
data["upload_id"],
|
||||
props.categoryIds.join(","),
|
||||
item.file.name,
|
||||
extension,
|
||||
item.file.size,
|
||||
0,
|
||||
""
|
||||
).then(() => {
|
||||
item.upload.progress = 100;
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
});
|
||||
item.upload.handler.on("progress", (p: number) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.progress = p >= 100 ? 99 : p;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
item.upload.handler.on("error", (msg: string) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.remark = msg;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
// 先插入到ref
|
||||
localFileList.current.push(item);
|
||||
// 再更新list
|
||||
setFileList([...localFileList.current]);
|
||||
} else {
|
||||
message.error(`${file.name} 并不是可上传文件`);
|
||||
}
|
||||
return Upload.LIST_IGNORE;
|
||||
},
|
||||
};
|
||||
|
||||
const closeWin = () => {
|
||||
if (fileList.length > 0) {
|
||||
fileList.forEach((item) => {
|
||||
if (item.upload.status !== 5 && item.upload.status !== 7) {
|
||||
item.upload.handler.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
props.onUpdate();
|
||||
localFileList.current = [];
|
||||
setFileList([]);
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
上传课件
|
||||
</Button>
|
||||
|
||||
{showModal ? (
|
||||
<Modal
|
||||
width={800}
|
||||
title="上传课件"
|
||||
open={true}
|
||||
onCancel={() => {
|
||||
closeWin();
|
||||
}}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
onOk={() => {
|
||||
closeWin();
|
||||
}}
|
||||
okText="完成"
|
||||
>
|
||||
<Row gutter={[0, 10]}>
|
||||
<Col span={24}>
|
||||
<Dragger {...uploadProps}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">请将文件拖拽到此处上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持一次上传多个 /
|
||||
支持word、excel、ppt、pdf、zip、rar、txt格式文件
|
||||
</p>
|
||||
</Dragger>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
columns={[
|
||||
{
|
||||
title: "课件",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (_, record) => <span>{record.file.name}</span>,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
{(record.file.size / 1024 / 1024).toFixed(2)}M
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
render: (_, record: FileItem) => (
|
||||
<>
|
||||
{record.upload.status === 0 ? (
|
||||
"等待上传"
|
||||
) : (
|
||||
<Progress
|
||||
size="small"
|
||||
steps={20}
|
||||
percent={record.upload.progress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.upload.status === 5 ? (
|
||||
<Tag color="red">{record.upload.remark}</Tag>
|
||||
) : null}
|
||||
|
||||
{record.upload.status === 7 ? (
|
||||
<Tag color="success">上传成功</Tag>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
dataSource={fileList}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
249
playedu-admin/src/compenents/upload-courseware-sub/index.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Col, Empty, Table, Spin, Typography, Input, Button } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { resource } from "../../api";
|
||||
import styles from "./index.module.less";
|
||||
import { TreeCategory } from "../../compenents";
|
||||
|
||||
interface VideoItem {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
defaultCheckedList: any[];
|
||||
label: string;
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: []) => void;
|
||||
}
|
||||
|
||||
export const UploadCoursewareSub = (props: PropsInterface) => {
|
||||
const [init, setInit] = useState(true);
|
||||
const [category_ids, setCategoryIds] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [videoList, setVideoList] = useState<VideoItem[]>([]);
|
||||
const [existingTypes, setExistingTypes] = useState<any>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||
const [title, setTitle] = useState("");
|
||||
|
||||
// 加载列表
|
||||
useEffect(() => {
|
||||
setInit(true);
|
||||
getvideoList();
|
||||
}, [props.open, category_ids, refresh, page, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultCheckedList.length > 0) {
|
||||
setSelectedRowKeys(props.defaultCheckedList);
|
||||
}
|
||||
}, [props.defaultCheckedList]);
|
||||
|
||||
// 获取列表
|
||||
const getvideoList = () => {
|
||||
setLoading(true);
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(
|
||||
page,
|
||||
size,
|
||||
"",
|
||||
"",
|
||||
title,
|
||||
"WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP",
|
||||
categoryIds
|
||||
)
|
||||
.then((res: any) => {
|
||||
setTotal(res.data.result.total);
|
||||
setExistingTypes(res.data.existing_types);
|
||||
setVideoList(res.data.result.data);
|
||||
setLoading(false);
|
||||
setInit(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setInit(false);
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
|
||||
// 重置列表
|
||||
const resetVideoList = () => {
|
||||
setPage(1);
|
||||
setVideoList([]);
|
||||
setTitle("");
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
const paginationProps = {
|
||||
current: page, //当前页码
|
||||
pageSize: size,
|
||||
total: total, // 总条数
|
||||
onChange: (page: number, pageSize: number) =>
|
||||
handlePageChange(page, pageSize), //改变页码的函数
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "课件",
|
||||
render: (_, record: any) => (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-file"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="video-title ml-8">{record.name}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
render: (_, record: any) => <span>{record.type}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
|
||||
let row: any = selectedRows;
|
||||
let arrVideos: any = [];
|
||||
if (row) {
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
if (props.defaultCheckedList.indexOf(row[i].id) === -1) {
|
||||
arrVideos.push({
|
||||
name: row[i].name,
|
||||
type: row[i].type,
|
||||
rid: row[i].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
props.onSelected(selectedRowKeys, arrVideos);
|
||||
}
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: props.defaultCheckedList.indexOf(record.id) !== -1, //禁用的条件
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row style={{ width: 752, minHeight: 520 }}>
|
||||
<Col span={7}>
|
||||
{init && (
|
||||
<div className="float-left text-center mt-30">
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="float-left"
|
||||
style={{ display: init ? "none" : "block" }}
|
||||
>
|
||||
<TreeCategory
|
||||
selected={[]}
|
||||
type="no-cate"
|
||||
text={props.label}
|
||||
onUpdate={(keys: any) => setCategoryIds(keys)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Row style={{ marginBottom: 24, paddingLeft: 10 }}>
|
||||
<div className="float-left j-b-flex">
|
||||
<div className="d-flex"></div>
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>名称:</Typography.Text>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入名称关键字"
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<Button className="mr-16" onClick={resetVideoList}>
|
||||
重 置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
>
|
||||
查 询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
{init && (
|
||||
<div className="float-left text-center mt-30">
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={styles["video-list"]}
|
||||
style={{ display: init ? "none" : "block" }}
|
||||
>
|
||||
{videoList.length === 0 && (
|
||||
<Col span={24} style={{ marginTop: 150 }}>
|
||||
<Empty description="暂无课件" />
|
||||
</Col>
|
||||
)}
|
||||
{videoList.length > 0 && (
|
||||
<div className="list-select-column-box c-flex">
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
...rowSelection,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={videoList}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
.categoryItem {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
&.active {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryTitle {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.checked {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #ff4d4f;
|
||||
border-radius: 3px;
|
||||
border: 2px solid #ff4d4f;
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
184
playedu-admin/src/compenents/upload-image-button/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
Image,
|
||||
Empty,
|
||||
message,
|
||||
Pagination,
|
||||
} from "antd";
|
||||
import { resource, resourceCategory } from "../../api";
|
||||
import styles from "./index.module.less";
|
||||
import { CreateResourceCategory } from "../create-rs-category";
|
||||
import { CloseOutlined, CheckOutlined } from "@ant-design/icons";
|
||||
import { UploadImageSub } from "./upload-image-sub";
|
||||
import { TreeCategory } from "../../compenents";
|
||||
|
||||
interface Option {
|
||||
id: string | number;
|
||||
name: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
interface ImageItem {
|
||||
id: number;
|
||||
category_id: number;
|
||||
name: string;
|
||||
extension: string;
|
||||
size: number;
|
||||
disk: string;
|
||||
file_id: string;
|
||||
path: string;
|
||||
url: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
text: any;
|
||||
onSelected: (url: string) => void;
|
||||
}
|
||||
|
||||
export const UploadImageButton = (props: PropsInterface) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [category_ids, setCategoryIds] = useState<any>([]);
|
||||
|
||||
const [imageList, setImageList] = useState<ImageItem[]>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(15);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selected, setSelected] = useState<string>("");
|
||||
|
||||
// 获取图片列表
|
||||
const getImageList = () => {
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(page, size, "", "", "", "IMAGE", categoryIds)
|
||||
.then((res: any) => {
|
||||
setTotal(res.data.result.total);
|
||||
setImageList(res.data.result.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
// 重置列表
|
||||
const resetImageList = () => {
|
||||
setPage(1);
|
||||
setImageList([]);
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
// 加载图片列表
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
getImageList();
|
||||
}
|
||||
}, [category_ids, refresh, page, size, showModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
{props.text ? props.text : "上传图片"}
|
||||
</Button>
|
||||
|
||||
{showModal && (
|
||||
<Modal
|
||||
title="图片素材库"
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
open={true}
|
||||
width={820}
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
if (!selected) {
|
||||
message.error("请选择图片后确定");
|
||||
return;
|
||||
}
|
||||
props.onSelected(selected);
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
<Row style={{ width: 752, minHeight: 520, marginTop: 24 }}>
|
||||
<Col span={7}>
|
||||
<TreeCategory
|
||||
selected={category_ids}
|
||||
type="no-cate"
|
||||
text={"图片"}
|
||||
onUpdate={(keys: any) => {
|
||||
setSelected("");
|
||||
setCategoryIds(keys);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Row style={{ marginBottom: 24, paddingLeft: 10 }}>
|
||||
<Col span={24}>
|
||||
<UploadImageSub
|
||||
categoryIds={category_ids}
|
||||
onUpdate={() => {
|
||||
resetImageList();
|
||||
}}
|
||||
></UploadImageSub>
|
||||
</Col>
|
||||
</Row>
|
||||
{imageList.length === 0 && (
|
||||
<Col span={24}>
|
||||
<Empty description="暂无图片" />
|
||||
</Col>
|
||||
)}
|
||||
<div className="image-list-box">
|
||||
{imageList.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="image-item"
|
||||
style={{ backgroundImage: `url(${item.url})` }}
|
||||
onClick={() => {
|
||||
setSelected(item.url);
|
||||
}}
|
||||
>
|
||||
{selected.indexOf(item.url) !== -1 && (
|
||||
<i
|
||||
className={styles.checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelected("");
|
||||
}}
|
||||
>
|
||||
<CheckOutlined />
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{imageList.length > 0 && (
|
||||
<Col
|
||||
span={24}
|
||||
style={{ display: "flex", flexDirection: "row-reverse" }}
|
||||
>
|
||||
<Pagination
|
||||
showSizeChanger
|
||||
onChange={(currentPage, currentSize) => {
|
||||
setPage(currentPage);
|
||||
setSize(currentSize);
|
||||
}}
|
||||
defaultCurrent={page}
|
||||
total={total}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Button, message, Modal } from "antd";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import { useState } from "react";
|
||||
import config from "../../../js/config";
|
||||
import { getToken, checkUrl } from "../../../utils";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
|
||||
interface PropsInterface {
|
||||
categoryIds: number[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const UploadImageSub = (props: PropsInterface) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const uploadProps = {
|
||||
name: "file",
|
||||
multiple: true,
|
||||
action:
|
||||
checkUrl(config.app_url) +
|
||||
"backend/v1/upload/minio?category_ids=" +
|
||||
props.categoryIds.join(","),
|
||||
headers: {
|
||||
authorization: "Bearer " + getToken(),
|
||||
},
|
||||
onChange(info: any) {
|
||||
const { status, response } = info.file;
|
||||
if (status === "done") {
|
||||
if (response.code === 0) {
|
||||
message.success(`${info.file.name} 上传成功`);
|
||||
} else {
|
||||
message.error(response.msg);
|
||||
}
|
||||
} else if (status === "error") {
|
||||
message.error(`${info.file.name} 上传失败`);
|
||||
}
|
||||
},
|
||||
showUploadList: {
|
||||
showRemoveIcon: false,
|
||||
showDownloadIcon: false,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
</Button>
|
||||
|
||||
{showModal && (
|
||||
<Modal
|
||||
open={true}
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
props.onUpdate();
|
||||
setShowModal(false);
|
||||
}}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Dragger {...uploadProps}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">请将图片拖拽到此处上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持一次上传多个 / 支持 png,jpg,jpeg,gif 格式图片
|
||||
</p>
|
||||
</Dragger>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
41
playedu-admin/src/compenents/upload-video-button/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Button } from "antd";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { uploadAction } from "../../store/user/loginUserSlice";
|
||||
|
||||
interface PropsInterface {
|
||||
categoryIds: number[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const UploadVideoButton = (props: PropsInterface) => {
|
||||
const dispatch = useDispatch();
|
||||
const uploadStatus = useSelector(
|
||||
(state: any) => state.loginUser.value.uploadStatus
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uploadStatus) {
|
||||
props.onUpdate();
|
||||
}
|
||||
}, [uploadStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
uploadAction({
|
||||
uploadStatus: true,
|
||||
uploadCateIds: props.categoryIds,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
上传视频
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
.float-button {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 20px;
|
||||
z-index: 50;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
span {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
289
playedu-admin/src/compenents/upload-video-float-button/index.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Row,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { generateUUID, parseVideo } from "../../utils";
|
||||
import styles from "./index.module.less";
|
||||
import { minioMergeVideo, minioUploadId } from "../../api/upload";
|
||||
import { UploadChunk } from "../../js/minio-upload-chunk";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { uploadAction } from "../../store/user/loginUserSlice";
|
||||
import upIcon from "../../assets/images/commen/upload.png";
|
||||
|
||||
export const UploadVideoFloatButton = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const localFileList = useRef<FileItem[]>([]);
|
||||
const intervalId = useRef<number>();
|
||||
const intervalId2 = useRef<number>();
|
||||
const [successNum, setSuccessNum] = useState(0);
|
||||
const [fileList, setFileList] = useState<FileItem[]>([]);
|
||||
const uploadStatus = useSelector(
|
||||
(state: any) => state.loginUser.value.uploadStatus
|
||||
);
|
||||
const categoryIds = useSelector(
|
||||
(state: any) => state.loginUser.value.uploadCateIds
|
||||
);
|
||||
|
||||
const getMinioUploadId = async () => {
|
||||
let resp: any = await minioUploadId("mp4");
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadStatus) {
|
||||
setShowModal(true);
|
||||
intervalId.current = setInterval(() => {
|
||||
let num = localFileList.current.filter(
|
||||
(it) => it.upload.status === 7
|
||||
).length;
|
||||
setSuccessNum(num);
|
||||
}, 5000);
|
||||
let timeDiv = document.createElement("div");
|
||||
document.body.appendChild(timeDiv);
|
||||
intervalId2.current = setInterval(() => {
|
||||
timeDiv && timeDiv.click();
|
||||
}, 10000);
|
||||
} else {
|
||||
window.clearInterval(intervalId.current);
|
||||
window.clearInterval(intervalId2.current);
|
||||
console.log("定时器已销毁");
|
||||
}
|
||||
}, [uploadStatus]);
|
||||
|
||||
const uploadProps = {
|
||||
multiple: true,
|
||||
beforeUpload: async (file: File, fileList: any) => {
|
||||
if (file.size > 2 * 1024 * 1024 * 1024) {
|
||||
message.error(`${file.name} 大小超过2G`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
if (fileList.length > 10) {
|
||||
message.config({ maxCount: 1 });
|
||||
message.error("单次最多上传10个视频");
|
||||
return Upload.LIST_IGNORE;
|
||||
} else {
|
||||
message.config({ maxCount: 10 });
|
||||
}
|
||||
if (file.type === "video/mp4") {
|
||||
// 视频封面解析 || 视频时长解析
|
||||
let videoInfo = await parseVideo(file);
|
||||
// 添加到本地待上传
|
||||
let data = await getMinioUploadId();
|
||||
let run = new UploadChunk(file, data["upload_id"], data["filename"]);
|
||||
let item: FileItem = {
|
||||
id: generateUUID(),
|
||||
file: file,
|
||||
upload: {
|
||||
handler: run,
|
||||
progress: 0,
|
||||
status: 0,
|
||||
remark: "",
|
||||
},
|
||||
video: {
|
||||
duration: videoInfo.duration,
|
||||
poster: videoInfo.poster,
|
||||
},
|
||||
};
|
||||
item.upload.handler.on("success", () => {
|
||||
minioMergeVideo(
|
||||
data["filename"],
|
||||
data["upload_id"],
|
||||
categoryIds.join(","),
|
||||
item.file.name,
|
||||
"mp4",
|
||||
item.file.size,
|
||||
item.video?.duration || 0,
|
||||
item.video?.poster || ""
|
||||
).then(() => {
|
||||
item.upload.progress = 100;
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
});
|
||||
item.upload.handler.on("progress", (p: number) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.progress = p >= 100 ? 99 : p;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
item.upload.handler.on("error", (msg: string) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.remark = msg;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
item.upload.handler.start();
|
||||
// 先插入到ref
|
||||
localFileList.current.push(item);
|
||||
// 再更新list
|
||||
setFileList([...localFileList.current]);
|
||||
} else {
|
||||
message.error(`${file.name} 并不是 mp4 视频文件`);
|
||||
}
|
||||
return Upload.LIST_IGNORE;
|
||||
},
|
||||
};
|
||||
|
||||
const closeWin = () => {
|
||||
if (fileList.length > 0) {
|
||||
fileList.forEach((item) => {
|
||||
if (item.upload.status !== 5 && item.upload.status !== 7) {
|
||||
item.upload.handler.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
localFileList.current = [];
|
||||
setFileList([]);
|
||||
setSuccessNum(0);
|
||||
setShowModal(false);
|
||||
dispatch(
|
||||
uploadAction({
|
||||
uploadStatus: false,
|
||||
uploadCateIds: [],
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{uploadStatus ? (
|
||||
<>
|
||||
<div
|
||||
style={{ display: showModal ? "none" : "flex" }}
|
||||
className={styles["float-button"]}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<img src={upIcon} />
|
||||
<span>
|
||||
视频上传成功 ({successNum}/{fileList.length})
|
||||
</span>
|
||||
</div>
|
||||
<Modal
|
||||
width={800}
|
||||
title="上传视频"
|
||||
open={showModal}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
closeWin();
|
||||
}}
|
||||
>
|
||||
<Row gutter={[0, 10]}>
|
||||
<Col span={24}>
|
||||
<Dragger {...uploadProps}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">请将视频拖拽到此处上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持一次上传多个 / 支持2G以内的mp4文件
|
||||
</p>
|
||||
</Dragger>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
columns={[
|
||||
{
|
||||
title: "视频",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (_, record) => <span>{record.file.name}</span>,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
{(record.file.size / 1024 / 1024).toFixed(2)}M
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
render: (_, record: FileItem) => (
|
||||
<>
|
||||
{record.upload.status === 0 ? (
|
||||
"等待上传"
|
||||
) : (
|
||||
<Progress
|
||||
size="small"
|
||||
steps={20}
|
||||
percent={record.upload.progress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.upload.status === 5 ? (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="b-n-link c-red"
|
||||
onClick={() => {
|
||||
record.upload.handler.retry();
|
||||
}}
|
||||
>
|
||||
失败.重试
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{record.upload.status === 7 ? (
|
||||
<Tag color="success">上传成功</Tag>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
dataSource={fileList}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="r-r-flex">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
closeWin();
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="mr-16"
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
最小化
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
240
playedu-admin/src/compenents/upload-video-sub/index.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Col, Empty, Table, Spin, Typography, Input, Button } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { resource } from "../../api";
|
||||
import styles from "./index.module.less";
|
||||
import { DurationText, TreeCategory } from "../../compenents";
|
||||
|
||||
interface VideoItem {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
defaultCheckedList: any[];
|
||||
label: string;
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: []) => void;
|
||||
}
|
||||
|
||||
export const UploadVideoSub = (props: PropsInterface) => {
|
||||
const [init, setInit] = useState(true);
|
||||
const [category_ids, setCategoryIds] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [videoList, setVideoList] = useState<VideoItem[]>([]);
|
||||
const [videosExtra, setVideoExtra] = useState<any>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||
const [title, setTitle] = useState("");
|
||||
|
||||
// 加载列表
|
||||
useEffect(() => {
|
||||
setInit(true);
|
||||
getvideoList();
|
||||
}, [props.open, category_ids, refresh, page, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultCheckedList.length > 0) {
|
||||
setSelectedRowKeys(props.defaultCheckedList);
|
||||
}
|
||||
}, [props.defaultCheckedList]);
|
||||
|
||||
// 获取列表
|
||||
const getvideoList = () => {
|
||||
setLoading(true);
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(page, size, "", "", title, "VIDEO", categoryIds)
|
||||
.then((res: any) => {
|
||||
setTotal(res.data.result.total);
|
||||
setVideoExtra(res.data.videos_extra);
|
||||
setVideoList(res.data.result.data);
|
||||
setLoading(false);
|
||||
setInit(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setInit(false);
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
|
||||
// 重置列表
|
||||
const resetVideoList = () => {
|
||||
setPage(1);
|
||||
setVideoList([]);
|
||||
setTitle("");
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
const paginationProps = {
|
||||
current: page, //当前页码
|
||||
pageSize: size,
|
||||
total: total, // 总条数
|
||||
onChange: (page: number, pageSize: number) =>
|
||||
handlePageChange(page, pageSize), //改变页码的函数
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "视频",
|
||||
render: (_, record: any) => (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-video"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="video-title ml-8">{record.name}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "时长",
|
||||
render: (_, record: any) => (
|
||||
<div>
|
||||
<DurationText
|
||||
duration={videosExtra[record.id].duration}
|
||||
></DurationText>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
|
||||
let row: any = selectedRows;
|
||||
let arrVideos: any = [];
|
||||
if (row) {
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
if (props.defaultCheckedList.indexOf(row[i].id) === -1) {
|
||||
arrVideos.push({
|
||||
name: row[i].name,
|
||||
type: row[i].type,
|
||||
rid: row[i].id,
|
||||
duration: videosExtra[row[i].id].duration,
|
||||
});
|
||||
}
|
||||
}
|
||||
props.onSelected(selectedRowKeys, arrVideos);
|
||||
}
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: props.defaultCheckedList.indexOf(record.id) !== -1, //禁用的条件
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row style={{ width: 752, minHeight: 520 }}>
|
||||
<Col span={7}>
|
||||
<div className="float-left">
|
||||
<TreeCategory
|
||||
selected={[]}
|
||||
type="no-cate"
|
||||
text={props.label}
|
||||
onUpdate={(keys: any) => setCategoryIds(keys)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Row style={{ marginBottom: 24, paddingLeft: 10 }}>
|
||||
<div className="float-left j-b-flex">
|
||||
<div className="d-flex"></div>
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>名称:</Typography.Text>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入名称关键字"
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<Button className="mr-16" onClick={resetVideoList}>
|
||||
重 置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
>
|
||||
查 询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
{init && (
|
||||
<div className="float-left text-center mt-30">
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={styles["video-list"]}
|
||||
style={{ display: init ? "none" : "block" }}
|
||||
>
|
||||
{videoList.length === 0 && (
|
||||
<Col span={24} style={{ marginTop: 150 }}>
|
||||
<Empty description="暂无视频" />
|
||||
</Col>
|
||||
)}
|
||||
{videoList.length > 0 && (
|
||||
<div className="list-select-column-box c-flex">
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
...rowSelection,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={videoList}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||