UserDictList.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import { useIntl } from "react-intl";
  2. import {
  3. Button,
  4. Space,
  5. Table,
  6. Dropdown,
  7. Drawer,
  8. message,
  9. Modal,
  10. Typography,
  11. Tag,
  12. Popover,
  13. } from "antd";
  14. import {
  15. PlusOutlined,
  16. ExclamationCircleOutlined,
  17. DeleteOutlined,
  18. InfoCircleOutlined,
  19. } from "@ant-design/icons";
  20. import { type ActionType, ProList } from "@ant-design/pro-components";
  21. import DictCreate from "./DictCreate";
  22. import type {
  23. IApiResponseDictList,
  24. IDictInfo,
  25. IUserDictDeleteRequest,
  26. } from "../../api/Dict";
  27. import { delete_2, get } from "../../request";
  28. import { useEffect, useRef, useState } from "react";
  29. import DictEdit from "./DictEdit";
  30. import type { IDeleteResponse } from "../../api/Article";
  31. import TimeShow from "../general/TimeShow";
  32. import { getSorterUrl } from "../../utils";
  33. import MdView from "../template/MdView";
  34. const { Link } = Typography;
  35. export interface IWord {
  36. sn: number;
  37. wordId: string;
  38. word: string;
  39. type?: string | null;
  40. grammar?: string | null;
  41. parent?: string | null;
  42. meaning?: string | null;
  43. note?: string | null;
  44. factors?: string | null;
  45. dict?: IDictInfo;
  46. status?: number;
  47. updated_at?: string;
  48. created_at?: string;
  49. }
  50. interface IParams {
  51. word?: string;
  52. parent?: string;
  53. dict?: string;
  54. }
  55. interface IWidget {
  56. studioName?: string;
  57. view?: "studio" | "all";
  58. dictName?: string;
  59. word?: string;
  60. compact?: boolean;
  61. refresh?: boolean;
  62. onRefresh?: Function;
  63. }
  64. const UserDictListWidget = ({
  65. studioName,
  66. view = "studio",
  67. dictName,
  68. word,
  69. compact = false,
  70. refresh = false,
  71. onRefresh,
  72. }: IWidget) => {
  73. const intl = useIntl();
  74. const [isEditOpen, setIsEditOpen] = useState(false);
  75. const [isCreateOpen, setIsCreateOpen] = useState(false);
  76. const [wordId, setWordId] = useState<string>();
  77. const [drawerTitle, setDrawerTitle] = useState("New Word");
  78. const showDeleteConfirm = (id: string[], title: string) => {
  79. Modal.confirm({
  80. icon: <ExclamationCircleOutlined />,
  81. title:
  82. intl.formatMessage({
  83. id: "message.delete.confirm",
  84. }) +
  85. intl.formatMessage({
  86. id: "message.irrevocable",
  87. }),
  88. content: title,
  89. okText: intl.formatMessage({
  90. id: "buttons.delete",
  91. }),
  92. okType: "danger",
  93. cancelText: intl.formatMessage({
  94. id: "buttons.no",
  95. }),
  96. onOk() {
  97. console.log("delete", id);
  98. return delete_2<IUserDictDeleteRequest, IDeleteResponse>(
  99. `/v2/userdict/${id}`,
  100. {
  101. id: JSON.stringify(id),
  102. }
  103. )
  104. .then((json: unknown) => {
  105. if (json.ok) {
  106. message.success("删除成功");
  107. ref.current?.reload();
  108. } else {
  109. message.error(json.message);
  110. }
  111. })
  112. .catch((e: unknown) => console.log("Oops errors!", e));
  113. },
  114. });
  115. };
  116. useEffect(() => {
  117. if (refresh === true) {
  118. ref.current?.reload();
  119. if (typeof onRefresh !== "undefined") {
  120. onRefresh(false);
  121. }
  122. }
  123. }, [onRefresh, refresh]);
  124. const ref = useRef<ActionType | null>(null);
  125. return (
  126. <>
  127. <ProList<IWord, IParams>
  128. actionRef={ref}
  129. metas={{
  130. title: {
  131. dataIndex: "word",
  132. title: "拼写",
  133. search: word ? false : undefined,
  134. render: (_text, entity, _index, _action) => {
  135. return (
  136. <Space>
  137. <span
  138. onClick={() => {
  139. setWordId(entity.wordId);
  140. setDrawerTitle(entity.word);
  141. setIsEditOpen(true);
  142. }}
  143. >
  144. {entity.word}
  145. </span>
  146. {entity.note ? (
  147. <Popover
  148. placement="bottom"
  149. content={<MdView html={entity.note} />}
  150. >
  151. <InfoCircleOutlined color="blue" />
  152. </Popover>
  153. ) : (
  154. <></>
  155. )}
  156. </Space>
  157. );
  158. },
  159. },
  160. subTitle: {
  161. search: false,
  162. render: (_text, row, _index, _action) => {
  163. return (
  164. <Space>
  165. {row.type ? (
  166. <Tag key="type" color="blue">
  167. {intl.formatMessage({
  168. id: `dict.fields.type.${row.type?.replaceAll(
  169. ".",
  170. ""
  171. )}.label`,
  172. defaultMessage: row.type,
  173. })}
  174. </Tag>
  175. ) : (
  176. <></>
  177. )}
  178. {row.grammar ? (
  179. <Tag key="grammar" color="#5BD8A6">
  180. {row.grammar
  181. ?.replaceAll(".", "")
  182. .split("$")
  183. .map((item) =>
  184. intl.formatMessage({
  185. id: `dict.fields.type.${item}.label`,
  186. defaultMessage: item,
  187. })
  188. )
  189. .join(".")}
  190. </Tag>
  191. ) : (
  192. <></>
  193. )}
  194. </Space>
  195. );
  196. },
  197. },
  198. description: {
  199. dataIndex: "meaning",
  200. title: "description",
  201. search: false,
  202. render(_dom, entity, _index, _action, _schema) {
  203. return (
  204. <div>
  205. <Space>
  206. {entity.meaning}
  207. {"|"}
  208. <TimeShow
  209. updatedAt={entity.updated_at}
  210. createdAt={entity.updated_at}
  211. type="secondary"
  212. />
  213. {"|"}
  214. {entity.status === 5 ? "私有" : "公开"}
  215. </Space>
  216. {compact ? (
  217. <div>
  218. <div>{entity.factors}</div>
  219. </div>
  220. ) : (
  221. <></>
  222. )}
  223. </div>
  224. );
  225. },
  226. },
  227. content: compact
  228. ? undefined
  229. : {
  230. search: false,
  231. render(_dom, entity, _index, _action, _schema) {
  232. return (
  233. <div>
  234. <div>{entity.factors}</div>
  235. </div>
  236. );
  237. },
  238. },
  239. }}
  240. columns={[
  241. {
  242. title: intl.formatMessage({
  243. id: "dict.fields.sn.label",
  244. }),
  245. dataIndex: "sn",
  246. key: "sn",
  247. width: 80,
  248. search: false,
  249. },
  250. {
  251. title: intl.formatMessage({
  252. id: "dict.fields.word.label",
  253. }),
  254. dataIndex: "word",
  255. key: "word",
  256. tooltip: "单词过长会自动收缩",
  257. ellipsis: true,
  258. },
  259. {
  260. title: intl.formatMessage({
  261. id: "dict.fields.type.label",
  262. }),
  263. dataIndex: "type",
  264. key: "type",
  265. search: false,
  266. filters: true,
  267. onFilter: true,
  268. valueEnum: {
  269. all: { text: "全部", status: "Default" },
  270. n: { text: "名词", status: "Default" },
  271. ti: { text: "三性", status: "Processing" },
  272. v: { text: "动词", status: "Success" },
  273. ind: { text: "不变词", status: "Success" },
  274. },
  275. },
  276. {
  277. title: intl.formatMessage({
  278. id: "dict.fields.grammar.label",
  279. }),
  280. dataIndex: "grammar",
  281. key: "grammar",
  282. search: false,
  283. },
  284. {
  285. title: intl.formatMessage({
  286. id: "dict.fields.parent.label",
  287. }),
  288. dataIndex: "parent",
  289. key: "parent",
  290. },
  291. {
  292. title: intl.formatMessage({
  293. id: "dict.fields.meaning.label",
  294. }),
  295. dataIndex: "meaning",
  296. key: "meaning",
  297. tooltip: "意思过长会自动收缩",
  298. ellipsis: true,
  299. search: false,
  300. },
  301. {
  302. title: intl.formatMessage({
  303. id: "dict.fields.note.label",
  304. }),
  305. dataIndex: "note",
  306. key: "note",
  307. search: false,
  308. tooltip: "注释过长会自动收缩",
  309. ellipsis: true,
  310. },
  311. {
  312. title: intl.formatMessage({
  313. id: "dict.fields.factors.label",
  314. }),
  315. dataIndex: "factors",
  316. key: "factors",
  317. search: false,
  318. },
  319. {
  320. title: intl.formatMessage({
  321. id: "forms.fields.dict.shortname.label",
  322. }),
  323. dataIndex: "dict",
  324. key: "dict",
  325. hideInTable: view !== "all",
  326. search: view !== "all" ? false : undefined,
  327. render: (_text, row, _index, _action) => {
  328. return row.dict?.shortname;
  329. },
  330. },
  331. {
  332. title: intl.formatMessage({
  333. id: "forms.fields.updated-at.label",
  334. }),
  335. key: "updated_at",
  336. width: 200,
  337. search: false,
  338. dataIndex: "updated_at",
  339. valueType: "date",
  340. sorter: true,
  341. render: (_text, row, _index, _action) => {
  342. return (
  343. <TimeShow
  344. updatedAt={row.updated_at}
  345. showIcon={false}
  346. showLabel={false}
  347. />
  348. );
  349. },
  350. },
  351. {
  352. title: intl.formatMessage({ id: "buttons.option" }),
  353. key: "option",
  354. hideInTable: view === "all",
  355. width: 120,
  356. valueType: "option",
  357. render: (_text, row, index, _action) => {
  358. return [
  359. <Dropdown.Button
  360. key={index}
  361. type="link"
  362. menu={{
  363. items: [
  364. {
  365. key: "remove",
  366. label: intl.formatMessage({
  367. id: "buttons.delete",
  368. }),
  369. icon: <DeleteOutlined />,
  370. danger: true,
  371. },
  372. ],
  373. onClick: (e) => {
  374. switch (e.key) {
  375. case "share":
  376. break;
  377. case "remove":
  378. showDeleteConfirm([row.wordId], row.word);
  379. break;
  380. default:
  381. break;
  382. }
  383. },
  384. }}
  385. >
  386. <Link
  387. onClick={() => {
  388. setWordId(row.wordId);
  389. setDrawerTitle(row.word);
  390. setIsEditOpen(true);
  391. }}
  392. >
  393. {intl.formatMessage({
  394. id: "buttons.edit",
  395. })}
  396. </Link>
  397. </Dropdown.Button>,
  398. ];
  399. },
  400. },
  401. ]}
  402. rowSelection={
  403. view === "all"
  404. ? undefined
  405. : {
  406. // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
  407. // 注释该行则默认不显示下拉选项
  408. selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
  409. }
  410. }
  411. tableAlertRender={
  412. view === "all"
  413. ? undefined
  414. : ({ selectedRowKeys, ___selectedRows, onCleanSelected }) => (
  415. <Space size={24}>
  416. <span>
  417. {intl.formatMessage({ id: "buttons.selected" })}
  418. {selectedRowKeys.length}
  419. <Button
  420. type="link"
  421. style={{ marginInlineStart: 8 }}
  422. onClick={onCleanSelected}
  423. >
  424. {intl.formatMessage({ id: "buttons.unselect" })}
  425. </Button>
  426. </span>
  427. </Space>
  428. )
  429. }
  430. tableAlertOptionRender={
  431. view === "all"
  432. ? undefined
  433. : ({
  434. ___intl,
  435. selectedRowKeys,
  436. ___selectedRows,
  437. onCleanSelected,
  438. }) => {
  439. return (
  440. <Space size={16}>
  441. <Button
  442. type="link"
  443. onClick={() => {
  444. console.log(selectedRowKeys);
  445. showDeleteConfirm(
  446. selectedRowKeys.map((item) => item.toString()),
  447. selectedRowKeys.length + "个单词"
  448. );
  449. onCleanSelected();
  450. }}
  451. >
  452. 批量删除
  453. </Button>
  454. </Space>
  455. );
  456. }
  457. }
  458. request={async (params = {}, sorter, filter) => {
  459. console.log(params, sorter, filter);
  460. const offset =
  461. ((params.current ? params.current : 1) - 1) *
  462. (params.pageSize ? params.pageSize : 20);
  463. let url = "/v2/userdict?";
  464. switch (view) {
  465. case "studio":
  466. url += `view=studio&name=${studioName}`;
  467. break;
  468. case "all":
  469. url += `view=all`;
  470. break;
  471. default:
  472. break;
  473. }
  474. url += `&limit=${params.pageSize}&offset=${offset}`;
  475. url += params.keyword ? "&search=" + params.keyword : "";
  476. url += params.word
  477. ? `&word=${params.word}`
  478. : word
  479. ? `&word=${word}`
  480. : "";
  481. url += params.parent ? `&parent=${params.parent}` : "";
  482. url += params.dict ? `&dict=${params.dict}` : "";
  483. url += dictName
  484. ? dictName !== "all"
  485. ? `&dict=${dictName}`
  486. : ""
  487. : "";
  488. url += getSorterUrl(sorter);
  489. console.log(url);
  490. const res = await get<IApiResponseDictList>(url);
  491. const items: IWord[] = res.data.rows.map((item, id) => {
  492. const id2 =
  493. ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
  494. return {
  495. sn: id2,
  496. wordId: item.id,
  497. word: item.word,
  498. type: item.type,
  499. grammar: item.grammar,
  500. parent: item.parent,
  501. meaning: item.mean,
  502. note: item.note,
  503. factors: item.factors,
  504. dict: item.dict,
  505. status: item.status,
  506. updated_at: item.updated_at,
  507. };
  508. });
  509. return {
  510. total: res.data.count,
  511. success: true,
  512. data: items,
  513. };
  514. }}
  515. rowKey="wordId"
  516. bordered
  517. pagination={{
  518. showQuickJumper: true,
  519. showSizeChanger: true,
  520. }}
  521. search={
  522. word
  523. ? undefined
  524. : {
  525. filterType: "light",
  526. }
  527. }
  528. options={{
  529. search: word ? false : true,
  530. }}
  531. headerTitle=""
  532. toolBarRender={
  533. view === "all"
  534. ? undefined
  535. : () => [
  536. <Button
  537. key="button"
  538. icon={<PlusOutlined />}
  539. type="primary"
  540. onClick={() => {
  541. setDrawerTitle("New word");
  542. setIsCreateOpen(true);
  543. }}
  544. disabled={true}
  545. >
  546. {intl.formatMessage({ id: "buttons.create" })}
  547. </Button>,
  548. ]
  549. }
  550. />
  551. <Drawer
  552. title={drawerTitle}
  553. placement="right"
  554. open={isCreateOpen}
  555. onClose={() => {
  556. setIsCreateOpen(false);
  557. }}
  558. key="create"
  559. style={{ maxWidth: "100%" }}
  560. contentWrapperStyle={{ overflowY: "auto" }}
  561. footer={null}
  562. >
  563. <DictCreate studio={studioName ? studioName : ""} />
  564. </Drawer>
  565. <Drawer
  566. title={drawerTitle}
  567. width={500}
  568. placement="right"
  569. open={isEditOpen}
  570. onClose={() => {
  571. setIsEditOpen(false);
  572. }}
  573. key="edit"
  574. style={{ maxWidth: "100%" }}
  575. contentWrapperStyle={{ overflowY: "auto" }}
  576. footer={null}
  577. >
  578. <DictEdit wordId={wordId} />
  579. </Drawer>
  580. </>
  581. );
  582. };
  583. export default UserDictListWidget;