ChannelTable.tsx 16 KB

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