!14 存储桶改为private

* 后台 使用许可页面
* 优化:移除API访问地址配置
* 后台、pc、h5 删除无用配置
* docker部署优化
* 2.0 networkMode=bridge
* changelog
* 学员端权限为空报错
* h5 我的页面请求优化
* 缓存查询
* 后台 学员列表报错、线上课-上架时间字段优化
* 后台、pc、h5 使用签名地址
* 学员端接口修改
* 后台、pc 使用签名地址
* 后台 使用签名地址
* 上传接口
* 上传接口
* 系统配置
* 线上课封面
* bucket由public改为private
* 资源相关表实体及对象修改
* 统一数据库脚本
This commit is contained in:
白书科技
2025-05-22 07:23:06 +00:00
parent c206fa4bf2
commit 12daa31ab9
134 changed files with 10054 additions and 1231 deletions

3635
playedu-pc/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -18,6 +18,7 @@ import { ChangePasswordModel } from "../change-password";
import { UserInfoModel } from "../user-info";
import { ExclamationCircleFilled } from "@ant-design/icons";
import logo from "../../assets/logo.png";
import memberDefaultAvatar from "../../assets/thumb/avatar.png";
const { confirm } = Modal;
export const Header: React.FC = () => {
@@ -35,6 +36,9 @@ export const Header: React.FC = () => {
const [departmentsMenu, setDepartmentsMenu] = useState<any>([]);
const [currentDepartment, setCurrentDepartment] = useState<string>("");
const [currentNav, serCurrentNav] = useState(location.pathname);
const resourceUrl = useSelector(
(state: any) => state.loginUser.value.resourceUrl
);
useEffect(() => {
if (departments.length > 0) {
@@ -178,7 +182,7 @@ export const Header: React.FC = () => {
<div className="d-flex">
<Link to="/" className={styles["App-logo"]}>
{/* 此处为版权标识,严禁删改 */}
<img src={config.systemLogo || logo} />
<img src={config.resourceUrl[config.systemLogo] || logo} />
</Link>
<div className={styles["navs"]}>
{navs.map((item: any) => (
@@ -221,7 +225,11 @@ export const Header: React.FC = () => {
<Image
loading="lazy"
style={{ width: 36, height: 36, borderRadius: "50%" }}
src={user.avatar}
src={
user.avatar === -1
? memberDefaultAvatar
: resourceUrl[user.avatar]
}
preview={false}
/>
<span className="ml-8 c-admin">{user.name}</span>

View File

@@ -13,7 +13,10 @@ export const NoHeader: React.FC = () => {
return (
<div className={styles["app-header"]}>
<div className={styles["main-header"]}>
<img src={config.systemLogo || logo} className={styles["App-logo"]} />
<img
src={config.resourceUrl[config.systemLogo] || logo}
className={styles["App-logo"]}
/>
</div>
</div>
);

View File

@@ -7,6 +7,7 @@ import { loginAction } from "../../store/user/loginUserSlice";
import type { UploadProps } from "antd";
import config from "../../js/config";
import { getToken, changeAppUrl } from "../../utils/index";
import memberDefaultAvatar from "../../assets/thumb/avatar.png";
interface PropInterface {
open: boolean;
@@ -17,9 +18,10 @@ export const UserInfoModel: React.FC<PropInterface> = ({ open, onCancel }) => {
const dispatch = useDispatch();
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(true);
const [avatar, setAvatar] = useState<string>("");
const [avatar, setAvatar] = useState(0);
const [name, setName] = useState<string>("");
const [idCard, setIdCard] = useState<string>("");
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
useEffect(() => {
if (open) {
@@ -30,6 +32,7 @@ export const UserInfoModel: React.FC<PropInterface> = ({ open, onCancel }) => {
const getUser = () => {
user.detail().then((res: any) => {
setAvatar(res.data.user.avatar);
setResourceUrl(res.data.resource_url);
setName(res.data.user.name);
setIdCard(res.data.user.id_card);
dispatch(loginAction(res.data));
@@ -106,7 +109,11 @@ export const UserInfoModel: React.FC<PropInterface> = ({ open, onCancel }) => {
width={60}
height={60}
style={{ borderRadius: "50%" }}
src={avatar}
src={
avatar === -1
? memberDefaultAvatar
: resourceUrl[avatar]
}
preview={false}
/>
)}

View File

@@ -7,6 +7,9 @@ import mediaIcon from "../../assets/images/commen/icon-medal.png";
import { HourCompenent } from "./compenents/hour";
import { Empty } from "../../compenents";
import iconRoute from "../../assets/images/commen/icon-route.png";
import defaultThumb1 from "../../assets/thumb/thumb1.png";
import defaultThumb2 from "../../assets/thumb/thumb2.png";
import defaultThumb3 from "../../assets/thumb/thumb3.png";
type TabModel = {
key: number;
@@ -56,6 +59,7 @@ const CoursePage = () => {
);
const [tabKey, setTabKey] = useState(Number(result.get("tab") || 1));
const [attachments, setAttachments] = useState<AttachModel[]>([]);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [items, setItems] = useState<TabModel[]>([]);
useEffect(() => {
@@ -70,6 +74,7 @@ const CoursePage = () => {
setCourse(res.data.course);
setChapters(res.data.chapters);
setHours(res.data.hours);
setResourceUrl(res.data.resource_url);
if (res.data.learn_record) {
setLearnRecord(res.data.learn_record);
}
@@ -104,9 +109,35 @@ const CoursePage = () => {
navigate("/course/" + params.courseId + "?tab=" + key);
};
const downLoadFile = (cid: number, id: number) => {
const downLoadFile = (
cid: number,
id: number,
rid: number,
fileName: string,
type: string
) => {
Course.downloadAttachment(cid, id).then((res: any) => {
window.open(res.data.download_url);
if (type === "TXT") {
fetch(res.data.resource_url[rid])
.then((response) => response.blob())
.then((blob) => {
const n_url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = n_url;
a.download = fileName; // 设置下载的文件名
document.body.appendChild(a);
a.click(); // 触发点击事件
// 释放 URL 对象
URL.revokeObjectURL(n_url);
document.body.removeChild(a);
})
.catch((error) => {
console.error("下载文件时出错:", error);
});
} else {
window.open(res.data.resource_url[rid]);
}
});
};
@@ -131,13 +162,23 @@ const CoursePage = () => {
<div className={styles["top-cont"]}>
<div className="j-b-flex">
<div className="d-flex">
<Image
width={120}
height={90}
style={{ borderRadius: 10 }}
preview={false}
src={course?.thumb}
/>
{course ? (
<Image
width={120}
height={90}
style={{ borderRadius: 10 }}
preview={false}
src={
course.thumb === -1
? defaultThumb1
: course.thumb === -2
? defaultThumb2
: course.thumb === -3
? defaultThumb3
: resourceUrl[course.thumb]
}
/>
) : null}
<div className={styles["info"]}>
<div className={styles["title"]}>{course?.title}</div>
<div className={styles["status"]}>
@@ -327,7 +368,15 @@ const CoursePage = () => {
</div>
<div
className={styles["download"]}
onClick={() => downLoadFile(item.course_id, item.id)}
onClick={() =>
downLoadFile(
item.course_id,
item.id,
item.rid,
`${item.title}.${item.ext}`,
item.type
)
}
>
</div>

View File

@@ -26,6 +26,7 @@ const CoursePalyPage = () => {
const [totalHours, setTotalHours] = useState<HourModel[]>([]);
const [playingTime, setPlayingTime] = useState(0);
const [watchedSeconds, setWatchedSeconds] = useState(0);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const myRef = useRef(0);
const playRef = useRef(0);
const watchRef = useRef(0);
@@ -105,7 +106,7 @@ const CoursePalyPage = () => {
} else if (record && record.is_finished === 1) {
setWatchedSeconds(res.data.hour.duration);
}
getVideoUrl(params);
getVideoUrl(res.data.hour.rid, params);
setLoading(false);
})
.catch((e) => {
@@ -113,11 +114,12 @@ const CoursePalyPage = () => {
});
};
const getVideoUrl = (data: any) => {
const getVideoUrl = (rid: number, data: any) => {
Course.playUrl(Number(params.courseId), Number(params.hourId)).then(
(res: any) => {
setPlayUrl(res.data.url);
initDPlayer(res.data.url, 0, data);
setResourceUrl(res.data.resource_url[rid]);
setPlayUrl(res.data.resource_url[rid]);
initDPlayer(res.data.resource_url[rid], 0, data);
savePlayId(String(params.courseId) + "-" + String(params.hourId));
}
);

View File

@@ -9,6 +9,9 @@ import { Empty } from "../../compenents";
import myLesoon from "../../assets/images/commen/icon-mylesoon.png";
import studyTime from "../../assets/images/commen/icon-studytime.png";
import iconRoute from "../../assets/images/commen/icon-route.png";
import defaultThumb1 from "../../assets/thumb/thumb1.png";
import defaultThumb2 from "../../assets/thumb/thumb2.png";
import defaultThumb3 from "../../assets/thumb/thumb3.png";
import { studyTimeFormat } from "../../utils/index";
type StatsModel = {
@@ -54,6 +57,7 @@ const IndexPage = () => {
useState<LearnCourseRecordsModel>({});
const [learnCourseHourCount, setLearnCourseHourCount] = useState<any>({});
const [stats, setStats] = useState<StatsModel | null>(null);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const currentDepId = useSelector(
(state: any) => state.loginUser.value.currentDepId
);
@@ -90,6 +94,7 @@ const IndexPage = () => {
setStats(res.data.stats);
setLearnCourseRecords(records);
setLearnCourseHourCount(res.data.user_course_hour_count);
setResourceUrl(res.data.resource_url);
if (tabKey === 0) {
setCoursesList(res.data.courses);
} else if (tabKey === 1) {
@@ -408,7 +413,15 @@ const IndexPage = () => {
<CoursesModel
id={item.id}
title={item.title}
thumb={item.thumb}
thumb={
item.thumb === -1
? defaultThumb1
: item.thumb === -2
? defaultThumb2
: item.thumb === -3
? defaultThumb3
: resourceUrl[item.thumb]
}
isRequired={item.is_required}
progress={Math.floor(
learnCourseRecords[item.id].progress / 100
@@ -422,7 +435,15 @@ const IndexPage = () => {
<CoursesModel
id={item.id}
title={item.title}
thumb={item.thumb}
thumb={
item.thumb === -1
? defaultThumb1
: item.thumb === -2
? defaultThumb2
: item.thumb === -3
? defaultThumb3
: resourceUrl[item.thumb]
}
isRequired={item.is_required}
progress={1}
></CoursesModel>
@@ -432,7 +453,15 @@ const IndexPage = () => {
<CoursesModel
id={item.id}
title={item.title}
thumb={item.thumb}
thumb={
item.thumb === -1
? defaultThumb1
: item.thumb === -2
? defaultThumb2
: item.thumb === -3
? defaultThumb3
: resourceUrl[item.thumb]
}
isRequired={item.is_required}
progress={0}
></CoursesModel>

View File

@@ -25,11 +25,11 @@ export const InitPage = (props: Props) => {
let config: SystemConfigStoreInterface = {
//系统配置
"ldap-enabled": props.configData["ldap-enabled"],
systemApiUrl: props.configData["system-api-url"],
systemH5Url: props.configData["system-h5-url"],
systemLogo: props.configData["system-logo"],
systemName: props.configData["system-name"],
systemPcUrl: props.configData["system-pc-url"],
resourceUrl: props.configData["resource_url"],
pcIndexFooterMsg: props.configData["system-pc-index-footer-msg"],
//播放器配置
playerPoster: props.configData["player-poster"],

View File

@@ -4,6 +4,9 @@ import { course } from "../../api/index";
import { Row, Col, Spin, Image, Progress } from "antd";
import { Empty } from "../../compenents";
import mediaIcon from "../../assets/images/commen/icon-medal.png";
import defaultThumb1 from "../../assets/thumb/thumb1.png";
import defaultThumb2 from "../../assets/thumb/thumb2.png";
import defaultThumb3 from "../../assets/thumb/thumb3.png";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
@@ -47,6 +50,7 @@ const LatestLearnPage = () => {
const systemConfig = useSelector((state: any) => state.systemConfig.value);
const [loading, setLoading] = useState<boolean>(false);
const [courses, setCourses] = useState<LastLearnModel[]>([]);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
useEffect(() => {
getCourses();
@@ -55,7 +59,10 @@ const LatestLearnPage = () => {
const getCourses = () => {
setLoading(true);
course.latestLearn().then((res: any) => {
setCourses(res.data);
if (res.data.resource_url && res.data.user_latest_learns) {
setResourceUrl(res.data.resource_url);
setCourses(res.data.user_latest_learns);
}
setLoading(false);
});
};
@@ -91,7 +98,15 @@ const LatestLearnPage = () => {
<div style={{ width: 120 }}>
<Image
loading="lazy"
src={item.course.thumb}
src={
item.course.thumb === -1
? defaultThumb1
: item.course.thumb === -2
? defaultThumb2
: item.course.thumb === -3
? defaultThumb3
: resourceUrl[item.course.thumb]
}
width={120}
height={90}
style={{ borderRadius: 10 }}

View File

@@ -7,7 +7,7 @@ declare global {
is_required: number;
is_show: number;
short_desc: string;
thumb: string;
thumb: number;
title: string;
}
@@ -48,6 +48,10 @@ declare global {
updated_at: string;
user_id: number;
}
interface ResourceUrlModel {
[key: number]: string;
}
}
export {};

View File

@@ -2,7 +2,6 @@ import { createSlice } from "@reduxjs/toolkit";
type SystemConfigStoreInterface = {
"ldap-enabled": string;
systemApiUrl: string;
systemPcUrl: string;
systemH5Url: string;
systemLogo: string;
@@ -14,11 +13,11 @@ type SystemConfigStoreInterface = {
playerBulletSecretText: string;
playerBulletSecretColor: string;
playerBulletSecretOpacity: string;
resourceUrl?: ResourceUrlModel;
};
let defaultValue: SystemConfigStoreInterface = {
"ldap-enabled": "",
systemApiUrl: "",
systemPcUrl: "",
systemH5Url: "",
systemLogo: "",
@@ -30,6 +29,7 @@ let defaultValue: SystemConfigStoreInterface = {
playerBulletSecretText: "",
playerBulletSecretColor: "",
playerBulletSecretOpacity: "",
resourceUrl: {},
};
const systemConfigSlice = createSlice({

View File

@@ -11,6 +11,7 @@ type UserStoreInterface = {
user: null;
departments: string[];
currentDepId: number;
resourceUrl: ResourceUrlModel;
isLogin: boolean;
};
@@ -18,6 +19,7 @@ let defaultValue: UserStoreInterface = {
user: null,
departments: [],
currentDepId: Number(getDepKey()) || 0,
resourceUrl: {},
isLogin: false,
};
@@ -30,6 +32,7 @@ const loginUserSlice = createSlice({
loginAction(stage, e) {
stage.value.user = e.payload.user;
stage.value.departments = e.payload.departments;
stage.value.resourceUrl = e.payload.resource_url;
stage.value.isLogin = true;
if (e.payload.departments.length > 0 && !getDepKey()) {
stage.value.currentDepId = e.payload.departments[0].id;
@@ -41,6 +44,7 @@ const loginUserSlice = createSlice({
stage.value.departments = [];
stage.value.isLogin = false;
stage.value.currentDepId = 0;
stage.value.resourceUrl = {};
clearToken();
clearDepKey();
clearDepName();