ChannelMy.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { useEffect, useState } from "react";
  2. import { useIntl } from "react-intl";
  3. import { Key } from "antd/es/table/interface";
  4. import {
  5. Badge,
  6. Button,
  7. Card,
  8. Dropdown,
  9. Select,
  10. Skeleton,
  11. Space,
  12. Tree,
  13. } from "antd";
  14. import {
  15. GlobalOutlined,
  16. EditOutlined,
  17. ReloadOutlined,
  18. MoreOutlined,
  19. CopyOutlined,
  20. InfoCircleOutlined,
  21. } from "@ant-design/icons";
  22. import { get, post } from "../../request";
  23. import {
  24. IApiResponseChannelList,
  25. ISentInChapterListResponse,
  26. } from "../api/Channel";
  27. import { IItem, IProgressRequest } from "./ChannelPickerTable";
  28. import { LockIcon } from "../../assets/icon";
  29. import StudioName from "../auth/Studio";
  30. import ProgressSvg from "./ProgressSvg";
  31. import { IChannel } from "./Channel";
  32. import CopyToModal from "./CopyToModal";
  33. import { ArticleType } from "../article/Article";
  34. import { ChannelInfoModal } from "./ChannelInfo";
  35. interface ChannelTreeNode {
  36. key: string;
  37. title: string | React.ReactNode;
  38. channel: IItem;
  39. icon?: React.ReactNode;
  40. children?: ChannelTreeNode[];
  41. }
  42. interface IWidget {
  43. type?: ArticleType | "editable";
  44. articleId?: string;
  45. selectedKeys?: string[];
  46. style?: React.CSSProperties;
  47. onSelect?: Function;
  48. }
  49. const ChannelMy = ({
  50. type,
  51. articleId,
  52. selectedKeys = [],
  53. style,
  54. onSelect,
  55. }: IWidget) => {
  56. const intl = useIntl();
  57. const [selectedRowKeys, setSelectedRowKeys] =
  58. useState<React.Key[]>(selectedKeys);
  59. const [treeData, setTreeData] = useState<ChannelTreeNode[]>();
  60. const [dirty, setDirty] = useState(false);
  61. const [channels, setChannels] = useState<IItem[]>([]);
  62. const [owner, setOwner] = useState("all");
  63. const [loading, setLoading] = useState(true);
  64. const [copyChannel, setCopyChannel] = useState<IChannel>();
  65. const [copyOpen, setCopyOpen] = useState<boolean>(false);
  66. const [infoOpen, setInfoOpen] = useState<boolean>(false);
  67. const [statistic, setStatistic] = useState<IItem>();
  68. const [sentenceCount, setSentenceCount] = useState<number>(0);
  69. const [sentencesId, setSentencesId] = useState<string[]>();
  70. console.debug("ChannelMy render", type, articleId);
  71. useEffect(() => {
  72. load();
  73. }, [type, articleId]);
  74. useEffect(() => {
  75. if (selectedRowKeys.join() !== selectedKeys.join()) {
  76. setSelectedRowKeys(selectedKeys);
  77. }
  78. }, [selectedKeys]);
  79. useEffect(() => {
  80. sortChannels(channels);
  81. }, [channels, selectedRowKeys, owner]);
  82. const sortChannels = (channelList: IItem[]) => {
  83. if (owner === "my") {
  84. //我自己的
  85. const myChannel = channelList.filter((value) => value.role === "owner");
  86. const data = myChannel.map((item, index) => {
  87. return { key: item.uid, title: item.title, channel: item };
  88. });
  89. setTreeData(data);
  90. } else {
  91. //当前被选择的
  92. let selectedChannel: IItem[] = [];
  93. selectedRowKeys.forEach((channelId) => {
  94. const channel = channelList.find((value) => value.uid === channelId);
  95. if (channel) {
  96. selectedChannel.push(channel);
  97. }
  98. });
  99. let show = selectedRowKeys;
  100. //有进度的
  101. const progressing = channelList.filter(
  102. (value) => value.progress > 0 && !show.includes(value.uid)
  103. );
  104. show = [...show, ...progressing.map((item) => item.uid)];
  105. //我自己的
  106. const myChannel = channelList.filter(
  107. (value) => value.role === "owner" && !show.includes(value.uid)
  108. );
  109. show = [...show, ...myChannel.map((item) => item.uid)];
  110. //其他的
  111. const others = channelList.filter(
  112. (value) => !show.includes(value.uid) && value.role !== "member"
  113. );
  114. const channelData = [
  115. ...selectedChannel,
  116. ...progressing,
  117. ...myChannel,
  118. ...others,
  119. ];
  120. const data = channelData.map((item, index) => {
  121. return { key: item.uid, title: item.title, channel: item };
  122. });
  123. setTreeData(data);
  124. }
  125. };
  126. const load = () => {
  127. let sentList: string[] = [];
  128. if (type === "chapter") {
  129. const id = articleId?.split("-");
  130. if (id?.length === 2) {
  131. const url = `/v2/sentences-in-chapter?book=${id[0]}&para=${id[1]}`;
  132. console.info("ChannelMy url", url);
  133. get<ISentInChapterListResponse>(url)
  134. .then((res) => {
  135. console.debug("ChannelMy ISentInChapterListResponse", res);
  136. if (res && res.ok) {
  137. sentList = res.data.rows.map((item) => {
  138. return `${item.book}-${item.paragraph}-${item.word_begin}-${item.word_end}`;
  139. });
  140. setSentencesId(sentList);
  141. loadChannel(sentList);
  142. } else {
  143. console.error("res", res);
  144. }
  145. })
  146. .catch((reason: any) => {
  147. console.error(reason);
  148. });
  149. }
  150. } else {
  151. const sentElement = document.querySelectorAll(".pcd_sent");
  152. for (let index = 0; index < sentElement.length; index++) {
  153. const element = sentElement[index];
  154. const id = element.id.split("_")[1];
  155. sentList.push(id);
  156. }
  157. setSentencesId(sentList);
  158. loadChannel(sentList);
  159. }
  160. };
  161. function loadChannel(sentences: string[]) {
  162. setSentenceCount(sentences.length);
  163. console.debug("sentences", sentences);
  164. const currOwner = "all";
  165. console.log("owner", currOwner);
  166. setLoading(true);
  167. post<IProgressRequest, IApiResponseChannelList>(`/v2/channel-progress`, {
  168. sentence: sentences,
  169. owner: currOwner,
  170. })
  171. .then((res) => {
  172. console.debug("progress data", res.data.rows);
  173. const items: IItem[] = res.data.rows
  174. .filter((value) => value.name.substring(0, 4) !== "_Sys")
  175. .map((item, id) => {
  176. const date = new Date(item.created_at);
  177. let all: number = 0;
  178. let finished: number = 0;
  179. item.final?.forEach((value) => {
  180. all += value[0];
  181. finished += value[1] ? value[0] : 0;
  182. });
  183. const progress = finished / all;
  184. return {
  185. id: id,
  186. uid: item.uid,
  187. title: item.name,
  188. summary: item.summary,
  189. studio: item.studio,
  190. shareType: "my",
  191. role: item.role,
  192. type: item.type,
  193. publicity: item.status,
  194. createdAt: date.getTime(),
  195. final: item.final,
  196. progress: progress,
  197. content_created_at: item.content_created_at,
  198. content_updated_at: item.content_updated_at,
  199. };
  200. });
  201. setChannels(items);
  202. })
  203. .finally(() => {
  204. setLoading(false);
  205. });
  206. }
  207. return (
  208. <div style={style}>
  209. <Card
  210. size="small"
  211. title={
  212. <Select
  213. defaultValue="all"
  214. style={{ width: 120 }}
  215. bordered={false}
  216. options={[
  217. {
  218. value: "all",
  219. label: intl.formatMessage({ id: "buttons.channel.all" }),
  220. },
  221. {
  222. value: "my",
  223. label: intl.formatMessage({ id: "buttons.channel.my" }),
  224. },
  225. ]}
  226. onSelect={(value: string) => {
  227. setOwner(value);
  228. }}
  229. />
  230. }
  231. extra={
  232. <Space size={"small"}>
  233. <Button
  234. size="small"
  235. type="link"
  236. disabled={!dirty}
  237. onClick={() => {
  238. if (typeof onSelect !== "undefined") {
  239. setDirty(false);
  240. onSelect(
  241. selectedRowKeys.map((item) => {
  242. return {
  243. id: item,
  244. name: treeData?.find(
  245. (value) => value.channel.uid === item
  246. )?.channel.title,
  247. };
  248. })
  249. );
  250. }
  251. }}
  252. >
  253. {intl.formatMessage({
  254. id: "buttons.ok",
  255. })}
  256. </Button>
  257. <Button
  258. size="small"
  259. type="link"
  260. disabled={!dirty}
  261. onClick={() => {
  262. setSelectedRowKeys(selectedKeys);
  263. setDirty(false);
  264. }}
  265. >
  266. {intl.formatMessage({
  267. id: "buttons.cancel",
  268. })}
  269. </Button>
  270. <Button
  271. type="link"
  272. size="small"
  273. icon={<ReloadOutlined />}
  274. onClick={() => {
  275. load();
  276. }}
  277. />
  278. </Space>
  279. }
  280. >
  281. {loading ? (
  282. <Skeleton active />
  283. ) : (
  284. <Tree
  285. selectedKeys={selectedRowKeys}
  286. multiple
  287. checkedKeys={selectedRowKeys}
  288. checkable
  289. treeData={treeData}
  290. blockNode
  291. onCheck={(
  292. checked: Key[] | { checked: Key[]; halfChecked: Key[] }
  293. ) => {
  294. setDirty(true);
  295. if (Array.isArray(checked)) {
  296. if (checked.length > selectedRowKeys.length) {
  297. const add = checked.filter(
  298. (value) => !selectedRowKeys.includes(value.toString())
  299. );
  300. if (add.length > 0) {
  301. setSelectedRowKeys([...selectedRowKeys, add[0]]);
  302. }
  303. } else {
  304. setSelectedRowKeys(
  305. selectedRowKeys.filter((value) => checked.includes(value))
  306. );
  307. }
  308. }
  309. }}
  310. onSelect={(keys: Key[]) => {}}
  311. titleRender={(node: ChannelTreeNode) => {
  312. let pIcon = <></>;
  313. switch (node.channel.publicity) {
  314. case 10:
  315. pIcon = <LockIcon />;
  316. break;
  317. case 30:
  318. pIcon = <GlobalOutlined />;
  319. break;
  320. }
  321. const badge = selectedRowKeys.findIndex(
  322. (value) => value === node.channel.uid
  323. );
  324. return (
  325. <div
  326. style={{
  327. display: "flex",
  328. justifyContent: "space-between",
  329. width: "100%",
  330. }}
  331. >
  332. <div
  333. style={{
  334. width: "100%",
  335. borderRadius: 5,
  336. padding: "0 5px",
  337. }}
  338. onClick={(
  339. e: React.MouseEvent<HTMLSpanElement, MouseEvent>
  340. ) => {
  341. console.log(node);
  342. if (channels) {
  343. sortChannels(channels);
  344. }
  345. setDirty(false);
  346. if (typeof onSelect !== "undefined") {
  347. onSelect([
  348. {
  349. id: node.key,
  350. name: node.title,
  351. },
  352. ]);
  353. }
  354. }}
  355. >
  356. <div
  357. key="info"
  358. style={{ overflowX: "clip", display: "flex" }}
  359. >
  360. <Space>
  361. {pIcon}
  362. {node.channel.role !== "member" ? (
  363. <EditOutlined />
  364. ) : undefined}
  365. </Space>
  366. <Button type="link">
  367. <Space>
  368. <StudioName data={node.channel.studio} hideName />
  369. {node.channel.title}
  370. </Space>
  371. </Button>
  372. </div>
  373. <div key="progress">
  374. <ProgressSvg data={node.channel.final} width={200} />
  375. </div>
  376. </div>
  377. <Badge count={dirty ? badge + 1 : 0}>
  378. <div>
  379. <Dropdown
  380. trigger={["click"]}
  381. menu={{
  382. items: [
  383. {
  384. key: "copy-to",
  385. label: intl.formatMessage({
  386. id: "buttons.copy.to",
  387. }),
  388. icon: <CopyOutlined />,
  389. },
  390. {
  391. key: "statistic",
  392. label: intl.formatMessage({
  393. id: "buttons.statistic",
  394. }),
  395. icon: <InfoCircleOutlined />,
  396. },
  397. ],
  398. onClick: (e) => {
  399. switch (e.key) {
  400. case "copy-to":
  401. setCopyChannel({
  402. id: node.channel.uid,
  403. name: node.channel.title,
  404. type: node.channel.type,
  405. });
  406. setCopyOpen(true);
  407. break;
  408. case "statistic":
  409. setInfoOpen(true);
  410. setStatistic(node.channel);
  411. break;
  412. default:
  413. break;
  414. }
  415. },
  416. }}
  417. placement="bottomRight"
  418. >
  419. <Button
  420. type="link"
  421. size="small"
  422. icon={<MoreOutlined />}
  423. ></Button>
  424. </Dropdown>
  425. </div>
  426. </Badge>
  427. </div>
  428. );
  429. }}
  430. />
  431. )}
  432. </Card>
  433. <CopyToModal
  434. sentencesId={sentencesId}
  435. channel={copyChannel}
  436. open={copyOpen}
  437. onClose={() => setCopyOpen(false)}
  438. />
  439. <ChannelInfoModal
  440. sentenceCount={sentenceCount}
  441. channel={statistic}
  442. open={infoOpen}
  443. onClose={() => setInfoOpen(false)}
  444. />
  445. </div>
  446. );
  447. };
  448. export default ChannelMy;