CourseMemberList.tsx 10.0 KB

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