TermCommunity.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import {
  2. Badge,
  3. Card,
  4. Dropdown,
  5. type MenuProps,
  6. Popover,
  7. Space,
  8. Typography,
  9. } from "antd";
  10. import { DownOutlined } from "@ant-design/icons";
  11. import { useState, useEffect } from "react";
  12. import { useIntl } from "react-intl";
  13. import { get } from "../../request";
  14. import type { IUser } from "../auth/User";
  15. import type { ITermListResponse } from "../../api/Term";
  16. import { Link } from "react-router";
  17. const { Title, Text } = Typography;
  18. interface IItem<R> {
  19. value: R;
  20. score: number;
  21. }
  22. interface IWord {
  23. meaning: IItem<string>[];
  24. note: IItem<string>[];
  25. editor: IItem<IUser>[];
  26. }
  27. interface IWidget {
  28. word: string | undefined;
  29. }
  30. const TermCommunityWidget = ({ word }: IWidget) => {
  31. const intl = useIntl();
  32. const [show, setShow] = useState(false);
  33. const [wordData, setWordData] = useState<IWord>();
  34. const minScore = 100; //分数阈值。低于这个分数只显示在弹出菜单中
  35. useEffect(() => {
  36. if (typeof word === "undefined") {
  37. return;
  38. }
  39. const url = `/v2/terms?view=word&word=${word}&exp=1`;
  40. console.log("url", url);
  41. get<ITermListResponse>(url)
  42. .then((json) => {
  43. if (json.ok === false) {
  44. return;
  45. }
  46. const meaning = new Map<string, number>();
  47. const note = new Map<string, number>();
  48. const editorId = new Map<string, number>();
  49. const editor = new Map<string, IUser>();
  50. for (const it of json.data.rows) {
  51. let score: number | undefined;
  52. let currScore = 100;
  53. if (it.exp) {
  54. //分数计算
  55. currScore = Math.floor(it.exp / 3600);
  56. }
  57. if (it.meaning) {
  58. score = meaning.get(it.meaning);
  59. meaning.set(it.meaning, score ? score + currScore : currScore);
  60. }
  61. if (it.note) {
  62. score = note.get(it.note);
  63. const noteScore = it.note.length;
  64. note.set(it.note, score ? score + noteScore : noteScore);
  65. }
  66. if (it.editor) {
  67. score = editorId.get(it.editor.id);
  68. editorId.set(it.editor.id, score ? score + currScore : currScore);
  69. editor.set(it.editor.id, it.editor);
  70. }
  71. }
  72. const _data: IWord = {
  73. meaning: [],
  74. note: [],
  75. editor: [],
  76. };
  77. meaning.forEach((value, key, _map) => {
  78. if (key && key.length > 0) {
  79. _data.meaning.push({ value: key, score: value });
  80. }
  81. });
  82. _data.meaning.sort((a, b) => b.score - a.score);
  83. note.forEach((value, key, _map) => {
  84. if (key && key.length > 0) {
  85. _data.note.push({ value: key, score: value });
  86. }
  87. });
  88. _data.note.sort((a, b) => b.score - a.score);
  89. editorId.forEach((value, key, _map) => {
  90. const currEditor = editor.get(key);
  91. if (currEditor) {
  92. _data.editor.push({ value: currEditor, score: value });
  93. }
  94. });
  95. _data.editor.sort((a, b) => b.score - a.score);
  96. setWordData(_data);
  97. if (_data.editor.length > 0) {
  98. setShow(true);
  99. }
  100. })
  101. .catch((error) => {
  102. console.error(error);
  103. });
  104. }, [word, setWordData]);
  105. const isShow = (score: number, index: number) => {
  106. const Ms = 500,
  107. Rd = 5,
  108. minScore = 15;
  109. const minOrder = Math.log(score) / Math.log(Math.pow(Ms, 1 / Rd));
  110. if (index < minOrder && score > minScore) {
  111. return true;
  112. } else {
  113. return false;
  114. }
  115. };
  116. const meaningLow = wordData?.meaning.filter(
  117. (value, index: number) => !isShow(value.score, index)
  118. );
  119. const meaningExtra = meaningLow?.map((item, id) => {
  120. return <span key={id}>{item.value}</span>;
  121. });
  122. const mainCollaboratorNum = 3; //默认显示的协作者数量,其余的在更多中显示
  123. const collaboratorRender = (name: string, id: number, score: number) => {
  124. return (
  125. <Space key={id}>
  126. {name}
  127. <Badge
  128. style={{ display: "none" }}
  129. color="geekblue"
  130. size="small"
  131. count={score}
  132. />
  133. </Space>
  134. );
  135. };
  136. const items: MenuProps["items"] = wordData?.editor
  137. .filter((_value, index) => index >= mainCollaboratorNum)
  138. .map((item, id) => {
  139. return {
  140. key: id,
  141. label: collaboratorRender(item.value.nickName, id, item.score),
  142. };
  143. });
  144. const more = wordData ? (
  145. wordData.editor.length > mainCollaboratorNum ? (
  146. <Dropdown menu={{ items }}>
  147. <Typography.Link>
  148. <Space>
  149. {intl.formatMessage({
  150. id: `buttons.more`,
  151. })}
  152. <DownOutlined />
  153. </Space>
  154. </Typography.Link>
  155. </Dropdown>
  156. ) : undefined
  157. ) : undefined;
  158. return show ? (
  159. <Card>
  160. <Space>
  161. <Title level={5} id={`community`}>
  162. {"社区术语"}
  163. </Title>
  164. <Link to={`/term/list/${word}`}>详情</Link>
  165. </Space>
  166. <div key="meaning">
  167. <Space style={{ flexWrap: "wrap" }}>
  168. <Text strong>{"意思:"}</Text>
  169. {wordData?.meaning
  170. .filter((value, index: number) => isShow(value.score, index))
  171. .map((item, id) => {
  172. return (
  173. <Space key={id}>
  174. {item.value}
  175. <Badge
  176. style={{ display: "none" }}
  177. color="geekblue"
  178. size="small"
  179. count={item.score}
  180. />
  181. </Space>
  182. );
  183. })}
  184. {meaningLow && meaningLow.length > 0 ? (
  185. <Popover content={<Space>{meaningExtra}</Space>} placement="bottom">
  186. <Typography.Link>
  187. <Space>
  188. {intl.formatMessage({
  189. id: `buttons.more`,
  190. })}
  191. <DownOutlined />
  192. </Space>
  193. </Typography.Link>
  194. </Popover>
  195. ) : undefined}
  196. </Space>
  197. </div>
  198. <div key="note">
  199. <Space style={{ flexWrap: "wrap" }}>
  200. <Text strong>{"note:"}</Text>
  201. {wordData?.note
  202. .filter((value) => value.score >= minScore)
  203. .map((item, id) => {
  204. return (
  205. <Space key={id}>
  206. {item.value}
  207. <Badge color="geekblue" size="small" count={item.score} />
  208. </Space>
  209. );
  210. })}
  211. </Space>
  212. </div>
  213. <div key="collaborator">
  214. <Space style={{ flexWrap: "wrap" }}>
  215. <Text strong>{"贡献者:"}</Text>
  216. {wordData?.editor
  217. .filter((_value, index) => index < mainCollaboratorNum)
  218. .map((item, id) => {
  219. return collaboratorRender(item.value.nickName, id, item.score);
  220. })}
  221. {more}
  222. </Space>
  223. </div>
  224. </Card>
  225. ) : (
  226. <></>
  227. );
  228. };
  229. export default TermCommunityWidget;