ChannelTable.tsx 16 KB

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