| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- import { useEffect, useState } from "react";
- import { useIntl } from "react-intl";
- import type { Key } from "antd/es/table/interface";
- import {
- Badge,
- Button,
- Card,
- Dropdown,
- Input,
- Select,
- Skeleton,
- Space,
- Tag,
- Tooltip,
- Tree,
- } from "antd";
- import {
- GlobalOutlined,
- EditOutlined,
- ReloadOutlined,
- MoreOutlined,
- CopyOutlined,
- InfoCircleOutlined,
- } from "@ant-design/icons";
- import { get, post } from "../../request";
- import type {
- IApiResponseChannelList,
- ISentInChapterListResponse,
- } from "../../api/Channel";
- import type { IItem, IProgressRequest } from "./ChannelPickerTable";
- import { LockFillIcon, LockIcon } from "../../assets/icon";
- import StudioName from "../auth/Studio";
- import ProgressSvg from "./ProgressSvg";
- import type { IChannel } from "./Channel";
- import CopyToModal from "./CopyToModal";
- import type { ArticleType } from "../article/Article";
- import { ChannelInfoModal } from "./ChannelInfo";
- import TokenModal from "../article/TokenModal";
- import NissayaAlignerModal from "../corpus/NissayaAlignerModal";
- const { Search } = Input;
- export const getSentIdInArticle = () => {
- const sentList: string[] = [];
- const sentElement = document.querySelectorAll(".pcd_sent");
- for (let index = 0; index < sentElement.length; index++) {
- const element = sentElement[index];
- const id = element.id.split("_")[1];
- sentList.push(id);
- }
- return sentList;
- };
- interface IToken {
- channelId?: string;
- articleId?: string;
- type?: ArticleType;
- }
- interface ChannelTreeNode {
- key: string;
- title: string | React.ReactNode;
- channel: IItem;
- icon?: React.ReactNode;
- children?: ChannelTreeNode[];
- }
- interface IWidget {
- type?: ArticleType | "editable";
- articleId?: string;
- selectedKeys?: string[];
- style?: React.CSSProperties;
- onSelect?: Function;
- }
- const ChannelMy = ({
- type,
- articleId,
- selectedKeys = [],
- style,
- onSelect,
- }: IWidget) => {
- const intl = useIntl();
- const [selectedRowKeys, setSelectedRowKeys] =
- useState<React.Key[]>(selectedKeys);
- const [treeData, setTreeData] = useState<ChannelTreeNode[]>();
- const [dirty, setDirty] = useState(false);
- const [channels, setChannels] = useState<IItem[]>([]);
- const [owner, setOwner] = useState("all");
- const [search, setSearch] = useState<string>();
- const [loading, setLoading] = useState(true);
- const [copyChannel, setCopyChannel] = useState<IChannel>();
- const [nissayaOpen, setNissayaOpen] = useState(false);
- const [copyOpen, setCopyOpen] = useState<boolean>(false);
- const [infoOpen, setInfoOpen] = useState<boolean>(false);
- const [statistic, setStatistic] = useState<IItem>();
- const [sentenceCount, setSentenceCount] = useState<number>(0);
- const [sentencesId, setSentencesId] = useState<string[]>();
- const [token, SetToken] = useState<IToken>();
- const [tokenOpen, setTokenOpen] = useState(false);
- console.debug("ChannelMy render", type, articleId);
- //TODO remove useEffect
- useEffect(() => {
- load();
- }, [type, articleId]);
- useEffect(() => {
- if (selectedRowKeys.join() !== selectedKeys.join()) {
- setSelectedRowKeys(selectedKeys);
- }
- }, [selectedKeys]);
- useEffect(() => {
- sortChannels(channels);
- }, [channels, selectedRowKeys, owner]);
- interface IChannelFilter {
- key?: string;
- owner?: string;
- selectedRowKeys?: React.Key[];
- }
- const sortChannels = (channelList: IItem[], filter?: IChannelFilter) => {
- const mOwner = filter?.owner ?? owner;
- if (mOwner === "my") {
- //我自己的
- const myChannel = channelList.filter((value) => value.role === "owner");
- const data = myChannel.map((item, _index) => {
- return { key: item.uid, title: item.title, channel: item };
- });
- setTreeData(data);
- } else {
- //当前被选择的
- const selectedChannel: IItem[] = [];
- const mSelectedRowKeys = filter?.selectedRowKeys ?? selectedRowKeys;
- mSelectedRowKeys.forEach((channelId) => {
- const channel = channelList.find((value) => value.uid === channelId);
- if (channel) {
- selectedChannel.push(channel);
- }
- });
- let show = mSelectedRowKeys;
- //有进度的
- const progressing = channelList.filter(
- (value) => value.progress > 0 && !show.includes(value.uid)
- );
- show = [...show, ...progressing.map((item) => item.uid)];
- //我自己的
- const myChannel = channelList.filter(
- (value) => value.role === "owner" && !show.includes(value.uid)
- );
- show = [...show, ...myChannel.map((item) => item.uid)];
- //其他的
- const others = channelList.filter(
- (value) => !show.includes(value.uid) && value.role !== "member"
- );
- let channelData = [
- ...selectedChannel,
- ...progressing,
- ...myChannel,
- ...others,
- ];
- const key = filter?.key ?? search;
- if (key) {
- channelData = channelData.filter((value) => value.title.includes(key));
- }
- const data = channelData.map((item, _index) => {
- return { key: item.uid, title: item.title, channel: item };
- });
- setTreeData(data);
- }
- };
- const load = () => {
- let sentList: string[] = [];
- if (type === "chapter") {
- const id = articleId?.split("-");
- if (id?.length === 2) {
- const url = `/v2/sentences-in-chapter?book=${id[0]}¶=${id[1]}`;
- console.info("ChannelMy url api request", url);
- get<ISentInChapterListResponse>(url)
- .then((res) => {
- console.debug(
- "ChannelMy ISentInChapterListResponse api response",
- res
- );
- if (res && res.ok) {
- sentList = res.data.rows.map((item) => {
- return `${item.book}-${item.paragraph}-${item.word_begin}-${item.word_end}`;
- });
- setSentencesId(sentList);
- loadChannel(sentList);
- } else {
- console.error("res", res);
- }
- })
- .catch((reason: any) => {
- console.error(reason);
- });
- }
- } else {
- sentList = getSentIdInArticle();
- setSentencesId(sentList);
- loadChannel(sentList);
- }
- };
- function loadChannel(sentences: string[]) {
- setSentenceCount(sentences.length);
- console.debug("sentences", sentences);
- const currOwner = "all";
- const url = `/v2/channel-progress`;
- console.info("api request", url);
- setLoading(true);
- post<IProgressRequest, IApiResponseChannelList>(url, {
- sentence: sentences,
- owner: currOwner,
- })
- .then((res) => {
- console.debug("progress data api response", res);
- const items: IItem[] = res.data.rows
- .filter((value) => value.name.substring(0, 4) !== "_sys")
- .map((item, id) => {
- const date = new Date(item.created_at);
- let all: number = 0;
- let finished: number = 0;
- item.final?.forEach((value) => {
- all += value[0];
- finished += value[1] ? value[0] : 0;
- });
- const progress = finished / all;
- return {
- id: id,
- uid: item.uid,
- title: item.name,
- summary: item.summary,
- studio: item.studio,
- shareType: "my",
- role: item.role,
- type: item.type,
- publicity: item.status,
- createdAt: date.getTime(),
- final: item.final,
- progress: progress,
- content_created_at: item.content_created_at,
- content_updated_at: item.content_updated_at,
- };
- });
- setChannels(items);
- })
- .finally(() => {
- setLoading(false);
- });
- }
- return (
- <div style={style}>
- <TokenModal
- {...token}
- open={tokenOpen}
- onClose={() => setTokenOpen(false)}
- />
- <Card
- size="small"
- title={
- <Space>
- <Search
- placeholder="版本名称"
- onSearch={(value) => {
- console.debug(value);
- setSearch(value);
- sortChannels(channels, { key: value });
- }}
- style={{ width: 120 }}
- />
- <Select
- defaultValue="all"
- style={{ width: 80 }}
- bordered={false}
- options={[
- {
- value: "all",
- label: intl.formatMessage({ id: "buttons.channel.all" }),
- },
- {
- value: "my",
- label: intl.formatMessage({ id: "buttons.channel.my" }),
- },
- ]}
- onSelect={(value: string) => {
- setOwner(value);
- }}
- />
- </Space>
- }
- extra={
- <Space size={"small"}>
- <Button
- size="small"
- type="link"
- disabled={!dirty}
- onClick={() => {
- if (typeof onSelect !== "undefined") {
- setDirty(false);
- onSelect(
- selectedRowKeys.map((item) => {
- return {
- id: item,
- name: treeData?.find(
- (value) => value.channel.uid === item
- )?.channel.title,
- };
- })
- );
- }
- }}
- >
- {intl.formatMessage({
- id: "buttons.ok",
- })}
- </Button>
- <Button
- size="small"
- type="link"
- disabled={!dirty}
- onClick={() => {
- setSelectedRowKeys(selectedKeys);
- setDirty(false);
- }}
- >
- {intl.formatMessage({
- id: "buttons.cancel",
- })}
- </Button>
- <Button
- type="link"
- size="small"
- icon={<ReloadOutlined />}
- onClick={() => {
- load();
- }}
- />
- </Space>
- }
- >
- {loading ? (
- <Skeleton active />
- ) : (
- <Tree
- selectedKeys={selectedRowKeys}
- multiple
- checkedKeys={selectedRowKeys}
- checkable
- treeData={treeData}
- blockNode
- onCheck={(
- checked: Key[] | { checked: Key[]; halfChecked: Key[] }
- ) => {
- setDirty(true);
- if (Array.isArray(checked)) {
- if (checked.length > selectedRowKeys.length) {
- const add = checked.filter(
- (value) => !selectedRowKeys.includes(value.toString())
- );
- if (add.length > 0) {
- setSelectedRowKeys([...selectedRowKeys, add[0]]);
- }
- } else {
- setSelectedRowKeys(
- selectedRowKeys.filter((value) => checked.includes(value))
- );
- }
- }
- }}
- onSelect={(_keys: Key[]) => {}}
- titleRender={(node: ChannelTreeNode) => {
- let pIcon = <></>;
- switch (node.channel.publicity) {
- case 5:
- pIcon = (
- <Tooltip title={"私有不可公开"}>
- <LockFillIcon />
- </Tooltip>
- );
- break;
- case 10:
- pIcon = (
- <Tooltip title={"私有"}>
- <LockIcon />
- </Tooltip>
- );
- break;
- case 30:
- pIcon = (
- <Tooltip title={"公开"}>
- <GlobalOutlined />
- </Tooltip>
- );
- break;
- }
- const badge = selectedRowKeys.findIndex(
- (value) => value === node.channel.uid
- );
- return (
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- width: "100%",
- }}
- >
- <div
- style={{
- width: "100%",
- borderRadius: 5,
- padding: "0 5px",
- }}
- onClick={(
- _e: React.MouseEvent<HTMLSpanElement, MouseEvent>
- ) => {
- console.log(node);
- if (channels) {
- sortChannels(channels);
- }
- setDirty(false);
- if (typeof onSelect !== "undefined") {
- onSelect([
- {
- id: node.key,
- name: node.title,
- },
- ]);
- }
- }}
- >
- <div
- key="info"
- style={{ overflowX: "clip", display: "flex" }}
- >
- <Space>
- {pIcon}
- {node.channel.role !== "member" ? (
- <EditOutlined />
- ) : undefined}
- </Space>
- <Button type="link">
- <Space>
- <StudioName data={node.channel.studio} hideName />
- <>{node.channel.title}</>
- <Tag>
- {intl.formatMessage({
- id: `channel.type.${node.channel.type}.label`,
- })}
- </Tag>
- </Space>
- </Button>
- </div>
- <div key="progress">
- <ProgressSvg data={node.channel.final} width={200} />
- </div>
- </div>
- <Badge count={dirty ? badge + 1 : 0}>
- <div>
- <Dropdown
- trigger={["click"]}
- menu={{
- items: [
- {
- key: "copy-to",
- label: intl.formatMessage({
- id: "buttons.copy.to",
- }),
- icon: <CopyOutlined />,
- },
- {
- key: "import-nissaya",
- label: intl.formatMessage({
- id: "buttons.import",
- }),
- icon: <CopyOutlined />,
- },
- {
- key: "statistic",
- label: intl.formatMessage({
- id: "buttons.statistic",
- }),
- icon: <InfoCircleOutlined />,
- },
- {
- key: "token",
- label: intl.formatMessage({
- id: "buttons.access-token.get",
- }),
- icon: <InfoCircleOutlined />,
- },
- ],
- onClick: (e) => {
- switch (e.key) {
- case "copy-to":
- setCopyChannel({
- id: node.channel.uid,
- name: node.channel.title,
- type: node.channel.type,
- });
- setCopyOpen(true);
- break;
- case "import-nissaya":
- setCopyChannel({
- id: node.channel.uid,
- name: node.channel.title,
- type: node.channel.type,
- });
- setNissayaOpen(true);
- break;
- case "statistic":
- setInfoOpen(true);
- setStatistic(node.channel);
- break;
- case "token":
- SetToken({
- channelId: node.channel.uid,
- type: type as ArticleType,
- articleId: articleId,
- });
- setTokenOpen(true);
- break;
- default:
- break;
- }
- },
- }}
- placement="bottomRight"
- >
- <Button
- type="link"
- size="small"
- icon={<MoreOutlined />}
- ></Button>
- </Dropdown>
- </div>
- </Badge>
- </div>
- );
- }}
- />
- )}
- </Card>
- <CopyToModal
- sentencesId={sentencesId}
- channel={copyChannel}
- open={copyOpen}
- onClose={() => setCopyOpen(false)}
- />
- <NissayaAlignerModal
- sentencesId={sentencesId}
- channel={copyChannel}
- open={nissayaOpen}
- onClose={() => setNissayaOpen(false)}
- />
- <ChannelInfoModal
- sentenceCount={sentenceCount}
- channel={statistic}
- open={infoOpen}
- onClose={() => setInfoOpen(false)}
- />
- </div>
- );
- };
- export default ChannelMy;
|