NotificationList.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { type ActionType, ProList } from "@ant-design/pro-components";
  2. import { Avatar, Button, Space, Switch, Tag, Typography } from "antd";
  3. import { ReloadOutlined } from "@ant-design/icons";
  4. import { get, put } from "../../request";
  5. import type {
  6. INotificationListResponse,
  7. INotificationPutResponse,
  8. INotificationRequest,
  9. } from "../../api/notification";
  10. import type { IUser } from "../auth/User";
  11. import TimeShow from "../general/TimeShow";
  12. import { useEffect, useRef, useState } from "react";
  13. import Marked from "../general/Marked";
  14. import type { IChannel } from "../channel/Channel";
  15. const { Text } = Typography;
  16. interface INotification {
  17. id: string;
  18. from: IUser;
  19. to: IUser;
  20. channel: IChannel;
  21. url?: string;
  22. title?: string;
  23. book_title?: string;
  24. content?: string;
  25. content_type: string;
  26. res_type: string;
  27. res_id: string;
  28. status: string;
  29. deleted_at?: string;
  30. created_at: string;
  31. updated_at: string;
  32. }
  33. interface IWidget {
  34. onChange?: Function;
  35. }
  36. const NotificationListWidget = ({ onChange }: IWidget) => {
  37. const ref = useRef<ActionType | null>(null);
  38. const [activeKey, setActiveKey] = useState<React.Key | undefined>("inbox");
  39. const [mute, setMute] = useState(false);
  40. useEffect(() => {
  41. const mute = localStorage.getItem("notification/mute");
  42. if (mute && mute === "true") {
  43. setMute(true);
  44. } else {
  45. setMute(false);
  46. }
  47. }, []);
  48. const putStatus = (id: string, status: string) => {
  49. const url = `/v2/notification/${id}`;
  50. console.info("api request", url);
  51. put<INotificationRequest, INotificationPutResponse>(url, {
  52. status: status,
  53. }).then((json) => {
  54. console.info("api response", json);
  55. if (json.ok) {
  56. ref.current?.reload();
  57. if (typeof onChange !== "undefined") {
  58. onChange(json.data.unread);
  59. }
  60. }
  61. });
  62. };
  63. return (
  64. <ProList<INotification>
  65. rowKey="id"
  66. actionRef={ref}
  67. onItem={(record: INotification, _index: number) => {
  68. return {
  69. onClick: (_event) => {
  70. // 点击行
  71. if (record.status === "unread") {
  72. putStatus(record.id, "read");
  73. }
  74. },
  75. };
  76. }}
  77. toolBarRender={() => {
  78. return [
  79. <>
  80. {"免打扰"}
  81. <Switch
  82. size="small"
  83. checked={mute}
  84. onChange={(checked: boolean) => {
  85. setMute(checked);
  86. if (checked) {
  87. localStorage.setItem("notification/mute", "true");
  88. } else {
  89. localStorage.setItem("notification/mute", "false");
  90. }
  91. }}
  92. />
  93. </>,
  94. <Button
  95. key="4"
  96. type="link"
  97. icon={<ReloadOutlined />}
  98. onClick={() => {
  99. ref.current?.reload();
  100. }}
  101. />,
  102. ];
  103. }}
  104. search={{
  105. filterType: "light",
  106. }}
  107. request={async (params = {}, sorter, filter) => {
  108. console.log(params, sorter, filter);
  109. let queryStatus = activeKey;
  110. if (activeKey === "inbox") {
  111. queryStatus = "read,unread";
  112. }
  113. let url = `/v2/notification?view=to&status=${queryStatus}`;
  114. const offset =
  115. ((params.current ? params.current : 1) - 1) *
  116. (params.pageSize ? params.pageSize : 5);
  117. url += `&limit=${params.pageSize}&offset=${offset}`;
  118. console.info("api request", url);
  119. const res = await get<INotificationListResponse>(url);
  120. console.info("api response", res);
  121. let items: INotification[] = [];
  122. if (res.ok) {
  123. items = res.data.rows.map((item, _id) => {
  124. return {
  125. id: item.id,
  126. from: item.from,
  127. to: item.to,
  128. channel: item.channel,
  129. url: item.url,
  130. title: item.title,
  131. book_title: item.book_title,
  132. content: item.content,
  133. content_type: item.content_type,
  134. res_type: item.res_type,
  135. res_id: item.res_id,
  136. status: item.status,
  137. deleted_at: item.deleted_at,
  138. created_at: item.created_at,
  139. updated_at: item.updated_at,
  140. };
  141. });
  142. if (typeof onChange !== "undefined") {
  143. onChange(res.data.unread);
  144. }
  145. }
  146. console.debug(items);
  147. return {
  148. total: res.data.count,
  149. succcess: true,
  150. data: items,
  151. };
  152. }}
  153. pagination={{
  154. pageSize: 5,
  155. }}
  156. showActions="hover"
  157. metas={{
  158. title: {
  159. dataIndex: "user",
  160. search: false,
  161. render: (_, row) => {
  162. return (
  163. <Text strong={row.status === "unread"}>{row.from.nickName}</Text>
  164. );
  165. },
  166. },
  167. avatar: {
  168. dataIndex: "avatar",
  169. search: false,
  170. render: (_, row) => {
  171. return (
  172. <Avatar size={"small"}>{row.from.nickName.slice(0, 1)}</Avatar>
  173. );
  174. },
  175. },
  176. description: {
  177. dataIndex: "title",
  178. search: false,
  179. render: (_, row) => {
  180. return (
  181. <Text
  182. style={{
  183. cursor: "pointer",
  184. opacity: row.status === "unread" ? 1 : 0.7,
  185. }}
  186. onClick={() => {
  187. window.open(row.url, "_blank");
  188. }}
  189. >
  190. <div>
  191. <Text type="secondary">{row.book_title}</Text>
  192. </div>
  193. <Text style={{ fontWeight: 700 }}>{row.title}</Text>
  194. <Marked style={{}} text={row.content} />
  195. </Text>
  196. );
  197. },
  198. },
  199. subTitle: {
  200. dataIndex: "labels",
  201. render: (_, row) => {
  202. return (
  203. <Space>
  204. <TimeShow createdAt={row.created_at} />
  205. <Tag color="#87d068">{row.channel?.name}</Tag>
  206. <Tag color="blue">{row.res_type}</Tag>
  207. </Space>
  208. );
  209. },
  210. search: false,
  211. },
  212. status: {
  213. // 自己扩展的字段,主要用于筛选,不在列表中显示
  214. title: "类型筛选",
  215. valueType: "select",
  216. valueEnum: {
  217. all: { text: "全部", status: "Default" },
  218. pr: {
  219. text: "修改建议",
  220. status: "Error",
  221. },
  222. discussion: {
  223. text: "讨论",
  224. status: "Success",
  225. },
  226. },
  227. },
  228. }}
  229. toolbar={{
  230. menu: {
  231. activeKey,
  232. items: [
  233. {
  234. key: "inbox",
  235. label: "Inbox",
  236. },
  237. {
  238. key: "unread",
  239. label: "Unread",
  240. },
  241. {
  242. key: "archived",
  243. label: "Archived",
  244. },
  245. ],
  246. onChange(key) {
  247. setActiveKey(key);
  248. ref.current?.reload();
  249. if (ref.current?.setPageInfo) {
  250. ref.current?.setPageInfo({ current: 1 });
  251. }
  252. },
  253. },
  254. }}
  255. />
  256. );
  257. };
  258. export default NotificationListWidget;