ArticleList.tsx 17 KB

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