Community.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import {
  2. Badge,
  3. Button,
  4. Card,
  5. Dropdown,
  6. type MenuProps,
  7. Popover,
  8. Skeleton,
  9. Space,
  10. Typography,
  11. } from "antd";
  12. import { DownOutlined } from "@ant-design/icons";
  13. import { useState, useEffect, useCallback } from "react";
  14. import { useIntl } from "react-intl";
  15. import { get } from "../../request";
  16. import type { IApiResponseDictList } from "../../api/Dict";
  17. import type { IUser } from "../auth/User";
  18. import GrammarPop from "./GrammarPop";
  19. import MdView from "../template/MdView";
  20. import MyCreate from "./MyCreate";
  21. const { Title, Link, Text } = Typography;
  22. interface IItem<R> {
  23. value: R;
  24. score: number;
  25. }
  26. interface IWord {
  27. grammar: IItem<string>[];
  28. parent: IItem<string>[];
  29. note: IItem<string>[];
  30. meaning: IItem<string>[];
  31. factors: IItem<string>[];
  32. editor: IItem<IUser>[];
  33. }
  34. interface IWidget {
  35. word: string | undefined;
  36. }
  37. const CommunityWidget = ({ word }: IWidget) => {
  38. const intl = useIntl();
  39. const [loaded, setLoaded] = useState(false);
  40. const [loading, setLoading] = useState(false);
  41. const [wordData, setWordData] = useState<IWord>();
  42. const [showCreate, setShowCreate] = useState(false);
  43. const [_myRefresh, setMyRefresh] = useState(false);
  44. const minScore = 100; //分数阈值。低于这个分数只显示在弹出菜单中
  45. const dictLoad = useCallback(async (input: string) => {
  46. setLoading(true);
  47. const url = `/v2/userdict?view=community&word=${input}`;
  48. console.info("dict community url", url);
  49. get<IApiResponseDictList>(url)
  50. .then((json) => {
  51. if (json.ok === false) {
  52. console.log("dict community", json.message);
  53. return;
  54. }
  55. console.debug("dict community", json.data);
  56. const meaning = new Map<string, number>();
  57. const grammar = new Map<string, number>();
  58. const parent = new Map<string, number>();
  59. const note = new Map<string, number>();
  60. const editorId = new Map<string, number>();
  61. const editor = new Map<string, IUser>();
  62. for (const it of json.data.rows) {
  63. let score: number | undefined;
  64. if (it.exp) {
  65. //分数计算
  66. let conf = it.confidence / 100;
  67. if (it.confidence <= 1) {
  68. conf = 1;
  69. }
  70. const currScore = Math.floor((it.exp / 3600) * conf);
  71. if (it.mean) {
  72. score = meaning.get(it.mean);
  73. meaning.set(it.mean, score ? score + currScore : currScore);
  74. }
  75. if (it.type || it.grammar) {
  76. const strCase = it.type + "$" + it.grammar;
  77. score = grammar.get(strCase);
  78. grammar.set(strCase, score ? score + currScore : currScore);
  79. }
  80. if (it.parent) {
  81. score = parent.get(it.parent);
  82. parent.set(it.parent, score ? score + currScore : currScore);
  83. }
  84. if (it.note) {
  85. score = note.get(it.note);
  86. note.set(it.note, score ? score + currScore : currScore);
  87. }
  88. if (it.editor) {
  89. score = editorId.get(it.editor.id);
  90. editorId.set(it.editor.id, score ? score + currScore : currScore);
  91. editor.set(it.editor.id, it.editor);
  92. }
  93. }
  94. }
  95. const _data: IWord = {
  96. grammar: [],
  97. parent: [],
  98. note: [],
  99. meaning: [],
  100. factors: [],
  101. editor: [],
  102. };
  103. meaning.forEach((value, key, _map) => {
  104. if (key && key.length > 0) {
  105. _data.meaning.push({ value: key, score: value });
  106. }
  107. });
  108. _data.meaning.sort((a, b) => b.score - a.score);
  109. grammar.forEach((value, key, _map) => {
  110. if (key && key.length > 0) {
  111. _data.grammar.push({ value: key, score: value });
  112. }
  113. });
  114. _data.grammar.sort((a, b) => b.score - a.score);
  115. parent.forEach((value, key, _map) => {
  116. if (key && key.length > 0) {
  117. _data.parent.push({ value: key, score: value });
  118. }
  119. });
  120. _data.parent.sort((a, b) => b.score - a.score);
  121. note.forEach((value, key, _map) => {
  122. if (key && key.length > 0) {
  123. _data.note.push({ value: key, score: value });
  124. }
  125. });
  126. _data.note.sort((a, b) => b.score - a.score);
  127. editorId.forEach((value, key, _map) => {
  128. const currEditor = editor.get(key);
  129. if (currEditor) {
  130. _data.editor.push({ value: currEditor, score: value });
  131. }
  132. });
  133. _data.editor.sort((a, b) => b.score - a.score);
  134. setWordData(_data);
  135. if (_data.editor.length > 0) {
  136. setLoaded(true);
  137. } else {
  138. setLoaded(false);
  139. }
  140. })
  141. .finally(() => setLoading(false))
  142. .catch((error) => {
  143. console.error(error);
  144. });
  145. }, []);
  146. useEffect(() => {
  147. if (typeof word === "undefined") {
  148. return;
  149. }
  150. dictLoad(word);
  151. }, [word, setWordData, dictLoad]);
  152. const isShow = (score: number, index: number) => {
  153. const Ms = 500,
  154. Rd = 5,
  155. minScore = 15;
  156. const minOrder = Math.log(score) / Math.log(Math.pow(Ms, 1 / Rd));
  157. if (index < minOrder && score > minScore) {
  158. return true;
  159. } else {
  160. return false;
  161. }
  162. };
  163. const meaningLow = wordData?.meaning.filter(
  164. (value, index: number) => !isShow(value.score, index)
  165. );
  166. const meaningExtra = meaningLow?.map((item, id) => {
  167. return <span key={id}>{item.value}</span>;
  168. });
  169. const mainCollaboratorNum = 3; //默认显示的协作者数量,其余的在更多中显示
  170. const collaboratorRender = (name: string, id: number, score: number) => {
  171. return (
  172. <Space key={id}>
  173. {name}
  174. <Badge color="geekblue" size="small" count={score} />
  175. </Space>
  176. );
  177. };
  178. const items: MenuProps["items"] = wordData?.editor
  179. .filter((_value, index) => index >= mainCollaboratorNum)
  180. .map((item, id) => {
  181. return {
  182. key: id,
  183. label: collaboratorRender(item.value.nickName, id, item.score),
  184. };
  185. });
  186. const more = wordData ? (
  187. wordData.editor.length > mainCollaboratorNum ? (
  188. <Dropdown menu={{ items }}>
  189. <Link>
  190. <Space>
  191. {intl.formatMessage({
  192. id: `buttons.more`,
  193. })}
  194. <DownOutlined />
  195. </Space>
  196. </Link>
  197. </Dropdown>
  198. ) : undefined
  199. ) : undefined;
  200. return (
  201. <Card>
  202. <Title level={5} id={`community`}>
  203. {"社区字典"}
  204. </Title>
  205. {loading ? (
  206. <Skeleton />
  207. ) : loaded ? (
  208. <div>
  209. <div key="meaning">
  210. <Space style={{ flexWrap: "wrap" }}>
  211. <Text strong>{"意思:"}</Text>
  212. {wordData?.meaning
  213. .filter((value, index: number) => isShow(value.score, index))
  214. .map((item, id) => {
  215. return (
  216. <Space key={id}>
  217. {item.value}
  218. <Badge color="geekblue" size="small" count={item.score} />
  219. </Space>
  220. );
  221. })}
  222. {meaningLow && meaningLow.length > 0 ? (
  223. <Popover
  224. content={<Space>{meaningExtra}</Space>}
  225. placement="bottom"
  226. >
  227. <Link>
  228. <Space>
  229. {intl.formatMessage({
  230. id: `buttons.more`,
  231. })}
  232. <DownOutlined />
  233. </Space>
  234. </Link>
  235. </Popover>
  236. ) : undefined}
  237. </Space>
  238. </div>
  239. <div key="grammar">
  240. <Space style={{ flexWrap: "wrap" }}>
  241. <Text strong>{"语法:"}</Text>
  242. {wordData?.grammar
  243. .filter((value) => value.score >= minScore)
  244. .map((item, id) => {
  245. const grammar = item.value.split("$");
  246. const grammarGuide = grammar.map((item, id) => {
  247. const strCase = item.replaceAll(".", "");
  248. return strCase.length > 0 ? (
  249. <GrammarPop
  250. key={id}
  251. gid={strCase}
  252. text={intl.formatMessage({
  253. id: `dict.fields.type.${strCase}.label`,
  254. defaultMessage: strCase,
  255. })}
  256. />
  257. ) : undefined;
  258. });
  259. return (
  260. <Space key={id}>
  261. <Space
  262. style={{
  263. backgroundColor: "rgba(0.5,0.5,0.5,0.2)",
  264. borderRadius: 5,
  265. paddingLeft: 5,
  266. paddingRight: 5,
  267. }}
  268. >
  269. {grammarGuide}
  270. </Space>
  271. <Badge color="geekblue" size="small" count={item.score} />
  272. </Space>
  273. );
  274. })}
  275. </Space>
  276. </div>
  277. <div key="base">
  278. <Space style={{ flexWrap: "wrap" }}>
  279. <Text strong>{"词干:"}</Text>
  280. {wordData?.parent
  281. .filter((value) => value.score >= minScore)
  282. .map((item, id) => {
  283. return (
  284. <Space key={id}>
  285. {item.value}
  286. <Badge color="geekblue" size="small" count={item.score} />
  287. </Space>
  288. );
  289. })}
  290. </Space>
  291. </div>
  292. <div key="collaborator">
  293. <Space style={{ flexWrap: "wrap" }}>
  294. <Text strong>{"贡献者:"}</Text>
  295. {wordData?.editor
  296. .filter((_value, index) => index < mainCollaboratorNum)
  297. .map((item, id) => {
  298. return collaboratorRender(
  299. item.value.nickName,
  300. id,
  301. item.score
  302. );
  303. })}
  304. {more}
  305. </Space>
  306. </div>
  307. <div key="note">
  308. <Text strong>{"注释:"}</Text>
  309. <div>
  310. {wordData?.note
  311. .filter((value) => value.score >= minScore)
  312. .slice(0, 1)
  313. .map((item, id) => {
  314. return <MdView html={item.value} key={id} />;
  315. })}
  316. </div>
  317. </div>
  318. </div>
  319. ) : showCreate ? (
  320. <MyCreate
  321. word={word}
  322. onSave={() => {
  323. setMyRefresh(true);
  324. if (word) {
  325. dictLoad(word);
  326. }
  327. }}
  328. />
  329. ) : (
  330. <>
  331. <Button type="link" onClick={() => setShowCreate(true)}>
  332. 新建
  333. </Button>
  334. </>
  335. )}
  336. </Card>
  337. );
  338. };
  339. export default CommunityWidget;