list.tsx 14 KB

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