ChannelMy.tsx 16 KB

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