list.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. import { useParams, Link } from "react-router-dom";
  2. import { useIntl } from "react-intl";
  3. import React, { useEffect, useRef, useState } from "react";
  4. import {
  5. Space,
  6. Badge,
  7. Button,
  8. Popover,
  9. Dropdown,
  10. Image,
  11. message,
  12. Modal,
  13. Tag,
  14. } from "antd";
  15. import { ProTable, ActionType } from "@ant-design/pro-components";
  16. import {
  17. PlusOutlined,
  18. DeleteOutlined,
  19. ExclamationCircleOutlined,
  20. } from "@ant-design/icons";
  21. import CourseCreate from "../../../components/course/CourseCreate";
  22. import { API_HOST, delete_, get } from "../../../request";
  23. import {
  24. ICourseDataResponse,
  25. ICourseListResponse,
  26. ICourseMemberData,
  27. ICourseNumberResponse,
  28. TCourseMemberAction,
  29. TCourseRole,
  30. actionMap,
  31. } from "../../../components/api/Course";
  32. import { PublicityValueEnum } from "../../../components/studio/table";
  33. import { IDeleteResponse } from "../../../components/api/Article";
  34. import { getSorterUrl } from "../../../utils";
  35. import { ItemType } from "antd/lib/menu/hooks/useItems";
  36. import {
  37. getStatusColor,
  38. studentCanDo,
  39. } from "../../../components/course/RolePower";
  40. import { ISetStatus, setStatus } from "../../../components/course/UserAction";
  41. import { useAppSelector } from "../../../hooks";
  42. import { currentUser } from "../../../reducers/current-user";
  43. import User from "../../../components/auth/User";
  44. const renderBadge = (count: number, active = false) => {
  45. return (
  46. <Badge
  47. count={count}
  48. style={{
  49. marginBlockStart: -2,
  50. marginInlineStart: 4,
  51. color: active ? "#1890FF" : "#999",
  52. backgroundColor: active ? "#E6F7FF" : "#eee",
  53. }}
  54. />
  55. );
  56. };
  57. const Widget = () => {
  58. const intl = useIntl(); //i18n
  59. const { studioname } = useParams(); //url 参数
  60. const [activeKey, setActiveKey] = useState<React.Key | undefined>("create");
  61. const [createNumber, setCreateNumber] = useState<number>(0);
  62. const [teachNumber, setTeachNumber] = useState<number>(0);
  63. const [studyNumber, setStudyNumber] = useState<number>(0);
  64. const ref = useRef<ActionType>();
  65. const [openCreate, setOpenCreate] = useState(false);
  66. const user = useAppSelector(currentUser);
  67. useEffect(() => {
  68. /**
  69. * 获取各种课程的数量
  70. */
  71. const url = `/v2/course-my-course?studio=${studioname}`;
  72. console.log("url", url);
  73. get<ICourseNumberResponse>(url).then((json) => {
  74. if (json.ok) {
  75. setCreateNumber(json.data.create);
  76. setTeachNumber(json.data.teach);
  77. setStudyNumber(json.data.study);
  78. }
  79. });
  80. }, [studioname]);
  81. const showDeleteConfirm = (id: string, title: string) => {
  82. Modal.confirm({
  83. icon: <ExclamationCircleOutlined />,
  84. title:
  85. intl.formatMessage({
  86. id: "message.delete.confirm",
  87. }) +
  88. intl.formatMessage({
  89. id: "message.irrevocable",
  90. }),
  91. content: title,
  92. okText: intl.formatMessage({
  93. id: "buttons.delete",
  94. }),
  95. okType: "danger",
  96. cancelText: intl.formatMessage({
  97. id: "buttons.no",
  98. }),
  99. onOk() {
  100. console.log("delete", id);
  101. return delete_<IDeleteResponse>(`/v2/course/${id}`)
  102. .then((json) => {
  103. if (json.ok) {
  104. message.success("删除成功");
  105. ref.current?.reload();
  106. } else {
  107. message.error(json.message);
  108. }
  109. })
  110. .catch((e) => console.log("Oops errors!", e));
  111. },
  112. });
  113. };
  114. const canCreate = !(activeKey !== "create" || user?.roles?.includes("basic"));
  115. const buttonEdit = (course: ICourseDataResponse, key: string | number) => {
  116. const canManage: TCourseRole[] = ["owner", "teacher", "manager"];
  117. if (course.my_role && canManage.includes(course.my_role)) {
  118. return (
  119. <Link
  120. to={`/studio/${studioname}/course/${course.id}/edit`}
  121. target="_blank"
  122. key={key}
  123. >
  124. {intl.formatMessage({
  125. //编辑
  126. id: "buttons.edit",
  127. })}
  128. </Link>
  129. );
  130. } else {
  131. return <></>;
  132. }
  133. };
  134. return (
  135. <>
  136. <ProTable<ICourseDataResponse>
  137. actionRef={ref}
  138. columns={[
  139. {
  140. title: intl.formatMessage({
  141. id: "dict.fields.sn.label",
  142. }),
  143. dataIndex: "sn",
  144. key: "sn",
  145. width: 50,
  146. search: false,
  147. },
  148. {
  149. //标题
  150. title: intl.formatMessage({
  151. id: "forms.fields.title.label",
  152. }),
  153. dataIndex: "title",
  154. key: "title",
  155. tip: "过长会自动收缩",
  156. ellipsis: true,
  157. width: 300,
  158. render: (text, row, index, action) => {
  159. return (
  160. <Space key={index}>
  161. <Image
  162. src={
  163. row.cover_url && row.cover_url.length > 1
  164. ? row.cover_url[1]
  165. : ""
  166. }
  167. preview={{
  168. src:
  169. row.cover_url && row.cover_url.length > 0
  170. ? row.cover_url[0]
  171. : "",
  172. }}
  173. width={64}
  174. fallback={`${API_HOST}/app/course/img/default.jpg`}
  175. />
  176. <div>
  177. <div>
  178. <Link to={`/course/show/${row.id}`} target="_blank">
  179. {row.title}
  180. </Link>
  181. <Tag>
  182. {intl.formatMessage({
  183. id: `course.join.mode.${row.join}.label`,
  184. })}
  185. </Tag>
  186. <Tag>
  187. {intl.formatMessage({
  188. id: `auth.role.${row.my_role}`,
  189. })}
  190. </Tag>
  191. </div>
  192. <div>{row.subtitle}</div>
  193. <div>
  194. <Space>
  195. {intl.formatMessage({
  196. id: "forms.fields.teacher.label",
  197. })}
  198. <User {...row.teacher} />
  199. </Space>
  200. </div>
  201. </div>
  202. </Space>
  203. );
  204. },
  205. },
  206. {
  207. title: intl.formatMessage({
  208. id: "course.table.count.member.title",
  209. }),
  210. dataIndex: "member_count",
  211. key: "member_count",
  212. width: 80,
  213. },
  214. {
  215. title: intl.formatMessage({
  216. id: "course.table.count.progressing.title",
  217. }),
  218. dataIndex: "count_progressing",
  219. key: "count_progressing",
  220. width: 80,
  221. hideInTable: activeKey === "study" ? true : false,
  222. },
  223. {
  224. //类型
  225. title: intl.formatMessage({
  226. id: "forms.fields.type.label",
  227. }),
  228. dataIndex: "type",
  229. key: "type",
  230. width: 80,
  231. search: false,
  232. filters: true,
  233. onFilter: true,
  234. valueEnum: PublicityValueEnum(),
  235. },
  236. {
  237. //创建时间
  238. title: intl.formatMessage({
  239. id: "forms.fields.created-at.label",
  240. }),
  241. key: "created-at",
  242. width: 100,
  243. search: false,
  244. dataIndex: "created_at",
  245. valueType: "date",
  246. sorter: true,
  247. },
  248. {
  249. //操作
  250. title: intl.formatMessage({ id: "buttons.option" }),
  251. key: "option",
  252. width: 120,
  253. valueType: "option",
  254. render: (text, row, index, action) => {
  255. let mainButton = <></>;
  256. switch (activeKey) {
  257. case "create":
  258. mainButton = buttonEdit(row, index);
  259. break;
  260. case "study":
  261. mainButton = (
  262. <span
  263. key={index}
  264. style={{ color: getStatusColor(row.my_status) }}
  265. >
  266. {intl.formatMessage({
  267. id: `course.member.status.${row.my_status}.label`,
  268. })}
  269. </span>
  270. );
  271. break;
  272. case "teach":
  273. mainButton = (
  274. <Space>
  275. {buttonEdit(row, index)}
  276. <span
  277. key={index}
  278. style={{ color: getStatusColor(row.my_status) }}
  279. >
  280. {intl.formatMessage({
  281. id: `course.member.status.${row.my_status}.label`,
  282. })}
  283. </span>
  284. </Space>
  285. );
  286. break;
  287. default:
  288. break;
  289. }
  290. let userItems: ItemType[] = [];
  291. const actions: TCourseMemberAction[] = [
  292. "join",
  293. "apply",
  294. "cancel",
  295. "agree",
  296. "disagree",
  297. "leave",
  298. ];
  299. if (activeKey !== "create") {
  300. userItems = actions.map((item) => {
  301. return {
  302. key: item,
  303. label: intl.formatMessage({
  304. id: `course.member.status.${item}.button`,
  305. }),
  306. disabled: !studentCanDo(
  307. item,
  308. row.start_at,
  309. row.end_at,
  310. row.join,
  311. row.my_status,
  312. row.sign_up_start_at,
  313. row.sign_up_end_at
  314. ),
  315. };
  316. });
  317. }
  318. return [
  319. <Dropdown.Button
  320. key={index}
  321. type="link"
  322. menu={{
  323. items:
  324. activeKey === "create"
  325. ? [
  326. {
  327. key: "remove",
  328. label: intl.formatMessage({
  329. id: "buttons.delete",
  330. }),
  331. icon: <DeleteOutlined />,
  332. danger: true,
  333. },
  334. ]
  335. : userItems,
  336. onClick: (e) => {
  337. if (e.key === "remove") {
  338. showDeleteConfirm(row.id, row.title);
  339. }
  340. const currAction = e.key as TCourseMemberAction;
  341. if (actions.includes(currAction)) {
  342. const newStatus = actionMap(currAction);
  343. if (newStatus) {
  344. const actionParam: ISetStatus = {
  345. courseMemberId: row.my_status_id,
  346. message: intl.formatMessage(
  347. {
  348. id: `course.member.status.${currAction}.message`,
  349. },
  350. { course: row.title }
  351. ),
  352. status: newStatus,
  353. onSuccess: (data: ICourseMemberData) => {
  354. message.success(
  355. intl.formatMessage({ id: "flashes.success" })
  356. );
  357. ref.current?.reload();
  358. },
  359. };
  360. setStatus(actionParam);
  361. }
  362. }
  363. },
  364. }}
  365. >
  366. {mainButton}
  367. </Dropdown.Button>,
  368. ];
  369. },
  370. },
  371. ]}
  372. //从服务端获取数据
  373. request={async (params = {}, sorter, filter) => {
  374. console.debug(params, sorter, filter);
  375. console.info(activeKey);
  376. let url = `/v2/course?view=${activeKey}&studio=${studioname}`;
  377. const offset =
  378. ((params.current ? params.current : 1) - 1) *
  379. (params.pageSize ? params.pageSize : 20);
  380. url += `&limit=${params.pageSize}&offset=${offset}`;
  381. if (typeof params.keyword !== "undefined") {
  382. url += "&search=" + (params.keyword ? params.keyword : "");
  383. }
  384. url += getSorterUrl(sorter);
  385. console.info("api request", url);
  386. const res = await get<ICourseListResponse>(url);
  387. console.debug("api response", res);
  388. return {
  389. total: res.data.count,
  390. succcess: true,
  391. data: res.data.rows,
  392. };
  393. }}
  394. rowKey="id"
  395. bordered
  396. pagination={{
  397. showQuickJumper: true,
  398. showSizeChanger: true,
  399. }}
  400. search={false}
  401. options={{
  402. search: true,
  403. }}
  404. toolBarRender={() => [
  405. canCreate ? (
  406. <Popover
  407. content={
  408. <CourseCreate
  409. studio={studioname}
  410. onCreate={() => {
  411. //新建课程成功后刷新
  412. setActiveKey("create");
  413. setCreateNumber(createNumber + 1);
  414. ref.current?.reload();
  415. setOpenCreate(false);
  416. }}
  417. />
  418. }
  419. title="Create"
  420. placement="bottomRight"
  421. trigger="click"
  422. open={openCreate}
  423. onOpenChange={(newOpen: boolean) => {
  424. setOpenCreate(newOpen);
  425. }}
  426. >
  427. <Button key="button" icon={<PlusOutlined />} type="primary">
  428. {intl.formatMessage({ id: "buttons.create" })}
  429. </Button>
  430. </Popover>
  431. ) : (
  432. <></>
  433. ),
  434. ]}
  435. toolbar={{
  436. menu: {
  437. activeKey,
  438. items: [
  439. {
  440. key: "create",
  441. label: (
  442. <span>
  443. 我建立的课程
  444. {renderBadge(createNumber, activeKey === "create")}
  445. </span>
  446. ),
  447. },
  448. {
  449. key: "study",
  450. label: (
  451. <span>
  452. 我参加的课程
  453. {renderBadge(studyNumber, activeKey === "study")}
  454. </span>
  455. ),
  456. },
  457. {
  458. key: "teach",
  459. label: (
  460. <span>
  461. 我任教的课程
  462. {renderBadge(teachNumber, activeKey === "teach")}
  463. </span>
  464. ),
  465. },
  466. ],
  467. onChange(key) {
  468. console.log("show course", key);
  469. setActiveKey(key);
  470. ref.current?.reload();
  471. },
  472. },
  473. }}
  474. />
  475. </>
  476. );
  477. };
  478. export default Widget;