CourseMemberList.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import { useIntl } from "react-intl";
  2. import { Dropdown, Tag, Tooltip, Typography, message } from "antd";
  3. import { type ActionType, ProList } from "@ant-design/pro-components";
  4. import { ExportOutlined } from "@ant-design/icons";
  5. import { API_HOST, get } from "../../request";
  6. import { useEffect, useRef, useState } from "react";
  7. import {
  8. type ICourseDataResponse,
  9. type ICourseMemberData,
  10. type ICourseMemberListResponse,
  11. type ICourseResponse,
  12. type TCourseMemberAction,
  13. type TCourseMemberStatus,
  14. type TCourseRole,
  15. actionMap,
  16. } from "../../api/Course";
  17. import { ItemType } from "antd/lib/menu/hooks/useItems";
  18. import User, { type IUser } from "../auth/User";
  19. import { getStatusColor, managerCanDo } from "./RolePower";
  20. import { type ISetStatus, setStatus } from "./UserAction";
  21. import type { IChannel } from "../channel/Channel";
  22. import CourseInvite from "./CourseInvite";
  23. interface IParam {
  24. role?: string;
  25. status?: string[];
  26. }
  27. interface IRoleTag {
  28. title: string;
  29. color: string;
  30. }
  31. export interface ICourseMember {
  32. sn?: number;
  33. id?: string;
  34. userId: string;
  35. user?: IUser;
  36. name?: string;
  37. tag?: IRoleTag[];
  38. image: string;
  39. role?: TCourseRole;
  40. channel?: IChannel;
  41. startExp?: number;
  42. endExp?: number;
  43. currentExp?: number;
  44. expByDay?: number;
  45. status?: TCourseMemberStatus;
  46. }
  47. interface IWidget {
  48. courseId?: string;
  49. onSelect?: Function;
  50. }
  51. const CourseMemberListWidget = ({ courseId, onSelect }: IWidget) => {
  52. const intl = useIntl(); //i18n
  53. const [canManage, setCanManage] = useState(false);
  54. const [course, setCourse] = useState<ICourseDataResponse>();
  55. const ref = useRef<ActionType | null>(null);
  56. const { Text } = Typography;
  57. useEffect(() => {
  58. if (courseId) {
  59. const url = `/v2/course/${courseId}`;
  60. console.debug("course url", url);
  61. get<ICourseResponse>(url)
  62. .then((json) => {
  63. console.debug("course data", json.data);
  64. if (json.ok) {
  65. setCourse(json.data);
  66. }
  67. })
  68. .catch((e) => console.error(e));
  69. }
  70. }, [courseId]);
  71. return (
  72. <>
  73. <ProList<ICourseMember, IParam>
  74. actionRef={ref}
  75. search={{
  76. filterType: "light",
  77. }}
  78. onItem={(record: ICourseMember, _index: number) => {
  79. return {
  80. onClick: (_event) => {
  81. // 点击行
  82. if (typeof onSelect !== "undefined") {
  83. onSelect(record);
  84. }
  85. },
  86. };
  87. }}
  88. metas={{
  89. title: {
  90. dataIndex: "name",
  91. search: false,
  92. },
  93. avatar: {
  94. search: false,
  95. render(_dom, entity, _index, _action, _schema) {
  96. return <User {...entity.user} showName={false} />;
  97. },
  98. editable: false,
  99. },
  100. description: {
  101. dataIndex: "desc",
  102. search: false,
  103. render(_dom, entity, _index, _action, _schema) {
  104. return (
  105. <div>
  106. {entity.role === "student" ? (
  107. <>
  108. {"channel:"}
  109. {entity.channel?.name ?? (
  110. <Text type="danger">
  111. {intl.formatMessage({
  112. id: `course.channel.unbound`,
  113. })}
  114. </Text>
  115. )}
  116. </>
  117. ) : (
  118. <></>
  119. )}
  120. </div>
  121. );
  122. },
  123. },
  124. subTitle: {
  125. search: false,
  126. render: (
  127. _dom: React.ReactNode,
  128. entity: ICourseMember,
  129. _index: number
  130. ) => {
  131. return (
  132. <Tag>
  133. {intl.formatMessage({
  134. id: `auth.role.${entity.role}`,
  135. })}
  136. </Tag>
  137. );
  138. },
  139. },
  140. actions: {
  141. search: false,
  142. render: (_text, row, index, _action) => {
  143. const statusColor = getStatusColor(row.status);
  144. const actions: TCourseMemberAction[] = [
  145. "invite",
  146. "revoke",
  147. "accept",
  148. "reject",
  149. "block",
  150. ];
  151. const items: ItemType[] = actions.map((item) => {
  152. return {
  153. key: item,
  154. label: intl.formatMessage({
  155. id: `course.member.status.${item}.button`,
  156. }),
  157. disabled: !managerCanDo(
  158. item,
  159. course?.start_at,
  160. course?.end_at,
  161. course?.join,
  162. row.status,
  163. course?.sign_up_start_at,
  164. course?.sign_up_end_at
  165. ),
  166. };
  167. });
  168. return [
  169. <span style={{ color: statusColor }}>
  170. {intl.formatMessage({
  171. id: `course.member.status.${row.status}.label`,
  172. })}
  173. </span>,
  174. canManage ? (
  175. <Dropdown.Button
  176. key={index}
  177. type="link"
  178. menu={{
  179. items,
  180. onClick: (e) => {
  181. console.debug("click", e);
  182. const currAction = e.key as TCourseMemberAction;
  183. if (actions.includes(currAction)) {
  184. const newStatus = actionMap(currAction);
  185. if (newStatus) {
  186. const actionParam: ISetStatus = {
  187. courseMemberId: row.id,
  188. message: intl.formatMessage(
  189. {
  190. id: `course.member.status.${currAction}.message`,
  191. },
  192. { user: row.user?.nickName }
  193. ),
  194. status: newStatus,
  195. onSuccess: (_data: ICourseMemberData) => {
  196. message.success(
  197. intl.formatMessage({ id: "flashes.success" })
  198. );
  199. ref.current?.reload();
  200. },
  201. };
  202. setStatus(actionParam);
  203. }
  204. }
  205. },
  206. }}
  207. >
  208. <></>
  209. </Dropdown.Button>
  210. ) : (
  211. <></>
  212. ),
  213. ];
  214. },
  215. },
  216. status: {
  217. // 自己扩展的字段,主要用于筛选,不在列表中显示
  218. title: "状态",
  219. valueType: "checkbox",
  220. valueEnum: {
  221. joined: {
  222. text: intl.formatMessage({
  223. id: "course.member.status.joined.label",
  224. }),
  225. status: "Default",
  226. },
  227. applied: {
  228. text: intl.formatMessage({
  229. id: "course.member.status.applied.label",
  230. }),
  231. status: "Success",
  232. },
  233. invited: {
  234. text: intl.formatMessage({
  235. id: "course.member.status.invited.label",
  236. }),
  237. status: "Success",
  238. },
  239. canceled: {
  240. text: intl.formatMessage({
  241. id: "course.member.status.canceled.label",
  242. }),
  243. status: "Success",
  244. },
  245. revoked: {
  246. text: intl.formatMessage({
  247. id: "course.member.status.revoked.label",
  248. }),
  249. status: "Success",
  250. },
  251. agreed: {
  252. text: intl.formatMessage({
  253. id: "course.member.status.agreed.label",
  254. }),
  255. status: "Success",
  256. },
  257. accepted: {
  258. text: intl.formatMessage({
  259. id: "course.member.status.accepted.label",
  260. }),
  261. status: "Success",
  262. },
  263. disagreed: {
  264. text: intl.formatMessage({
  265. id: "course.member.status.disagreed.label",
  266. }),
  267. status: "Success",
  268. },
  269. rejected: {
  270. text: intl.formatMessage({
  271. id: "course.member.status.rejected.label",
  272. }),
  273. status: "Success",
  274. },
  275. left: {
  276. text: intl.formatMessage({
  277. id: "course.member.status.left.label",
  278. }),
  279. status: "Success",
  280. },
  281. blocked: {
  282. text: intl.formatMessage({
  283. id: "course.member.status.blocked.label",
  284. }),
  285. status: "Success",
  286. },
  287. },
  288. },
  289. role: {
  290. // 自己扩展的字段,主要用于筛选,不在列表中显示
  291. title: "角色",
  292. valueType: "select",
  293. valueEnum: {
  294. student: {
  295. text: intl.formatMessage({
  296. id: "auth.role.student",
  297. }),
  298. status: "Default",
  299. },
  300. manager: {
  301. text: intl.formatMessage({
  302. id: "auth.role.manager",
  303. }),
  304. status: "Success",
  305. },
  306. assistant: {
  307. text: intl.formatMessage({
  308. id: "auth.role.assistant",
  309. }),
  310. status: "Success",
  311. },
  312. },
  313. },
  314. }}
  315. request={async (params = {}, sorter, filter) => {
  316. console.log(params, sorter, filter);
  317. let url = `/v2/course-member?view=course&id=${courseId}`;
  318. const offset =
  319. ((params.current ? params.current : 1) - 1) *
  320. (params.pageSize ? params.pageSize : 20);
  321. url += `&limit=${params.pageSize}&offset=${offset}`;
  322. if (
  323. typeof params.keyword !== "undefined" &&
  324. params.keyword.trim() !== ""
  325. ) {
  326. url += "&search=" + params.keyword;
  327. }
  328. if (params.role) {
  329. url += `&role=${params.role}`;
  330. }
  331. if (params.status) {
  332. url += `&status=${params.status}`;
  333. }
  334. console.info("api request", url);
  335. const res = await get<ICourseMemberListResponse>(url);
  336. if (res.ok) {
  337. console.debug("api response", res.data);
  338. if (res.data.role === "owner" || res.data.role === "manager") {
  339. setCanManage(true);
  340. }
  341. const items: ICourseMember[] = res.data.rows.map((item, id) => {
  342. const member: ICourseMember = {
  343. sn: id + 1,
  344. id: item.id,
  345. userId: item.user_id,
  346. user: item.user,
  347. name: item.user?.nickName,
  348. role: item.role,
  349. status: item.status,
  350. channel: item.channel,
  351. tag: [],
  352. image: "",
  353. };
  354. return member;
  355. });
  356. console.log(items);
  357. return {
  358. total: res.data.count,
  359. succcess: true,
  360. data: items,
  361. };
  362. } else {
  363. console.error(res.message);
  364. return {
  365. total: 0,
  366. succcess: false,
  367. data: [],
  368. };
  369. }
  370. }}
  371. rowKey="id"
  372. bordered
  373. pagination={{
  374. showQuickJumper: true,
  375. showSizeChanger: true,
  376. }}
  377. options={{
  378. search: true,
  379. }}
  380. toolBarRender={() => [
  381. <CourseInvite
  382. courseId={courseId}
  383. onCreated={() => {
  384. ref.current?.reload();
  385. }}
  386. />,
  387. <Tooltip title="导出成员列表">
  388. <a
  389. href={`${API_HOST}/api/v2/course-member-export?course_id=${courseId}`}
  390. target="_blank"
  391. key="export"
  392. rel="noreferrer"
  393. >
  394. <ExportOutlined />
  395. </a>
  396. </Tooltip>,
  397. ]}
  398. />
  399. </>
  400. );
  401. };
  402. export default CourseMemberListWidget;