AttachmentList.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import { useIntl } from "react-intl";
  2. import {
  3. Button,
  4. Space,
  5. Table,
  6. Dropdown,
  7. message,
  8. Modal,
  9. Typography,
  10. Image,
  11. Segmented,
  12. } from "antd";
  13. import {
  14. PlusOutlined,
  15. ExclamationCircleOutlined,
  16. FileOutlined,
  17. AudioOutlined,
  18. FileImageOutlined,
  19. MoreOutlined,
  20. BarsOutlined,
  21. AppstoreOutlined,
  22. } from "@ant-design/icons";
  23. import { ActionType, ProList } from "@ant-design/pro-components";
  24. import { IUserDictDeleteRequest } from "../api/Dict";
  25. import { delete_2, get, put } from "../../request";
  26. import { useRef, useState } from "react";
  27. import { IDeleteResponse } from "../api/Article";
  28. import TimeShow from "../general/TimeShow";
  29. import { getSorterUrl } from "../../utils";
  30. import {
  31. IAttachmentListResponse,
  32. IAttachmentRequest,
  33. IAttachmentResponse,
  34. IAttachmentUpdate,
  35. } from "../api/Attachments";
  36. import { VideoIcon } from "../../assets/icon";
  37. import AttachmentImport, { deleteRes } from "./AttachmentImport";
  38. import VideoModal from "../general/VideoModal";
  39. import FileSize from "../general/FileSize";
  40. import modal from "antd/lib/modal";
  41. const { Text } = Typography;
  42. export interface IAttachment {
  43. id: string;
  44. name: string;
  45. filename: string;
  46. title: string;
  47. size: number;
  48. content_type: string;
  49. url: string;
  50. }
  51. interface IParams {
  52. content_type?: string;
  53. }
  54. interface IWidget {
  55. studioName?: string;
  56. view?: "studio" | "all";
  57. multiSelect?: boolean;
  58. onClick?: Function;
  59. }
  60. const AttachmentWidget = ({
  61. studioName,
  62. view = "studio",
  63. multiSelect = false,
  64. onClick,
  65. }: IWidget) => {
  66. const intl = useIntl();
  67. const [replaceId, setReplaceId] = useState<string>();
  68. const [importOpen, setImportOpen] = useState(false);
  69. const [imgVisible, setImgVisible] = useState(false);
  70. const [imgPrev, setImgPrev] = useState<string>();
  71. const [list, setList] = useState("list");
  72. const [videoVisible, setVideoVisible] = useState(false);
  73. const [videoUrl, setVideoUrl] = useState<string>();
  74. const showDeleteConfirm = (id: string[], title: string) => {
  75. Modal.confirm({
  76. icon: <ExclamationCircleOutlined />,
  77. title:
  78. intl.formatMessage({
  79. id: "message.delete.confirm",
  80. }) +
  81. intl.formatMessage({
  82. id: "message.irrevocable",
  83. }),
  84. content: title,
  85. okText: intl.formatMessage({
  86. id: "buttons.delete",
  87. }),
  88. okType: "danger",
  89. cancelText: intl.formatMessage({
  90. id: "buttons.no",
  91. }),
  92. onOk() {
  93. console.log("delete", id);
  94. return delete_2<IUserDictDeleteRequest, IDeleteResponse>(
  95. `/v2/userdict/${id}`,
  96. {
  97. id: JSON.stringify(id),
  98. }
  99. )
  100. .then((json) => {
  101. if (json.ok) {
  102. message.success("删除成功");
  103. ref.current?.reload();
  104. } else {
  105. message.error(json.message);
  106. }
  107. })
  108. .catch((e) => console.log("Oops errors!", e));
  109. },
  110. });
  111. };
  112. const ref = useRef<ActionType>();
  113. return (
  114. <>
  115. <ProList<IAttachmentRequest, IParams>
  116. actionRef={ref}
  117. editable={{
  118. onSave: async (key, record, originRow) => {
  119. console.log(key, record, originRow);
  120. const url = `/v2/attachment/${key}`;
  121. const res = await put<IAttachmentUpdate, IAttachmentResponse>(url, {
  122. title: record.title,
  123. });
  124. return res.ok;
  125. },
  126. }}
  127. ghost={list === "list" ? false : true}
  128. onItem={(record: IAttachmentRequest, index: number) => {
  129. return {
  130. onClick: (event) => {
  131. // 点击行
  132. if (typeof onClick !== "undefined") {
  133. onClick(record);
  134. }
  135. },
  136. };
  137. }}
  138. metas={{
  139. title: {
  140. dataIndex: "title",
  141. search: false,
  142. render: (dom, entity, index, action, schema) => {
  143. return (
  144. <Button
  145. type="link"
  146. onClick={() => {
  147. const ct = entity.content_type.split("/");
  148. switch (ct[0]) {
  149. case "image":
  150. setImgPrev(entity.url);
  151. setImgVisible(true);
  152. break;
  153. case "video":
  154. setVideoUrl(entity.url);
  155. setVideoVisible(true);
  156. break;
  157. default:
  158. break;
  159. }
  160. }}
  161. >
  162. {entity.title}
  163. </Button>
  164. );
  165. },
  166. },
  167. description: {
  168. render: (dom, entity, index, action, schema) => {
  169. return (
  170. <Text type="secondary">
  171. <Space>
  172. {entity.content_type}
  173. <FileSize size={entity.size} />
  174. <TimeShow
  175. type="secondary"
  176. createdAt={entity.created_at}
  177. updatedAt={entity.updated_at}
  178. />
  179. </Space>
  180. </Text>
  181. );
  182. },
  183. editable: false,
  184. search: false,
  185. },
  186. content:
  187. list === "list"
  188. ? undefined
  189. : {
  190. editable: false,
  191. search: false,
  192. render: (dom, entity, index, action, schema) => {
  193. const thumbnail = entity.thumbnail
  194. ? entity.thumbnail.middle
  195. : entity.url;
  196. return (
  197. <Image
  198. src={thumbnail}
  199. preview={{
  200. src: entity.url,
  201. }}
  202. />
  203. );
  204. },
  205. },
  206. avatar: {
  207. editable: false,
  208. search: false,
  209. render: (dom, entity, index, action, schema) => {
  210. const ct = entity.content_type.split("/");
  211. let icon = <FileOutlined />;
  212. switch (ct[0]) {
  213. case "video":
  214. icon = <VideoIcon />;
  215. break;
  216. case "audio":
  217. icon = <AudioOutlined />;
  218. break;
  219. case "image":
  220. icon = <FileImageOutlined />;
  221. break;
  222. }
  223. return icon;
  224. },
  225. },
  226. actions: {
  227. render: (text, row, index, action) => {
  228. return [
  229. <Button
  230. type="link"
  231. size="small"
  232. onClick={() => {
  233. action?.startEditable(row.id);
  234. }}
  235. >
  236. 编辑
  237. </Button>,
  238. <Dropdown
  239. menu={{
  240. items: [
  241. { label: "替换", key: "replace" },
  242. { label: "引用模版", key: "tpl" },
  243. { label: "删除", key: "delete", danger: true },
  244. ],
  245. onClick: (e) => {
  246. console.log("click ", e.key);
  247. switch (e.key) {
  248. case "replace":
  249. setReplaceId(row.id);
  250. setImportOpen(true);
  251. break;
  252. case "delete":
  253. modal.confirm({
  254. title: intl.formatMessage({
  255. id: "message.delete.confirm",
  256. }),
  257. icon: <ExclamationCircleOutlined />,
  258. content: intl.formatMessage({
  259. id: "message.irrevocable",
  260. }),
  261. okText: "确认",
  262. cancelText: "取消",
  263. okType: "danger",
  264. onOk: () => {
  265. deleteRes(row.id);
  266. ref.current?.reload();
  267. },
  268. });
  269. break;
  270. default:
  271. break;
  272. }
  273. },
  274. }}
  275. placement="bottomRight"
  276. >
  277. <Button
  278. type="link"
  279. size="small"
  280. icon={<MoreOutlined />}
  281. onClick={(e) => e.preventDefault()}
  282. />
  283. </Dropdown>,
  284. ];
  285. },
  286. },
  287. content_type: {
  288. // 自己扩展的字段,主要用于筛选,不在列表中显示
  289. title: "类型",
  290. valueType: "select",
  291. valueEnum: {
  292. all: { text: "全部", status: "Default" },
  293. image: {
  294. text: "图片",
  295. status: "Error",
  296. },
  297. video: {
  298. text: "视频",
  299. status: "Success",
  300. },
  301. audio: {
  302. text: "音频",
  303. status: "Processing",
  304. },
  305. },
  306. },
  307. }}
  308. rowSelection={
  309. view === "all"
  310. ? undefined
  311. : multiSelect
  312. ? {
  313. // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
  314. // 注释该行则默认不显示下拉选项
  315. selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
  316. }
  317. : undefined
  318. }
  319. tableAlertRender={
  320. view === "all"
  321. ? undefined
  322. : ({ selectedRowKeys, selectedRows, onCleanSelected }) => (
  323. <Space size={24}>
  324. <span>
  325. {intl.formatMessage({ id: "buttons.selected" })}
  326. {selectedRowKeys.length}
  327. <Button
  328. type="link"
  329. style={{ marginInlineStart: 8 }}
  330. onClick={onCleanSelected}
  331. >
  332. {intl.formatMessage({ id: "buttons.unselect" })}
  333. </Button>
  334. </span>
  335. </Space>
  336. )
  337. }
  338. tableAlertOptionRender={
  339. view === "all"
  340. ? undefined
  341. : ({ intl, selectedRowKeys, selectedRows, onCleanSelected }) => {
  342. return (
  343. <Space size={16}>
  344. <Button
  345. type="link"
  346. onClick={() => {
  347. console.log(selectedRowKeys);
  348. showDeleteConfirm(
  349. selectedRowKeys.map((item) => item.toString()),
  350. selectedRowKeys.length + "个单词"
  351. );
  352. onCleanSelected();
  353. }}
  354. >
  355. 批量删除
  356. </Button>
  357. </Space>
  358. );
  359. }
  360. }
  361. request={async (params = {}, sorter, filter) => {
  362. console.log(params, sorter, filter);
  363. const offset =
  364. ((params.current ? params.current : 1) - 1) *
  365. (params.pageSize ? params.pageSize : 20);
  366. let url = "/v2/attachment?";
  367. switch (view) {
  368. case "studio":
  369. url += `view=studio&studio=${studioName}`;
  370. break;
  371. case "all":
  372. url += `view=all`;
  373. break;
  374. default:
  375. break;
  376. }
  377. url += `&limit=${params.pageSize}&offset=${offset}`;
  378. url += params.keyword ? "&search=" + params.keyword : "";
  379. if (params.content_type && params.content_type !== "all") {
  380. url += "&content_type=" + params.content_type;
  381. }
  382. url += getSorterUrl(sorter);
  383. console.log(url);
  384. const res = await get<IAttachmentListResponse>(url);
  385. return {
  386. total: res.data.count,
  387. success: res.ok,
  388. data: res.data.rows,
  389. };
  390. }}
  391. rowKey="id"
  392. bordered
  393. pagination={{
  394. showQuickJumper: true,
  395. showSizeChanger: true,
  396. }}
  397. search={{
  398. filterType: "light",
  399. }}
  400. options={{
  401. search: true,
  402. }}
  403. grid={list === "list" ? undefined : { gutter: 16, column: 3 }}
  404. headerTitle=""
  405. toolBarRender={() => [
  406. <Segmented
  407. options={[
  408. { label: "List", value: "list", icon: <BarsOutlined /> },
  409. {
  410. label: "Thumbnail",
  411. value: "thumbnail",
  412. icon: <AppstoreOutlined />,
  413. },
  414. ]}
  415. onChange={(value) => {
  416. console.log(value); // string
  417. setList(value.toString());
  418. }}
  419. />,
  420. <Button
  421. key="button"
  422. icon={<PlusOutlined />}
  423. type="primary"
  424. onClick={() => {
  425. setReplaceId(undefined);
  426. setImportOpen(true);
  427. }}
  428. disabled={view === "all"}
  429. >
  430. {intl.formatMessage({ id: "buttons.import" })}
  431. </Button>,
  432. ]}
  433. />
  434. <AttachmentImport
  435. replaceId={replaceId}
  436. open={importOpen}
  437. onOpenChange={(open: boolean) => {
  438. setImportOpen(open);
  439. ref.current?.reload();
  440. }}
  441. />
  442. <Image
  443. width={200}
  444. style={{ display: "none" }}
  445. preview={{
  446. visible: imgVisible,
  447. src: imgPrev,
  448. onVisibleChange: (value) => {
  449. setImgVisible(value);
  450. },
  451. }}
  452. />
  453. <VideoModal
  454. src={videoUrl}
  455. open={videoVisible}
  456. onOpenChange={(open: boolean) => setVideoVisible(open)}
  457. />
  458. </>
  459. );
  460. };
  461. export default AttachmentWidget;