ArticleList.tsx 17 KB

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