ChannelTable.tsx 16 KB

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