ChannelTable.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import { useParams } from "react-router-dom";
  2. import { ActionType, ProTable } from "@ant-design/pro-components";
  3. import { useIntl } from "react-intl";
  4. import { Link } from "react-router-dom";
  5. import { Badge, message, Modal, Typography } from "antd";
  6. import { Button, Dropdown, Popover } from "antd";
  7. import {
  8. PlusOutlined,
  9. ExclamationCircleOutlined,
  10. DeleteOutlined,
  11. TeamOutlined,
  12. } from "@ant-design/icons";
  13. import ChannelCreate from "../../components/channel/ChannelCreate";
  14. import { delete_, get } from "../../request";
  15. import {
  16. IApiResponseChannelList,
  17. TChannelType,
  18. } from "../../components/api/Channel";
  19. import { PublicityValueEnum } from "../../components/studio/table";
  20. import { IDeleteResponse } from "../../components/api/Article";
  21. import { useEffect, useRef, useState } from "react";
  22. import { TRole } from "../../components/api/Auth";
  23. import ShareModal from "../../components/share/ShareModal";
  24. import { EResType } from "../../components/share/Share";
  25. import StudioName, { IStudio } from "../../components/auth/StudioName";
  26. import StudioSelect from "../../components/channel/StudioSelect";
  27. import { ArticleType } from "../article/Article";
  28. import { IChannel } from "./Channel";
  29. const { Text } = Typography;
  30. export interface IResNumberResponse {
  31. ok: boolean;
  32. message: string;
  33. data: {
  34. my: number;
  35. collaboration: number;
  36. };
  37. }
  38. export const renderBadge = (count: number, active = false) => {
  39. return (
  40. <Badge
  41. count={count}
  42. style={{
  43. marginBlockStart: -2,
  44. marginInlineStart: 4,
  45. color: active ? "#1890FF" : "#999",
  46. backgroundColor: active ? "#E6F7FF" : "#eee",
  47. }}
  48. />
  49. );
  50. };
  51. interface IChannelItem {
  52. id: number;
  53. uid: string;
  54. title: string;
  55. summary: string;
  56. type: TChannelType;
  57. role?: TRole;
  58. studio?: IStudio;
  59. publicity: number;
  60. createdAt: number;
  61. }
  62. interface IWidget {
  63. studioName?: string;
  64. type?: string;
  65. onSelect?: Function;
  66. }
  67. const ChannelTableWidget = ({ studioName, type, onSelect }: IWidget) => {
  68. const intl = useIntl();
  69. const [openCreate, setOpenCreate] = useState(false);
  70. const [activeKey, setActiveKey] = useState<React.Key | undefined>("my");
  71. const [myNumber, setMyNumber] = useState<number>(0);
  72. const [collaborationNumber, setCollaborationNumber] = useState<number>(0);
  73. const [collaborator, setCollaborator] = useState<string>();
  74. useEffect(() => {
  75. /**
  76. * 获取各种课程的数量
  77. */
  78. const url = `/v2/channel-my-number?studio=${studioName}`;
  79. console.log("url", url);
  80. get<IResNumberResponse>(url).then((json) => {
  81. if (json.ok) {
  82. setMyNumber(json.data.my);
  83. setCollaborationNumber(json.data.collaboration);
  84. }
  85. });
  86. }, [studioName]);
  87. const showDeleteConfirm = (id: string, title: string) => {
  88. Modal.confirm({
  89. icon: <ExclamationCircleOutlined />,
  90. title:
  91. intl.formatMessage({
  92. id: "message.delete.sure",
  93. }) +
  94. intl.formatMessage({
  95. id: "message.irrevocable",
  96. }),
  97. content: title,
  98. okText: intl.formatMessage({
  99. id: "buttons.delete",
  100. }),
  101. okType: "danger",
  102. cancelText: intl.formatMessage({
  103. id: "buttons.no",
  104. }),
  105. onOk() {
  106. console.log("delete", id);
  107. return delete_<IDeleteResponse>(`/v2/channel/${id}`)
  108. .then((json) => {
  109. if (json.ok) {
  110. message.success("删除成功");
  111. ref.current?.reload();
  112. } else {
  113. message.error(json.message);
  114. }
  115. })
  116. .catch((e) => console.log("Oops errors!", e));
  117. },
  118. });
  119. };
  120. const ref = useRef<ActionType>();
  121. return (
  122. <>
  123. <ProTable<IChannelItem>
  124. actionRef={ref}
  125. columns={[
  126. {
  127. title: intl.formatMessage({
  128. id: "dict.fields.sn.label",
  129. }),
  130. dataIndex: "id",
  131. key: "id",
  132. width: 50,
  133. search: false,
  134. },
  135. {
  136. title: intl.formatMessage({
  137. id: "forms.fields.title.label",
  138. }),
  139. dataIndex: "title",
  140. key: "title",
  141. tip: "过长会自动收缩",
  142. ellipsis: true,
  143. render: (text, row, index, action) => {
  144. return (
  145. <>
  146. <div key={1}>
  147. <Button
  148. type="link"
  149. key={index}
  150. onClick={() => {
  151. if (typeof onSelect !== "undefined") {
  152. const channel: IChannel = {
  153. name: row.title,
  154. id: row.uid,
  155. type: row.type,
  156. };
  157. onSelect(channel);
  158. }
  159. }}
  160. >
  161. {row.title}
  162. </Button>
  163. </div>
  164. {activeKey !== "my" ? (
  165. <div key={3}>
  166. <Text type="secondary">
  167. <StudioName data={row.studio} />
  168. </Text>
  169. </div>
  170. ) : undefined}
  171. </>
  172. );
  173. },
  174. },
  175. {
  176. title: intl.formatMessage({
  177. id: "forms.fields.summary.label",
  178. }),
  179. dataIndex: "summary",
  180. key: "summary",
  181. tip: "过长会自动收缩",
  182. ellipsis: true,
  183. },
  184. {
  185. title: intl.formatMessage({
  186. id: "forms.fields.role.label",
  187. }),
  188. dataIndex: "role",
  189. key: "role",
  190. width: 100,
  191. search: false,
  192. filters: true,
  193. onFilter: true,
  194. valueEnum: {
  195. all: {
  196. text: intl.formatMessage({
  197. id: "channel.type.all.title",
  198. }),
  199. status: "Default",
  200. },
  201. owner: {
  202. text: intl.formatMessage({
  203. id: "auth.role.owner",
  204. }),
  205. },
  206. manager: {
  207. text: intl.formatMessage({
  208. id: "auth.role.manager",
  209. }),
  210. },
  211. editor: {
  212. text: intl.formatMessage({
  213. id: "auth.role.editor",
  214. }),
  215. },
  216. member: {
  217. text: intl.formatMessage({
  218. id: "auth.role.member",
  219. }),
  220. },
  221. },
  222. },
  223. {
  224. title: intl.formatMessage({
  225. id: "forms.fields.type.label",
  226. }),
  227. dataIndex: "type",
  228. key: "type",
  229. width: 100,
  230. search: false,
  231. filters: true,
  232. onFilter: true,
  233. valueEnum: {
  234. all: {
  235. text: intl.formatMessage({
  236. id: "channel.type.all.title",
  237. }),
  238. status: "Default",
  239. },
  240. translation: {
  241. text: intl.formatMessage({
  242. id: "channel.type.translation.label",
  243. }),
  244. status: "Success",
  245. },
  246. nissaya: {
  247. text: intl.formatMessage({
  248. id: "channel.type.nissaya.label",
  249. }),
  250. status: "Processing",
  251. },
  252. commentary: {
  253. text: intl.formatMessage({
  254. id: "channel.type.commentary.label",
  255. }),
  256. status: "Default",
  257. },
  258. original: {
  259. text: intl.formatMessage({
  260. id: "channel.type.original.label",
  261. }),
  262. status: "Default",
  263. },
  264. general: {
  265. text: intl.formatMessage({
  266. id: "channel.type.general.label",
  267. }),
  268. status: "Default",
  269. },
  270. },
  271. },
  272. {
  273. title: intl.formatMessage({
  274. id: "forms.fields.publicity.label",
  275. }),
  276. dataIndex: "publicity",
  277. key: "publicity",
  278. width: 100,
  279. search: false,
  280. filters: true,
  281. onFilter: true,
  282. valueEnum: PublicityValueEnum(),
  283. },
  284. {
  285. title: intl.formatMessage({
  286. id: "forms.fields.created-at.label",
  287. }),
  288. key: "created-at",
  289. width: 100,
  290. search: false,
  291. dataIndex: "createdAt",
  292. valueType: "date",
  293. sorter: (a, b) => a.createdAt - b.createdAt,
  294. },
  295. {
  296. title: intl.formatMessage({ id: "buttons.option" }),
  297. key: "option",
  298. width: 120,
  299. valueType: "option",
  300. render: (text, row, index, action) => {
  301. return [
  302. <Dropdown.Button
  303. key={index}
  304. type="link"
  305. trigger={["click", "contextMenu"]}
  306. menu={{
  307. items: [
  308. {
  309. key: "share",
  310. label: (
  311. <ShareModal
  312. trigger={intl.formatMessage({
  313. id: "buttons.share",
  314. })}
  315. resId={row.uid}
  316. resType={EResType.channel}
  317. />
  318. ),
  319. icon: <TeamOutlined />,
  320. },
  321. {
  322. key: "remove",
  323. label: intl.formatMessage({
  324. id: "buttons.delete",
  325. }),
  326. icon: <DeleteOutlined />,
  327. danger: true,
  328. },
  329. ],
  330. onClick: (e) => {
  331. switch (e.key) {
  332. case "remove":
  333. showDeleteConfirm(row.uid, row.title);
  334. break;
  335. default:
  336. break;
  337. }
  338. },
  339. }}
  340. >
  341. <Link to={`/studio/${studioName}/channel/${row.uid}/edit`}>
  342. {intl.formatMessage({
  343. id: "buttons.edit",
  344. })}
  345. </Link>
  346. </Dropdown.Button>,
  347. ];
  348. },
  349. },
  350. ]}
  351. /*
  352. rowSelection={{
  353. // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
  354. // 注释该行则默认不显示下拉选项
  355. selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
  356. }}
  357. tableAlertRender={({
  358. selectedRowKeys,
  359. selectedRows,
  360. onCleanSelected,
  361. }) => (
  362. <Space size={24}>
  363. <span>
  364. {intl.formatMessage({ id: "buttons.selected" })}
  365. {selectedRowKeys.length}
  366. <Button
  367. type="link"
  368. style={{ marginInlineStart: 8 }}
  369. onClick={onCleanSelected}
  370. >
  371. {intl.formatMessage({ id: "buttons.unselect" })}
  372. </Button>
  373. </span>
  374. </Space>
  375. )}
  376. tableAlertOptionRender={() => {
  377. return (
  378. <Space size={16}>
  379. <Button type="link">
  380. {intl.formatMessage({
  381. id: "buttons.delete.all",
  382. })}
  383. </Button>
  384. </Space>
  385. );
  386. }}
  387. */
  388. request={async (params = {}, sorter, filter) => {
  389. // TODO 分页
  390. console.log(params, sorter, filter);
  391. let url = `/v2/channel?view=studio&view2=${activeKey}&name=${studioName}`;
  392. url += collaborator ? "&collaborator=" + collaborator : "";
  393. url += params.keyword ? "&search=" + params.keyword : "";
  394. console.log("url", url);
  395. const res: IApiResponseChannelList = await get(url);
  396. const items: IChannelItem[] = res.data.rows.map((item, id) => {
  397. const date = new Date(item.created_at);
  398. return {
  399. id: id + 1,
  400. uid: item.uid,
  401. title: item.name,
  402. summary: item.summary,
  403. type: item.type,
  404. role: item.role,
  405. studio: item.studio,
  406. publicity: item.status,
  407. createdAt: date.getTime(),
  408. };
  409. });
  410. return {
  411. total: res.data.count,
  412. succcess: true,
  413. data: items,
  414. };
  415. }}
  416. rowKey="id"
  417. bordered
  418. pagination={{
  419. showQuickJumper: true,
  420. showSizeChanger: true,
  421. }}
  422. search={false}
  423. options={{
  424. search: true,
  425. }}
  426. toolBarRender={() => [
  427. activeKey !== "my" ? (
  428. <StudioSelect
  429. studioName={studioName}
  430. onSelect={(value: string) => {
  431. setCollaborator(value);
  432. ref.current?.reload();
  433. }}
  434. />
  435. ) : undefined,
  436. <Popover
  437. content={
  438. <ChannelCreate
  439. studio={studioName}
  440. onSuccess={() => {
  441. setOpenCreate(false);
  442. ref.current?.reload();
  443. }}
  444. />
  445. }
  446. placement="bottomRight"
  447. trigger="click"
  448. open={openCreate}
  449. onOpenChange={(open: boolean) => {
  450. setOpenCreate(open);
  451. }}
  452. >
  453. <Button key="button" icon={<PlusOutlined />} type="primary">
  454. {intl.formatMessage({ id: "buttons.create" })}
  455. </Button>
  456. </Popover>,
  457. ]}
  458. toolbar={{
  459. menu: {
  460. activeKey,
  461. items: [
  462. {
  463. key: "my",
  464. label: (
  465. <span>
  466. 此工作室的
  467. {renderBadge(myNumber, activeKey === "my")}
  468. </span>
  469. ),
  470. },
  471. {
  472. key: "collaboration",
  473. label: (
  474. <span>
  475. 协作
  476. {renderBadge(
  477. collaborationNumber,
  478. activeKey === "collaboration"
  479. )}
  480. </span>
  481. ),
  482. },
  483. ],
  484. onChange(key) {
  485. console.log("show course", key);
  486. setActiveKey(key);
  487. setCollaborator(undefined);
  488. ref.current?.reload();
  489. },
  490. },
  491. }}
  492. />
  493. </>
  494. );
  495. };
  496. export default ChannelTableWidget;