TermList.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import { type ActionType, ProTable } from "@ant-design/pro-components";
  2. import { useIntl } from "react-intl";
  3. import { Button, Space, Table, Dropdown, Modal, message } from "antd";
  4. import {
  5. ExclamationCircleOutlined,
  6. DeleteOutlined,
  7. ImportOutlined,
  8. PlusOutlined,
  9. } from "@ant-design/icons";
  10. import type { ITermDeleteRequest, ITermListResponse } from "../../api/Term";
  11. import { delete_2, get } from "../../request";
  12. import type { IDeleteResponse } from "../../api/Article";
  13. import { useRef } from "react";
  14. import type { IChannel } from "../channel/Channel";
  15. import TermExport from "./TermExport";
  16. import DataImport from "../admin/relation/DataImport";
  17. import TermModal from "./TermModal";
  18. import { getSorterUrl } from "../../utils";
  19. import { useAppSelector } from "../../hooks";
  20. import { currentUser } from "../../reducers/current-user";
  21. interface IItem {
  22. sn: number;
  23. id: string;
  24. word: string;
  25. tag: string;
  26. channel?: IChannel;
  27. meaning: string;
  28. meaning2: string;
  29. note: string | null;
  30. updated_at: string;
  31. }
  32. interface IWidget {
  33. studioName?: string;
  34. channelId?: string;
  35. }
  36. const TermListWidget = ({ studioName, channelId }: IWidget) => {
  37. const intl = useIntl();
  38. const currUser = useAppSelector(currentUser);
  39. const showDeleteConfirm = (id: string[], title: string) => {
  40. Modal.confirm({
  41. icon: <ExclamationCircleOutlined />,
  42. title:
  43. intl.formatMessage({
  44. id: "message.delete.confirm",
  45. }) +
  46. intl.formatMessage({
  47. id: "message.irrevocable",
  48. }),
  49. content: title,
  50. okText: intl.formatMessage({
  51. id: "buttons.delete",
  52. }),
  53. okType: "danger",
  54. cancelText: intl.formatMessage({
  55. id: "buttons.no",
  56. }),
  57. onOk() {
  58. console.log("delete", id);
  59. return delete_2<ITermDeleteRequest, IDeleteResponse>(
  60. `/v2/terms/${id}`,
  61. {
  62. uuid: true,
  63. id: id,
  64. }
  65. )
  66. .then((json: unknown) => {
  67. if (json.ok) {
  68. message.success("删除成功");
  69. ref.current?.reload();
  70. } else {
  71. message.error(json.message);
  72. }
  73. })
  74. .catch((e: unknown) => console.log("Oops errors!", e));
  75. },
  76. });
  77. };
  78. const ref = useRef<ActionType | null>(null);
  79. return (
  80. <>
  81. <ProTable<IItem>
  82. actionRef={ref}
  83. columns={[
  84. {
  85. title: intl.formatMessage({
  86. id: "term.fields.sn.label",
  87. }),
  88. dataIndex: "sn",
  89. key: "sn",
  90. width: 80,
  91. search: false,
  92. },
  93. {
  94. title: intl.formatMessage({
  95. id: "term.fields.word.label",
  96. }),
  97. dataIndex: "word",
  98. key: "word",
  99. tooltip: "单词过长会自动收缩",
  100. ellipsis: true,
  101. },
  102. {
  103. title: intl.formatMessage({
  104. id: "term.fields.description.label",
  105. }),
  106. dataIndex: "tag",
  107. key: "tag",
  108. search: false,
  109. },
  110. {
  111. title: intl.formatMessage({
  112. id: "term.fields.channel.label",
  113. }),
  114. dataIndex: "channel",
  115. key: "channel",
  116. render(_dom, entity, _index, _action, _schema) {
  117. return entity.channel?.name;
  118. },
  119. },
  120. {
  121. title: intl.formatMessage({
  122. id: "term.fields.meaning.label",
  123. }),
  124. dataIndex: "meaning",
  125. key: "meaning",
  126. },
  127. {
  128. title: intl.formatMessage({
  129. id: "term.fields.meaning2.label",
  130. }),
  131. dataIndex: "meaning2",
  132. key: "meaning2",
  133. tooltip: "意思过长会自动收缩",
  134. ellipsis: true,
  135. },
  136. {
  137. title: intl.formatMessage({
  138. id: "term.fields.note.label",
  139. }),
  140. dataIndex: "note",
  141. key: "note",
  142. search: false,
  143. tooltip: "注释过长会自动收缩",
  144. ellipsis: true,
  145. },
  146. {
  147. title: intl.formatMessage({
  148. id: "forms.fields.updated-at.label",
  149. }),
  150. key: "updated_at",
  151. width: 200,
  152. search: false,
  153. dataIndex: "updated_at",
  154. valueType: "date",
  155. sorter: true,
  156. },
  157. {
  158. title: intl.formatMessage({ id: "buttons.option" }),
  159. key: "option",
  160. width: 120,
  161. valueType: "option",
  162. render: (_text, row, index, _action) => {
  163. return [
  164. <Dropdown.Button
  165. key={index}
  166. type="link"
  167. menu={{
  168. items: [
  169. {
  170. key: "remove",
  171. label: intl.formatMessage({
  172. id: "buttons.delete",
  173. }),
  174. icon: <DeleteOutlined />,
  175. danger: true,
  176. },
  177. ],
  178. onClick: (e) => {
  179. switch (e.key) {
  180. case "remove":
  181. showDeleteConfirm([row.id], row.word);
  182. break;
  183. default:
  184. break;
  185. }
  186. },
  187. }}
  188. >
  189. <TermModal
  190. trigger={intl.formatMessage({
  191. id: "buttons.edit",
  192. })}
  193. id={row.id}
  194. studioName={studioName}
  195. channelId={channelId}
  196. onUpdate={() => ref.current?.reload()}
  197. />
  198. </Dropdown.Button>,
  199. ];
  200. },
  201. },
  202. ]}
  203. rowSelection={{
  204. // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
  205. // 注释该行则默认不显示下拉选项
  206. selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
  207. }}
  208. tableAlertRender={({
  209. selectedRowKeys,
  210. ___selectedRows,
  211. onCleanSelected,
  212. }) => (
  213. <Space size={24}>
  214. <span>
  215. {intl.formatMessage({ id: "buttons.selected" })}
  216. {selectedRowKeys.length}
  217. <Button
  218. type="link"
  219. style={{ marginInlineStart: 8 }}
  220. onClick={onCleanSelected}
  221. >
  222. {intl.formatMessage({ id: "buttons.unselect" })}
  223. </Button>
  224. </span>
  225. </Space>
  226. )}
  227. tableAlertOptionRender={({
  228. ___intl,
  229. selectedRowKeys,
  230. ___selectedRows,
  231. onCleanSelected,
  232. }) => {
  233. return (
  234. <Space size={16}>
  235. <Button
  236. type="link"
  237. onClick={() => {
  238. console.log(selectedRowKeys);
  239. showDeleteConfirm(
  240. selectedRowKeys.map((item) => item.toString()),
  241. selectedRowKeys.length + "个单词"
  242. );
  243. onCleanSelected();
  244. }}
  245. >
  246. 批量删除
  247. </Button>
  248. </Space>
  249. );
  250. }}
  251. request={async (params = {}, sorter, filter) => {
  252. console.log(params, sorter, filter);
  253. const offset =
  254. ((params.current ? params.current : 1) - 1) *
  255. (params.pageSize ? params.pageSize : 20);
  256. let url = `/v2/terms?`;
  257. if (typeof channelId === "string") {
  258. url += `view=channel&id=${channelId}`;
  259. } else {
  260. url += `view=studio&name=${studioName}`;
  261. }
  262. url += `&limit=${params.pageSize}&offset=${offset}`;
  263. if (typeof params.keyword !== "undefined") {
  264. url += "&search=" + (params.keyword ? params.keyword : "");
  265. }
  266. url += getSorterUrl(sorter);
  267. const res = await get<ITermListResponse>(url);
  268. console.log(res);
  269. const items: IItem[] = res.data.rows.map((item, id) => {
  270. return {
  271. sn: id + offset + 1,
  272. id: item.guid,
  273. word: item.word,
  274. tag: item.tag,
  275. channel: item.channel,
  276. meaning: item.meaning,
  277. meaning2: item.other_meaning,
  278. note: item.note,
  279. updated_at: item.updated_at,
  280. };
  281. });
  282. return {
  283. total: res.data.count,
  284. success: true,
  285. data: items,
  286. };
  287. }}
  288. rowKey="id"
  289. //bordered
  290. pagination={{
  291. showQuickJumper: true,
  292. showSizeChanger: true,
  293. }}
  294. toolBarRender={() => [
  295. <DataImport
  296. url="/v2/terms-import"
  297. urlExtra={
  298. channelId
  299. ? `view=channel&id=${channelId}`
  300. : `view=studio&name=${studioName}`
  301. }
  302. trigger={
  303. <Button icon={<ImportOutlined />}>
  304. {intl.formatMessage({ id: "buttons.import" })}
  305. </Button>
  306. }
  307. onSuccess={() => {
  308. ref.current?.reload();
  309. }}
  310. />,
  311. <TermExport channelId={channelId} studioName={studioName} />,
  312. <TermModal
  313. trigger={
  314. <Button
  315. key="button"
  316. icon={<PlusOutlined />}
  317. type="primary"
  318. disabled={currUser?.roles?.includes("basic")}
  319. >
  320. {intl.formatMessage({ id: "buttons.create" })}
  321. </Button>
  322. }
  323. studioName={studioName}
  324. channelId={channelId}
  325. onUpdate={() => ref.current?.reload()}
  326. />,
  327. ]}
  328. search={false}
  329. options={{
  330. search: true,
  331. }}
  332. dateFormatter="string"
  333. />
  334. </>
  335. );
  336. };
  337. export default TermListWidget;