提交 77cf6ebc authored 作者: 王鹏飞's avatar 王鹏飞

refactor(admin): move legacy implementations into modules

上级 9ec935b8
import { Outlet } from 'react-router'
const AccessLayout = () => <Outlet />
export default AccessLayout
......@@ -100,3 +100,7 @@ export function setRolePermissions(roleId: number, powerList: Array<string | num
export function getAllPermissions() {
return getPayload<PermissionRecord[]>("/system/power/getListAll", {});
}
export function getTeacherRoles() {
return getPayload<RoleRow[]>("/user/role/getAllList", { type: 2 });
}
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const AccessLayout = lazy(() => import("@/pages/jurisdiction/index"));
const AccessLayout = lazy(() => import("./Layout"));
const AdminRoutes = lazy(() => import("./admin/routes"));
const RoleRoutes = lazy(() => import("./role/routes"));
......
import axios from "@/utils/request.js";
import qs from "qs";
import { message } from "antd";
import { get } from "lodash-es";
// oss
const getAliOSSSTSToken = async (obj) => {
try {
const res = await axios.post("/common/OssUpload/getUploadToken", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "获取OSS令牌失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export { getAliOSSSTSToken };
export { default } from "@/pages/user-module/userInfo";
import { useState, useEffect } from "react";
import { Form, Input, Button, Space, Row, Col, Drawer } from "antd";
import users from "@/assets/images/icon/userinfo.png";
import { changePass, changeInfo, getUserInfo } from "@/layout/request";
import { useNavigate } from "react-router";
import UploadImage from "@/components/uploadImage";
const UserInfo = () => {
const navigator = useNavigate();
const [imageUrl, setImageUrl] = useState("");
const [form] = Form.useForm();
const [form2] = Form.useForm();
const [showModal, setShowModal] = useState(false);
const fetchData = async (obj) => {
try {
const userData = await getUserInfo(obj);
form.setFieldsValue({
real_name: userData.real_name,
tel: userData.tel,
pic: userData.pic,
});
const imageUrl = userData.pic;
setImageUrl(imageUrl);
} catch (error) {
console.error("Error fetching user data:", error);
}
};
useEffect(() => {
fetchData();
}, []);
const changePasswrd = async (obj) => {
await changePass(obj);
setShowModal(false);
};
const submitForm = async (obj) => {
const updatedData = await changeInfo({ ...obj });
if (updatedData) {
await fetchData();
}
};
return (
<div style={{ padding: "20px 0" }}>
<Form form={form} labelCol={{ span: 2 }} onFinish={submitForm}>
<Form.Item label="头像" className="uploadPar" name="pic">
<UploadImage
value={imageUrl ? imageUrl : users}
onChange={(url) => setImageUrl(url)}
></UploadImage>
</Form.Item>
<Form.Item
label="管理员名称"
name="real_name"
rules={[{ required: true, message: "请输入管理员名称" }]}
>
<Input autoComplete="off" placeholder="请输入管理员名称" style={{ width: 200 }}></Input>
</Form.Item>
<Form.Item label="手机号" name="tel" rules={[{ required: true, message: "请输入手机号" }]}>
<Input autoComplete="off" placeholder="请输入手机号" style={{ width: 200 }}></Input>
</Form.Item>
<Form.Item label="密码">
<Button type="primary" ghost onClick={() => setShowModal(true)}>
修改密码
</Button>
</Form.Item>
<Form.Item wrapperCol={{ offset: 2 }}>
<Space size={20}>
<Button onClick={() => navigator(-1)}>取消</Button>
<Button type="primary" htmlType="submit">
保存
</Button>
</Space>
</Form.Item>
</Form>
<Drawer
open={showModal}
onClose={() => {
setShowModal(false);
}}
mask={false}
maskClosable={false}
footer={null}
centered
destroyOnClose
keyboard={false}
>
<Form labelCol={{ span: 5 }} form={form2} onFinish={changePasswrd}>
<Form.Item
label="旧密码"
name="old_password"
rules={[{ required: true, message: "请输入旧密码" }]}
>
<Input.Password autoComplete="off" placeholder="请输入旧密码"></Input.Password>
</Form.Item>
<Form.Item
label="新密码"
name="password"
rules={[
{ required: true, message: "请输入新密码" },
{
pattern:
/^((?=.*\d)(?=.*[A-Z])(?=.*[a-z]))|((?=.*\d)(?=.*[A-Z])(?=.*[\W_]))|((?=.*\d)(?=.*[a-z])(?=.*[\W_]))|((?=.*[A-Z])(?=.*[a-z])(?=.*[\W_]))[a-zA-Z\d\W_]{8,12}$/,
message:
"密码至少满足三种格式(大写字母、小写字母、数字、特殊符号), 长度为8~12位字符!",
trigger: ["change", "blur"],
},
{
validator(_, value) {
if (!value || form2.getFieldValue("re_password") === value) {
form2.setFields([{ name: "re_password", errors: [""] }]);
return Promise.resolve();
}
form2.setFields([{ name: "re_password", errors: ["两次输入的密码不一致"] }]);
return Promise.reject();
},
},
]}
>
<Input.Password autoComplete="off" placeholder="请输入新密码"></Input.Password>
</Form.Item>
<Form.Item
label="确认新密码"
name="re_password"
rules={[
{ required: true, message: "请确认新密码" },
{
pattern:
/^((?=.*\d)(?=.*[A-Z])(?=.*[a-z]))|((?=.*\d)(?=.*[A-Z])(?=.*[\W_]))|((?=.*\d)(?=.*[a-z])(?=.*[\W_]))|((?=.*[A-Z])(?=.*[a-z])(?=.*[\W_]))[a-zA-Z\d\W_]{8,12}$/,
message:
"密码至少满足三种格式(大写字母、小写字母、数字、特殊符号), 长度为8~12位字符!",
trigger: ["change", "blur"],
},
{
validator(_, value) {
if (!value || form2.getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(new Error("两次输入的密码不一致"));
},
},
]}
>
<Input.Password autoComplete="off" placeholder="请确认新密码"></Input.Password>
</Form.Item>
<Form.Item wrapperCol={{ offset: 5 }}>
<Row justify={"center"}>
<Space size={20}>
<Col>
<Button onClick={() => setShowModal(false)}>取消</Button>
</Col>
<Col>
<Button type="primary" htmlType="submit">
确认
</Button>
</Col>
</Space>
</Row>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default UserInfo;
import { Outlet } from 'react-router'
const AdvertisementsLayout = () => <Outlet />
export default AdvertisementsLayout
......@@ -2,7 +2,6 @@ import { message } from "antd";
// @ts-expect-error The legacy workspace does not install the qs declaration package.
import qs from "qs";
import { getPositionList as getLegacyPositionList } from "@/pages/advertisement/adsense/request.js";
import request from "@/utils/request.js";
import type { AdvertQuery } from "./query";
......@@ -90,7 +89,7 @@ export function changeAdvertStatus(id: number, status: "0" | "1") {
}
export async function getAdvertPositions(): Promise<AdvertPosition[] | false> {
const result = await getLegacyPositionList();
const result = await getPayload<{ list?: AdvertPosition[] }>("/ad/position/getList", {});
if (!result) return false;
return (result.list ?? []) as AdvertPosition[];
}
......@@ -5,7 +5,7 @@ import type { UploadProps } from "antd";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { getAliOSSSTSToken } from "@/pages/setting/help/addedit/requet";
import { getAliOSSSTSToken } from "@/modules/settings/help/request";
interface OssClient {
put: (path: string, file: File) => Promise<{ url: string }>;
......
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const AdvertisementsLayout = lazy(() => import("@/pages/advertisement/index"));
const AdvertisementsLayout = lazy(() => import("./Layout"));
const AdvertRoutes = lazy(() => import("./advert/routes"));
const AdsenseRoutes = lazy(() => import("./adsense/routes"));
......
export * from "@/pages/user-module/login/request";
export * from "./request/login";
const api = {
login: "/user/login",
getCode: "/user/login/verify",
};
export default api;
import axios from "@/utils/request.js";
import qs from "qs";
import api from "./login-api.js";
import { get } from "lodash-es";
import { message } from "antd";
const login = async (obj) => {
try {
const res = await axios.post(api.login, qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (msg === "Undefined index: verify") {
message.error("请先点击按钮进行验证");
return false;
}
if (code === 200) {
return {
code,
...data,
};
} else {
message.error(msg);
return {
msg,
};
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
export { login };
const api = {
getCaptcha: "/users/generate_image_code", // 获取验证码 GET
sendSMS: "/docapi/users/send_sms", // 发送手机验证码
loginByPhone: "/docapi/users/phone_login", // 手机号码形式登录
loginByUsername: "/docapi/users/login", // 用户名密码登录
checkSSOLogin: "/user/checkSsoLogin", // sso 登录
};
export default api;
import axios from "@/utils/request";
import { message } from "antd";
import api from "./sso-api";
import { get } from "lodash-es";
import qs from "qs";
// 获取图形验证码
const getCaptcha = async (obj) => {
try {
const res = await axios.get(api.getCaptcha, obj);
const status = get(res, "status", 0);
const data = get(res, "data", "");
if (status === 200) {
return data;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 发送短信验证码
const sendSMS = async (data) => {
try {
const res = await axios.post(api.sendSMS, data);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "发送失败");
if (code === 200) {
message.success("验证码发送成功!");
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 用户名登录
const userNameLogin = async (obj) => {
try {
const res = await axios.post(api.loginByUsername, obj);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "登录失败");
const data = get(res, "data.data", "");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 手机号登录
const phoneLogin = async (obj) => {
try {
const res = await axios.post(api.loginByPhone, obj);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "登录失败");
const data = get(res, "data.data", "");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
const checkSSOLogin = async (obj) => {
try {
const res = await axios.post(api.checkSSOLogin, qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (msg === "Undefined index: verify") {
message.error("请先点击按钮进行验证");
return false;
}
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
export { getCaptcha, sendSMS, userNameLogin, phoneLogin, checkSSOLogin };
export { default } from "@/pages/user-module";
import { Outlet } from "react-router";
import "./AuthLayout.less";
const User = () => {
return (
<div className="user-content">
<div className="form-container">
<div className="left-container"></div>
<div className="form-field-box">
<div className="form-field">
<Outlet />
</div>
</div>
</div>
</div>
);
};
export default User;
export { default } from "@/pages/user-module/login";
import React, { useState, useEffect, useRef } from "react";
import { Form, Input, Button, Row, Col, Space, Image, message } from "antd";
import { CloseOutlined, UserOutlined, LockOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router";
import { checkSSOLogin } from "../request/sso";
import "./LoginView.less";
import loginBtn from "@/assets/images/loginBtn.png";
import loginBtn2 from "@/assets/images/loginBtn2.png";
import Icon1 from "@/assets/login/1.png";
import Icon2 from "@/assets/login/2.png";
import Icon3 from "@/assets/login/3.png";
import Icon4 from "@/assets/login/4.png";
import Icon5 from "@/assets/login/5.png";
import Icon6 from "@/assets/login/6.png";
import Icon7 from "@/assets/login/7.png";
import Icon8 from "@/assets/login/8.png";
import Icon9 from "@/assets/login/9.png";
import Icon10 from "@/assets/login/10.png";
import Icon11 from "@/assets/login/11.png";
import Icon12 from "@/assets/login/12.png";
import Icon13 from "@/assets/login/13.png";
import Icon14 from "@/assets/login/14.png";
import Icon15 from "@/assets/login/15.png";
import Icon16 from "@/assets/login/16.png";
import Icon17 from "@/assets/login/17.png";
import Icon18 from "@/assets/login/18.png";
import Icon19 from "@/assets/login/19.png";
import Icon20 from "@/assets/login/20.png";
import Icon21 from "@/assets/login/21.png";
import Icon22 from "@/assets/login/22.png";
import Icon23 from "@/assets/login/23.png";
import { login } from "../request/login";
import logo from "@/assets/images/icon_logo_login.png";
import { RotateVerify } from "../../../utils/captcha";
const Login = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [isActive, setIsActive] = useState(0);
const [loading, setLoading] = useState(false);
const [captchaURL, setCaptchaURL] = useState("");
const [captchaStatus, setCaptchaStatus] = useState(false);
const [captchaText, setCaptchaText] = useState("发送验证码");
const [showCaptcha, setShowCaptcha] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false); //登录图片切换
const [isLoginBtn, setIsLoginBtn] = useState(false); //是否点击了登录按钮
const [code, setCode] = useState("");
const [disLf, setDisLf] = useState("");
const [initialValues, setinitialValues] = useState({
tel: "",
password: "",
});
const timer = useRef();
const getSSOInfo = async () => {
const isSSOData = await checkSSOLogin();
if (isSSOData.run === "login") {
} else if (isSSOData.token) {
localStorage.setItem("kiwi.token", isSSOData.token);
navigate("/");
return;
}
};
useEffect(() => {
getSSOInfo();
}, []);
// 登录
const toLayout = async (obj) => {
setLoading(true);
if (isLoginBtn) {
const data = await login({
...obj,
code: code,
disLf: disLf,
});
if (data.code === 200) {
localStorage.setItem("kiwi.token", data.token);
navigate("/");
return;
} else {
form.setFields([{ name: "password", errors: [data.msg] }]);
}
} else {
message.error("请先完成按钮验证");
}
// 刷新验证码
// await refreshCaptcha();
setLoading(false);
};
const handleStartVerification = () => {
setShowCaptcha(true);
};
// 点击×
const handleGoBack = () => {
setShowCaptcha(false);
};
useEffect(() => {
if (showCaptcha) {
const rotateWrapEle = document.querySelector("#rotateWrap1");
// 确保 DOM 元素存在之后再进行初始化
if (rotateWrapEle) {
const myRotateVerify = new RotateVerify("#rotateWrap1", {
initText: "滑动将图片转正",
slideImage: [
Icon1,
Icon2,
Icon3,
Icon4,
Icon5,
Icon6,
Icon7,
Icon8,
Icon9,
Icon10,
Icon11,
Icon12,
Icon13,
Icon14,
Icon15,
Icon16,
Icon17,
Icon18,
Icon4,
Icon19,
Icon20,
Icon21,
Icon22,
Icon23,
],
initUrl: "/user/login/initData",
verifyUrl: "/user/login/checkVerify",
getSuccessState: function (data) {
console.log("验证成功", data);
setDisLf(data.disLf);
setCode(data.code);
if (data.disLf) {
setShowCaptcha(false);
setIsLoggedIn(true);
setIsLoginBtn(true);
}
},
});
// 如果需要,在这里处理卸载时清理资源
return () => {
console.log(myRotateVerify.initUrl);
// 清理或销毁 RotateVerify 实例等操作
if (myRotateVerify && typeof myRotateVerify.destroy === "function") {
myRotateVerify.destroy();
}
};
}
}
}, [showCaptcha]);
return (
<div className={showCaptcha ? "captcha-container" : "normal-container"}>
{showCaptcha && (
<div className="rotateWrap">
<div className="cuo" onClick={handleGoBack}>
<CloseOutlined />
</div>
<span className="top-s">身份验证</span>
<span className="top-x">拖动滑块,使图片角度为正</span>
<div id="rotateWrap1">
<div className="rotateverify-contaniner">
<div className="rotate-can-wrap">
<canvas className="rotateCan rotate-can" width="200" height="200"></canvas>
<div className="statusBg status-bg"></div>
</div>
<div className="control-wrap slideDragWrap">
<div className="control-tips">
<p className="c-tips-txt cTipsTxt">滑动将图片转正</p>
</div>
<div className="control-bor-wrap controlBorWrap"></div>
<div className="control-btn slideDragBtn">
{/* <i className='control-btn-ico'></i> */}
<div className="control-btn-ico"></div>
</div>
</div>
</div>
</div>
</div>
)}
{!showCaptcha && (
<div className="img-logo">
<img src={logo} className="img" />
</div>
)}
{!showCaptcha && <div className="login-header">欢迎登录</div>}{" "}
{!showCaptcha && (
<div className="form-data-container">
<Form form={form} onFinish={toLayout} autoComplete="off" initialValues={initialValues}>
<>
<Form.Item
name="tel"
rules={[
{ required: true, message: "请输入手机号!" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确格式的手机号" },
]}
>
<Input
autoComplete="off"
prefix={<UserOutlined />}
size="large"
placeholder="请输入手机号"
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: "请输入密码!" }]}>
<Input.Password
autoComplete="off"
prefix={<LockOutlined />}
size="large"
placeholder="请输入密码"
/>
</Form.Item>
{/* <Form.Item name='verify' rules={[{ required: true, message: '请输入验证码!' }]}>
<Row gutter={16}>
<Col span={16}>
<Input
prefix={<SetIcon src={IconCaptcha} />}
size='large'
placeholder='请输入验证码'
/>
</Col>
<Col span={8}>
<img
src={captchaURL}
alt='图形验证码'
className='captcha'
onClick={refreshCaptcha}
/>
</Col>
</Row>
</Form.Item> */}
<Form.Item name="verify" onClick={handleStartVerification}>
<div style={{ width: "100%", height: "100%", pointerEvents: "none" }}>
{isLoggedIn ? (
<Image
src={loginBtn2}
style={{
width: "100%",
height: "100%",
}}
/>
) : (
<Image
src={loginBtn}
style={{
width: "100%",
height: "100%",
}}
/>
)}
</div>
</Form.Item>
</>
<Form.Item className="login-submit-btn">
<Button htmlType="submit" loading={loading} type="primary" size="large">
登录
</Button>
</Form.Item>
</Form>
</div>
)}
</div>
);
};
export default Login;
import React from "react";
import { Outlet } from "react-router";
const Book = () => {
return <Outlet />;
};
export default Book;
.auditPanel {
background: #fbfbfb;
margin-bottom: 30px;
}
.auditdetail {
.ant-steps-item-icon {
width: 50px !important;
height: 100px;
}
.imgel {
.user {
width: 50px;
height: 50px;
position: relative;
background-size: cover;
}
.status {
width: 20px;
height: 20px;
position: absolute;
right: 0;
bottom: 0;
}
}
// .ant-steps-item-description {
// height: 150px;
// }
.ant-steps-item-tail {
display: none !important;
}
}
export { default } from "@/pages/books/audit/preview";
import React, { useEffect, useState, useRef, useImperativeHandle, forwardRef } from "react";
import { Modal, Drawer, Tree, Spin, Space, Button, Input, Image } from "antd";
import { CloseOutlined, LeftOutlined, BarsOutlined } from "@ant-design/icons";
import { getInfoByChapterId, getAllList } from "@/modules/books/management/chapters/request";
import { getChapterTopic, expandReadInfo } from "@/common/wangeditor-customer/utils/request";
import "@/common/wangeditor-customer/utils/iconfont";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
clacTopicText,
} from "@/utils/common";
import { get } from "lodash-es";
import $ from "jquery";
import "@/common/preview.less";
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
const correctList = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
];
// 递归遍历树,为叶子节点分配页码,返回 Map<key, pageNumber> 和总页数
const buildPageMap = (treeData) => {
const map = {};
let page = 0;
const walk = (nodes) => {
nodes.forEach((node) => {
if (node.children && node.children.length > 0) {
walk(node.children);
} else {
page += 1;
map[node.key] = page;
}
});
};
walk(treeData);
return { map, total: page };
};
const PreviewScreen = (props, ref) => {
const { bookId } = props;
const [gData, setGData] = useState([]);
const [loading, setLoading] = useState(false); // 展示
const [prviewHtml, setPrviewHtml] = useState(""); // 预览html
const [open, setOpen] = useState(false); // 抽屉
const [secondType, setSecondType] = useState(false);
const [pageMap, setPageMap] = useState({}); // 叶子节点页码映射
const [totalPages, setTotalPages] = useState(0); // 总页数
const [currentPage, setCurrentPage] = useState(0); // 当前页码
const [toolTip, setTooltip] = useState({});
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [headTitle, setHeadTitle] = useState("");
const [oldTitle, setOldTitle] = useState("");
const [newChapterId, setNewChapterId] = useState("");
const [opend, setOpend] = useState(false);
const [gallery, setGallery] = useState([]);
const [galleryIndex, setGalleryIndex] = useState(0);
const [isType, setIsType] = useState("");
const [topicList, setTopicList] = useState([]); // 题库
const [radioList, setRadioList] = useState([]); // 单选
const [checkBoxList, setCheckBoxList] = useState([]); // 多选
const [judgeList, setJudgeList] = useState([]); // 判断
const [fillList, setFillList] = useState([]); // 填空
const [textareaList, setTextareaList] = useState([]); // 简答
const [expandContent, setExpandContent] = useState(""); // 扩展阅读内容
const [previewImg, setPreviewImg] = useState(""); // 扩展阅读内容
const newId = useRef(null);
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const getChapterTreeList = async () => {
setLoading(true);
const data = await getAllList({ book_id: bookId });
if (!data.data.length) {
setLoading(false);
return;
}
const arr = convertToAntdTreeData(data.data, "name");
const { map, total } = buildPageMap(arr);
setPageMap(map);
setTotalPages(total);
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setOldTitle(first.title);
setNewChapterId(first.key);
setCurrentPage(map[first.key] || 0);
setLoading(false);
};
const getTopicList = async ({ practicenum, chapterid, bookid }) => {
const data = await getChapterTopic({
position: practicenum,
book_id: bookid,
chapter_id: chapterid,
});
const temp = [];
data.forEach((item) => {
let obj = { ...item };
if ([1, 2, 3].includes(parseInt(item.question_style))) {
try {
obj.question_list = JSON.parse(item.question_list);
} catch (err) {
obj.question_list = [];
}
}
temp.push(obj);
});
const tempRadio = temp.filter((item) => parseInt(item.question_style) === 1);
const tempCheckbox = temp.filter((item) => parseInt(item.question_style) === 2);
const tempJudge = temp.filter((item) => parseInt(item.question_style) === 3);
const tempFill = temp.filter((item) => parseInt(item.question_style) === 4);
const tempTextarea = temp.filter((item) => parseInt(item.question_style) === 5);
setRadioList(tempRadio);
setCheckBoxList(tempCheckbox);
setJudgeList(tempJudge);
setFillList(tempFill);
setTextareaList(tempTextarea);
setTopicList(temp);
};
const getExpandInfo = async ({ chapterId, position }) => {
const data = await expandReadInfo({
book_id: bookId,
chapter_id: chapterId,
position: position,
});
if (data) {
setExpandContent(data.content);
}
};
const getChapterId = async () => {
setLoading(true);
newId.current = newChapterId;
const data = await getInfoByChapterId({ chapter_id: newChapterId });
if (data) {
const content = get(data, "content", "");
setPrviewHtml(content);
$("#previee-container")
.find("video audio")
.each((index, item) => {
$(item).attr("controls", true).attr("controlslist", "nodownload");
});
setTimeout(() => {
document
.querySelectorAll(".preview-content-show ")[0]
.querySelectorAll("pre code")
.forEach((el) => {
hljs.highlightElement(el);
});
}, 200);
}
setLoading(false);
};
const handlerClick = () => {
const iParentW = $(".preview-content-it").innerWidth();
const iParentH = $(".preview-content-it").innerHeight();
const parentOffset = $(".preview-content-it").offset(); // 获取父元素相对于文档的位置
const parentRect = $(".preview-content-it")[0].getBoundingClientRect(); // 获取父元素相对于文档的位置
$(".preview-content-html").on("click", async (ev) => {
ev.stopPropagation();
const target = ev.target;
const tooltip = $(target).closest(".chapter-item-tooltip");
const alink = $(target).closest(".chapter-item-link");
const aGallery = $(target).closest(".chapter-gallery-container");
const practice = $(target).closest(".chapter-practice");
const aA = $(target).closest("a");
const aExpand = $(target).closest(".chapter-expand");
if (tooltip.length > 0) {
const random = $(tooltip).data("random");
const link = $(tooltip).data("link");
const content = $(tooltip).data("content");
const tooltipType = $(tooltip).data("tooltip-type");
const title = $(tooltip).data("title");
const tooltipParent = $(target).closest(
"p, div, h1, h2, h3, h4, h5, h6, li, dt, dd, section",
);
const tooltipParentPosition = $(tooltipParent).position();
const tooltipPosition = $(tooltip).position();
const childOffset = $(tooltip).offset(); // 获取子元素相对于文档的位置
const childWidth = $(tooltip).outerWidth(true); // 获取元素的宽度
const childHeight = $(tooltip).outerHeight(true); // 获取元素的高度
let toolRect = null;
let targetPositon = null;
if (target.nodeName.toLowerCase() === "svg" || target.nodeName.toLowerCase() === "use") {
toolRect = target.parentNode.parentNode.getBoundingClientRect();
targetPositon = $(target).closest(".chapter-item-tooltip").position();
} else {
toolRect = target.parentNode.getBoundingClientRect();
targetPositon = $(target).position();
}
const lineHeight = $(tooltip).css("line-height");
let inSide = "";
let left = childOffset.left - parentOffset.left;
let top = 0;
let squareStyle = {};
if (childWidth + left >= iParentW) {
left = iParentW - childWidth - 10;
}
if (left <= 0) {
left = 10;
}
let style1 = {
top: `${top}px`,
left: `${left}px`,
};
setTooltip({
title,
random,
link,
content,
tooltipType,
inSide,
style: style1,
squareStyle: squareStyle,
});
setTimeout(() => {
const childMoveHeight = $(".tooltip").outerHeight(); // 获取元素的高度
const childMoveWidth = $(".tooltip").outerWidth(); // 获取元素的高度
const scrollTop = $(".preview-content-show").scrollTop();
let squareStyle = {};
if (left <= 0) {
left = 10;
squareStyle.left = 10;
} else if (left + childMoveWidth >= iParentW) {
left = iParentW - childMoveWidth - 20;
squareStyle.left = targetPositon.left - left;
} else {
squareStyle.left = childWidth / 2;
}
// 换行了
if (toolRect.height / 1.5 > parseInt(lineHeight)) {
left = toolRect.right - parentRect.left - childMoveWidth + 15;
squareStyle.left = childMoveWidth - 30;
}
// 是否被遮挡
if (tooltipPosition.top + childMoveHeight + childHeight + 30 >= scrollTop + iParentH) {
inSide = "bottom";
top = tooltipPosition.top - childMoveHeight;
squareStyle.left = childWidth / 2;
} else {
inSide = "top";
if (toolRect.height / 1.5 > parseInt(lineHeight)) {
top = tooltipPosition.top + childHeight;
} else {
top = tooltipPosition.top + childHeight + 25;
}
// squareStyle.left = childWidth / 2;
}
top = top - scrollTop;
let style2 = {
top: `${top}px`,
left: `${left}px`,
zIndex: 1000,
opacity: 1,
};
setTooltip({
title,
random,
link,
content,
tooltipType,
inSide,
style: style2,
squareStyle: squareStyle,
});
}, 100);
return false;
} else if (alink.length > 0) {
const link = $(alink).data("link");
const linkType = $(alink).data("linktype");
const chapters = $(alink).data("chapters");
const title = $(alink).data("title");
let last = "";
if (parseInt(linkType) === 2) {
if (chapters.toString().indexOf(",") > -1) {
const arr = chapters.split(",");
last = arr[arr.length - 1];
} else {
last = chapters;
}
setHeadTitle(title);
setOpend(true);
setNewChapterId(last);
} else {
window.open(link);
}
ev.preventDefault();
return false;
} else if (aGallery.length > 0) {
const title = $(aGallery).data("title");
const flex = $(aGallery).data("flex");
const gallerylist = $(aGallery).data("gallerylist");
const galleryArr = JSON.parse(decodeURI(gallerylist));
if (galleryArr && galleryArr.length > 0 && galleryArr instanceof Array) {
setGallery(galleryArr);
if (parseInt(flex) === 2) {
setGalleryIndex(0);
} else {
setGalleryIndex($(target).closest(".chapter-gallery-item").index());
}
} else {
setGallery([]);
}
setOpend(true);
setHeadTitle(title);
setIsType("gallery");
setTooltip({});
return false;
} else if (practice.length > 0) {
const title = $(practice).data("title");
const bookid = $(practice).data("bookid");
const chapterid = $(practice).data("chapterid");
const practicenum = $(practice).data("practicenum");
getTopicList({ practicenum, chapterid, bookid });
setOpend(true);
setHeadTitle(title);
setIsType("practice");
ev.preventDefault();
return false;
} else if (aA.length > 0) {
const href = $(aA).attr("href");
let newLink = "";
if (!/^https*:\/\//.test(href)) {
newLink = `http://${href}`;
} else {
newLink = href;
}
window.open(newLink);
ev.preventDefault();
return false;
} else if (aExpand.length > 0) {
const title = $(aExpand).data("title");
const name = $(aExpand).data("name");
const position = $(aExpand).data("random");
setOpend(true);
setIsType("expand");
setHeadTitle(name);
await getExpandInfo({ chapterId: newId.current, position });
} else if (target.nodeName.toLowerCase() === "img") {
const alt = $(target).attr("alt");
const src = $(target).attr("src");
setOpend(true);
setIsType("img");
setPreviewImg(src);
setHeadTitle(alt ? alt : "图片展示");
}
setTooltip({});
setGallery([]);
});
$(".expand-content, .gallery-prview-container").on("click", (ev) => {
const target = ev.target;
if (target.nodeName.toLowerCase() === "img") {
const alt = $(target).attr("alt");
const src = $(target).attr("src");
setOpend(true);
setIsType("img");
setPreviewImg(src);
setSecondType("扩展图片");
}
});
};
useEffect(() => {
handlerClick();
}, [isType]);
useEffect(() => {
if (bookId) {
getChapterTreeList();
}
}, [bookId]);
useEffect(() => {
if (newChapterId) {
getChapterId();
}
}, [newChapterId]);
const onClose = () => {
setOpen(false);
};
const openDrawer = (e) => {
e.preventDefault();
e.stopPropagation();
setOpen(true);
};
const handleSelect = async (checkedKeys, info) => {
if (info.node.children && info.node.children.length > 0) {
const temp = JSON.parse(JSON.stringify(expandedKeys));
if (temp.includes(info.node.key)) {
const newExpands = temp.filter((item) => item !== info.node.key);
setExpandedKeys([...newExpands]);
} else {
setExpandedKeys([...temp, info.node.key]);
}
} else {
setOldTitle(info.node.title);
setNewChapterId(info.node.key);
newId.current = info.node.key;
setCheckedKeys(checkedKeys);
setCurrentPage(pageMap[info.node.key] || 0);
setOpen(false);
}
};
const galleryLeft = () => {
let temp = galleryIndex;
if (temp <= 0) {
temp = 0;
} else {
temp = temp - 1;
}
setGalleryIndex(temp);
};
const galleryRight = () => {
let temp = galleryIndex;
if (temp >= gallery.length - 1) {
temp = gallery.length - 1;
} else {
temp = temp + 1;
}
setGalleryIndex(temp);
};
return (
<div className="priview-modal">
{/* <Spin spinning={loading}> */}
<div className="previee-container" id="previee-container">
<div className="chapter-head-title">
<h2 className="tree">
{opend ? (
<Space>
<Button
type="text"
onClick={() => {
if (secondType) {
setSecondType(false);
setPreviewImg(null);
setIsType("expand");
return;
}
setOpend(false);
setIsType("");
setExpandContent("");
setTopicList([]);
setGalleryIndex(-1);
setNewChapterId(newChapterId);
setPreviewImg("");
}}
>
<LeftOutlined />
</Button>
{secondType ? secondType : headTitle}
</Space>
) : (
<>
<span className="no">{oldTitle}</span>
<span className="menu">
<BarsOutlined style={{ fontSize: 22 }} onClick={() => setOpen(true)} />
</span>
</>
)}
</h2>
</div>
<div className="preview-content-it">
<div className="preview-content-show">
<Spin spinning={loading} wrapperClassName="chapter-loading">
<div
className="preview-content-html"
dangerouslySetInnerHTML={{ __html: prviewHtml }}
></div>
</Spin>
{currentPage > 0 && (
<div className="preview-page-number">
<span>
{currentPage} / {totalPages}
</span>
</div>
)}
</div>
{isType && (
<div className="preview-content-other">
{isType === "gallery" && (
<div className="gallery-prview-container">
{gallery && gallery.length > 0 ? (
<>
<div className="gallery-img">
<img
src={gallery[parseInt(galleryIndex)].url}
alt={gallery[parseInt(galleryIndex)].title}
/>
<div className="opa prev" onClick={galleryLeft}></div>
<div className="opa next" onClick={galleryRight}></div>
</div>
<div className="steps">
当前 {parseInt(galleryIndex) + 1}/{gallery.length} 张
</div>
<div className="title">{gallery[parseInt(galleryIndex)].title}</div>
<div className="desc">{gallery[parseInt(galleryIndex)].desc}</div>
</>
) : (
<>
<div className="gallery-img noData">画廊数据异常</div>
</>
)}
</div>
)}
{isType === "practice" && (
<div className="practice-insert-topic">
{radioList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("radio")}. 单选题</h3>
<div className="practice-list">
{radioList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{checkBoxList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("checkbox", radioList)}. 多选题</h3>
<div className="practice-list">
{checkBoxList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{judgeList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("judge", radioList, checkBoxList)}. 判断题</h3>
<div className="practice-list">
{judgeList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{fillList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("fill", radioList, checkBoxList, judgeList)}. 填空题</h3>
<div className="practice-list">
{fillList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{textareaList.length > 0 && (
<div className="topic_style">
<h3>
{clacTopicText("textarea", radioList, checkBoxList, judgeList, fillList)}.
简答题
</h3>
<div className="practice-list">
{textareaList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `${correctList[cindex]}. ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
)}
{isType === "expand" && (
<div className="expand-container">
<div
className="expand-content"
dangerouslySetInnerHTML={{ __html: expandContent }}
></div>
</div>
)}
{isType === "img" && (
<div className="img-preview">
<Image width={430} src={previewImg} />
</div>
)}
</div>
)}
<div className="tooltip" style={toolTip.style}>
<div
className={`square ${
toolTip.inSide === "top"
? "square_top"
: toolTip.inSide === "bottom"
? "square_bottom"
: ""
}`}
style={toolTip.squareStyle}
></div>
<div className="tooltip-content-container">
<div className="tooltip-content">
<div className="content">
<p>{toolTip.content}</p>
</div>
{toolTip.link && (
<div className="content-opa">
<a href={toolTip.link} target="_blank" rel="noreferrer" className="c-link"></a>
{/* <a
href={`https://baike.baidu.com/item/${toolTip?.title}`}
target='_blank'
rel='noreferrer'
>
<img src={baike} alt='baike' />
</a> */}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<Drawer
onClose={onClose}
open={open}
getContainer={false}
placement="left"
classNames={{ header: "priview-drawer-header", body: "priview-drawer-body" }}
width="300"
title="章节列表"
rootClassName="priview-drawer-container"
>
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultExpandAll
blockNode
treeData={gData}
titleRender={(nodeData) => {
const page = pageMap[nodeData.key];
return (
<span className="tree-node-title">
<span className="tree-node-name">{nodeData.title}</span>
{page != null && <span className="tree-node-page">P{page}</span>}
</span>
);
}}
/>
</Drawer>
{/* </Spin> */}
</div>
);
};
export default forwardRef(PreviewScreen);
import { Navigate, Outlet, useRoutes } from "react-router";
import AuditDetailView from "./views/AuditDetailView";
import AuditListView from "./views/AuditListView";
import "@/pages/books/audit/index.less";
import "./audit.less";
function Layout() { return <Outlet />; }
export default function AuditRoutes() { return useRoutes([{ Component: Layout, children: [{ index: true, Component: AuditListView }, { path: "dataset", Component: AuditListView }, { path: "detail", Component: AuditDetailView }, { path: "*", element: <Navigate to="." replace /> }] }]); }
......@@ -4,7 +4,7 @@ import { useLocation, useNavigate } from "react-router";
import { useSelector } from "react-redux";
import { addExamine, getExamineList } from "../api";
import AuditHistory from "../components/AuditHistory";
import "@/pages/books/audit/index.less";
import "../audit.less";
export default function AuditDetailView() {
const location = useLocation(); const navigate = useNavigate(); const id = (location.state as { id?: string | number } | null)?.id; const { operationPermissionsList = [] } = useSelector((state: any) => state.user); const [form] = Form.useForm(); const [history, setHistory] = useState<any[]>([]); const [loading, setLoading] = useState(false); const [forceOpen, setForceOpen] = useState(false); const [forceMessage, setForceMessage] = useState("");
const loadHistory = async () => { if (!id) return; const data: any = await getExamineList(id); if (data) setHistory(data); };
......
......@@ -3,6 +3,7 @@ import { message } from "antd";
import qs from "qs";
import request from "@/utils/request.js";
import legacyAxios from "@/utils/axios";
import type { ManagementQuery } from "./query";
......@@ -80,6 +81,19 @@ export function deleteManagementBook(id: number) {
return mutate("/book/teacher/del", { id });
}
// 编辑页仍沿用旧接口封装的返回值形状;保留在管理模块中,避免页面反向依赖旧目录。
export function getBook(params: ApiParams) {
return legacyAxios.post("/api/book/teacher/getInfoById", params);
}
export function addBook(params: ApiParams) {
return legacyAxios.post("/api/book/teacher/add", params);
}
export function updateBook(params: ApiParams) {
return legacyAxios.post("/api/book/teacher/edit", params);
}
export function exportManagementBook(id: number) {
return getPayload<ExportBookResult>("/book/teacher/export", { id });
}
......
import axios from "@/utils/axios";
// 获取书籍详情
export function getBook(data) {
return axios.post("/api/book/teacher/getInfoById", data);
}
// 获取章节列表
export function getChapterList(data) {
return axios.post("/api/book/teacher/chapter/getAllList", data);
}
// 获取章节详情
export function getChapter(data) {
return axios.post("/api/book/teacher/chapter/getInfoById", data);
}
// 添加章节
export function addChapter(data) {
return axios.post("/api/book/teacher/chapter/add", data);
}
// 修改章节
export function updateChapter(data) {
return axios.post("/api/book/teacher/chapter/edit", data);
}
// 删除章节
export function delChapter(data) {
return axios.post("/api/book/teacher/chapter/del", data);
}
// 章节排序
export function sortChapter(data) {
return axios.post("/api/book/teacher/chapter/dragOrder", data);
}
// 获取章节编辑者信息
export function getChapterEditors(data) {
return axios.post("/api/book/teacher/chapter/getEditors", data);
}
// 保存章节编辑者
export function updateChapterEditors(data) {
return axios.post("/api/book/teacher/chapter/saveEditors", data);
}
// 知识图谱列表
export function getTagList(params) {
return axios.get("/api/resource/v1/course/course/tag-list", { params });
}
// 知识图谱关联书籍章节
export function bindTag(data) {
return axios.post("/api/resource/v1/course/course/tag-add-book", data);
}
import { useState, useEffect, useCallback } from "react";
import { Modal, Tree, App, Button, Flex, Empty } from "antd";
import { getTagList, bindTag } from "../api";
function getIds(nodes) {
let ids = [];
nodes.forEach((node) => {
ids.push(node.id);
if (node.children && node.children.length > 0) {
ids = ids.concat(getIds(node.children));
}
});
return ids;
}
const BindKnowledgeGraph = ({ chapter = {}, ...props }) => {
const { message } = App.useApp();
const [treeData, setTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = useState([]);
const fetchList = useCallback(() => {
getTagList({ type: 2, parent_id: chapter.book_id, chapter_id: chapter.id }).then((res) => {
setTreeData(res.data);
setExpandedKeys(getIds(res.data));
});
}, [chapter]);
useEffect(() => {
fetchList();
}, [fetchList]);
const toggleBind = async (node) => {
await bindTag({
tag_id: node.id,
book_id: chapter.book_id,
chapter_id: chapter.id,
option_type: node.is_checked ? "delete" : "add",
});
message.success("操作成功");
fetchList();
};
const titleRender = (node) => {
return (
<Flex justify="space-between">
<span>{node.name}</span>
<Button color="primary" variant="link" size="small" onClick={() => toggleBind(node)}>
{node.is_checked ? "取消关联" : "关联"}
</Button>
</Flex>
);
};
return (
<Modal title="关联知识图谱" centered {...props} footer={null}>
{treeData.length > 0 ? (
<Tree
blockNode
selectable={false}
treeData={treeData}
fieldNames={{ title: "name", key: "id" }}
expandedKeys={expandedKeys}
titleRender={titleRender}
onExpand={setExpandedKeys}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无知识图谱" />
)}
</Modal>
);
};
export default BindKnowledgeGraph;
import { useState, useEffect } from "react";
import { Modal, Form, Radio, Space, App } from "antd";
import { getChapterEditors, updateChapterEditors } from "../api";
const EditChapterEditors = ({ chapter = {}, onChange, ...props }) => {
const { message } = App.useApp();
const [editors, setEditors] = useState([]);
const [value, setValue] = useState("");
useEffect(() => {
getChapterEditors({ book_id: chapter.book_id, chapter_id: chapter.id }).then((res) => {
setEditors(res.data.editors || []);
const value = res.data.selected_editor_ids[0] || "";
setValue(value);
});
}, [chapter]);
const handleSubmit = async () => {
await updateChapterEditors({
book_id: chapter.book_id,
chapter_id: chapter.id,
editor_id: value,
});
message.success("设置成功");
onChange && onChange();
};
return (
<Modal title="设置章节编写者" centered {...props} onOk={handleSubmit}>
<Form labelCol={{ span: 8 }}>
<Form.Item label="当前章节">{chapter.title}</Form.Item>
<Form.Item label="编写者">
<Radio.Group value={value} onChange={(e) => setValue(e.target.value)}>
<Space direction="vertical">
{editors.map((item) => {
return (
<Radio value={item.editor_id} key={item.editor_id}>
{item.real_name}({item.tel})
</Radio>
);
})}
</Space>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};
export default EditChapterEditors;
import { useState, useEffect, useRef } from "react";
import { Form, Button, Space, Input } from "antd";
import { setTreeChapter } from "@/store/modules/user";
import { useDispatch } from "react-redux";
import { chapterEdit, chapterAdd, sectionAdd } from "../request";
const EditChapterTitle = (props) => {
const {
editValue,
setEditValue,
editKey,
setEditKey,
parentId,
bookId,
getChapterTreeList,
expandedKeys,
setExpandedKeys,
editorLoading,
newChapterChange,
} = props;
const dispatch = useDispatch();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [initValues, setInitValues] = useState({ title: editValue });
const inputRef = useRef();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, [editorLoading]);
useEffect(() => {
form.setFieldsValue({ title: editValue });
setInitValues({ title: editValue });
}, [editValue]);
const onFinish = async (values) => {
setLoading(true);
let data = null;
if (parentId) {
data = await chapterAdd({
pid: parentId === -1 ? 0 : parentId,
book_id: bookId,
name: values.title,
});
} else {
data = await chapterEdit({ id: editKey, name: values.title });
}
await dispatch(setTreeChapter({ saveBookId: bookId, saveChapterId: data.id }));
if (data) {
if (parentId) {
await sectionAdd({ book_id: bookId, chapter_id: data.id, content: "", word_count: 0 });
}
setEditKey(false);
setEditValue(null);
await getChapterTreeList();
if (parentId) {
let temp = [...expandedKeys, parentId];
setExpandedKeys(temp);
await newChapterChange(data.id, values.title);
}
}
setLoading(false);
};
return (
<Form layout="vertical" form={form} onFinish={onFinish} initialValues={initValues}>
<Form.Item
label="章节标题"
name="title"
rules={[{ required: true, message: "请输入章节标题" }]}
extra="最多30个字符"
>
<Input ref={inputRef} autoFocus maxLength={30} placeholder="" allowClear />
</Form.Item>
<Form.Item className="editor-form-buttons" wrapperCol={{ offset: 9, span: 16 }}>
<Space size={20}>
<Button type="default" disabled={loading} onClick={() => setEditKey(false)}>
取消
</Button>
<Button type="primary" loading={loading} htmlType="submit">
确认
</Button>
</Space>
</Form.Item>
</Form>
);
};
export default EditChapterTitle;
import { useState, useEffect, useRef, useCallback } from "react";
import { Button, Row, Col, Descriptions, Tree, Dropdown, Modal, Spin, App } from "antd";
import {
EllipsisOutlined,
DiffOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from "@ant-design/icons";
import WangEditorCustomer from "@/common/wangeditor-customer";
import { useLocation, useNavigate } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { setAutosaveTime } from "@/store/modules/editor";
import { setTreeChapter } from "@/store/modules/user";
import EditChapterTitle from "./components/form-chapter-title";
import EditChapterEditors from "./components/EditChapterEditors";
import BindKnowledgeGraph from "./components/BindKnowledgeGraph";
import { get } from "lodash-es";
import md5 from "js-md5";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
findParentLevelOne,
findTreeToIndex,
findNodeById,
} from "@/utils/common";
import { getAllList, getInfoByChapterId, sectionEdit, chapterDel, dragOrder } from "./request";
import "./index.less";
const Examine = () => {
const { modal } = App.useApp();
const location = useLocation();
const id = get(location, "state.id", "");
const isAdminOrCreator = get(location, "state.isAdminOrCreator", false);
const { treeChapter, userInfo } = useSelector((state) => state.user);
const navigate = useNavigate();
const dispatch = useDispatch();
const [gData, setGData] = useState([]);
const [chapterId, setChapterId] = useState(0);
const [bookId, setBookId] = useState(0);
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [nowTitle, setNowTitle] = useState("");
// 编辑操作
const [isCollapse, setisCollapse] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(280);
const isResizing = useRef(false);
const sidebarRef = useRef(null);
const [editKey, setEditKey] = useState(null);
const [parentId, setParentId] = useState(null);
const [editValue, setEditValue] = useState("");
const [loading, setLoading] = useState(false);
const [contentMd5, setContentMd5] = useState("");
// 设置编者
const [currentChapter, setCurrentChapter] = useState(null);
const [editChapterEditorsIsOpen, setEditChapterEditorsIsOpen] = useState(false);
const onChapterEditorsChange = () => {
setEditChapterEditorsIsOpen(false);
};
// 关联知识图谱
const [bindKnowledgeGraphIsOpen, setBindKnowledgeGraphIsOpen] = useState(false);
// 拖拽改变侧边栏宽度
const handleMouseDown = useCallback(
(e) => {
e.preventDefault();
isResizing.current = true;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const startX = e.clientX;
const startWidth = sidebarWidth;
const handleMouseMove = (e) => {
if (!isResizing.current) return;
const delta = e.clientX - startX;
const newWidth = Math.min(Math.max(startWidth + delta, 200), 600);
setSidebarWidth(newWidth);
};
const handleMouseUp = () => {
isResizing.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[sidebarWidth],
);
// 编辑器内容
const editorRef = useRef();
const saveInterRef = useRef();
const [html, setHtml] = useState("");
const [contentId, setContentId] = useState(false);
// 获取目录结构
const getChapterTreeList = async () => {
setLoading(true);
const { data = [], code } = await getAllList({ book_id: id });
if (code !== 200) {
setLoading(false);
navigate("/books/management/list", { replace: true });
return;
}
if (data.length === 0) {
setLoading(false);
return;
}
const uniqueBookList = [...new Set(data.map((item) => item.book_id))][0];
setBookId(uniqueBookList);
const arr = convertToAntdTreeData(data, "name");
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setNowTitle(first.title);
if (treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(arr, "key", treeChapter.saveChapterId);
if (childInKey) {
setChapterId(childInKey.key);
setCheckedKeys([childInKey.key]);
setNowTitle(childInKey.title);
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }));
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
setLoading(false);
};
const newChapterChange = (id, title) => {
setChapterId(parseInt(id));
setCheckedKeys([parseInt(id)]);
setNowTitle(title);
};
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const [disabled, setDisabled] = useState(false);
// 获取内容
const getChapterVal = async () => {
setLoading(true);
const data = await getInfoByChapterId({ chapter_id: chapterId });
setDisabled(!data.selected_editor_ids.includes(userInfo.id) && !isAdminOrCreator);
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId, ...data }));
const { content, id: cId } = data;
if (content) {
setContentMd5(md5(content));
}
setHtml(content);
setContentId(cId);
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.setHtml(content);
}
setLoading(false);
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
};
const handleSelect = async (selectedKeys, info) => {
if (!info.node) return;
const { key, title } = info.node;
// 保持选中状态,防止重复点击导致选中状态消失
setCheckedKeys([key]);
// 如果点击的是当前已经选中的章节,且没有发生章节切换,则直接返回
if (key === chapterId) {
return;
}
setLoading(true);
// 切换章节前清理保存定时器并执行一次保存
clearInterval(saveInterRef.current);
await saveContent();
// 清理旧内容
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.clear();
}
// 设置新章节 ID,触发 useEffect 加载数据
setChapterId(key);
setNowTitle(title);
setLoading(false);
};
useEffect(() => {
getChapterTreeList();
}, []);
useEffect(() => {
if (chapterId) {
getChapterVal();
}
}, [chapterId]);
// 保存方法
const saveContent = async () => {
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml();
const newMd5 = md5(contentHtml);
clearInterval(saveInterRef.current);
const word_count = editorRef.current.editor.getText().length;
// 保存
const data = await sectionEdit({
id: contentId,
content: contentHtml,
word_count,
});
if (data) {
if ((data.code && data.code === 3000) || data.code === "3000") {
// navigate('/books/management/list')
return;
} else {
setContentMd5(newMd5);
dispatch(setAutosaveTime(Date.now()));
}
}
}
};
useEffect(() => {
if (!loading && contentId) {
clearInterval(saveInterRef.current);
} else {
clearInterval(saveInterRef.current);
}
}, [contentId, loading, contentMd5, html]);
const addChapterParent = () => {
setEditKey(-1);
setEditValue("");
setParentId(-1);
};
const delChapter = async (node) => {
const childInKey = findParentLevelOne(gData, "key", node.key);
let current = null;
if (childInKey && childInKey.children && childInKey.children.length > 0) {
const index = findTreeToIndex(childInKey.children, "key", node.key);
if (childInKey.children.length === 1) {
current = childInKey;
} else {
// 前面还有
if (index <= childInKey.children.length) {
if (index === 0) {
current = childInKey.children[index + 1];
} else {
current = childInKey.children[index - 1];
}
}
}
} else {
const index = findTreeToIndex(gData, "key", node.key);
let child = null;
if (index === 0) {
child = gData[index + 1];
} else {
child = gData[index - 1];
}
if (child && child.children && child.children.length > 0) {
current = child.children[0];
} else {
current = child;
}
}
const data = await chapterDel({ id: node.key });
if (data) {
await dispatch(
setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }),
);
await getChapterTreeList();
}
};
useEffect(() => {
if (gData && gData.length > 0 && treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(gData, "key", treeChapter.saveChapterId);
if (childInKey && Object.entries(childInKey).length > 0) {
setChapterId(parseInt(treeChapter.saveChapterId));
setCheckedKeys([parseInt(treeChapter.saveChapterId)]);
setNowTitle(childInKey.title);
}
}
}
}, [gData, treeChapter]);
let chapterMenuItems = [
{ key: "1", label: "展开全部" },
{ key: "2", label: "添加子节" },
{ key: "7", label: "关联知识图谱" },
{ key: "5", label: "设置编者" },
{
key: "6",
label: "一键AI",
children: [
{ key: "61", label: "一键AI创作课件", disabled: true },
{ key: "62", label: "一键AI创作教案", disabled: true },
],
},
{ key: "3", label: "编辑" },
{ key: "4", label: "删除" },
];
if (!isAdminOrCreator) {
chapterMenuItems = [{ key: "1", label: "展开全部" }];
}
const handleMenuClick = async (e, node) => {
e.domEvent.stopPropagation();
setCurrentChapter(node);
if (parseInt(e.key) === 1) {
// 展开全部
const ids = findNodeById([node], "key", node.key);
const expandedKeysTemp = [...expandedKeys, node.key, ...ids];
setExpandedKeys(expandedKeysTemp);
} else if (parseInt(e.key) === 2) {
// 添加子节
setEditKey(-1);
setEditValue("");
setParentId(node.key);
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key);
setEditValue(node.title);
setParentId(false);
} else if (parseInt(e.key) === 4) {
await modal.confirm({
title: "删除确认",
content: `确认删除子节【${node.title}】?`,
onOk() {
delChapter(node);
},
});
} else if (e.key == 5) {
setEditChapterEditorsIsOpen(true);
} else if (e.key == 7) {
setBindKnowledgeGraphIsOpen(true);
}
};
// 章节名称
const chapterTitleRender = (node) => {
return (
<div className="chapter-tree-item">
<p className="title">{node.title}</p>
<Dropdown
trigger={["click"]}
menu={{ items: chapterMenuItems, onClick: (e) => handleMenuClick(e, node) }}
>
<EllipsisOutlined onClick={(e) => e.stopPropagation()} />
</Dropdown>
</div>
);
};
const onDrop = async (info) => {
const dropToGap = info.dropToGap; // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key;
const sideDropNode = info.node.key;
let data = null;
if (dropToGap) {
// 同级
const dragOverGapBottom = info.node.dragOverGapBottom;
const dragOver = info.node.dragOver;
const dragOverGapTop = info.node.dragOverGapTop;
if (!dragOverGapTop && !dragOver && !dragOverGapBottom) {
// 拖到最底部
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
} else {
if (dragOverGapTop) {
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "before",
});
} else if (dragOverGapBottom) {
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
}
}
} else {
// 子级
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "inner",
});
}
if (data) {
await getChapterTreeList();
}
};
return (
<div className="examine">
<div className="content-box">
<div className="book-content-row">
<div
ref={sidebarRef}
className="book-content-tree"
style={{
width: isCollapse ? 50 : sidebarWidth,
minWidth: isCollapse ? 50 : 200,
flexShrink: 0,
}}
>
<div className="border">
{!isCollapse ? (
<>
<Descriptions
layout="vertical"
rootClassName="section-left-top"
bordered
items={[
{
key: "1",
label: (
<Row
gutter={5}
justify={"space-between"}
style={{ alignItems: "center" }}
>
<Col>章节目录</Col>
<Col>
{isAdminOrCreator && (
<Button
type="text"
icon={<DiffOutlined />}
onClick={addChapterParent}
></Button>
)}
<Button
type="text"
icon={<MenuFoldOutlined />}
onClick={() => setisCollapse(true)}
></Button>
</Col>
</Row>
),
},
]}
/>
{gData && gData.length > 0 && (
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandAll
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultSelectedKeys={checkedKeys}
selectedKeys={checkedKeys}
onDrop={onDrop}
draggable
blockNode
disabled={loading}
treeData={gData}
onExpand={onExpand}
titleRender={(nodeData) => chapterTitleRender(nodeData)}
/>
)}
</>
) : (
<Descriptions
layout="vertical"
bordered
items={[
{
key: "1",
label: (
<Row gutter={5} justify={"space-between"} style={{ alignItems: "center" }}>
<Col>
<Button
type="text"
icon={<MenuUnfoldOutlined />}
onClick={() => setisCollapse(false)}
></Button>
</Col>
</Row>
),
},
]}
/>
)}
</div>
</div>
{!isCollapse && <div className="resize-handle" onMouseDown={handleMouseDown} />}
<div className="book-content-editor" style={{ flex: 1, minWidth: 0 }}>
<div className="editor-right">
<Spin spinning={loading}>
<WangEditorCustomer
ref={editorRef}
chapterId={chapterId}
bookId={id}
contentId={contentId}
setHtml={setHtml}
html={html}
saveContent={saveContent}
gData={gData}
nowTitle={nowTitle}
disabled={disabled}
/>
</Spin>
</div>
</div>
</div>
</div>
<Modal
open={editKey}
footer={null}
centered
destroyOnClose
title={editKey > 0 ? "修改章节" : "新增章节"}
maskClosable={false}
mask={false}
closeIcon={false} // 添加这一行以隐藏关闭按钮
onCancel={() => setEditKey(false)}
classNames={{
wrapper: "chapter-title-modal",
}}
>
<EditChapterTitle
setEditKey={setEditKey}
editValue={editValue}
editKey={editKey}
getChapterTreeList={getChapterTreeList}
setEditValue={setEditValue}
parentId={parentId}
bookId={id}
expandedKeys={expandedKeys}
setExpandedKeys={setExpandedKeys}
editorLoading={loading}
newChapterChange={newChapterChange}
/>
</Modal>
{/* 设置章节编写者 */}
{currentChapter && editChapterEditorsIsOpen && (
<EditChapterEditors
open={editChapterEditorsIsOpen}
chapter={currentChapter}
onCancel={() => setEditChapterEditorsIsOpen(false)}
onChange={onChapterEditorsChange}
></EditChapterEditors>
)}
{/* 关联知识图谱 */}
{currentChapter && bindKnowledgeGraphIsOpen && (
<BindKnowledgeGraph
open={bindKnowledgeGraphIsOpen}
chapter={currentChapter}
onCancel={() => setBindKnowledgeGraphIsOpen(false)}
onOk={() => setBindKnowledgeGraphIsOpen(false)}
></BindKnowledgeGraph>
)}
</div>
);
};
export default Examine;
.examine {
height: 100%;
.content-box {
height: 100%;
box-sizing: border-box;
overflow: hidden;
.book-content-row {
height: 100%;
overflow: hidden;
display: flex;
gap: 0;
.book-content-tree {
height: 100%;
overflow: hidden;
transition: width 0s;
}
.book-content-editor {
height: 100%;
overflow: hidden;
}
}
.resize-handle {
width: 6px;
cursor: col-resize;
background: transparent;
position: relative;
flex-shrink: 0;
z-index: 10;
transition: background 0.2s;
&:hover,
&:active {
background: #1890ff;
border-radius: 3px;
}
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 30px;
background: #d9d9d9;
border-radius: 1px;
transition: background 0.2s;
}
&:hover::after,
&:active::after {
background: #fff;
}
}
.ant-descriptions-item-label {
padding: 10px !important;
}
.ant-descriptions-view {
border: none;
}
.border {
border: 1px solid #e5e5e5;
height: 100%;
display: flex;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
border-radius: 6px;
.section-left-top {
height: 85px;
}
.ant-descriptions.ant-descriptions-bordered > .ant-descriptions-view .ant-descriptions-row {
border-bottom: 1px solid #e5e5e5;
&:nth-of-type(2) {
display: none;
}
}
}
.editor-right {
height: 100%;
.ant-spin-nested-loading,
.ant-spin-container {
height: 100%;
}
}
}
.draggable-tree {
flex: 1;
height: calc(100vh - 285px);
overflow-y: auto;
overflow-x: hidden;
width: 98%;
&.ant-tree .ant-tree-node-content-wrapper {
width: 100%;
}
.ant-tree-switcher-noop {
// display: none;
width: 10px;
}
&.ant-tree .ant-tree-node-content-wrapper {
overflow: hidden;
}
}
.chapter-tree-item {
display: flex;
justify-content: space-between;
overflow: hidden;
.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
max-height: 24px;
line-height: 24px;
white-space: nowrap;
}
}
}
.chapter-title-modal {
.ant-modal {
.ant-modal-close {
inset-inline-end: 17px;
top: 16px;
}
.ant-modal-content {
padding: 15px 18px;
}
.ant-divider {
margin: 15px 0;
}
}
}
import axios from "@/utils/request.js";
import qs from "qs";
import { get } from "lodash-es";
import { message } from "antd";
// 用户信息
const getUserInfo = async (obj) => {
try {
const res = await axios.post("/user/user/getUserInfo", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取角色已选权限
const getPowerByRoleId = async (obj) => {
try {
const res = await axios.post("/user/role/getPowerByRoleId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节列表
const getAllList = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getAllList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return {
code,
data,
};
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节
const chapterAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success('添加成功');
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节详情
const getInfoById = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getInfoById", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节
const chapterEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 删除章节
const chapterDel = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 拖拽章节排序
const dragOrder = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/dragOrder", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取章节内容
const getInfoByChapterId = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getInfoByChapterId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节内容
const sectionAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节内容
const sectionEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success(msg);
return true;
} else {
message.error(msg);
return { code, msg, data };
}
} catch (error) {
return false;
}
};
// 获取章节的题库列表
const getQuestionList = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/getQuestionList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 移除课后题
const questionDel = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加课后题
const questionAdd = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 发布审核
const release = async (obj) => {
try {
const res = await axios.post("/book/teacher/release", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板列表
const templateGetList = async (obj) => {
try {
const res = await axios.post("/book/template/getList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板导入
const templateImport = async (obj) => {
try {
const res = await axios.post("/book/template/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 书籍导入
const teacherImport = async (obj) => {
try {
const res = await axios.post("/book/teacher/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 保存历史记录
const getRecordList = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getRecordList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export {
getAllList,
chapterAdd,
getInfoById,
chapterEdit,
chapterDel,
dragOrder,
getInfoByChapterId,
sectionAdd,
sectionEdit,
getQuestionList,
questionDel,
questionAdd,
release,
templateGetList,
templateImport,
teacherImport,
getRecordList,
getPowerByRoleId,
getUserInfo,
};
export { default } from "@/pages/books/management/addedit";
import { useState, useEffect } from "react";
import { Button, Form, Input, Flex, message } from "antd";
import WangEditor from "@/components/editor";
import UploadImage from "@/components/uploadImage";
import { useLocation, useNavigate } from "react-router";
import { addBook, updateBook, getBook } from "../api";
import SelectTeacher from "@/components/selectTeacher";
const BookAddEdit = () => {
const navigate = useNavigate();
const location = useLocation();
const isEdit = !!location.state?.id;
const [form] = Form.useForm();
const [img, setImg] = useState("");
// 编辑器内容
const [html, setHtml] = useState("");
// 详情信息
const fetchInfo = async () => {
const { data } = await getBook({ id: location.state.id });
setImg(data.img);
setHtml(data.content);
const formFields = {
...data,
editors: data.editors_list.map((item) => {
return { label: item.real_name, value: item.editor_id };
}),
};
form.setFieldsValue(formFields);
};
useEffect(() => {
if (isEdit) fetchInfo();
}, [isEdit]);
const onFinish = async (values) => {
const editors = values.editors?.map((item) => item.value) || [];
const params = { ...values, img, editors };
if (editors.length === 0) delete params.editors;
const res = isEdit
? await updateBook({ id: location.state.id, ...params })
: await addBook(params);
message.success(res.message);
navigate(-1);
};
return (
<Form form={form} onFinish={onFinish} labelCol={{ span: 2 }}>
<Form.Item
label="书籍名称"
name="name"
rules={[
{ required: true, message: "请输入书籍名称" },
{ max: 30, message: "最大30字符" },
]}
>
<Input autoComplete="off" placeholder="请输入书籍名称" style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item
label="作者"
name="authors"
rules={[
{ required: true, message: "请输入作者" },
{ max: 30, message: "最大30字符" },
]}
>
<Input autoComplete="off" placeholder="请输入作者" style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item name="img" label="缩略图" className="uploadPar">
<UploadImage value={img} onChange={(url) => setImg(url)}></UploadImage>
</Form.Item>
<Form.Item
label="编辑推荐"
name="introduction"
rules={[{ max: 65535, message: "最多65535字符" }]}
>
<Input.TextArea
placeholder="请输入编辑推荐"
autoSize={{ maxRows: 8, minRows: 5 }}
style={{ width: 910 }}
/>
</Form.Item>
<Form.Item label="编写者" name="editors">
<SelectTeacher placeholder="请选择编写者" style={{ width: 260 }}></SelectTeacher>
</Form.Item>
<Form.Item label="简介" name="content">
<WangEditor
value={html}
onChange={(editor) => form.setFieldValue("content", editor.getHtml())}
></WangEditor>
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
<Flex gap={20} justify="center">
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type="primary" htmlType="submit">
确认
</Button>
</Flex>
</Form.Item>
</Form>
);
};
export default BookAddEdit;
export { default } from "@/pages/books/section";
export { default } from "../chapters";
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const BooksLayout = lazy(() => import("@/pages/books/index"));
const BooksLayout = lazy(() => import("./BooksLayout"));
const ManagementRoutes = lazy(() => import("./management/routes"));
const AuditRoutes = lazy(() => import("./audit/routes"));
const ClassifyRoutes = lazy(() => import("./classify/routes"));
......
......@@ -8,3 +8,24 @@ async function post(path: string, data: Record<string, unknown>, notify = false)
export const getSaleList = (query: SaleQuery) => post("/book/book/getList", query);
export const changeSaleStatus = (id: string | number, status: string) => post("/book/book/changeStatus", { id, status }, true);
export const getSaleDiscussionList = (query: SaleDiscussionQuery) => post("/comment/comment/getCommentList", query);
export const getListAll = (data: Record<string, unknown>) =>
post("/book/chapter/getListAll", data);
export const getInfoById = (data: Record<string, unknown>) =>
post("/book/book/getInfoById", data);
export const publisherList = (data: Record<string, unknown>) =>
post("/book/publisher/getListAll", data);
export const publisherAdd = (data: Record<string, unknown>) =>
post("/book/publisher/add", data, true);
export const editBook = (data: Record<string, unknown>) =>
post("/book/book/edit", data, true);
export const getCommentInfo = (data: Record<string, unknown>) =>
post("/comment/comment/getCommentInfo", data);
export const commentDel = async (data: Record<string, unknown>) => {
const result = await post("/comment/comment/del", data, true);
return result !== false;
};
export const commentReply = async (data: Record<string, unknown>) => {
const result = await post("/comment/comment/reply", data, true);
return result !== false;
};
export { default } from "@/pages/books/sale/discussDetail";
import { Card, Input, Button, Space, Row, Col, Modal, Form } from "antd";
import { DeleteOutlined, FacebookFilled, MessageOutlined } from "@ant-design/icons";
import "./SaleDiscussionDetail.less";
import User from "@/assets/images/icon/user.png";
import { useState, useEffect } from "react";
import { useLocation } from "react-router";
import { getCommentInfo, commentReply, commentDel } from "../api";
const { TextArea } = Input;
const DiscussDetail = () => {
const [showReply, setShowReply] = useState(false);
const [delMoadal, setDelModal] = useState(false);
const {
state: { id, name, book_id },
} = useLocation();
const [form] = Form.useForm();
const [data, setData] = useState({});
const [loading, setLoading] = useState(false);
const [commentId, setcommentId] = useState(0);
// 获取列表
const init = async () => {
setLoading(true);
const data = await getCommentInfo({ id, book_id });
console.log(data);
setData(data);
setLoading(false);
};
useEffect(() => {
init();
}, []);
useEffect(() => {
if (!showReply) {
form.setFieldsValue({ content: "" });
form.resetFields();
}
}, [showReply]);
// 显示回复弹框
const showReplyFn = (id) => {
setcommentId(id);
setShowReply(true);
};
// 点击确认回复
const handleReply = async (obj) => {
const bool = await commentReply({ ...obj, id: commentId, book_id });
if (!bool) return;
setShowReply(false);
init();
};
const handleDel = async () => {
const bool = await commentDel({ id: commentId, book_id });
if (!bool) return;
setDelModal(false);
init();
};
return (
<div className="DiscussDetail">
<Modal
open={showReply}
onCancel={() => {
setShowReply(false);
}}
maskClosable={false}
footer={null}
centered
closeIcon={null}
keyboard={false}
>
<br />
<Form form={form} onFinish={handleReply}>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: "请输入内容" }]}
>
<TextArea
placeholder="请输入内容"
autoSize={{
minRows: 3,
maxRows: 5,
}}
></TextArea>
</Form.Item>
<Form.Item>
<Row gutter={20} justify={"center"}>
<Col>
<Button onClick={() => setShowReply(false)}>取消</Button>
</Col>
<Col>
<Button type="primary" htmlType="submit">
确认
</Button>
</Col>
</Row>
</Form.Item>
</Form>
</Modal>
<Modal
mask={false}
centered
open={delMoadal}
onOk={handleDel}
onCancel={() => setDelModal(false)}
footer={(_, { OkBtn, CancelBtn }) => {
return (
<Row justify={"center"} className="delModal">
<Space>
<CancelBtn />
<OkBtn />
</Space>
</Row>
);
}}
okText="确认"
>
<p style={{ textAlign: "center", padding: "50px 0 30px", fontSize: 16 }}>
评论将永久删除,是否继续?
</p>
</Modal>
<Card className="card" title={`书名:《${name}》`} loading={loading}>
<div className="discussBox">
<Row className="discussItem" gutter={10}>
<Col className="img">
<img
src={data.person_pic || User}
width={28}
height={28}
style={{ borderRadius: "50%", objectFit: "cover" }}
/>
</Col>
<Col className="discuss" flex={1}>
<div className="name">{data.person_name}</div>
{/* <div className='content'>{data?.content && data.content[0].content}</div> */}
<div className="content">
{data?.content &&
data.content.map((item, index) => {
const type = item.types;
const value = item.content;
if (type === "audio") {
return (
<div key={index}>
<span>
<audio controls>
<source src={value} type="audio/mp3" />
Your browser does not support the audio element.
</audio>
</span>
</div>
);
} else if (type === "text") {
return <span key={index}>{value}</span>;
} else if (type === "image") {
return <img height={100} src={value}></img>;
}
})}
</div>
<Space>
<Button
type="text"
className="reply"
onClick={() => showReplyFn(data.id)}
icon={<MessageOutlined />}
>
回复
</Button>
<Button
type="text"
className="del"
icon={<DeleteOutlined />}
onClick={() => {
setDelModal(true);
setcommentId(data.id);
}}
>
删除
</Button>
</Space>
</Col>
</Row>
{data.child &&
data.child.map((item) => (
<Row className="discussItem reply" key={item.id} gutter={10}>
<Col span={1}></Col>
<Col className="img">
<img
src={item.person_pic || User}
width={28}
height={28}
style={{ borderRadius: "50%", objectFit: "cover" }}
/>
</Col>
<Col className="discuss" flex={1}>
<div className="name">{item.person_name}</div>
<div className="content">
{item.content &&
item.content.map((item, index) => (
<div key={index}>
{item.types == "text" && <span>{item.content}</span>}
{item.types == "image" && <img src={item.content}></img>}
{item.types == "audio" && <audio controls src={item.content} />}
</div>
))}
</div>
<Space>
<Button
type="text"
className="reply"
onClick={() => showReplyFn(item.id)}
icon={<MessageOutlined />}
>
回复
</Button>
<Button
type="text"
className="del"
icon={<DeleteOutlined />}
onClick={() => {
setDelModal(true);
setcommentId(item.id);
}}
>
删除
</Button>
</Space>
</Col>
</Row>
))}
</div>
{/* <div className="discussCreate">
<Row gutter={10}>
<Col><div className="img"><img src={User} width={28} height={28} /></div></Col>
<Col><Input autoComplete="off" placeholder="发布你的评论" style={{width:754}}></Input ></Col>
<Col>
<Space>
<Button>取消</Button>
<Button type="primary">回复</Button>
</Space>
</Col>
</Row>
</div> */}
</Card>
</div>
);
};
export default DiscussDetail;
.DiscussDetail {
.reply {
color: #aa1941;
}
.del {
color: #999999;
}
.card {
.ant-card-body {
height: calc(100vh - 260px);
overflow: auto;
}
}
.discussItem {
.discuss {
&::after {
display: block;
content: "";
width: 100%;
height: 1px;
background: #e5e5e57f;
margin-top: 16px;
margin-bottom: 10px;
}
.name {
font-size: 12px;
color: #333333;
font-weight: 500;
margin-bottom: 4px;
}
.content {
font-size: 12px;
color: #333333;
}
}
}
}
export { default } from "@/pages/books/sale/edit";
import React, { useEffect, useState, useRef } from "react";
import {
Form,
Input,
Select,
Button,
Radio,
Space,
Image,
Spin,
Tree,
Drawer,
Row,
Col,
message,
} from "antd";
import { getInfoById, publisherList, publisherAdd, getListAll, editBook } from "../api";
import { getClassifyList } from "@/modules/books/classify/api";
import { useLocation } from "react-router";
import { useNavigate } from "react-router";
import { convertToAntdTreeData } from "@/utils/common.js";
const SaleEdit = () => {
const {
state: { id },
} = useLocation();
const [initialValues, setinitialValues] = useState({
name: "",
authors: "",
category_id: null,
producers_id: null,
press_id: null,
is_free: null,
});
const [form] = Form.useForm();
const navigate = useNavigate();
const [showAddCategory, setShowAddCategory] = useState(false); //弹出添加出品方
const [showAddCategory1, setShowAddCategory1] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
const [publisher_name, setPublisherName] = useState(""); //出品方
const [publishers, setPublishers] = useState([]);
const [publisher_name1, setPublisherName1] = useState(""); //出版社
const [publishers1, setPublishers1] = useState([]);
const [treeData, setTreeData] = useState([]);
// 在组件中添加一个状态来保存书籍类型的值
const [bookType, setBookType] = useState(0); // 默认值为免费
const [producersConfig, setproducersConfig] = useState({
validateStatus: "",
help: "",
noStyle: false,
style: {},
});
const [pressConfig, setpressConfig] = useState({
validateStatus: "",
help: "",
noStyle: false,
style: {},
});
const addproducers = async (obj, callbackFn) => {
const { types } = obj;
let flag = true;
if (types == 1) {
if (!publisher_name) {
flag = false;
setproducersConfig({
...producersConfig,
validateStatus: "error",
help: "请输入新的出品方",
noStyle: false,
});
}
} else if (types == 2) {
if (!publisher_name1) {
flag = false;
setpressConfig({
...pressConfig,
validateStatus: "error",
help: "请输入新的出版社",
noStyle: false,
});
}
}
if (!flag) return;
// 上面代码是校验
const bool = await publisherAdd(obj);
if (!bool) return;
callbackFn();
setproducersConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setpressConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
getProducers();
};
const [price, setprice] = useState(0);
const [vip_price, setvip_price] = useState(0);
const [discountVal, setDiscountVal] = useState(0);
const calculateDiscountVal = (price, vip_price) => {
if (Number(price) > 0 && Number(vip_price) > 0) {
return (Number(vip_price) / Number(price)).toFixed(2) * 10;
} else {
return 0;
}
};
// 只在零售价改变时更新折扣率
useEffect(() => {
let price = form.getFieldValue("price");
let vip_price = form.getFieldValue("vip_price");
setDiscountVal(calculateDiscountVal(price, vip_price).toFixed(2));
console.log("discountVal", discountVal);
}, [form.getFieldValue("price"), form.getFieldValue("vip_price")]);
// // 问题:输入3.3折后,输入框变成3.30000 ; 原因:toFixed()数字转字符串时可能会导致精度问题 ; 解决:parseFloat来解析浮点数
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// setDiscountVal(newVal);
// const price = form.getFieldValue('price');
// updateVipPriceFromDiscountRate(price, newVal);
// }
// };
// const updateVipPriceFromDiscountRate = (price, discountRate) => {
// if (Number(price) > 0 && Number(discountRate) >= 0 && Number(discountRate) <= 10) {
// const vip_price = price * (discountRate / 10);
// form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
// }
// };
// 在onChange中用parseFloat来解析浮点数,输入折扣数后,就触发折扣价改变,发现还是不太行,因为浮点数的精度问题可能会导致在 onChange 事件中出现意外行为。目前会更报错出现NaN,当在用户输入小数点和小数部分时,浮点数可能会被不完整地解析,导致计算错误,出现NaN
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// const roundedVal = parseFloat(newVal).toFixed(2); // 对输入的值进行四舍五入处理
// setDiscountVal(roundedVal);
// const price = form.getFieldValue('price');
// // 添加计算并更新折扣金额的逻辑
// if (Number(price) > 0) {
// const vip_price = price * (roundedVal / 10);
// form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
// }
// }
// };
// 将parseFloat放在失去焦点(onBlur)中处理,更靠谱
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10 && /^\d*\.?\d{0,3}$/.test(newVal)) {
// 不在这里进行四舍五入处理,而是在失去焦点时处理
setDiscountVal(newVal);
} else {
message.error("最多输入小数点后3位,且不能大于10折");
newVal.slice(0, -1);
}
};
const handleDiscountRateBlur = () => {
const newVal = parseFloat(discountVal);
if (Number.isFinite(newVal) && newVal >= 0 && newVal <= 10) {
const roundedVal = newVal.toFixed(3);
setDiscountVal(roundedVal);
const price = form.getFieldValue("price");
// 添加计算并更新折扣元的逻辑
if (Number(price) > 0) {
const vip_price = price * (roundedVal / 10);
form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
}
}
};
const handleVippriceChange = (ev) => {
const newVal = ev.target.value;
let price = form.getFieldValue("price");
if (Number(newVal) <= price && /^\d*\.?\d{0,3}$/.test(newVal)) {
setvip_price(newVal);
} else {
message.error("最多输入小数点后3位,且不能大于零售价");
newVal.slice(0, -1);
}
};
useEffect(() => {
if (!showAddCategory) {
setPublisherName("");
}
if (!showAddCategory1) {
setPublisherName1("");
}
}, [showAddCategory, showAddCategory1]);
const handleOpenDrawer = async () => {
setShowDrawer(true);
const data = await getListAll({ book_id: id });
let arr = convertToAntdTreeData(data, "name");
setTreeData(arr);
};
const handleCloseDrawer = () => {
setShowDrawer(false);
};
const [classData, setClassData] = useState([]);
const [producersData, setproducersData] = useState([]);
const [pressData, setpressData] = useState([]);
// 出品方/出版社
const getProducers = async () => {
const producersList = await publisherList({ types: 1 });
const press = await publisherList({ types: 2 });
setproducersData(producersList);
setpressData(press);
};
const [FormLoad, setFormLoad] = useState(true);
const [checkedKeys, setcheckedKeys] = useState([]);
const submitTree = (obj) => {
form.setFieldValue("trials", checkedKeys);
handleCloseDrawer(false);
};
const ontreeCheck = (keys, { checked, checkedNodes, node, halfCheckedKeys }) => {
setcheckedKeys(keys);
/* const handleCheck = (parentNode) => {
const keys = [];
const traverse = (currentNode) => {
keys.push(currentNode.key);
if (currentNode.children) {
currentNode.children.forEach((childNode) => {
traverse(childNode, currentNode.key);
});
}
};
traverse(parentNode);
return keys;
};
if (checked) {
const keysToCheck = handleCheck(node);
setcheckedKeys([...checkedKeys.checked, ...keysToCheck]);
} else {
const keysToUncheck = handleCheck(node);
const newCheckedKeys = checkedKeys.checked.filter((key) => !keysToUncheck.includes(key));
setcheckedKeys(newCheckedKeys);
} */
};
const getinfo = async () => {
setFormLoad(true);
// 详情
const data = await getInfoById({ id });
const { category_id, producers_id, press_id } = data;
let obj = { category_id, producers_id, press_id };
setBookType(data.is_free);
for (let key in obj) {
if (obj[key] === 0) {
obj[key] = null;
}
}
form.setFieldsValue({ ...data, ...obj });
setFormLoad(false);
};
const getInfo = async () => {
// 分类
const { list } = await getClassifyList({ page: 1, page_size: 999 });
setClassData(list);
getProducers();
getinfo();
};
const onFinish = async (obj) => {
const bool = await editBook({ ...obj, id });
if (!bool) return;
navigate(-1);
};
useEffect(() => {
getInfo();
}, []);
return (
<Spin spinning={FormLoad} style={{ padding: 20 }}>
<Form labelCol={{ span: 2 }} form={form} initialValues={initialValues} onFinish={onFinish}>
<Form.Item label="书籍名称" name="name">
<Input
disabled
allowClear
autoComplete="off"
placeholder="请输入书籍名称"
style={{ width: 400 }}
></Input>
</Form.Item>
<Form.Item label="书籍作者" name="authors">
<Input autoComplete="off" disabled style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item
label="分类"
name="category_id"
rules={[{ required: true, message: "请选择分类" }]}
>
<Select placeholder="请选择分类" style={{ width: 260 }}>
{classData &&
classData.map((item) => (
<Select.Option value={item.id} key={item.id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="出品方" required style={producersConfig.style}>
<Space>
<Form.Item
noStyle
name="producers_id"
rules={[{ required: true, message: "请选择出品方" }]}
>
<Select placeholder="请选择出品方" style={{ width: 260 }}>
{producersData?.map((item) => (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
))}
</Select>
</Form.Item>
<a
style={{
textDecoration: "underline",
color: "#1672EC",
cursor: "pointer",
}}
onClick={() => {
setproducersConfig({ ...producersConfig, style: { marginBottom: 0 } });
setShowAddCategory(true);
}}
>
添加出品方
</a>
</Space>
{showAddCategory && (
<div style={{ marginTop: "10px" }}>
<Space>
<Form.Item
noStyle={producersConfig.noStyle}
validateStatus={producersConfig.validateStatus}
help={producersConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete="off"
placeholder="请输入新的出品方"
value={publisher_name}
allowClear
onChange={(e) => {
let val = e.target.value;
if (val) {
setproducersConfig({ ...producersConfig, help: "", validateStatus: "" });
}
setPublisherName(val);
}}
/>
</Form.Item>
<a
type="link"
style={{ textDecoration: "underline", color: "#1672EC" }}
onClick={() =>
addproducers({ types: 1, publisher_name }, () => setShowAddCategory(false))
}
>
添加
</a>
<a
type="link"
style={{ textDecoration: "underline", color: "#AA1941" }}
onClick={() => {
setproducersConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setShowAddCategory(false); // 可选:添加后关闭输入框
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label="出版社" required style={pressConfig.style}>
<Space>
<Form.Item
noStyle
name="press_id"
rules={[{ required: true, message: "请选择出版社" }]}
>
<Select placeholder="请选择出品方" style={{ width: 260 }}>
{pressData?.map((item) => {
return (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
);
})}
</Select>
</Form.Item>
<a
style={{
textDecoration: "underline",
color: "#1672EC",
cursor: "pointer",
}}
onClick={() => {
setpressConfig({ ...pressConfig, style: { marginBottom: 0 } });
setShowAddCategory1(true);
}}
>
添加出版社
</a>
</Space>
{showAddCategory1 && (
<div style={{ marginTop: "10px" }}>
<Space>
<Form.Item
noStyle={pressConfig.noStyle}
validateStatus={pressConfig.validateStatus}
help={pressConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete="off"
placeholder="请输入新的出版社"
allowClear
value={publisher_name1}
onChange={(e) => {
let val = e.target.value;
if (val) {
setpressConfig({ ...pressConfig, help: "", validateStatus: "" });
}
setPublisherName1(e.target.value);
}}
/>
</Form.Item>
<a
type="link"
style={{ textDecoration: "underline", color: "#1672EC" }}
onClick={() =>
addproducers({ types: 2, publisher_name: publisher_name1 }, () =>
setShowAddCategory1(false),
)
}
>
添加
</a>
<a
type="link"
style={{ textDecoration: "underline", color: "#AA1941" }}
onClick={() => {
setpressConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setShowAddCategory1(false);
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label="书籍类型" name="is_free">
<Radio.Group
onChange={(e) => {
setBookType(e.target.value);
}}
>
<Radio value={1}>免费</Radio>
<Radio value={0}>付费</Radio>
</Radio.Group>
</Form.Item>
{bookType === 0 && (
<Form.Item label="试读章节" name="trials">
<a type="link" style={{ textDecoration: "underline" }} onClick={handleOpenDrawer}>
选择试读章节
</a>
</Form.Item>
)}
{bookType === 0 && (
<>
<Form.Item label="零售价(元):" name="price">
<Input
value={price}
onChange={(e) => setprice(e.target.value)}
autoComplete="off"
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item label="折扣价(元):">
<Form.Item name="vip_price" style={{ display: "inline-block" }}>
<Input
value={vip_price}
// onChange={(e) => setvip_price(e.target.value)}
onChange={handleVippriceChange}
autoComplete="off"
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item style={{ display: "inline-block", margin: "0 8px" }}>
<Input
// value={Number(discountVal).toFixed(2)}
value={discountVal}
allowClear
onChange={handleDiscountRateChange}
onBlur={handleDiscountRateBlur} // 添加失去焦点事件处理函数
autoComplete="off"
style={{ width: 80 }}
></Input>
&nbsp;&nbsp;折
</Form.Item>
</Form.Item>
</>
)}
<Form.Item wrapperCol={{ offset: 2 }}>
<Space className="footer" size={20}>
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type="primary" htmlType="submit">
确认
</Button>
</Space>
</Form.Item>
</Form>
<Drawer
placement="right"
onClose={handleCloseDrawer}
open={showDrawer}
labelCol={{ span: 7 }}
mask={false}
>
<Form onFinish={submitTree}>
<Form.Item name="power_list" style={{ padding: 10 }}>
<Tree
checkable
checkedKeys={checkedKeys}
defaultExpandAll={false} //让授权后的弹窗只展示根标签
treeData={treeData}
// showLine //删除这里,树形结构左侧的下拉线消失,图标从+-更改为默认的△
// checkStrictly
onCheck={ontreeCheck}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 10, span: 16 }}>
<Space size={20}>
<Button onClick={() => handleCloseDrawer(false)}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</Spin>
);
};
export default SaleEdit;
import { Outlet } from 'react-router'
const MembersLayout = () => <Outlet />
export default MembersLayout
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const MembersLayout = lazy(() => import("@/pages/member/index"));
const MembersLayout = lazy(() => import("./Layout"));
const MemberListRoutes = lazy(() => import("./list/routes"));
const MemberLevelRoutes = lazy(() => import("./level/routes"));
......
import React from "react";
import { Outlet } from "react-router";
const Role = () => {
return (
<div>
<Outlet />
</div>
);
};
export default Role;
......@@ -21,7 +21,7 @@ import dayjs from "dayjs";
import Add from "@/assets/images/icon/add.png";
import { useSelector } from "react-redux";
import AliOSS from "ali-oss";
import { getAliOSSSTSToken } from "@/pages/setting/help/addedit/requet";
import { getAliOSSSTSToken } from "../../help/request";
const AppVersion = () => {
const [data, setData] = useState([]);
const [page_size, setpage_size] = useState(10);
......
import axios from "@/utils/request.js";
import qs from "qs";
import { message } from "antd";
import { get } from "lodash-es";
// oss
const getAliOSSSTSToken = async (obj) => {
try {
const res = await axios.post("/common/OssUpload/getUploadToken", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "获取OSS令牌失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export { getAliOSSSTSToken };
......@@ -11,7 +11,7 @@ import { uploadFiles } from "@/utils/upload";
import AliOSS from "ali-oss";
import dayjs from "dayjs";
import { getAliOSSSTSToken } from "@/pages/setting/help/addedit/requet";
import { getAliOSSSTSToken } from "../request";
import "./HelpAddEditView.less";
......
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const SettingsLayout = lazy(() => import("@/pages/setting/index"));
const SettingsLayout = lazy(() => import("./SettingsLayout"));
const HelpRoutes = lazy(() => import("./help/routes"));
const AppliedRoutes = lazy(() => import("./applied/routes"));
const PersonalizedRoutes = lazy(() => import("./personalized/routes"));
......
export * from "@/pages/student/book/request";
export * from "./request";
import axios from "@/utils/request.js";
import qs from "qs";
import { get } from "lodash-es";
import { message } from "antd";
// 用户信息
const getUserInfo = async (obj) => {
try {
const res = await axios.post("/user/user/getUserInfo", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取角色已选权限
const getPowerByRoleId = async (obj) => {
try {
const res = await axios.post("/user/role/getPowerByRoleId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节列表
const getAllList = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getAllList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return {
code,
data,
};
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节
const chapterAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success('添加成功');
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节详情
const getInfoById = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getInfoById", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节
const chapterEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 删除章节
const chapterDel = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 拖拽章节排序
const dragOrder = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/dragOrder", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取章节内容
const getInfoByChapterId = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getInfoByChapterId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节内容
const sectionAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节内容
const sectionEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success(msg);
return true;
} else {
message.error(msg);
return { code, msg, data };
}
} catch (error) {
return false;
}
};
// 获取章节的题库列表
const getQuestionList = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/getQuestionList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 移除课后题
const questionDel = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加课后题
const questionAdd = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 发布审核
const release = async (obj) => {
try {
const res = await axios.post("/book/teacher/release", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板列表
const templateGetList = async (obj) => {
try {
const res = await axios.post("/book/template/getList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板导入
const templateImport = async (obj) => {
try {
const res = await axios.post("/book/template/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 书籍导入
const teacherImport = async (obj) => {
try {
const res = await axios.post("/book/teacher/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 保存历史记录
const getRecordList = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getRecordList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export {
getAllList,
chapterAdd,
getInfoById,
chapterEdit,
chapterDel,
dragOrder,
getInfoByChapterId,
sectionAdd,
sectionEdit,
getQuestionList,
questionDel,
questionAdd,
release,
templateGetList,
templateImport,
teacherImport,
getRecordList,
getPowerByRoleId,
getUserInfo,
};
export { default } from "@/pages/student/book";
import { useState, useEffect, useRef } from "react";
import { Button, Row, Col, Descriptions, Tree, Dropdown, Spin, App } from "antd";
import {
EllipsisOutlined,
DiffOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from "@ant-design/icons";
import WangEditorCustomer from "@/common/wangeditor-customer";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { setAutosaveTime } from "@/store/modules/editor";
import { setTreeChapter } from "@/store/modules/user";
import { get } from "lodash-es";
import md5 from "js-md5";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
findParentLevelOne,
findTreeToIndex,
findNodeById,
} from "@/utils/common";
import { getAllList, getInfoByChapterId, sectionEdit, chapterDel, dragOrder } from "../request";
import "./StudentBookView.less";
const Examine = () => {
const { modal } = App.useApp();
const [searchParams] = useSearchParams();
const location = useLocation();
const id = searchParams.get("book_id");
const isAdminOrCreator = get(location, "state.isAdminOrCreator", false);
const { treeChapter, userInfo } = useSelector((state) => state.user);
const navigate = useNavigate();
const dispatch = useDispatch();
const [gData, setGData] = useState([]);
const [chapterId, setChapterId] = useState(0);
const [bookId, setBookId] = useState(0);
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [nowTitle, setNowTitle] = useState("");
// 编辑操作
const [isCollapse, setisCollapse] = useState(false);
const [editKey, setEditKey] = useState(null);
const [parentId, setParentId] = useState(null);
const [editValue, setEditValue] = useState("");
const [loading, setLoading] = useState(false);
const [contentMd5, setContentMd5] = useState("");
// 设置编者
const [currentChapter, setCurrentChapter] = useState(null);
const [editChapterEditorsIsOpen, setEditChapterEditorsIsOpen] = useState(false);
const onChapterEditorsChange = () => {
setEditChapterEditorsIsOpen(false);
};
// 编辑器内容
const editorRef = useRef();
const saveInterRef = useRef();
const [html, setHtml] = useState("");
const [contentId, setContentId] = useState(false);
// 获取目录结构
const getChapterTreeList = async () => {
setLoading(true);
const { data = [], code } = await getAllList({ book_id: id });
if (code !== 200) {
setLoading(false);
navigate("/books/management/list", { replace: true });
return;
}
if (data.length === 0) {
setLoading(false);
return;
}
const uniqueBookList = [...new Set(data.map((item) => item.book_id))][0];
setBookId(uniqueBookList);
const arr = convertToAntdTreeData(data, "name");
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setNowTitle(first.title);
if (treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(
arr,
"key",
searchParams.get("chapter_id") || treeChapter.saveChapterId,
);
if (childInKey) {
setChapterId(childInKey.key);
setCheckedKeys([childInKey.key]);
setNowTitle(childInKey.title);
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }));
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
setLoading(false);
};
const newChapterChange = (id, title) => {
setChapterId(parseInt(id));
setCheckedKeys([parseInt(id)]);
setNowTitle(title);
};
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const [disabled, setDisabled] = useState(false);
// 获取内容
const getChapterVal = async () => {
setLoading(true);
const data = await getInfoByChapterId({ chapter_id: chapterId });
setDisabled(!data.selected_editor_ids.includes(userInfo.id) && !isAdminOrCreator);
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId, ...data }));
const { content, id: cId } = data;
if (content) {
setContentMd5(md5(content));
}
setHtml(content);
setContentId(cId);
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.setHtml(content);
}
setLoading(false);
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
};
const handleSelect = async (checkedKeys, info) => {
const { key } = info.node;
const childInKey = findTreeElementByKey(gData, "key", key);
if (childInKey.children && childInKey.children.length > 0) {
clearInterval(saveInterRef.current);
// 进行展开操作
const tempExpandeds = JSON.parse(JSON.stringify(expandedKeys));
let newExpand = "";
if (expandedKeys.includes(key)) {
newExpand = tempExpandeds.filter((item) => parseInt(item) !== parseInt(key));
} else {
newExpand = [...tempExpandeds, key];
}
setExpandedKeys(newExpand);
setCheckedKeys([key]);
return;
} else {
const { key, title } = info.node;
if (info.selected === false) {
setLoading(true);
setChapterId(key);
setCheckedKeys([key]);
setNowTitle(title);
setLoading(false);
} else {
setLoading(true);
clearInterval(saveInterRef.current);
await saveContent();
if (key !== chapterId) {
editorRef.current.editor.clear();
}
setLoading(true);
setChapterId(checkedKeys[0]);
setCheckedKeys([checkedKeys[0]]);
setNowTitle(info.node.title);
setLoading(false);
}
}
};
useEffect(() => {
getChapterTreeList();
}, []);
useEffect(() => {
if (chapterId) {
setCheckedKeys([chapterId]);
getChapterVal();
}
}, [chapterId]);
// 保存方法
const saveContent = async () => {
return;
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml();
const newMd5 = md5(contentHtml);
clearInterval(saveInterRef.current);
const word_count = editorRef.current.editor.getText().length;
// 保存
const data = await sectionEdit({
id: contentId,
content: contentHtml,
word_count,
});
if (data) {
if ((data.code && data.code === 3000) || data.code === "3000") {
navigate("/books/management/list");
return;
} else {
setContentMd5(newMd5);
dispatch(setAutosaveTime(Date.now()));
}
}
}
};
useEffect(() => {
if (!loading && contentId) {
clearInterval(saveInterRef.current);
} else {
clearInterval(saveInterRef.current);
}
}, [contentId, loading, contentMd5, html]);
const addChapterParent = () => {
setEditKey(-1);
setEditValue("");
setParentId(-1);
};
const delChapter = async (node) => {
const childInKey = findParentLevelOne(gData, "key", node.key);
let current = null;
if (childInKey && childInKey.children && childInKey.children.length > 0) {
const index = findTreeToIndex(childInKey.children, "key", node.key);
if (childInKey.children.length === 1) {
current = childInKey;
} else {
// 前面还有
if (index <= childInKey.children.length) {
if (index === 0) {
current = childInKey.children[index + 1];
} else {
current = childInKey.children[index - 1];
}
}
}
} else {
const index = findTreeToIndex(gData, "key", node.key);
let child = null;
if (index === 0) {
child = gData[index + 1];
} else {
child = gData[index - 1];
}
if (child && child.children && child.children.length > 0) {
current = child.children[0];
} else {
current = child;
}
}
const data = await chapterDel({ id: node.key });
if (data) {
await dispatch(
setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }),
);
await getChapterTreeList();
}
};
useEffect(() => {
if (gData && gData.length > 0 && treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(gData, "key", treeChapter.saveChapterId);
if (childInKey && Object.entries(childInKey).length > 0) {
setChapterId(parseInt(treeChapter.saveChapterId));
setCheckedKeys([parseInt(treeChapter.saveChapterId)]);
setNowTitle(childInKey.title);
}
}
}
}, [gData, treeChapter]);
let chapterMenuItems = [
{ key: "1", label: "展开全部" },
// { key: '2', label: '添加子节' },
// { key: '5', label: '设置编者' },
// { key: '3', label: '编辑' },
// { key: '4', label: '删除' }
];
// if (!isAdminOrCreator) {
// chapterMenuItems = [{ key: '1', label: '展开全部' }]
// }
const handleMenuClick = async (e, node) => {
e.domEvent.stopPropagation();
setCurrentChapter(node);
if (parseInt(e.key) === 1) {
// 展开全部
const ids = findNodeById([node], "key", node.key);
const expandedKeysTemp = [...expandedKeys, node.key, ...ids];
setExpandedKeys(expandedKeysTemp);
} else if (parseInt(e.key) === 2) {
// 添加子节
setEditKey(-1);
setEditValue("");
setParentId(node.key);
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key);
setEditValue(node.title);
setParentId(false);
} else if (parseInt(e.key) === 4) {
await modal.confirm({
title: "删除确认",
content: `确认删除子节【${node.title}】?`,
onOk() {
delChapter(node);
},
});
} else if (e.key == 5) {
setEditChapterEditorsIsOpen(true);
}
};
// 章节名称
const chapterTitleRender = (node) => {
return (
<div className="chapter-tree-item">
<p className="title">{node.title}</p>
<Dropdown
trigger={["click"]}
menu={{ items: chapterMenuItems, onClick: (e) => handleMenuClick(e, node) }}
>
<EllipsisOutlined onClick={(e) => e.stopPropagation()} />
</Dropdown>
</div>
);
};
const onDrop = async (info) => {
const dropToGap = info.dropToGap; // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key;
const sideDropNode = info.node.key;
let data = null;
if (dropToGap) {
// 同级
const dragOverGapBottom = info.node.dragOverGapBottom;
const dragOver = info.node.dragOver;
const dragOverGapTop = info.node.dragOverGapTop;
if (!dragOverGapTop && !dragOver && !dragOverGapBottom) {
// 拖到最底部
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
} else {
if (dragOverGapTop) {
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "before",
});
} else if (dragOverGapBottom) {
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
}
}
} else {
// 子级
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "inner",
});
}
if (data) {
await getChapterTreeList();
}
};
return (
<div className="examine" style={{ padding: "20px", height: "100vh", boxSizing: "border-box" }}>
<div className="content-box">
<Row gutter={10} style={{ height: "100%" }} className="book-content-row">
<Col span={isCollapse ? 1 : 4} className="book-content-tree">
<div className="border">
{!isCollapse ? (
<>
<Descriptions
layout="vertical"
rootClassName="section-left-top"
bordered
items={[
{
key: "1",
label: (
<Row
gutter={5}
justify={"space-between"}
style={{ alignItems: "center" }}
>
<Col>章节目录</Col>
<Col>
{isAdminOrCreator && (
<Button
type="text"
icon={<DiffOutlined />}
onClick={addChapterParent}
></Button>
)}
<Button
type="text"
icon={<MenuFoldOutlined />}
onClick={() => setisCollapse(true)}
></Button>
</Col>
</Row>
),
},
]}
/>
{gData && gData.length > 0 && (
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandAll
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultSelectedKeys={checkedKeys}
selectedKeys={checkedKeys}
blockNode
disabled={loading}
treeData={gData}
onExpand={onExpand}
titleRender={(nodeData) => chapterTitleRender(nodeData)}
/>
)}
</>
) : (
<Descriptions
layout="vertical"
bordered
items={[
{
key: "1",
label: (
<Row gutter={5} justify={"space-between"} style={{ alignItems: "center" }}>
<Col>
<Button
type="text"
icon={<MenuUnfoldOutlined />}
onClick={() => setisCollapse(false)}
></Button>
</Col>
</Row>
),
},
]}
/>
)}
</div>
</Col>
<Col span={isCollapse ? 23 : 20} className="book-content-tree">
<div className="editor-right">
<Spin spinning={loading}>
<WangEditorCustomer
ref={editorRef}
chapterId={chapterId}
bookId={id}
contentId={contentId}
setHtml={setHtml}
html={html}
saveContent={saveContent}
gData={gData}
nowTitle={nowTitle}
disabled={disabled}
isView={true}
/>
</Spin>
</div>
</Col>
</Row>
</div>
</div>
);
};
export default Examine;
import { Outlet } from 'react-router'
const TeachersLayout = () => <Outlet />
export default TeachersLayout
......@@ -18,7 +18,7 @@ import dayjs from 'dayjs'
import { useSelector } from 'react-redux'
import { AppList } from '@/shared/components/AppList'
import { getAllList } from '@/pages/jurisdiction/role/request.js'
import { getTeacherRoles } from '@/modules/access/role/api'
import add from '@/assets/images/icon/add.png'
import {
......@@ -65,7 +65,7 @@ export default function TeacherListView() {
const reloadList = (nextFilters = filters) => init(nextFilters)
useEffect(() => {
getAllList({ type: 2 }).then((result) => result && setRoleList(result))
getTeacherRoles().then((result) => result && setRoleList(result))
}, [])
useEffect(() => {
......
......@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Navigate, useRoutes } from "react-router";
import ModuleBoundary from "@/app/ModuleBoundary";
const TeachersLayout = lazy(() => import("@/pages/teacher/index"));
const TeachersLayout = lazy(() => import("./Layout"));
const TeacherListRoutes = lazy(() => import("./list/routes"));
export default function TeachersRoutes() {
......
import { Outlet } from "react-router";
const AdvertisingModule = () => {
return <Outlet />;
};
export default AdvertisingModule;
export { default } from '@/modules/advertisements/Layout'
.auditPanel {
background: #fbfbfb;
margin-bottom: 30px;
}
.auditdetail {
.ant-steps-item-icon {
width: 50px !important;
height: 100px;
}
.imgel {
.user {
width: 50px;
height: 50px;
position: relative;
background-size: cover;
}
.status {
width: 20px;
height: 20px;
position: absolute;
right: 0;
bottom: 0;
}
}
// .ant-steps-item-description {
// height: 150px;
// }
.ant-steps-item-tail {
display: none !important;
}
}
@import "@/modules/books/audit/audit.less";
import React, { useEffect, useState, useRef, useImperativeHandle, forwardRef } from "react";
import { Modal, Drawer, Tree, Spin, Space, Button, Input, Image } from "antd";
import { CloseOutlined, LeftOutlined, BarsOutlined } from "@ant-design/icons";
import { getInfoByChapterId, getAllList } from "@/pages/books/section/request";
import { getChapterTopic, expandReadInfo } from "@/common/wangeditor-customer/utils/request";
import "@/common/wangeditor-customer/utils/iconfont";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
clacTopicText,
} from "@/utils/common";
import { get } from "lodash-es";
import $ from "jquery";
import "@/common/preview.less";
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
const correctList = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
];
// 递归遍历树,为叶子节点分配页码,返回 Map<key, pageNumber> 和总页数
const buildPageMap = (treeData) => {
const map = {};
let page = 0;
const walk = (nodes) => {
nodes.forEach((node) => {
if (node.children && node.children.length > 0) {
walk(node.children);
} else {
page += 1;
map[node.key] = page;
}
});
};
walk(treeData);
return { map, total: page };
};
const PreviewScreen = (props, ref) => {
const { bookId } = props;
const [gData, setGData] = useState([]);
const [loading, setLoading] = useState(false); // 展示
const [prviewHtml, setPrviewHtml] = useState(""); // 预览html
const [open, setOpen] = useState(false); // 抽屉
const [secondType, setSecondType] = useState(false);
const [pageMap, setPageMap] = useState({}); // 叶子节点页码映射
const [totalPages, setTotalPages] = useState(0); // 总页数
const [currentPage, setCurrentPage] = useState(0); // 当前页码
const [toolTip, setTooltip] = useState({});
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [headTitle, setHeadTitle] = useState("");
const [oldTitle, setOldTitle] = useState("");
const [newChapterId, setNewChapterId] = useState("");
const [opend, setOpend] = useState(false);
const [gallery, setGallery] = useState([]);
const [galleryIndex, setGalleryIndex] = useState(0);
const [isType, setIsType] = useState("");
const [topicList, setTopicList] = useState([]); // 题库
const [radioList, setRadioList] = useState([]); // 单选
const [checkBoxList, setCheckBoxList] = useState([]); // 多选
const [judgeList, setJudgeList] = useState([]); // 判断
const [fillList, setFillList] = useState([]); // 填空
const [textareaList, setTextareaList] = useState([]); // 简答
const [expandContent, setExpandContent] = useState(""); // 扩展阅读内容
const [previewImg, setPreviewImg] = useState(""); // 扩展阅读内容
const newId = useRef(null);
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const getChapterTreeList = async () => {
setLoading(true);
const data = await getAllList({ book_id: bookId });
if (!data.data.length) {
setLoading(false);
return;
}
const arr = convertToAntdTreeData(data.data, "name");
const { map, total } = buildPageMap(arr);
setPageMap(map);
setTotalPages(total);
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setOldTitle(first.title);
setNewChapterId(first.key);
setCurrentPage(map[first.key] || 0);
setLoading(false);
};
const getTopicList = async ({ practicenum, chapterid, bookid }) => {
const data = await getChapterTopic({
position: practicenum,
book_id: bookid,
chapter_id: chapterid,
});
const temp = [];
data.forEach((item) => {
let obj = { ...item };
if ([1, 2, 3].includes(parseInt(item.question_style))) {
try {
obj.question_list = JSON.parse(item.question_list);
} catch (err) {
obj.question_list = [];
}
}
temp.push(obj);
});
const tempRadio = temp.filter((item) => parseInt(item.question_style) === 1);
const tempCheckbox = temp.filter((item) => parseInt(item.question_style) === 2);
const tempJudge = temp.filter((item) => parseInt(item.question_style) === 3);
const tempFill = temp.filter((item) => parseInt(item.question_style) === 4);
const tempTextarea = temp.filter((item) => parseInt(item.question_style) === 5);
setRadioList(tempRadio);
setCheckBoxList(tempCheckbox);
setJudgeList(tempJudge);
setFillList(tempFill);
setTextareaList(tempTextarea);
setTopicList(temp);
};
const getExpandInfo = async ({ chapterId, position }) => {
const data = await expandReadInfo({
book_id: bookId,
chapter_id: chapterId,
position: position,
});
if (data) {
setExpandContent(data.content);
}
};
const getChapterId = async () => {
setLoading(true);
newId.current = newChapterId;
const data = await getInfoByChapterId({ chapter_id: newChapterId });
if (data) {
const content = get(data, "content", "");
setPrviewHtml(content);
$("#previee-container")
.find("video audio")
.each((index, item) => {
$(item).attr("controls", true).attr("controlslist", "nodownload");
});
setTimeout(() => {
document
.querySelectorAll(".preview-content-show ")[0]
.querySelectorAll("pre code")
.forEach((el) => {
hljs.highlightElement(el);
});
}, 200);
}
setLoading(false);
};
const handlerClick = () => {
const iParentW = $(".preview-content-it").innerWidth();
const iParentH = $(".preview-content-it").innerHeight();
const parentOffset = $(".preview-content-it").offset(); // 获取父元素相对于文档的位置
const parentRect = $(".preview-content-it")[0].getBoundingClientRect(); // 获取父元素相对于文档的位置
$(".preview-content-html").on("click", async (ev) => {
ev.stopPropagation();
const target = ev.target;
const tooltip = $(target).closest(".chapter-item-tooltip");
const alink = $(target).closest(".chapter-item-link");
const aGallery = $(target).closest(".chapter-gallery-container");
const practice = $(target).closest(".chapter-practice");
const aA = $(target).closest("a");
const aExpand = $(target).closest(".chapter-expand");
if (tooltip.length > 0) {
const random = $(tooltip).data("random");
const link = $(tooltip).data("link");
const content = $(tooltip).data("content");
const tooltipType = $(tooltip).data("tooltip-type");
const title = $(tooltip).data("title");
const tooltipParent = $(target).closest(
"p, div, h1, h2, h3, h4, h5, h6, li, dt, dd, section",
);
const tooltipParentPosition = $(tooltipParent).position();
const tooltipPosition = $(tooltip).position();
const childOffset = $(tooltip).offset(); // 获取子元素相对于文档的位置
const childWidth = $(tooltip).outerWidth(true); // 获取元素的宽度
const childHeight = $(tooltip).outerHeight(true); // 获取元素的高度
let toolRect = null;
let targetPositon = null;
if (target.nodeName.toLowerCase() === "svg" || target.nodeName.toLowerCase() === "use") {
toolRect = target.parentNode.parentNode.getBoundingClientRect();
targetPositon = $(target).closest(".chapter-item-tooltip").position();
} else {
toolRect = target.parentNode.getBoundingClientRect();
targetPositon = $(target).position();
}
const lineHeight = $(tooltip).css("line-height");
let inSide = "";
let left = childOffset.left - parentOffset.left;
let top = 0;
let squareStyle = {};
if (childWidth + left >= iParentW) {
left = iParentW - childWidth - 10;
}
if (left <= 0) {
left = 10;
}
let style1 = {
top: `${top}px`,
left: `${left}px`,
};
setTooltip({
title,
random,
link,
content,
tooltipType,
inSide,
style: style1,
squareStyle: squareStyle,
});
setTimeout(() => {
const childMoveHeight = $(".tooltip").outerHeight(); // 获取元素的高度
const childMoveWidth = $(".tooltip").outerWidth(); // 获取元素的高度
const scrollTop = $(".preview-content-show").scrollTop();
let squareStyle = {};
if (left <= 0) {
left = 10;
squareStyle.left = 10;
} else if (left + childMoveWidth >= iParentW) {
left = iParentW - childMoveWidth - 20;
squareStyle.left = targetPositon.left - left;
} else {
squareStyle.left = childWidth / 2;
}
// 换行了
if (toolRect.height / 1.5 > parseInt(lineHeight)) {
left = toolRect.right - parentRect.left - childMoveWidth + 15;
squareStyle.left = childMoveWidth - 30;
}
// 是否被遮挡
if (tooltipPosition.top + childMoveHeight + childHeight + 30 >= scrollTop + iParentH) {
inSide = "bottom";
top = tooltipPosition.top - childMoveHeight;
squareStyle.left = childWidth / 2;
} else {
inSide = "top";
if (toolRect.height / 1.5 > parseInt(lineHeight)) {
top = tooltipPosition.top + childHeight;
} else {
top = tooltipPosition.top + childHeight + 25;
}
// squareStyle.left = childWidth / 2;
}
top = top - scrollTop;
let style2 = {
top: `${top}px`,
left: `${left}px`,
zIndex: 1000,
opacity: 1,
};
setTooltip({
title,
random,
link,
content,
tooltipType,
inSide,
style: style2,
squareStyle: squareStyle,
});
}, 100);
return false;
} else if (alink.length > 0) {
const link = $(alink).data("link");
const linkType = $(alink).data("linktype");
const chapters = $(alink).data("chapters");
const title = $(alink).data("title");
let last = "";
if (parseInt(linkType) === 2) {
if (chapters.toString().indexOf(",") > -1) {
const arr = chapters.split(",");
last = arr[arr.length - 1];
} else {
last = chapters;
}
setHeadTitle(title);
setOpend(true);
setNewChapterId(last);
} else {
window.open(link);
}
ev.preventDefault();
return false;
} else if (aGallery.length > 0) {
const title = $(aGallery).data("title");
const flex = $(aGallery).data("flex");
const gallerylist = $(aGallery).data("gallerylist");
const galleryArr = JSON.parse(decodeURI(gallerylist));
if (galleryArr && galleryArr.length > 0 && galleryArr instanceof Array) {
setGallery(galleryArr);
if (parseInt(flex) === 2) {
setGalleryIndex(0);
} else {
setGalleryIndex($(target).closest(".chapter-gallery-item").index());
}
} else {
setGallery([]);
}
setOpend(true);
setHeadTitle(title);
setIsType("gallery");
setTooltip({});
return false;
} else if (practice.length > 0) {
const title = $(practice).data("title");
const bookid = $(practice).data("bookid");
const chapterid = $(practice).data("chapterid");
const practicenum = $(practice).data("practicenum");
getTopicList({ practicenum, chapterid, bookid });
setOpend(true);
setHeadTitle(title);
setIsType("practice");
ev.preventDefault();
return false;
} else if (aA.length > 0) {
const href = $(aA).attr("href");
let newLink = "";
if (!/^https*:\/\//.test(href)) {
newLink = `http://${href}`;
} else {
newLink = href;
}
window.open(newLink);
ev.preventDefault();
return false;
} else if (aExpand.length > 0) {
const title = $(aExpand).data("title");
const name = $(aExpand).data("name");
const position = $(aExpand).data("random");
setOpend(true);
setIsType("expand");
setHeadTitle(name);
await getExpandInfo({ chapterId: newId.current, position });
} else if (target.nodeName.toLowerCase() === "img") {
const alt = $(target).attr("alt");
const src = $(target).attr("src");
setOpend(true);
setIsType("img");
setPreviewImg(src);
setHeadTitle(alt ? alt : "图片展示");
}
setTooltip({});
setGallery([]);
});
$(".expand-content, .gallery-prview-container").on("click", (ev) => {
const target = ev.target;
if (target.nodeName.toLowerCase() === "img") {
const alt = $(target).attr("alt");
const src = $(target).attr("src");
setOpend(true);
setIsType("img");
setPreviewImg(src);
setSecondType("扩展图片");
}
});
};
useEffect(() => {
handlerClick();
}, [isType]);
useEffect(() => {
if (bookId) {
getChapterTreeList();
}
}, [bookId]);
useEffect(() => {
if (newChapterId) {
getChapterId();
}
}, [newChapterId]);
const onClose = () => {
setOpen(false);
};
const openDrawer = (e) => {
e.preventDefault();
e.stopPropagation();
setOpen(true);
};
const handleSelect = async (checkedKeys, info) => {
if (info.node.children && info.node.children.length > 0) {
const temp = JSON.parse(JSON.stringify(expandedKeys));
if (temp.includes(info.node.key)) {
const newExpands = temp.filter((item) => item !== info.node.key);
setExpandedKeys([...newExpands]);
} else {
setExpandedKeys([...temp, info.node.key]);
}
} else {
setOldTitle(info.node.title);
setNewChapterId(info.node.key);
newId.current = info.node.key;
setCheckedKeys(checkedKeys);
setCurrentPage(pageMap[info.node.key] || 0);
setOpen(false);
}
};
const galleryLeft = () => {
let temp = galleryIndex;
if (temp <= 0) {
temp = 0;
} else {
temp = temp - 1;
}
setGalleryIndex(temp);
};
const galleryRight = () => {
let temp = galleryIndex;
if (temp >= gallery.length - 1) {
temp = gallery.length - 1;
} else {
temp = temp + 1;
}
setGalleryIndex(temp);
};
return (
<div className="priview-modal">
{/* <Spin spinning={loading}> */}
<div className="previee-container" id="previee-container">
<div className="chapter-head-title">
<h2 className="tree">
{opend ? (
<Space>
<Button
type="text"
onClick={() => {
if (secondType) {
setSecondType(false);
setPreviewImg(null);
setIsType("expand");
return;
}
setOpend(false);
setIsType("");
setExpandContent("");
setTopicList([]);
setGalleryIndex(-1);
setNewChapterId(newChapterId);
setPreviewImg("");
}}
>
<LeftOutlined />
</Button>
{secondType ? secondType : headTitle}
</Space>
) : (
<>
<span className="no">{oldTitle}</span>
<span className="menu">
<BarsOutlined style={{ fontSize: 22 }} onClick={() => setOpen(true)} />
</span>
</>
)}
</h2>
</div>
<div className="preview-content-it">
<div className="preview-content-show">
<Spin spinning={loading} wrapperClassName="chapter-loading">
<div
className="preview-content-html"
dangerouslySetInnerHTML={{ __html: prviewHtml }}
></div>
</Spin>
{currentPage > 0 && (
<div className="preview-page-number">
<span>
{currentPage} / {totalPages}
</span>
</div>
)}
</div>
{isType && (
<div className="preview-content-other">
{isType === "gallery" && (
<div className="gallery-prview-container">
{gallery && gallery.length > 0 ? (
<>
<div className="gallery-img">
<img
src={gallery[parseInt(galleryIndex)].url}
alt={gallery[parseInt(galleryIndex)].title}
/>
<div className="opa prev" onClick={galleryLeft}></div>
<div className="opa next" onClick={galleryRight}></div>
</div>
<div className="steps">
当前 {parseInt(galleryIndex) + 1}/{gallery.length} 张
</div>
<div className="title">{gallery[parseInt(galleryIndex)].title}</div>
<div className="desc">{gallery[parseInt(galleryIndex)].desc}</div>
</>
) : (
<>
<div className="gallery-img noData">画廊数据异常</div>
</>
)}
</div>
)}
{isType === "practice" && (
<div className="practice-insert-topic">
{radioList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("radio")}. 单选题</h3>
<div className="practice-list">
{radioList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{checkBoxList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("checkbox", radioList)}. 多选题</h3>
<div className="practice-list">
{checkBoxList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{judgeList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("judge", radioList, checkBoxList)}. 判断题</h3>
<div className="practice-list">
{judgeList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{fillList.length > 0 && (
<div className="topic_style">
<h3>{clacTopicText("fill", radioList, checkBoxList, judgeList)}. 填空题</h3>
<div className="practice-list">
{fillList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `<span class="correct">${correctList[cindex]}.</span> ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{textareaList.length > 0 && (
<div className="topic_style">
<h3>
{clacTopicText("textarea", radioList, checkBoxList, judgeList, fillList)}.
简答题
</h3>
<div className="practice-list">
{textareaList.map((item, index) => {
return (
<div className="practice-item" key={index}>
<div
className="topic-choose-title"
dangerouslySetInnerHTML={{
__html: `<span class="index">${index + 1}.</span>${item.titles}`,
}}
></div>
<div className="topic_choose-list">
{[1, 2, 3].includes(parseInt(item.question_style)) &&
item.question_list.map((citem, cindex) => {
return (
<div className="topic_choose-item" key={cindex}>
<div className="choose-ico">
<i
className={`ico ${item.question_style === 2 ? "checkbox" : "radio"}`}
></i>
</div>
<div
className="topic_content"
dangerouslySetInnerHTML={{
__html: `${correctList[cindex]}. ${citem.option}`,
}}
></div>
</div>
);
})}
{item.question_style === 5 && (
<div>
<Input.TextArea
disabled={true}
autoSize={{ minRows: 4, maxRows: 6 }}
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
)}
{isType === "expand" && (
<div className="expand-container">
<div
className="expand-content"
dangerouslySetInnerHTML={{ __html: expandContent }}
></div>
</div>
)}
{isType === "img" && (
<div className="img-preview">
<Image width={430} src={previewImg} />
</div>
)}
</div>
)}
<div className="tooltip" style={toolTip.style}>
<div
className={`square ${
toolTip.inSide === "top"
? "square_top"
: toolTip.inSide === "bottom"
? "square_bottom"
: ""
}`}
style={toolTip.squareStyle}
></div>
<div className="tooltip-content-container">
<div className="tooltip-content">
<div className="content">
<p>{toolTip.content}</p>
</div>
{toolTip.link && (
<div className="content-opa">
<a href={toolTip.link} target="_blank" rel="noreferrer" className="c-link"></a>
{/* <a
href={`https://baike.baidu.com/item/${toolTip?.title}`}
target='_blank'
rel='noreferrer'
>
<img src={baike} alt='baike' />
</a> */}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<Drawer
onClose={onClose}
open={open}
getContainer={false}
placement="left"
classNames={{ header: "priview-drawer-header", body: "priview-drawer-body" }}
width="300"
title="章节列表"
rootClassName="priview-drawer-container"
>
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultExpandAll
blockNode
treeData={gData}
titleRender={(nodeData) => {
const page = pageMap[nodeData.key];
return (
<span className="tree-node-title">
<span className="tree-node-name">{nodeData.title}</span>
{page != null && <span className="tree-node-page">P{page}</span>}
</span>
);
}}
/>
</Drawer>
{/* </Spin> */}
</div>
);
};
export default forwardRef(PreviewScreen);
export { default } from "@/modules/books/audit/components/PreviewModal";
import React from "react";
import { Outlet } from "react-router";
const Book = () => {
return <Outlet />;
};
export default Book;
export { default } from "@/modules/books/BooksLayout";
import { useState, useEffect } from "react";
import { Button, Form, Input, Flex, message } from "antd";
import WangEditor from "@/components/editor";
import UploadImage from "@/components/uploadImage";
import { useLocation, useNavigate } from "react-router";
import { addBook, updateBook, getBook } from "../api";
import SelectTeacher from "@/components/selectTeacher";
const BookAddEdit = () => {
const navigate = useNavigate();
const location = useLocation();
const isEdit = !!location.state?.id;
const [form] = Form.useForm();
const [img, setImg] = useState("");
// 编辑器内容
const [html, setHtml] = useState("");
// 详情信息
const fetchInfo = async () => {
const { data } = await getBook({ id: location.state.id });
setImg(data.img);
setHtml(data.content);
const formFields = {
...data,
editors: data.editors_list.map((item) => {
return { label: item.real_name, value: item.editor_id };
}),
};
form.setFieldsValue(formFields);
};
useEffect(() => {
if (isEdit) fetchInfo();
}, [isEdit]);
const onFinish = async (values) => {
const editors = values.editors?.map((item) => item.value) || [];
const params = { ...values, img, editors };
if (editors.length === 0) delete params.editors;
const res = isEdit
? await updateBook({ id: location.state.id, ...params })
: await addBook(params);
message.success(res.message);
navigate(-1);
};
return (
<Form form={form} onFinish={onFinish} labelCol={{ span: 2 }}>
<Form.Item
label="书籍名称"
name="name"
rules={[
{ required: true, message: "请输入书籍名称" },
{ max: 30, message: "最大30字符" },
]}
>
<Input autoComplete="off" placeholder="请输入书籍名称" style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item
label="作者"
name="authors"
rules={[
{ required: true, message: "请输入作者" },
{ max: 30, message: "最大30字符" },
]}
>
<Input autoComplete="off" placeholder="请输入作者" style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item name="img" label="缩略图" className="uploadPar">
<UploadImage value={img} onChange={(url) => setImg(url)}></UploadImage>
</Form.Item>
<Form.Item
label="编辑推荐"
name="introduction"
rules={[{ max: 65535, message: "最多65535字符" }]}
>
<Input.TextArea
placeholder="请输入编辑推荐"
autoSize={{ maxRows: 8, minRows: 5 }}
style={{ width: 910 }}
/>
</Form.Item>
<Form.Item label="编写者" name="editors">
<SelectTeacher placeholder="请选择编写者" style={{ width: 260 }}></SelectTeacher>
</Form.Item>
<Form.Item label="简介" name="content">
<WangEditor
value={html}
onChange={(editor) => form.setFieldValue("content", editor.getHtml())}
></WangEditor>
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
<Flex gap={20} justify="center">
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type="primary" htmlType="submit">
确认
</Button>
</Flex>
</Form.Item>
</Form>
);
};
export default BookAddEdit;
export { default } from "@/modules/books/management/components/ManagementAddEdit";
import { Card, Input, Button, Space, Row, Col, Modal, Form } from "antd";
import { DeleteOutlined, FacebookFilled, MessageOutlined } from "@ant-design/icons";
import "./index.less";
import User from "@/assets/images/icon/user.png";
import { useState, useEffect } from "react";
import { useLocation } from "react-router";
import { getCommentInfo, commentReply, commentDel } from "../request";
const { TextArea } = Input;
const DiscussDetail = () => {
const [showReply, setShowReply] = useState(false);
const [delMoadal, setDelModal] = useState(false);
const {
state: { id, name, book_id },
} = useLocation();
const [form] = Form.useForm();
const [data, setData] = useState({});
const [loading, setLoading] = useState(false);
const [commentId, setcommentId] = useState(0);
// 获取列表
const init = async () => {
setLoading(true);
const data = await getCommentInfo({ id, book_id });
console.log(data);
setData(data);
setLoading(false);
};
useEffect(() => {
init();
}, []);
useEffect(() => {
if (!showReply) {
form.setFieldsValue({ content: "" });
form.resetFields();
}
}, [showReply]);
// 显示回复弹框
const showReplyFn = (id) => {
setcommentId(id);
setShowReply(true);
};
// 点击确认回复
const handleReply = async (obj) => {
const bool = await commentReply({ ...obj, id: commentId, book_id });
if (!bool) return;
setShowReply(false);
init();
};
const handleDel = async () => {
const bool = await commentDel({ id: commentId, book_id });
if (!bool) return;
setDelModal(false);
init();
};
return (
<div className="DiscussDetail">
<Modal
open={showReply}
onCancel={() => {
setShowReply(false);
}}
maskClosable={false}
footer={null}
centered
closeIcon={null}
keyboard={false}
>
<br />
<Form form={form} onFinish={handleReply}>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: "请输入内容" }]}
>
<TextArea
placeholder="请输入内容"
autoSize={{
minRows: 3,
maxRows: 5,
}}
></TextArea>
</Form.Item>
<Form.Item>
<Row gutter={20} justify={"center"}>
<Col>
<Button onClick={() => setShowReply(false)}>取消</Button>
</Col>
<Col>
<Button type="primary" htmlType="submit">
确认
</Button>
</Col>
</Row>
</Form.Item>
</Form>
</Modal>
<Modal
mask={false}
centered
open={delMoadal}
onOk={handleDel}
onCancel={() => setDelModal(false)}
footer={(_, { OkBtn, CancelBtn }) => {
return (
<Row justify={"center"} className="delModal">
<Space>
<CancelBtn />
<OkBtn />
</Space>
</Row>
);
}}
okText="确认"
>
<p style={{ textAlign: "center", padding: "50px 0 30px", fontSize: 16 }}>
评论将永久删除,是否继续?
</p>
</Modal>
<Card className="card" title={`书名:《${name}》`} loading={loading}>
<div className="discussBox">
<Row className="discussItem" gutter={10}>
<Col className="img">
<img
src={data.person_pic || User}
width={28}
height={28}
style={{ borderRadius: "50%", objectFit: "cover" }}
/>
</Col>
<Col className="discuss" flex={1}>
<div className="name">{data.person_name}</div>
{/* <div className='content'>{data?.content && data.content[0].content}</div> */}
<div className="content">
{data?.content &&
data.content.map((item, index) => {
const type = item.types;
const value = item.content;
if (type === "audio") {
return (
<div key={index}>
<span>
<audio controls>
<source src={value} type="audio/mp3" />
Your browser does not support the audio element.
</audio>
</span>
</div>
);
} else if (type === "text") {
return <span key={index}>{value}</span>;
} else if (type === "image") {
return <img height={100} src={value}></img>;
}
})}
</div>
<Space>
<Button
type="text"
className="reply"
onClick={() => showReplyFn(data.id)}
icon={<MessageOutlined />}
>
回复
</Button>
<Button
type="text"
className="del"
icon={<DeleteOutlined />}
onClick={() => {
setDelModal(true);
setcommentId(data.id);
}}
>
删除
</Button>
</Space>
</Col>
</Row>
{data.child &&
data.child.map((item) => (
<Row className="discussItem reply" key={item.id} gutter={10}>
<Col span={1}></Col>
<Col className="img">
<img
src={item.person_pic || User}
width={28}
height={28}
style={{ borderRadius: "50%", objectFit: "cover" }}
/>
</Col>
<Col className="discuss" flex={1}>
<div className="name">{item.person_name}</div>
<div className="content">
{item.content &&
item.content.map((item, index) => (
<div key={index}>
{item.types == "text" && <span>{item.content}</span>}
{item.types == "image" && <img src={item.content}></img>}
{item.types == "audio" && <audio controls src={item.content} />}
</div>
))}
</div>
<Space>
<Button
type="text"
className="reply"
onClick={() => showReplyFn(item.id)}
icon={<MessageOutlined />}
>
回复
</Button>
<Button
type="text"
className="del"
icon={<DeleteOutlined />}
onClick={() => {
setDelModal(true);
setcommentId(item.id);
}}
>
删除
</Button>
</Space>
</Col>
</Row>
))}
</div>
{/* <div className="discussCreate">
<Row gutter={10}>
<Col><div className="img"><img src={User} width={28} height={28} /></div></Col>
<Col><Input autoComplete="off" placeholder="发布你的评论" style={{width:754}}></Input ></Col>
<Col>
<Space>
<Button>取消</Button>
<Button type="primary">回复</Button>
</Space>
</Col>
</Row>
</div> */}
</Card>
</div>
);
};
export default DiscussDetail;
export { default } from "@/modules/books/sale/components/SaleDiscussionDetail";
.DiscussDetail {
.reply {
color: #aa1941;
}
.del {
color: #999999;
}
.card {
.ant-card-body {
height: calc(100vh - 260px);
overflow: auto;
}
}
.discussItem {
.discuss {
&::after {
display: block;
content: "";
width: 100%;
height: 1px;
background: #e5e5e57f;
margin-top: 16px;
margin-bottom: 10px;
}
.name {
font-size: 12px;
color: #333333;
font-weight: 500;
margin-bottom: 4px;
}
.content {
font-size: 12px;
color: #333333;
}
}
}
}
@import "@/modules/books/sale/components/SaleDiscussionDetail.less";
import React, { useEffect, useState, useRef } from "react";
import {
Form,
Input,
Select,
Button,
Radio,
Space,
Image,
Spin,
Tree,
Drawer,
Row,
Col,
message,
} from "antd";
import { getInfoById, publisherList, publisherAdd, getListAll, editBook } from "./request";
import { getList } from "../classify/request";
import { useLocation } from "react-router";
import { useNavigate } from "react-router";
import { convertToAntdTreeData } from "@/utils/common.js";
const SaleEdit = () => {
const {
state: { id },
} = useLocation();
const [initialValues, setinitialValues] = useState({
name: "",
authors: "",
category_id: null,
producers_id: null,
press_id: null,
is_free: null,
});
const [form] = Form.useForm();
const navigate = useNavigate();
const [showAddCategory, setShowAddCategory] = useState(false); //弹出添加出品方
const [showAddCategory1, setShowAddCategory1] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
const [publisher_name, setPublisherName] = useState(""); //出品方
const [publishers, setPublishers] = useState([]);
const [publisher_name1, setPublisherName1] = useState(""); //出版社
const [publishers1, setPublishers1] = useState([]);
const [treeData, setTreeData] = useState([]);
// 在组件中添加一个状态来保存书籍类型的值
const [bookType, setBookType] = useState(0); // 默认值为免费
const [producersConfig, setproducersConfig] = useState({
validateStatus: "",
help: "",
noStyle: false,
style: {},
});
const [pressConfig, setpressConfig] = useState({
validateStatus: "",
help: "",
noStyle: false,
style: {},
});
const addproducers = async (obj, callbackFn) => {
const { types } = obj;
let flag = true;
if (types == 1) {
if (!publisher_name) {
flag = false;
setproducersConfig({
...producersConfig,
validateStatus: "error",
help: "请输入新的出品方",
noStyle: false,
});
}
} else if (types == 2) {
if (!publisher_name1) {
flag = false;
setpressConfig({
...pressConfig,
validateStatus: "error",
help: "请输入新的出版社",
noStyle: false,
});
}
}
if (!flag) return;
// 上面代码是校验
const bool = await publisherAdd(obj);
if (!bool) return;
callbackFn();
setproducersConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setpressConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
getProducers();
};
const [price, setprice] = useState(0);
const [vip_price, setvip_price] = useState(0);
const [discountVal, setDiscountVal] = useState(0);
const calculateDiscountVal = (price, vip_price) => {
if (Number(price) > 0 && Number(vip_price) > 0) {
return (Number(vip_price) / Number(price)).toFixed(2) * 10;
} else {
return 0;
}
};
// 只在零售价改变时更新折扣率
useEffect(() => {
let price = form.getFieldValue("price");
let vip_price = form.getFieldValue("vip_price");
setDiscountVal(calculateDiscountVal(price, vip_price).toFixed(2));
console.log("discountVal", discountVal);
}, [form.getFieldValue("price"), form.getFieldValue("vip_price")]);
// // 问题:输入3.3折后,输入框变成3.30000 ; 原因:toFixed()数字转字符串时可能会导致精度问题 ; 解决:parseFloat来解析浮点数
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// setDiscountVal(newVal);
// const price = form.getFieldValue('price');
// updateVipPriceFromDiscountRate(price, newVal);
// }
// };
// const updateVipPriceFromDiscountRate = (price, discountRate) => {
// if (Number(price) > 0 && Number(discountRate) >= 0 && Number(discountRate) <= 10) {
// const vip_price = price * (discountRate / 10);
// form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
// }
// };
// 在onChange中用parseFloat来解析浮点数,输入折扣数后,就触发折扣价改变,发现还是不太行,因为浮点数的精度问题可能会导致在 onChange 事件中出现意外行为。目前会更报错出现NaN,当在用户输入小数点和小数部分时,浮点数可能会被不完整地解析,导致计算错误,出现NaN
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// const roundedVal = parseFloat(newVal).toFixed(2); // 对输入的值进行四舍五入处理
// setDiscountVal(roundedVal);
// const price = form.getFieldValue('price');
// // 添加计算并更新折扣金额的逻辑
// if (Number(price) > 0) {
// const vip_price = price * (roundedVal / 10);
// form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
// }
// }
// };
// 将parseFloat放在失去焦点(onBlur)中处理,更靠谱
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10 && /^\d*\.?\d{0,3}$/.test(newVal)) {
// 不在这里进行四舍五入处理,而是在失去焦点时处理
setDiscountVal(newVal);
} else {
message.error("最多输入小数点后3位,且不能大于10折");
newVal.slice(0, -1);
}
};
const handleDiscountRateBlur = () => {
const newVal = parseFloat(discountVal);
if (Number.isFinite(newVal) && newVal >= 0 && newVal <= 10) {
const roundedVal = newVal.toFixed(3);
setDiscountVal(roundedVal);
const price = form.getFieldValue("price");
// 添加计算并更新折扣元的逻辑
if (Number(price) > 0) {
const vip_price = price * (roundedVal / 10);
form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
}
}
};
const handleVippriceChange = (ev) => {
const newVal = ev.target.value;
let price = form.getFieldValue("price");
if (Number(newVal) <= price && /^\d*\.?\d{0,3}$/.test(newVal)) {
setvip_price(newVal);
} else {
message.error("最多输入小数点后3位,且不能大于零售价");
newVal.slice(0, -1);
}
};
useEffect(() => {
if (!showAddCategory) {
setPublisherName("");
}
if (!showAddCategory1) {
setPublisherName1("");
}
}, [showAddCategory, showAddCategory1]);
const handleOpenDrawer = async () => {
setShowDrawer(true);
const data = await getListAll({ book_id: id });
let arr = convertToAntdTreeData(data, "name");
setTreeData(arr);
};
const handleCloseDrawer = () => {
setShowDrawer(false);
};
const [classData, setClassData] = useState([]);
const [producersData, setproducersData] = useState([]);
const [pressData, setpressData] = useState([]);
// 出品方/出版社
const getProducers = async () => {
const producersList = await publisherList({ types: 1 });
const press = await publisherList({ types: 2 });
setproducersData(producersList);
setpressData(press);
};
const [FormLoad, setFormLoad] = useState(true);
const [checkedKeys, setcheckedKeys] = useState([]);
const submitTree = (obj) => {
form.setFieldValue("trials", checkedKeys);
handleCloseDrawer(false);
};
const ontreeCheck = (keys, { checked, checkedNodes, node, halfCheckedKeys }) => {
setcheckedKeys(keys);
/* const handleCheck = (parentNode) => {
const keys = [];
const traverse = (currentNode) => {
keys.push(currentNode.key);
if (currentNode.children) {
currentNode.children.forEach((childNode) => {
traverse(childNode, currentNode.key);
});
}
};
traverse(parentNode);
return keys;
};
if (checked) {
const keysToCheck = handleCheck(node);
setcheckedKeys([...checkedKeys.checked, ...keysToCheck]);
} else {
const keysToUncheck = handleCheck(node);
const newCheckedKeys = checkedKeys.checked.filter((key) => !keysToUncheck.includes(key));
setcheckedKeys(newCheckedKeys);
} */
};
const getinfo = async () => {
setFormLoad(true);
// 详情
const data = await getInfoById({ id });
const { category_id, producers_id, press_id } = data;
let obj = { category_id, producers_id, press_id };
setBookType(data.is_free);
for (let key in obj) {
if (obj[key] === 0) {
obj[key] = null;
}
}
form.setFieldsValue({ ...data, ...obj });
setFormLoad(false);
};
const getInfo = async () => {
// 分类
const { list } = await getList({ page: 1, page_size: 999 });
setClassData(list);
getProducers();
getinfo();
};
const onFinish = async (obj) => {
const bool = await editBook({ ...obj, id });
if (!bool) return;
navigate(-1);
};
useEffect(() => {
getInfo();
}, []);
return (
<Spin spinning={FormLoad} style={{ padding: 20 }}>
<Form labelCol={{ span: 2 }} form={form} initialValues={initialValues} onFinish={onFinish}>
<Form.Item label="书籍名称" name="name">
<Input
disabled
allowClear
autoComplete="off"
placeholder="请输入书籍名称"
style={{ width: 400 }}
></Input>
</Form.Item>
<Form.Item label="书籍作者" name="authors">
<Input autoComplete="off" disabled style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item
label="分类"
name="category_id"
rules={[{ required: true, message: "请选择分类" }]}
>
<Select placeholder="请选择分类" style={{ width: 260 }}>
{classData &&
classData.map((item) => (
<Select.Option value={item.id} key={item.id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="出品方" required style={producersConfig.style}>
<Space>
<Form.Item
noStyle
name="producers_id"
rules={[{ required: true, message: "请选择出品方" }]}
>
<Select placeholder="请选择出品方" style={{ width: 260 }}>
{producersData?.map((item) => (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
))}
</Select>
</Form.Item>
<a
style={{
textDecoration: "underline",
color: "#1672EC",
cursor: "pointer",
}}
onClick={() => {
setproducersConfig({ ...producersConfig, style: { marginBottom: 0 } });
setShowAddCategory(true);
}}
>
添加出品方
</a>
</Space>
{showAddCategory && (
<div style={{ marginTop: "10px" }}>
<Space>
<Form.Item
noStyle={producersConfig.noStyle}
validateStatus={producersConfig.validateStatus}
help={producersConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete="off"
placeholder="请输入新的出品方"
value={publisher_name}
allowClear
onChange={(e) => {
let val = e.target.value;
if (val) {
setproducersConfig({ ...producersConfig, help: "", validateStatus: "" });
}
setPublisherName(val);
}}
/>
</Form.Item>
<a
type="link"
style={{ textDecoration: "underline", color: "#1672EC" }}
onClick={() =>
addproducers({ types: 1, publisher_name }, () => setShowAddCategory(false))
}
>
添加
</a>
<a
type="link"
style={{ textDecoration: "underline", color: "#AA1941" }}
onClick={() => {
setproducersConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setShowAddCategory(false); // 可选:添加后关闭输入框
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label="出版社" required style={pressConfig.style}>
<Space>
<Form.Item
noStyle
name="press_id"
rules={[{ required: true, message: "请选择出版社" }]}
>
<Select placeholder="请选择出品方" style={{ width: 260 }}>
{pressData?.map((item) => {
return (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
);
})}
</Select>
</Form.Item>
<a
style={{
textDecoration: "underline",
color: "#1672EC",
cursor: "pointer",
}}
onClick={() => {
setpressConfig({ ...pressConfig, style: { marginBottom: 0 } });
setShowAddCategory1(true);
}}
>
添加出版社
</a>
</Space>
{showAddCategory1 && (
<div style={{ marginTop: "10px" }}>
<Space>
<Form.Item
noStyle={pressConfig.noStyle}
validateStatus={pressConfig.validateStatus}
help={pressConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete="off"
placeholder="请输入新的出版社"
allowClear
value={publisher_name1}
onChange={(e) => {
let val = e.target.value;
if (val) {
setpressConfig({ ...pressConfig, help: "", validateStatus: "" });
}
setPublisherName1(e.target.value);
}}
/>
</Form.Item>
<a
type="link"
style={{ textDecoration: "underline", color: "#1672EC" }}
onClick={() =>
addproducers({ types: 2, publisher_name: publisher_name1 }, () =>
setShowAddCategory1(false),
)
}
>
添加
</a>
<a
type="link"
style={{ textDecoration: "underline", color: "#AA1941" }}
onClick={() => {
setpressConfig({ validateStatus: "", help: "", noStyle: false, style: {} });
setShowAddCategory1(false);
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label="书籍类型" name="is_free">
<Radio.Group
onChange={(e) => {
setBookType(e.target.value);
}}
>
<Radio value={1}>免费</Radio>
<Radio value={0}>付费</Radio>
</Radio.Group>
</Form.Item>
{bookType === 0 && (
<Form.Item label="试读章节" name="trials">
<a type="link" style={{ textDecoration: "underline" }} onClick={handleOpenDrawer}>
选择试读章节
</a>
</Form.Item>
)}
{bookType === 0 && (
<>
<Form.Item label="零售价(元):" name="price">
<Input
value={price}
onChange={(e) => setprice(e.target.value)}
autoComplete="off"
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item label="折扣价(元):">
<Form.Item name="vip_price" style={{ display: "inline-block" }}>
<Input
value={vip_price}
// onChange={(e) => setvip_price(e.target.value)}
onChange={handleVippriceChange}
autoComplete="off"
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item style={{ display: "inline-block", margin: "0 8px" }}>
<Input
// value={Number(discountVal).toFixed(2)}
value={discountVal}
allowClear
onChange={handleDiscountRateChange}
onBlur={handleDiscountRateBlur} // 添加失去焦点事件处理函数
autoComplete="off"
style={{ width: 80 }}
></Input>
&nbsp;&nbsp;折
</Form.Item>
</Form.Item>
</>
)}
<Form.Item wrapperCol={{ offset: 2 }}>
<Space className="footer" size={20}>
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type="primary" htmlType="submit">
确认
</Button>
</Space>
</Form.Item>
</Form>
<Drawer
placement="right"
onClose={handleCloseDrawer}
open={showDrawer}
labelCol={{ span: 7 }}
mask={false}
>
<Form onFinish={submitTree}>
<Form.Item name="power_list" style={{ padding: 10 }}>
<Tree
checkable
checkedKeys={checkedKeys}
defaultExpandAll={false} //让授权后的弹窗只展示根标签
treeData={treeData}
// showLine //删除这里,树形结构左侧的下拉线消失,图标从+-更改为默认的△
// checkStrictly
onCheck={ontreeCheck}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 10, span: 16 }}>
<Space size={20}>
<Button onClick={() => handleCloseDrawer(false)}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</Spin>
);
};
export default SaleEdit;
export { default } from "@/modules/books/sale/components/SaleEdit";
import axios from "@/utils/axios";
// 获取书籍详情
export function getBook(data) {
return axios.post("/api/book/teacher/getInfoById", data);
}
// 获取章节列表
export function getChapterList(data) {
return axios.post("/api/book/teacher/chapter/getAllList", data);
}
// 获取章节详情
export function getChapter(data) {
return axios.post("/api/book/teacher/chapter/getInfoById", data);
}
// 添加章节
export function addChapter(data) {
return axios.post("/api/book/teacher/chapter/add", data);
}
// 修改章节
export function updateChapter(data) {
return axios.post("/api/book/teacher/chapter/edit", data);
}
// 删除章节
export function delChapter(data) {
return axios.post("/api/book/teacher/chapter/del", data);
}
// 章节排序
export function sortChapter(data) {
return axios.post("/api/book/teacher/chapter/dragOrder", data);
}
// 获取章节编辑者信息
export function getChapterEditors(data) {
return axios.post("/api/book/teacher/chapter/getEditors", data);
}
// 保存章节编辑者
export function updateChapterEditors(data) {
return axios.post("/api/book/teacher/chapter/saveEditors", data);
}
// 知识图谱列表
export function getTagList(params) {
return axios.get("/api/resource/v1/course/course/tag-list", { params });
}
// 知识图谱关联书籍章节
export function bindTag(data) {
return axios.post("/api/resource/v1/course/course/tag-add-book", data);
}
export * from "@/modules/books/management/chapters/api";
import { useState, useEffect, useCallback } from "react";
import { Modal, Tree, App, Button, Flex, Empty } from "antd";
import { getTagList, bindTag } from "../api";
function getIds(nodes) {
let ids = [];
nodes.forEach((node) => {
ids.push(node.id);
if (node.children && node.children.length > 0) {
ids = ids.concat(getIds(node.children));
}
});
return ids;
}
const BindKnowledgeGraph = ({ chapter = {}, ...props }) => {
const { message } = App.useApp();
const [treeData, setTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = useState([]);
const fetchList = useCallback(() => {
getTagList({ type: 2, parent_id: chapter.book_id, chapter_id: chapter.id }).then((res) => {
setTreeData(res.data);
setExpandedKeys(getIds(res.data));
});
}, [chapter]);
useEffect(() => {
fetchList();
}, [fetchList]);
const toggleBind = async (node) => {
await bindTag({
tag_id: node.id,
book_id: chapter.book_id,
chapter_id: chapter.id,
option_type: node.is_checked ? "delete" : "add",
});
message.success("操作成功");
fetchList();
};
const titleRender = (node) => {
return (
<Flex justify="space-between">
<span>{node.name}</span>
<Button color="primary" variant="link" size="small" onClick={() => toggleBind(node)}>
{node.is_checked ? "取消关联" : "关联"}
</Button>
</Flex>
);
};
return (
<Modal title="关联知识图谱" centered {...props} footer={null}>
{treeData.length > 0 ? (
<Tree
blockNode
selectable={false}
treeData={treeData}
fieldNames={{ title: "name", key: "id" }}
expandedKeys={expandedKeys}
titleRender={titleRender}
onExpand={setExpandedKeys}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无知识图谱" />
)}
</Modal>
);
};
export default BindKnowledgeGraph;
export { default } from "@/modules/books/management/chapters/components/BindKnowledgeGraph";
import { useState, useEffect } from "react";
import { Modal, Form, Radio, Space, App } from "antd";
import { getChapterEditors, updateChapterEditors } from "../api";
const EditChapterEditors = ({ chapter = {}, onChange, ...props }) => {
const { message } = App.useApp();
const [editors, setEditors] = useState([]);
const [value, setValue] = useState("");
useEffect(() => {
getChapterEditors({ book_id: chapter.book_id, chapter_id: chapter.id }).then((res) => {
setEditors(res.data.editors || []);
const value = res.data.selected_editor_ids[0] || "";
setValue(value);
});
}, [chapter]);
const handleSubmit = async () => {
await updateChapterEditors({
book_id: chapter.book_id,
chapter_id: chapter.id,
editor_id: value,
});
message.success("设置成功");
onChange && onChange();
};
return (
<Modal title="设置章节编写者" centered {...props} onOk={handleSubmit}>
<Form labelCol={{ span: 8 }}>
<Form.Item label="当前章节">{chapter.title}</Form.Item>
<Form.Item label="编写者">
<Radio.Group value={value} onChange={(e) => setValue(e.target.value)}>
<Space direction="vertical">
{editors.map((item) => {
return (
<Radio value={item.editor_id} key={item.editor_id}>
{item.real_name}({item.tel})
</Radio>
);
})}
</Space>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};
export default EditChapterEditors;
export { default } from "@/modules/books/management/chapters/components/EditChapterEditors";
import { useState, useEffect, useRef } from "react";
import { Form, Button, Space, Input } from "antd";
import { setTreeChapter } from "@/store/modules/user";
import { useDispatch } from "react-redux";
import { chapterEdit, chapterAdd, sectionAdd } from "../request";
const EditChapterTitle = (props) => {
const {
editValue,
setEditValue,
editKey,
setEditKey,
parentId,
bookId,
getChapterTreeList,
expandedKeys,
setExpandedKeys,
editorLoading,
newChapterChange,
} = props;
const dispatch = useDispatch();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [initValues, setInitValues] = useState({ title: editValue });
const inputRef = useRef();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, [editorLoading]);
useEffect(() => {
form.setFieldsValue({ title: editValue });
setInitValues({ title: editValue });
}, [editValue]);
const onFinish = async (values) => {
setLoading(true);
let data = null;
if (parentId) {
data = await chapterAdd({
pid: parentId === -1 ? 0 : parentId,
book_id: bookId,
name: values.title,
});
} else {
data = await chapterEdit({ id: editKey, name: values.title });
}
await dispatch(setTreeChapter({ saveBookId: bookId, saveChapterId: data.id }));
if (data) {
if (parentId) {
await sectionAdd({ book_id: bookId, chapter_id: data.id, content: "", word_count: 0 });
}
setEditKey(false);
setEditValue(null);
await getChapterTreeList();
if (parentId) {
let temp = [...expandedKeys, parentId];
setExpandedKeys(temp);
await newChapterChange(data.id, values.title);
}
}
setLoading(false);
};
return (
<Form layout="vertical" form={form} onFinish={onFinish} initialValues={initValues}>
<Form.Item
label="章节标题"
name="title"
rules={[{ required: true, message: "请输入章节标题" }]}
extra="最多30个字符"
>
<Input ref={inputRef} autoFocus maxLength={30} placeholder="" allowClear />
</Form.Item>
<Form.Item className="editor-form-buttons" wrapperCol={{ offset: 9, span: 16 }}>
<Space size={20}>
<Button type="default" disabled={loading} onClick={() => setEditKey(false)}>
取消
</Button>
<Button type="primary" loading={loading} htmlType="submit">
确认
</Button>
</Space>
</Form.Item>
</Form>
);
};
export default EditChapterTitle;
export { default } from "@/modules/books/management/chapters/components/form-chapter-title";
import { useState, useEffect, useRef, useCallback } from "react";
import { Button, Row, Col, Descriptions, Tree, Dropdown, Modal, Spin, App } from "antd";
import {
EllipsisOutlined,
DiffOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from "@ant-design/icons";
import WangEditorCustomer from "@/common/wangeditor-customer";
import { useLocation, useNavigate } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { setAutosaveTime } from "@/store/modules/editor";
import { setTreeChapter } from "@/store/modules/user";
import EditChapterTitle from "./components/form-chapter-title";
import EditChapterEditors from "./components/EditChapterEditors";
import BindKnowledgeGraph from "./components/BindKnowledgeGraph";
import { get } from "lodash-es";
import md5 from "js-md5";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
findParentLevelOne,
findTreeToIndex,
findNodeById,
} from "@/utils/common";
import { getAllList, getInfoByChapterId, sectionEdit, chapterDel, dragOrder } from "./request";
import "./index.less";
const Examine = () => {
const { modal } = App.useApp();
const location = useLocation();
const id = get(location, "state.id", "");
const isAdminOrCreator = get(location, "state.isAdminOrCreator", false);
const { treeChapter, userInfo } = useSelector((state) => state.user);
const navigate = useNavigate();
const dispatch = useDispatch();
const [gData, setGData] = useState([]);
const [chapterId, setChapterId] = useState(0);
const [bookId, setBookId] = useState(0);
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [nowTitle, setNowTitle] = useState("");
// 编辑操作
const [isCollapse, setisCollapse] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(280);
const isResizing = useRef(false);
const sidebarRef = useRef(null);
const [editKey, setEditKey] = useState(null);
const [parentId, setParentId] = useState(null);
const [editValue, setEditValue] = useState("");
const [loading, setLoading] = useState(false);
const [contentMd5, setContentMd5] = useState("");
// 设置编者
const [currentChapter, setCurrentChapter] = useState(null);
const [editChapterEditorsIsOpen, setEditChapterEditorsIsOpen] = useState(false);
const onChapterEditorsChange = () => {
setEditChapterEditorsIsOpen(false);
};
// 关联知识图谱
const [bindKnowledgeGraphIsOpen, setBindKnowledgeGraphIsOpen] = useState(false);
// 拖拽改变侧边栏宽度
const handleMouseDown = useCallback(
(e) => {
e.preventDefault();
isResizing.current = true;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
const startX = e.clientX;
const startWidth = sidebarWidth;
const handleMouseMove = (e) => {
if (!isResizing.current) return;
const delta = e.clientX - startX;
const newWidth = Math.min(Math.max(startWidth + delta, 200), 600);
setSidebarWidth(newWidth);
};
const handleMouseUp = () => {
isResizing.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[sidebarWidth],
);
// 编辑器内容
const editorRef = useRef();
const saveInterRef = useRef();
const [html, setHtml] = useState("");
const [contentId, setContentId] = useState(false);
// 获取目录结构
const getChapterTreeList = async () => {
setLoading(true);
const { data = [], code } = await getAllList({ book_id: id });
if (code !== 200) {
setLoading(false);
navigate("/books/management/list", { replace: true });
return;
}
if (data.length === 0) {
setLoading(false);
return;
}
const uniqueBookList = [...new Set(data.map((item) => item.book_id))][0];
setBookId(uniqueBookList);
const arr = convertToAntdTreeData(data, "name");
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setNowTitle(first.title);
if (treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(arr, "key", treeChapter.saveChapterId);
if (childInKey) {
setChapterId(childInKey.key);
setCheckedKeys([childInKey.key]);
setNowTitle(childInKey.title);
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }));
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
setLoading(false);
};
const newChapterChange = (id, title) => {
setChapterId(parseInt(id));
setCheckedKeys([parseInt(id)]);
setNowTitle(title);
};
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const [disabled, setDisabled] = useState(false);
// 获取内容
const getChapterVal = async () => {
setLoading(true);
const data = await getInfoByChapterId({ chapter_id: chapterId });
setDisabled(!data.selected_editor_ids.includes(userInfo.id) && !isAdminOrCreator);
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId, ...data }));
const { content, id: cId } = data;
if (content) {
setContentMd5(md5(content));
}
setHtml(content);
setContentId(cId);
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.setHtml(content);
}
setLoading(false);
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
};
const handleSelect = async (selectedKeys, info) => {
if (!info.node) return;
const { key, title } = info.node;
// 保持选中状态,防止重复点击导致选中状态消失
setCheckedKeys([key]);
// 如果点击的是当前已经选中的章节,且没有发生章节切换,则直接返回
if (key === chapterId) {
return;
}
setLoading(true);
// 切换章节前清理保存定时器并执行一次保存
clearInterval(saveInterRef.current);
await saveContent();
// 清理旧内容
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.clear();
}
// 设置新章节 ID,触发 useEffect 加载数据
setChapterId(key);
setNowTitle(title);
setLoading(false);
};
useEffect(() => {
getChapterTreeList();
}, []);
useEffect(() => {
if (chapterId) {
getChapterVal();
}
}, [chapterId]);
// 保存方法
const saveContent = async () => {
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml();
const newMd5 = md5(contentHtml);
clearInterval(saveInterRef.current);
const word_count = editorRef.current.editor.getText().length;
// 保存
const data = await sectionEdit({
id: contentId,
content: contentHtml,
word_count,
});
if (data) {
if ((data.code && data.code === 3000) || data.code === "3000") {
// navigate('/books/management/list')
return;
} else {
setContentMd5(newMd5);
dispatch(setAutosaveTime(Date.now()));
}
}
}
};
useEffect(() => {
if (!loading && contentId) {
clearInterval(saveInterRef.current);
} else {
clearInterval(saveInterRef.current);
}
}, [contentId, loading, contentMd5, html]);
const addChapterParent = () => {
setEditKey(-1);
setEditValue("");
setParentId(-1);
};
const delChapter = async (node) => {
const childInKey = findParentLevelOne(gData, "key", node.key);
let current = null;
if (childInKey && childInKey.children && childInKey.children.length > 0) {
const index = findTreeToIndex(childInKey.children, "key", node.key);
if (childInKey.children.length === 1) {
current = childInKey;
} else {
// 前面还有
if (index <= childInKey.children.length) {
if (index === 0) {
current = childInKey.children[index + 1];
} else {
current = childInKey.children[index - 1];
}
}
}
} else {
const index = findTreeToIndex(gData, "key", node.key);
let child = null;
if (index === 0) {
child = gData[index + 1];
} else {
child = gData[index - 1];
}
if (child && child.children && child.children.length > 0) {
current = child.children[0];
} else {
current = child;
}
}
const data = await chapterDel({ id: node.key });
if (data) {
await dispatch(
setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }),
);
await getChapterTreeList();
}
};
useEffect(() => {
if (gData && gData.length > 0 && treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(gData, "key", treeChapter.saveChapterId);
if (childInKey && Object.entries(childInKey).length > 0) {
setChapterId(parseInt(treeChapter.saveChapterId));
setCheckedKeys([parseInt(treeChapter.saveChapterId)]);
setNowTitle(childInKey.title);
}
}
}
}, [gData, treeChapter]);
let chapterMenuItems = [
{ key: "1", label: "展开全部" },
{ key: "2", label: "添加子节" },
{ key: "7", label: "关联知识图谱" },
{ key: "5", label: "设置编者" },
{
key: "6",
label: "一键AI",
children: [
{ key: "61", label: "一键AI创作课件", disabled: true },
{ key: "62", label: "一键AI创作教案", disabled: true },
],
},
{ key: "3", label: "编辑" },
{ key: "4", label: "删除" },
];
if (!isAdminOrCreator) {
chapterMenuItems = [{ key: "1", label: "展开全部" }];
}
const handleMenuClick = async (e, node) => {
e.domEvent.stopPropagation();
setCurrentChapter(node);
if (parseInt(e.key) === 1) {
// 展开全部
const ids = findNodeById([node], "key", node.key);
const expandedKeysTemp = [...expandedKeys, node.key, ...ids];
setExpandedKeys(expandedKeysTemp);
} else if (parseInt(e.key) === 2) {
// 添加子节
setEditKey(-1);
setEditValue("");
setParentId(node.key);
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key);
setEditValue(node.title);
setParentId(false);
} else if (parseInt(e.key) === 4) {
await modal.confirm({
title: "删除确认",
content: `确认删除子节【${node.title}】?`,
onOk() {
delChapter(node);
},
});
} else if (e.key == 5) {
setEditChapterEditorsIsOpen(true);
} else if (e.key == 7) {
setBindKnowledgeGraphIsOpen(true);
}
};
// 章节名称
const chapterTitleRender = (node) => {
return (
<div className="chapter-tree-item">
<p className="title">{node.title}</p>
<Dropdown
trigger={["click"]}
menu={{ items: chapterMenuItems, onClick: (e) => handleMenuClick(e, node) }}
>
<EllipsisOutlined onClick={(e) => e.stopPropagation()} />
</Dropdown>
</div>
);
};
const onDrop = async (info) => {
const dropToGap = info.dropToGap; // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key;
const sideDropNode = info.node.key;
let data = null;
if (dropToGap) {
// 同级
const dragOverGapBottom = info.node.dragOverGapBottom;
const dragOver = info.node.dragOver;
const dragOverGapTop = info.node.dragOverGapTop;
if (!dragOverGapTop && !dragOver && !dragOverGapBottom) {
// 拖到最底部
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
} else {
if (dragOverGapTop) {
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "before",
});
} else if (dragOverGapBottom) {
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
}
}
} else {
// 子级
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "inner",
});
}
if (data) {
await getChapterTreeList();
}
};
return (
<div className="examine">
<div className="content-box">
<div className="book-content-row">
<div
ref={sidebarRef}
className="book-content-tree"
style={{
width: isCollapse ? 50 : sidebarWidth,
minWidth: isCollapse ? 50 : 200,
flexShrink: 0,
}}
>
<div className="border">
{!isCollapse ? (
<>
<Descriptions
layout="vertical"
rootClassName="section-left-top"
bordered
items={[
{
key: "1",
label: (
<Row
gutter={5}
justify={"space-between"}
style={{ alignItems: "center" }}
>
<Col>章节目录</Col>
<Col>
{isAdminOrCreator && (
<Button
type="text"
icon={<DiffOutlined />}
onClick={addChapterParent}
></Button>
)}
<Button
type="text"
icon={<MenuFoldOutlined />}
onClick={() => setisCollapse(true)}
></Button>
</Col>
</Row>
),
},
]}
/>
{gData && gData.length > 0 && (
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandAll
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultSelectedKeys={checkedKeys}
selectedKeys={checkedKeys}
onDrop={onDrop}
draggable
blockNode
disabled={loading}
treeData={gData}
onExpand={onExpand}
titleRender={(nodeData) => chapterTitleRender(nodeData)}
/>
)}
</>
) : (
<Descriptions
layout="vertical"
bordered
items={[
{
key: "1",
label: (
<Row gutter={5} justify={"space-between"} style={{ alignItems: "center" }}>
<Col>
<Button
type="text"
icon={<MenuUnfoldOutlined />}
onClick={() => setisCollapse(false)}
></Button>
</Col>
</Row>
),
},
]}
/>
)}
</div>
</div>
{!isCollapse && <div className="resize-handle" onMouseDown={handleMouseDown} />}
<div className="book-content-editor" style={{ flex: 1, minWidth: 0 }}>
<div className="editor-right">
<Spin spinning={loading}>
<WangEditorCustomer
ref={editorRef}
chapterId={chapterId}
bookId={id}
contentId={contentId}
setHtml={setHtml}
html={html}
saveContent={saveContent}
gData={gData}
nowTitle={nowTitle}
disabled={disabled}
/>
</Spin>
</div>
</div>
</div>
</div>
<Modal
open={editKey}
footer={null}
centered
destroyOnClose
title={editKey > 0 ? "修改章节" : "新增章节"}
maskClosable={false}
mask={false}
closeIcon={false} // 添加这一行以隐藏关闭按钮
onCancel={() => setEditKey(false)}
classNames={{
wrapper: "chapter-title-modal",
}}
>
<EditChapterTitle
setEditKey={setEditKey}
editValue={editValue}
editKey={editKey}
getChapterTreeList={getChapterTreeList}
setEditValue={setEditValue}
parentId={parentId}
bookId={id}
expandedKeys={expandedKeys}
setExpandedKeys={setExpandedKeys}
editorLoading={loading}
newChapterChange={newChapterChange}
/>
</Modal>
{/* 设置章节编写者 */}
{currentChapter && editChapterEditorsIsOpen && (
<EditChapterEditors
open={editChapterEditorsIsOpen}
chapter={currentChapter}
onCancel={() => setEditChapterEditorsIsOpen(false)}
onChange={onChapterEditorsChange}
></EditChapterEditors>
)}
{/* 关联知识图谱 */}
{currentChapter && bindKnowledgeGraphIsOpen && (
<BindKnowledgeGraph
open={bindKnowledgeGraphIsOpen}
chapter={currentChapter}
onCancel={() => setBindKnowledgeGraphIsOpen(false)}
onOk={() => setBindKnowledgeGraphIsOpen(false)}
></BindKnowledgeGraph>
)}
</div>
);
};
export default Examine;
export { default } from "@/modules/books/management/components/ManagementChapter";
.examine {
height: 100%;
.content-box {
height: 100%;
box-sizing: border-box;
overflow: hidden;
.book-content-row {
height: 100%;
overflow: hidden;
display: flex;
gap: 0;
.book-content-tree {
height: 100%;
overflow: hidden;
transition: width 0s;
}
.book-content-editor {
height: 100%;
overflow: hidden;
}
}
.resize-handle {
width: 6px;
cursor: col-resize;
background: transparent;
position: relative;
flex-shrink: 0;
z-index: 10;
transition: background 0.2s;
&:hover,
&:active {
background: #1890ff;
border-radius: 3px;
}
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 30px;
background: #d9d9d9;
border-radius: 1px;
transition: background 0.2s;
}
&:hover::after,
&:active::after {
background: #fff;
}
}
.ant-descriptions-item-label {
padding: 10px !important;
}
.ant-descriptions-view {
border: none;
}
.border {
border: 1px solid #e5e5e5;
height: 100%;
display: flex;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
border-radius: 6px;
.section-left-top {
height: 85px;
}
.ant-descriptions.ant-descriptions-bordered > .ant-descriptions-view .ant-descriptions-row {
border-bottom: 1px solid #e5e5e5;
&:nth-of-type(2) {
display: none;
}
}
}
.editor-right {
height: 100%;
.ant-spin-nested-loading,
.ant-spin-container {
height: 100%;
}
}
}
.draggable-tree {
flex: 1;
height: calc(100vh - 285px);
overflow-y: auto;
overflow-x: hidden;
width: 98%;
&.ant-tree .ant-tree-node-content-wrapper {
width: 100%;
}
.ant-tree-switcher-noop {
// display: none;
width: 10px;
}
&.ant-tree .ant-tree-node-content-wrapper {
overflow: hidden;
}
}
.chapter-tree-item {
display: flex;
justify-content: space-between;
overflow: hidden;
.title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
max-height: 24px;
line-height: 24px;
white-space: nowrap;
}
}
}
.chapter-title-modal {
.ant-modal {
.ant-modal-close {
inset-inline-end: 17px;
top: 16px;
}
.ant-modal-content {
padding: 15px 18px;
}
.ant-divider {
margin: 15px 0;
}
}
}
@import "@/modules/books/management/chapters/index.less";
import axios from "@/utils/request.js";
import qs from "qs";
import { get } from "lodash-es";
import { message } from "antd";
// 用户信息
const getUserInfo = async (obj) => {
try {
const res = await axios.post("/user/user/getUserInfo", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取角色已选权限
const getPowerByRoleId = async (obj) => {
try {
const res = await axios.post("/user/role/getPowerByRoleId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节列表
const getAllList = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getAllList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return {
code,
data,
};
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节
const chapterAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success('添加成功');
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节详情
const getInfoById = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getInfoById", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节
const chapterEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 删除章节
const chapterDel = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 拖拽章节排序
const dragOrder = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/dragOrder", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取章节内容
const getInfoByChapterId = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getInfoByChapterId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节内容
const sectionAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节内容
const sectionEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success(msg);
return true;
} else {
message.error(msg);
return { code, msg, data };
}
} catch (error) {
return false;
}
};
// 获取章节的题库列表
const getQuestionList = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/getQuestionList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 移除课后题
const questionDel = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加课后题
const questionAdd = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 发布审核
const release = async (obj) => {
try {
const res = await axios.post("/book/teacher/release", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板列表
const templateGetList = async (obj) => {
try {
const res = await axios.post("/book/template/getList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板导入
const templateImport = async (obj) => {
try {
const res = await axios.post("/book/template/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 书籍导入
const teacherImport = async (obj) => {
try {
const res = await axios.post("/book/teacher/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 保存历史记录
const getRecordList = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getRecordList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export {
getAllList,
chapterAdd,
getInfoById,
chapterEdit,
chapterDel,
dragOrder,
getInfoByChapterId,
sectionAdd,
sectionEdit,
getQuestionList,
questionDel,
questionAdd,
release,
templateGetList,
templateImport,
teacherImport,
getRecordList,
getPowerByRoleId,
getUserInfo,
};
export * from "@/modules/books/management/chapters/request";
import React from "react";
import { Outlet } from "react-router";
const Jurisdiction = () => {
return (
<div>
<Outlet />
</div>
);
};
export default Jurisdiction;
export { default } from '@/modules/access/Layout'
import React from "react";
import { Outlet } from "react-router";
const UserList = () => {
return (
<div>
<Outlet />
</div>
);
};
export default UserList;
export { default } from '@/modules/members/Layout'
import axios from "@/utils/request.js";
import qs from "qs";
import { message } from "antd";
import { get } from "lodash-es";
// oss
const getAliOSSSTSToken = async (obj) => {
try {
const res = await axios.post("/common/OssUpload/getUploadToken", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "获取OSS令牌失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export { getAliOSSSTSToken };
export * from "@/modules/settings/help/request";
import React from "react";
import { Outlet } from "react-router";
const Role = () => {
return (
<div>
<Outlet />
</div>
);
};
export default Role;
export { default } from "@/modules/settings/SettingsLayout";
import React, { useState, useEffect, useRef } from "react";
import { Form, Input, Radio, Space, Button, Spin, DatePicker } from "antd";
import { getSystem, systemEdit } from "../point-management/request";
const Personalized = () => {
const [form] = Form.useForm();
const flag = useRef(true);
const [loading, setLoading] = useState(false);
// 获取信息
const getInfo = async () => {
console.log(222);
setLoading(true);
const data = await getSystem();
form.setFieldsValue(data);
setLoading(false);
};
// 设置个性化信息
const handleSystemEdit = async (obj) => {
const bool = await systemEdit(obj);
bool && getInfo();
};
useEffect(() => {
getInfo();
}, []);
return (
<div style={{ padding: 20 }}>
<Spin spinning={loading}>
<Form form={form} onFinish={handleSystemEdit}>
<Form.Item
label="老师分成百分比(%)"
name="teacher_divide"
rules={[{ required: true, message: "老师分成百分比不能为空" }]}
extra="老师佣金计算(预计总收入),编写书籍售卖总金额+分成比例"
>
<Input
autoComplete="off"
style={{ width: 260 }}
placeholder="请输入老师分成百分比"
></Input>
</Form.Item>
<Form.Item
label="用户等级有效期(月)"
name="user_grade_validity"
rules={[{ required: true, message: "请输入用户等级有效期(月)" }]}
extra="用于计算用户等级,根据当前时间之前有效期内的消费金额总和来计算"
>
<Input
autoComplete="off"
style={{ width: 260 }}
placeholder="请输入用户等级有效期"
></Input>
</Form.Item>
<Form.Item
name="comments_need_approval"
label="评论是否需要审核"
rules={[{ required: true, message: "请选择评论是否需要审核" }]}
>
<Radio.Group>
<Radio value={"1"}>是</Radio>
<Radio value={"0"}>否</Radio>
</Radio.Group>
</Form.Item>
<Form.Item wrapperCol={{ offset: 2 }}>
<Space>
<Button
type="primary"
ghost
onClick={() => {
form.setFieldsValue({
teacher_divide: "",
user_grade_validity: "",
comments_need_approval: "",
});
}}
>
重置
</Button>
<Button type="primary" loading={loading} htmlType="submit">
确定
</Button>
</Space>
</Form.Item>
</Form>
</Spin>
</div>
);
};
export default Personalized;
export { default } from "@/modules/settings/personalized/views/PersonalizedSettingsView";
import React, { useState, useRef, useEffect } from "react";
import { Button, Form, Input, Switch, Space, Drawer, Spin } from "antd";
import {
getIntegral,
integralSet,
integralEdit,
integralAdd,
getSystem,
systemEdit,
getList,
} from "./request";
import { useSelector } from "react-redux";
const { TextArea } = Input;
const Point = () => {
const flag = useRef(true);
const [form] = Form.useForm();
const [form2] = Form.useForm();
const [initialValues] = useState({
reward_points_ratio: "",
one_point_deduction_amount: "",
integral_use_limit: "",
});
const [rechargeModal, setRechargeModal] = useState(false);
const [integralSwitch, setIntegralSwitch] = useState(false);
const [loading, setLoading] = useState(false);
// 获取操作权限
const { operationPermissionsList } = useSelector((state) => state.user);
// 获取信息
const geySettingInfo = async () => {
setLoading(true);
const data = await getIntegral();
const { integral_switch } = data;
if (integral_switch == "0") {
setIntegralSwitch(false);
} else {
setIntegralSwitch(true);
}
form.setFieldsValue(data);
setLoading(false);
};
// 充值积分
const submitForm = async (obj) => {
const bool = await integralAdd(obj);
bool && setRechargeModal(false);
};
// 修改设置
const editInfo = async (obj) => {
let copyObj = { ...obj };
delete copyObj.integral_switch;
const bool = await integralEdit(obj);
bool && geySettingInfo();
};
// 修改开关
const changeIntegralSwitch = async (val) => {
const integral_switch = val ? "1" : "0";
const bool = await integralSet({ integral_switch });
bool && geySettingInfo();
if (!val) {
setRechargeModal(false);
}
};
useEffect(() => {
// if(!flag.current){
geySettingInfo();
// }else{
// flag.current=false
// }
}, []);
const responsiveFormItemLayout = {
labelCol: { xxl: 2, xl: 3, lg: 4, md: 5, sm: 6, xs: 8 },
wrapperCol: { xs: 24, sm: 18 },
};
return (
<div style={{ padding: 20 }}>
<Spin spinning={loading}>
<Form
form={form}
{...responsiveFormItemLayout}
initialValues={initialValues}
onFinish={editInfo}
>
<Form.Item label="积分开关">
<div>
<Switch
checked={integralSwitch}
onChange={changeIntegralSwitch}
disabled={
!operationPermissionsList.includes("/setting/point-management/integralSet")
}
></Switch>
{operationPermissionsList.includes("/setting/point-management/add") && (
<Button
disabled={!integralSwitch}
style={{ marginLeft: 30, opacity: integralSwitch ? "1" : "0" }}
onClick={() => {
setRechargeModal(true);
}}
type="primary"
ghost
>
积分充值
</Button>
)}
</div>
</Form.Item>
<Form.Item
label="赠送积分比例(%)"
name="reward_points_ratio"
rules={[
{ required: true, message: "请输入赠送积分比例" },
{
validator: (_, value) => {
if (/^(?:100|[1-9]?[0-9])$/.test(value) || !value) {
return Promise.resolve();
} else {
return Promise.reject("请输入区间0-100的正整数");
}
},
},
]}
>
<Input
autoComplete="off"
style={{ width: 340 }}
placeholder="请输入赠送积分比例"
></Input>
</Form.Item>
<Form.Item
label="1积分抵扣金额(元)"
name="one_point_deduction_amount"
rules={[
{ required: true, message: "请输入1积分抵扣金额" },
{
validator: (_, value) => {
if (/^(?:100(?:\.0{1,2})?|\d{1,2}(?:\.\d{1,2})?)$/.test(value) || !value) {
return Promise.resolve();
} else {
return Promise.reject("请输入区间0-100的数字,可以是小数,支持俩位小数");
}
},
},
]}
>
<Input
autoComplete="off"
style={{ width: 340 }}
placeholder="请输入1积分抵扣金额"
></Input>
</Form.Item>
<Form.Item
label="积分使用上限(%)"
name="integral_use_limit"
rules={[
{ required: true, message: "请输入积分使用上限" },
{
validator: (_, value) => {
if (/^(?:100|[1-9]?[0-9])$/.test(value) || !value) {
return Promise.resolve();
} else {
return Promise.reject("请输入区间0-100的正整数");
}
},
},
]}
>
<Input
autoComplete="off"
style={{ width: 340 }}
placeholder="请输入积分使用上限"
></Input>
</Form.Item>
<Form.Item wrapperCol={{ offset: 3, span: 16 }}>
<Space size={20}>
{operationPermissionsList.includes("/setting/point-management/integralEdit") && (
<Button
onClick={() => {
form.setFieldsValue(initialValues);
}}
>
重置
</Button>
)}
{operationPermissionsList.includes("/setting/point-management/integralEdit") && (
<Button type="primary" htmlType="submit">
确定
</Button>
)}
</Space>
</Form.Item>
</Form>
</Spin>
<Drawer
placement="right"
onClose={() => setRechargeModal(false)}
open={rechargeModal}
mask={false}
>
<Form form={form2} onFinish={submitForm} labelCol={{ span: 6 }}>
<Form.Item
name="phone"
label="用户手机号"
rules={[{ required: true, message: "请输入用户手机号" }]}
>
<TextArea
placeholder="请输入用户手机号,用逗号隔开,每次最多100个"
autoSize={{
minRows: 5,
maxRows: 8,
}}
></TextArea>
</Form.Item>
<Form.Item
name="num"
label="充值积分"
rules={[
{ required: true, message: "请输入充值积分数量" },
{ pattern: /^[1-9]\d*$/, message: "请输入正整数" },
]}
>
<Input autoComplete="off" placeholder="请输入充值积分数量"></Input>
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 16 }} style={{ textAlign: "center" }}>
<Space size={20}>
<Button onClick={() => setRechargeModal(false)}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default Point;
export { default } from "@/modules/settings/point-management/views/PointManagementView";
import axios from "@/utils/request.js";
import qs from "qs";
import { message } from "antd";
import { get } from "lodash-es";
// 获取积分设置信息
const getIntegral = async (obj) => {
try {
const res = await axios.post("/system/config/getIntegral", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
// 设置积分开关
const integralSet = async (obj) => {
try {
const res = await axios.post("/system/config/integralSet", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
// 修改积分配置
const integralEdit = async (obj) => {
try {
const res = await axios.post("/system/config/integralEdit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
// 充值积分
const integralAdd = async (obj) => {
try {
const res = await axios.post("/members/integral/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 3000) {
message.warning("优惠券已过期");
return true;
}
// 先检查空手机号的情况,如果满足就跳出后续错误提示
if (
data.err.msg === " 手机号:" &&
data.suc.title &&
!data.suc.title.includes("充值成功(0)")
) {
if (data.suc.msg !== "") {
message.success(`${data.suc.msg}充值成功`, 10);
}
// message.error(`空手机号充值失败`, 10);
return true;
}
// 接下来处理其他非空错误消息和成功消息
if (data.suc.title && !data.suc.title.includes("充值成功(0)")) {
if (data.suc.msg !== "") {
message.success(`${data.suc.msg}充值成功`, 10);
}
}
if (data.err.title && !data.err.title.includes("充值失败(0)")) {
if (data.err.msg !== "") {
message.error(`${data.err.msg}充值失败`, 10);
}
}
return true;
} catch (error) {
return error;
}
};
// 获取设置信息
const getSystem = async (obj) => {
try {
const res = await axios.post("/system/config/getSystem", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
// 修改设置
const systemEdit = async (obj) => {
try {
const res = await axios.post("/system/config/systemEdit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
// 获取意见反馈列表
const getList = async (obj) => {
try {
const res = await axios.post("/system/proposal/getList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return error;
}
};
export { getIntegral, integralSet, integralEdit, integralAdd, getSystem, systemEdit, getList };
export {
getPointSettings as getIntegral,
setPointSwitch as integralSet,
savePointSettings as integralEdit,
rechargePoints as integralAdd,
} from "@/modules/settings/point-management/api";
export {
getPersonalizedSettings as getSystem,
savePersonalizedSettings as systemEdit,
} from "@/modules/settings/personalized/api";
export { getFeedbackList as getList } from "@/modules/settings/feedback/api";
import { useState, useEffect, useRef } from "react";
import { Button, Row, Col, Descriptions, Tree, Dropdown, Spin, App } from "antd";
import {
EllipsisOutlined,
DiffOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
} from "@ant-design/icons";
import WangEditorCustomer from "@/common/wangeditor-customer";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { setAutosaveTime } from "@/store/modules/editor";
import { setTreeChapter } from "@/store/modules/user";
import { get } from "lodash-es";
import md5 from "js-md5";
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
findParentLevelOne,
findTreeToIndex,
findNodeById,
} from "@/utils/common";
import { getAllList, getInfoByChapterId, sectionEdit, chapterDel, dragOrder } from "./request";
import "./index.less";
const Examine = () => {
const { modal } = App.useApp();
const [searchParams] = useSearchParams();
const location = useLocation();
const id = searchParams.get("book_id");
const isAdminOrCreator = get(location, "state.isAdminOrCreator", false);
const { treeChapter, userInfo } = useSelector((state) => state.user);
const navigate = useNavigate();
const dispatch = useDispatch();
const [gData, setGData] = useState([]);
const [chapterId, setChapterId] = useState(0);
const [bookId, setBookId] = useState(0);
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [nowTitle, setNowTitle] = useState("");
// 编辑操作
const [isCollapse, setisCollapse] = useState(false);
const [editKey, setEditKey] = useState(null);
const [parentId, setParentId] = useState(null);
const [editValue, setEditValue] = useState("");
const [loading, setLoading] = useState(false);
const [contentMd5, setContentMd5] = useState("");
// 设置编者
const [currentChapter, setCurrentChapter] = useState(null);
const [editChapterEditorsIsOpen, setEditChapterEditorsIsOpen] = useState(false);
const onChapterEditorsChange = () => {
setEditChapterEditorsIsOpen(false);
};
// 编辑器内容
const editorRef = useRef();
const saveInterRef = useRef();
const [html, setHtml] = useState("");
const [contentId, setContentId] = useState(false);
// 获取目录结构
const getChapterTreeList = async () => {
setLoading(true);
const { data = [], code } = await getAllList({ book_id: id });
if (code !== 200) {
setLoading(false);
navigate("/books/management/list", { replace: true });
return;
}
if (data.length === 0) {
setLoading(false);
return;
}
const uniqueBookList = [...new Set(data.map((item) => item.book_id))][0];
setBookId(uniqueBookList);
const arr = convertToAntdTreeData(data, "name");
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const first = findFirstNotHasChildren(arr);
setNowTitle(first.title);
if (treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(
arr,
"key",
searchParams.get("chapter_id") || treeChapter.saveChapterId,
);
if (childInKey) {
setChapterId(childInKey.key);
setCheckedKeys([childInKey.key]);
setNowTitle(childInKey.title);
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }));
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
}
setLoading(false);
};
const newChapterChange = (id, title) => {
setChapterId(parseInt(id));
setCheckedKeys([parseInt(id)]);
setNowTitle(title);
};
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
}
});
return keys;
};
const [disabled, setDisabled] = useState(false);
// 获取内容
const getChapterVal = async () => {
setLoading(true);
const data = await getInfoByChapterId({ chapter_id: chapterId });
setDisabled(!data.selected_editor_ids.includes(userInfo.id) && !isAdminOrCreator);
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId, ...data }));
const { content, id: cId } = data;
if (content) {
setContentMd5(md5(content));
}
setHtml(content);
setContentId(cId);
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.setHtml(content);
}
setLoading(false);
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
};
const handleSelect = async (checkedKeys, info) => {
const { key } = info.node;
const childInKey = findTreeElementByKey(gData, "key", key);
if (childInKey.children && childInKey.children.length > 0) {
clearInterval(saveInterRef.current);
// 进行展开操作
const tempExpandeds = JSON.parse(JSON.stringify(expandedKeys));
let newExpand = "";
if (expandedKeys.includes(key)) {
newExpand = tempExpandeds.filter((item) => parseInt(item) !== parseInt(key));
} else {
newExpand = [...tempExpandeds, key];
}
setExpandedKeys(newExpand);
setCheckedKeys([key]);
return;
} else {
const { key, title } = info.node;
if (info.selected === false) {
setLoading(true);
setChapterId(key);
setCheckedKeys([key]);
setNowTitle(title);
setLoading(false);
} else {
setLoading(true);
clearInterval(saveInterRef.current);
await saveContent();
if (key !== chapterId) {
editorRef.current.editor.clear();
}
setLoading(true);
setChapterId(checkedKeys[0]);
setCheckedKeys([checkedKeys[0]]);
setNowTitle(info.node.title);
setLoading(false);
}
}
};
useEffect(() => {
getChapterTreeList();
}, []);
useEffect(() => {
if (chapterId) {
setCheckedKeys([chapterId]);
getChapterVal();
}
}, [chapterId]);
// 保存方法
const saveContent = async () => {
return;
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml();
const newMd5 = md5(contentHtml);
clearInterval(saveInterRef.current);
const word_count = editorRef.current.editor.getText().length;
// 保存
const data = await sectionEdit({
id: contentId,
content: contentHtml,
word_count,
});
if (data) {
if ((data.code && data.code === 3000) || data.code === "3000") {
navigate("/books/management/list");
return;
} else {
setContentMd5(newMd5);
dispatch(setAutosaveTime(Date.now()));
}
}
}
};
useEffect(() => {
if (!loading && contentId) {
clearInterval(saveInterRef.current);
} else {
clearInterval(saveInterRef.current);
}
}, [contentId, loading, contentMd5, html]);
const addChapterParent = () => {
setEditKey(-1);
setEditValue("");
setParentId(-1);
};
const delChapter = async (node) => {
const childInKey = findParentLevelOne(gData, "key", node.key);
let current = null;
if (childInKey && childInKey.children && childInKey.children.length > 0) {
const index = findTreeToIndex(childInKey.children, "key", node.key);
if (childInKey.children.length === 1) {
current = childInKey;
} else {
// 前面还有
if (index <= childInKey.children.length) {
if (index === 0) {
current = childInKey.children[index + 1];
} else {
current = childInKey.children[index - 1];
}
}
}
} else {
const index = findTreeToIndex(gData, "key", node.key);
let child = null;
if (index === 0) {
child = gData[index + 1];
} else {
child = gData[index - 1];
}
if (child && child.children && child.children.length > 0) {
current = child.children[0];
} else {
current = child;
}
}
const data = await chapterDel({ id: node.key });
if (data) {
await dispatch(
setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }),
);
await getChapterTreeList();
}
};
useEffect(() => {
if (gData && gData.length > 0 && treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(gData, "key", treeChapter.saveChapterId);
if (childInKey && Object.entries(childInKey).length > 0) {
setChapterId(parseInt(treeChapter.saveChapterId));
setCheckedKeys([parseInt(treeChapter.saveChapterId)]);
setNowTitle(childInKey.title);
}
}
}
}, [gData, treeChapter]);
let chapterMenuItems = [
{ key: "1", label: "展开全部" },
// { key: '2', label: '添加子节' },
// { key: '5', label: '设置编者' },
// { key: '3', label: '编辑' },
// { key: '4', label: '删除' }
];
// if (!isAdminOrCreator) {
// chapterMenuItems = [{ key: '1', label: '展开全部' }]
// }
const handleMenuClick = async (e, node) => {
e.domEvent.stopPropagation();
setCurrentChapter(node);
if (parseInt(e.key) === 1) {
// 展开全部
const ids = findNodeById([node], "key", node.key);
const expandedKeysTemp = [...expandedKeys, node.key, ...ids];
setExpandedKeys(expandedKeysTemp);
} else if (parseInt(e.key) === 2) {
// 添加子节
setEditKey(-1);
setEditValue("");
setParentId(node.key);
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key);
setEditValue(node.title);
setParentId(false);
} else if (parseInt(e.key) === 4) {
await modal.confirm({
title: "删除确认",
content: `确认删除子节【${node.title}】?`,
onOk() {
delChapter(node);
},
});
} else if (e.key == 5) {
setEditChapterEditorsIsOpen(true);
}
};
// 章节名称
const chapterTitleRender = (node) => {
return (
<div className="chapter-tree-item">
<p className="title">{node.title}</p>
<Dropdown
trigger={["click"]}
menu={{ items: chapterMenuItems, onClick: (e) => handleMenuClick(e, node) }}
>
<EllipsisOutlined onClick={(e) => e.stopPropagation()} />
</Dropdown>
</div>
);
};
const onDrop = async (info) => {
const dropToGap = info.dropToGap; // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key;
const sideDropNode = info.node.key;
let data = null;
if (dropToGap) {
// 同级
const dragOverGapBottom = info.node.dragOverGapBottom;
const dragOver = info.node.dragOver;
const dragOverGapTop = info.node.dragOverGapTop;
if (!dragOverGapTop && !dragOver && !dragOverGapBottom) {
// 拖到最底部
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
} else {
if (dragOverGapTop) {
// 拖拽到最顶层
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "before",
});
} else if (dragOverGapBottom) {
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "after",
});
}
}
} else {
// 子级
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: "inner",
});
}
if (data) {
await getChapterTreeList();
}
};
return (
<div className="examine" style={{ padding: "20px", height: "100vh", boxSizing: "border-box" }}>
<div className="content-box">
<Row gutter={10} style={{ height: "100%" }} className="book-content-row">
<Col span={isCollapse ? 1 : 4} className="book-content-tree">
<div className="border">
{!isCollapse ? (
<>
<Descriptions
layout="vertical"
rootClassName="section-left-top"
bordered
items={[
{
key: "1",
label: (
<Row
gutter={5}
justify={"space-between"}
style={{ alignItems: "center" }}
>
<Col>章节目录</Col>
<Col>
{isAdminOrCreator && (
<Button
type="text"
icon={<DiffOutlined />}
onClick={addChapterParent}
></Button>
)}
<Button
type="text"
icon={<MenuFoldOutlined />}
onClick={() => setisCollapse(true)}
></Button>
</Col>
</Row>
),
},
]}
/>
{gData && gData.length > 0 && (
<Tree
className="draggable-tree"
onSelect={handleSelect}
defaultExpandAll
defaultExpandedKeys={expandedKeys}
expandedKeys={expandedKeys}
defaultSelectedKeys={checkedKeys}
selectedKeys={checkedKeys}
blockNode
disabled={loading}
treeData={gData}
onExpand={onExpand}
titleRender={(nodeData) => chapterTitleRender(nodeData)}
/>
)}
</>
) : (
<Descriptions
layout="vertical"
bordered
items={[
{
key: "1",
label: (
<Row gutter={5} justify={"space-between"} style={{ alignItems: "center" }}>
<Col>
<Button
type="text"
icon={<MenuUnfoldOutlined />}
onClick={() => setisCollapse(false)}
></Button>
</Col>
</Row>
),
},
]}
/>
)}
</div>
</Col>
<Col span={isCollapse ? 23 : 20} className="book-content-tree">
<div className="editor-right">
<Spin spinning={loading}>
<WangEditorCustomer
ref={editorRef}
chapterId={chapterId}
bookId={id}
contentId={contentId}
setHtml={setHtml}
html={html}
saveContent={saveContent}
gData={gData}
nowTitle={nowTitle}
disabled={disabled}
isView={true}
/>
</Spin>
</div>
</Col>
</Row>
</div>
</div>
);
};
export default Examine;
export { default } from "@/modules/student-book/views/StudentBookView";
import axios from "@/utils/request.js";
import qs from "qs";
import { get } from "lodash-es";
import { message } from "antd";
// 用户信息
const getUserInfo = async (obj) => {
try {
const res = await axios.post("/user/user/getUserInfo", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取角色已选权限
const getPowerByRoleId = async (obj) => {
try {
const res = await axios.post("/user/role/getPowerByRoleId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节列表
const getAllList = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getAllList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return {
code,
data,
};
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节
const chapterAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success('添加成功');
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 章节详情
const getInfoById = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/getInfoById", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节
const chapterEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 删除章节
const chapterDel = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 拖拽章节排序
const dragOrder = async (obj) => {
try {
const res = await axios.post("/book/teacher/chapter/dragOrder", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 获取章节内容
const getInfoByChapterId = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getInfoByChapterId", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加章节内容
const sectionAdd = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
message.success(msg);
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 修改章节内容
const sectionEdit = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/edit", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
// message.success(msg);
return true;
} else {
message.error(msg);
return { code, msg, data };
}
} catch (error) {
return false;
}
};
// 获取章节的题库列表
const getQuestionList = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/getQuestionList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 移除课后题
const questionDel = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/del", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 添加课后题
const questionAdd = async (obj) => {
try {
const res = await axios.post("/book/chapter/question/add", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 发布审核
const release = async (obj) => {
try {
const res = await axios.post("/book/teacher/release", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板列表
const templateGetList = async (obj) => {
try {
const res = await axios.post("/book/template/getList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 模板导入
const templateImport = async (obj) => {
try {
const res = await axios.post("/book/template/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 书籍导入
const teacherImport = async (obj) => {
try {
const res = await axios.post("/book/teacher/import", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
// 保存历史记录
const getRecordList = async (obj) => {
try {
const res = await axios.post("/book/teacher/section/getRecordList", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export {
getAllList,
chapterAdd,
getInfoById,
chapterEdit,
chapterDel,
dragOrder,
getInfoByChapterId,
sectionAdd,
sectionEdit,
getQuestionList,
questionDel,
questionAdd,
release,
templateGetList,
templateImport,
teacherImport,
getRecordList,
getPowerByRoleId,
getUserInfo,
};
export * from "@/modules/student-book/request";
import React from "react";
import { Outlet } from "react-router";
const Teacher = () => {
return <Outlet></Outlet>;
};
export default Teacher;
export { default } from '@/modules/teachers/Layout'
import { Outlet } from "react-router";
import "./index.less";
const User = () => {
return (
<div className="user-content">
<div className="form-container">
<div className="left-container"></div>
<div className="form-field-box">
<div className="form-field">
<Outlet />
</div>
</div>
</div>
</div>
);
};
export default User;
export { default } from "@/modules/auth/views/AuthLayout";
import React, { useState, useEffect, useRef } from "react";
import { Form, Input, Button, Row, Col, Space, Image, message } from "antd";
import { CloseOutlined, UserOutlined, LockOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router";
import { checkSSOLogin } from "../request/request";
import "./index.less";
import loginBtn from "@/assets/images/loginBtn.png";
import loginBtn2 from "@/assets/images/loginBtn2.png";
import Icon1 from "@/assets/login/1.png";
import Icon2 from "@/assets/login/2.png";
import Icon3 from "@/assets/login/3.png";
import Icon4 from "@/assets/login/4.png";
import Icon5 from "@/assets/login/5.png";
import Icon6 from "@/assets/login/6.png";
import Icon7 from "@/assets/login/7.png";
import Icon8 from "@/assets/login/8.png";
import Icon9 from "@/assets/login/9.png";
import Icon10 from "@/assets/login/10.png";
import Icon11 from "@/assets/login/11.png";
import Icon12 from "@/assets/login/12.png";
import Icon13 from "@/assets/login/13.png";
import Icon14 from "@/assets/login/14.png";
import Icon15 from "@/assets/login/15.png";
import Icon16 from "@/assets/login/16.png";
import Icon17 from "@/assets/login/17.png";
import Icon18 from "@/assets/login/18.png";
import Icon19 from "@/assets/login/19.png";
import Icon20 from "@/assets/login/20.png";
import Icon21 from "@/assets/login/21.png";
import Icon22 from "@/assets/login/22.png";
import Icon23 from "@/assets/login/23.png";
import { login } from "./request";
import logo from "@/assets/images/icon_logo_login.png";
import { RotateVerify } from "../../../utils/captcha";
const Login = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [isActive, setIsActive] = useState(0);
const [loading, setLoading] = useState(false);
const [captchaURL, setCaptchaURL] = useState("");
const [captchaStatus, setCaptchaStatus] = useState(false);
const [captchaText, setCaptchaText] = useState("发送验证码");
const [showCaptcha, setShowCaptcha] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false); //登录图片切换
const [isLoginBtn, setIsLoginBtn] = useState(false); //是否点击了登录按钮
const [code, setCode] = useState("");
const [disLf, setDisLf] = useState("");
const [initialValues, setinitialValues] = useState({
tel: "",
password: "",
});
const timer = useRef();
const getSSOInfo = async () => {
const isSSOData = await checkSSOLogin();
if (isSSOData.run === "login") {
} else if (isSSOData.token) {
localStorage.setItem("kiwi.token", isSSOData.token);
navigate("/");
return;
}
};
useEffect(() => {
getSSOInfo();
}, []);
// 登录
const toLayout = async (obj) => {
setLoading(true);
if (isLoginBtn) {
const data = await login({
...obj,
code: code,
disLf: disLf,
});
if (data.code === 200) {
localStorage.setItem("kiwi.token", data.token);
navigate("/");
return;
} else {
form.setFields([{ name: "password", errors: [data.msg] }]);
}
} else {
message.error("请先完成按钮验证");
}
// 刷新验证码
// await refreshCaptcha();
setLoading(false);
};
const handleStartVerification = () => {
setShowCaptcha(true);
};
// 点击×
const handleGoBack = () => {
setShowCaptcha(false);
};
useEffect(() => {
if (showCaptcha) {
const rotateWrapEle = document.querySelector("#rotateWrap1");
// 确保 DOM 元素存在之后再进行初始化
if (rotateWrapEle) {
const myRotateVerify = new RotateVerify("#rotateWrap1", {
initText: "滑动将图片转正",
slideImage: [
Icon1,
Icon2,
Icon3,
Icon4,
Icon5,
Icon6,
Icon7,
Icon8,
Icon9,
Icon10,
Icon11,
Icon12,
Icon13,
Icon14,
Icon15,
Icon16,
Icon17,
Icon18,
Icon4,
Icon19,
Icon20,
Icon21,
Icon22,
Icon23,
],
initUrl: "/user/login/initData",
verifyUrl: "/user/login/checkVerify",
getSuccessState: function (data) {
console.log("验证成功", data);
setDisLf(data.disLf);
setCode(data.code);
if (data.disLf) {
setShowCaptcha(false);
setIsLoggedIn(true);
setIsLoginBtn(true);
}
},
});
// 如果需要,在这里处理卸载时清理资源
return () => {
console.log(myRotateVerify.initUrl);
// 清理或销毁 RotateVerify 实例等操作
if (myRotateVerify && typeof myRotateVerify.destroy === "function") {
myRotateVerify.destroy();
}
};
}
}
}, [showCaptcha]);
return (
<div className={showCaptcha ? "captcha-container" : "normal-container"}>
{showCaptcha && (
<div className="rotateWrap">
<div className="cuo" onClick={handleGoBack}>
<CloseOutlined />
</div>
<span className="top-s">身份验证</span>
<span className="top-x">拖动滑块,使图片角度为正</span>
<div id="rotateWrap1">
<div className="rotateverify-contaniner">
<div className="rotate-can-wrap">
<canvas className="rotateCan rotate-can" width="200" height="200"></canvas>
<div className="statusBg status-bg"></div>
</div>
<div className="control-wrap slideDragWrap">
<div className="control-tips">
<p className="c-tips-txt cTipsTxt">滑动将图片转正</p>
</div>
<div className="control-bor-wrap controlBorWrap"></div>
<div className="control-btn slideDragBtn">
{/* <i className='control-btn-ico'></i> */}
<div className="control-btn-ico"></div>
</div>
</div>
</div>
</div>
</div>
)}
{!showCaptcha && (
<div className="img-logo">
<img src={logo} className="img" />
</div>
)}
{!showCaptcha && <div className="login-header">欢迎登录</div>}{" "}
{!showCaptcha && (
<div className="form-data-container">
<Form form={form} onFinish={toLayout} autoComplete="off" initialValues={initialValues}>
<>
<Form.Item
name="tel"
rules={[
{ required: true, message: "请输入手机号!" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确格式的手机号" },
]}
>
<Input
autoComplete="off"
prefix={<UserOutlined />}
size="large"
placeholder="请输入手机号"
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: "请输入密码!" }]}>
<Input.Password
autoComplete="off"
prefix={<LockOutlined />}
size="large"
placeholder="请输入密码"
/>
</Form.Item>
{/* <Form.Item name='verify' rules={[{ required: true, message: '请输入验证码!' }]}>
<Row gutter={16}>
<Col span={16}>
<Input
prefix={<SetIcon src={IconCaptcha} />}
size='large'
placeholder='请输入验证码'
/>
</Col>
<Col span={8}>
<img
src={captchaURL}
alt='图形验证码'
className='captcha'
onClick={refreshCaptcha}
/>
</Col>
</Row>
</Form.Item> */}
<Form.Item name="verify" onClick={handleStartVerification}>
<div style={{ width: "100%", height: "100%", pointerEvents: "none" }}>
{isLoggedIn ? (
<Image
src={loginBtn2}
style={{
width: "100%",
height: "100%",
}}
/>
) : (
<Image
src={loginBtn}
style={{
width: "100%",
height: "100%",
}}
/>
)}
</div>
</Form.Item>
</>
<Form.Item className="login-submit-btn">
<Button htmlType="submit" loading={loading} type="primary" size="large">
登录
</Button>
</Form.Item>
</Form>
</div>
)}
</div>
);
};
export default Login;
export { default } from "@/modules/auth/views/LoginView";
const api = {
login: "/user/login",
getCode: "/user/login/verify",
};
export default api;
export { default } from "@/modules/auth/request/login-api";
import axios from "@/utils/request.js";
import qs from "qs";
import api from "./api.js";
import { get } from "lodash-es";
import { message } from "antd";
const login = async (obj) => {
try {
const res = await axios.post(api.login, qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (msg === "Undefined index: verify") {
message.error("请先点击按钮进行验证");
return false;
}
if (code === 200) {
return {
code,
...data,
};
} else {
message.error(msg);
return {
msg,
};
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
export { login };
export * from "@/modules/auth/request/login";
const api = {
getCaptcha: "/users/generate_image_code", // 获取验证码 GET
sendSMS: "/docapi/users/send_sms", // 发送手机验证码
loginByPhone: "/docapi/users/phone_login", // 手机号码形式登录
loginByUsername: "/docapi/users/login", // 用户名密码登录
checkSSOLogin: "/user/checkSsoLogin", // sso 登录
};
export default api;
export { default } from "@/modules/auth/request/sso-api";
import axios from "@/utils/request";
import { message } from "antd";
import api from "./api";
import { get } from "lodash-es";
import qs from "qs";
// 获取图形验证码
const getCaptcha = async (obj) => {
try {
const res = await axios.get(api.getCaptcha, obj);
const status = get(res, "status", 0);
const data = get(res, "data", "");
if (status === 200) {
return data;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 发送短信验证码
const sendSMS = async (data) => {
try {
const res = await axios.post(api.sendSMS, data);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "发送失败");
if (code === 200) {
message.success("验证码发送成功!");
return true;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 用户名登录
const userNameLogin = async (obj) => {
try {
const res = await axios.post(api.loginByUsername, obj);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "登录失败");
const data = get(res, "data.data", "");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
// 手机号登录
const phoneLogin = async (obj) => {
try {
const res = await axios.post(api.loginByPhone, obj);
const code = get(res, "data.code", 0);
const msg = get(res, "data.message", "登录失败");
const data = get(res, "data.data", "");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
const checkSSOLogin = async (obj) => {
try {
const res = await axios.post(api.checkSSOLogin, qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "数据获取失败!");
if (msg === "Undefined index: verify") {
message.error("请先点击按钮进行验证");
return false;
}
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
console.error(error);
let msg = get(error.response, "data.msg", "");
if (msg) {
message.error(msg);
}
return false;
}
};
export { getCaptcha, sendSMS, userNameLogin, phoneLogin, checkSSOLogin };
export * from "@/modules/auth/request/sso";
import { useState, useEffect } from "react";
import { Form, Input, Button, Space, Row, Col, Drawer } from "antd";
import users from "@/assets/images/icon/userinfo.png";
import { changePass, changeInfo, getUserInfo } from "@/layout/request";
import { useNavigate } from "react-router";
import UploadImage from "@/components/uploadImage";
const UserInfo = () => {
const navigator = useNavigate();
const [imageUrl, setImageUrl] = useState("");
const [form] = Form.useForm();
const [form2] = Form.useForm();
const [showModal, setShowModal] = useState(false);
const fetchData = async (obj) => {
try {
const userData = await getUserInfo(obj);
form.setFieldsValue({
real_name: userData.real_name,
tel: userData.tel,
pic: userData.pic,
});
const imageUrl = userData.pic;
setImageUrl(imageUrl);
} catch (error) {
console.error("Error fetching user data:", error);
}
};
useEffect(() => {
fetchData();
}, []);
const changePasswrd = async (obj) => {
await changePass(obj);
setShowModal(false);
};
const submitForm = async (obj) => {
const updatedData = await changeInfo({ ...obj });
if (updatedData) {
await fetchData();
}
};
return (
<div style={{ padding: "20px 0" }}>
<Form form={form} labelCol={{ span: 2 }} onFinish={submitForm}>
<Form.Item label="头像" className="uploadPar" name="pic">
<UploadImage
value={imageUrl ? imageUrl : users}
onChange={(url) => setImageUrl(url)}
></UploadImage>
</Form.Item>
<Form.Item
label="管理员名称"
name="real_name"
rules={[{ required: true, message: "请输入管理员名称" }]}
>
<Input autoComplete="off" placeholder="请输入管理员名称" style={{ width: 200 }}></Input>
</Form.Item>
<Form.Item label="手机号" name="tel" rules={[{ required: true, message: "请输入手机号" }]}>
<Input autoComplete="off" placeholder="请输入手机号" style={{ width: 200 }}></Input>
</Form.Item>
<Form.Item label="密码">
<Button type="primary" ghost onClick={() => setShowModal(true)}>
修改密码
</Button>
</Form.Item>
<Form.Item wrapperCol={{ offset: 2 }}>
<Space size={20}>
<Button onClick={() => navigator(-1)}>取消</Button>
<Button type="primary" htmlType="submit">
保存
</Button>
</Space>
</Form.Item>
</Form>
<Drawer
open={showModal}
onClose={() => {
setShowModal(false);
}}
mask={false}
maskClosable={false}
footer={null}
centered
destroyOnClose
keyboard={false}
>
<Form labelCol={{ span: 5 }} form={form2} onFinish={changePasswrd}>
<Form.Item
label="旧密码"
name="old_password"
rules={[{ required: true, message: "请输入旧密码" }]}
>
<Input.Password autoComplete="off" placeholder="请输入旧密码"></Input.Password>
</Form.Item>
<Form.Item
label="新密码"
name="password"
rules={[
{ required: true, message: "请输入新密码" },
{
pattern:
/^((?=.*\d)(?=.*[A-Z])(?=.*[a-z]))|((?=.*\d)(?=.*[A-Z])(?=.*[\W_]))|((?=.*\d)(?=.*[a-z])(?=.*[\W_]))|((?=.*[A-Z])(?=.*[a-z])(?=.*[\W_]))[a-zA-Z\d\W_]{8,12}$/,
message:
"密码至少满足三种格式(大写字母、小写字母、数字、特殊符号), 长度为8~12位字符!",
trigger: ["change", "blur"],
},
{
validator(_, value) {
if (!value || form2.getFieldValue("re_password") === value) {
form2.setFields([{ name: "re_password", errors: [""] }]);
return Promise.resolve();
}
form2.setFields([{ name: "re_password", errors: ["两次输入的密码不一致"] }]);
return Promise.reject();
},
},
]}
>
<Input.Password autoComplete="off" placeholder="请输入新密码"></Input.Password>
</Form.Item>
<Form.Item
label="确认新密码"
name="re_password"
rules={[
{ required: true, message: "请确认新密码" },
{
pattern:
/^((?=.*\d)(?=.*[A-Z])(?=.*[a-z]))|((?=.*\d)(?=.*[A-Z])(?=.*[\W_]))|((?=.*\d)(?=.*[a-z])(?=.*[\W_]))|((?=.*[A-Z])(?=.*[a-z])(?=.*[\W_]))[a-zA-Z\d\W_]{8,12}$/,
message:
"密码至少满足三种格式(大写字母、小写字母、数字、特殊符号), 长度为8~12位字符!",
trigger: ["change", "blur"],
},
{
validator(_, value) {
if (!value || form2.getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(new Error("两次输入的密码不一致"));
},
},
]}
>
<Input.Password autoComplete="off" placeholder="请确认新密码"></Input.Password>
</Form.Item>
<Form.Item wrapperCol={{ offset: 5 }}>
<Row justify={"center"}>
<Space size={20}>
<Col>
<Button onClick={() => setShowModal(false)}>取消</Button>
</Col>
<Col>
<Button type="primary" htmlType="submit">
确认
</Button>
</Col>
</Space>
</Row>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default UserInfo;
export { default } from "@/modules/account/views/AccountView";
import axios from "@/utils/request.js";
import qs from "qs";
import { message } from "antd";
import { get } from "lodash-es";
// oss
const getAliOSSSTSToken = async (obj) => {
try {
const res = await axios.post("/common/OssUpload/getUploadToken", qs.stringify(obj));
const code = get(res, "data.code", 0);
const data = get(res, "data.data", []);
const msg = get(res, "data.message", "获取OSS令牌失败!");
if (code === 200) {
return data;
} else {
message.error(msg);
return false;
}
} catch (error) {
return false;
}
};
export { getAliOSSSTSToken };
export * from "@/modules/account/oss";
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论