GrammarBook.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { Button, Dropdown, Input, List } from "antd";
  2. import { useEffect, useState } from "react";
  3. import {
  4. ArrowLeftOutlined,
  5. FieldTimeOutlined,
  6. MoreOutlined,
  7. FileAddOutlined,
  8. } from "@ant-design/icons";
  9. import { type ITerm, getGrammar } from "../../reducers/term-vocabulary";
  10. import { useAppSelector } from "../../hooks";
  11. import TermSearch from "./TermSearch";
  12. import {
  13. grammar,
  14. grammarId,
  15. grammarWord,
  16. grammarWordId,
  17. } from "../../reducers/command";
  18. import store from "../../store";
  19. import GrammarRecent, {
  20. type IGrammarRecent,
  21. popRecent,
  22. pushRecent,
  23. } from "./GrammarRecent";
  24. import { useIntl } from "react-intl";
  25. import TermModal from "./TermModal";
  26. import { get } from "../../request";
  27. import type {
  28. IApiResponseChannelData,
  29. IApiResponseChannelList,
  30. } from "../../api/Channel";
  31. import { grammarTermFetch } from "../../load";
  32. const { Search } = Input;
  33. interface IGrammarList {
  34. term: ITerm;
  35. weight: number;
  36. }
  37. const GrammarBookWidget = () => {
  38. const intl = useIntl();
  39. const [result, setResult] = useState<IGrammarList[]>();
  40. const [termId, setTermId] = useState<string>();
  41. const [termSearch, setTermSearch] = useState<string>();
  42. const [showRecent, setShowRecent] = useState(false);
  43. const [create, setCreate] = useState(false);
  44. const [grammarChannel, setGrammarChannel] =
  45. useState<IApiResponseChannelData>();
  46. const sysGrammar = useAppSelector(getGrammar);
  47. const searchWord = useAppSelector(grammarWord);
  48. const searchWordId = useAppSelector(grammarWordId);
  49. useEffect(() => {
  50. const url = `/v2/channel?view=system`;
  51. get<IApiResponseChannelList>(url).then((json) => {
  52. if (json.ok) {
  53. const channel = json.data.rows.find(
  54. (value) => value.name === "_System_Grammar_Term_zh-hans_"
  55. );
  56. setGrammarChannel(channel);
  57. }
  58. });
  59. }, []);
  60. useEffect(() => {
  61. console.debug("grammar book", searchWord);
  62. if (searchWord && searchWord.length > 0) {
  63. setTermId(undefined);
  64. setTermSearch(searchWord);
  65. pushRecent({
  66. title: searchWord,
  67. description: searchWord,
  68. word: searchWord,
  69. });
  70. store.dispatch(grammar(""));
  71. }
  72. }, [searchWord]);
  73. useEffect(() => {
  74. console.debug("grammar book", searchWordId);
  75. if (searchWordId && searchWordId.length > 0) {
  76. setTermId(searchWordId);
  77. setTermSearch(undefined);
  78. pushRecent({
  79. title: searchWordId,
  80. description: searchWordId,
  81. wordId: searchWordId,
  82. });
  83. store.dispatch(grammarId(""));
  84. }
  85. }, [searchWordId]);
  86. return (
  87. <div>
  88. <div style={{ display: "flex" }}>
  89. <Button
  90. icon={<ArrowLeftOutlined />}
  91. type="text"
  92. onClick={() => {
  93. const top = popRecent();
  94. if (top) {
  95. setTermId(top.wordId);
  96. setTermSearch(top.word);
  97. }
  98. }}
  99. />
  100. <Search
  101. placeholder="input search text"
  102. onSearch={(_value: string) => {}}
  103. onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
  104. console.debug("on change", event.target.value);
  105. setTermId(undefined);
  106. setTermSearch(undefined);
  107. const keyWord = event.target.value;
  108. if (keyWord.trim().length === 0) {
  109. setShowRecent(true);
  110. } else {
  111. setShowRecent(false);
  112. }
  113. /**
  114. * 权重算法
  115. * 约靠近头,分数约高
  116. * 剩余尾巴约短,分数越高
  117. */
  118. const search = sysGrammar
  119. ?.map((item) => {
  120. let weight = 0;
  121. const wordBegin = item.word
  122. .toLocaleLowerCase()
  123. .indexOf(keyWord.toLocaleLowerCase());
  124. if (wordBegin >= 0) {
  125. weight += (1 / (wordBegin + 1)) * 1000;
  126. const wordRemain =
  127. item.word.length - keyWord.length - wordBegin;
  128. weight += (1 / (wordRemain + 1)) * 100;
  129. }
  130. const meaningBegin = (item.meaning + item.other_meaning)
  131. .toLocaleLowerCase()
  132. .indexOf(keyWord);
  133. if (meaningBegin >= 0) {
  134. weight += (1 / (meaningBegin + 1)) * 1000;
  135. const meaningRemain =
  136. item.meaning.length - keyWord.length - wordBegin;
  137. weight += (1 / (meaningRemain + 1)) * 100;
  138. }
  139. return { term: item, weight: weight };
  140. })
  141. .filter((value) => value.weight > 0)
  142. .sort((a, b) => b.weight - a.weight);
  143. setResult(search);
  144. }}
  145. style={{ width: "100%" }}
  146. />
  147. <Dropdown
  148. trigger={["click"]}
  149. menu={{
  150. items: [
  151. {
  152. key: "recent",
  153. label: "最近查询",
  154. icon: <FieldTimeOutlined />,
  155. },
  156. {
  157. key: "create",
  158. label: intl.formatMessage({ id: "buttons.create" }),
  159. icon: <FileAddOutlined />,
  160. children: [
  161. {
  162. key: "create_collection",
  163. label: "固定搭配",
  164. },
  165. ],
  166. },
  167. ],
  168. onClick: (e) => {
  169. switch (e.key) {
  170. case "recent":
  171. setShowRecent(true);
  172. break;
  173. case "create_collection":
  174. setCreate(true);
  175. break;
  176. }
  177. },
  178. }}
  179. >
  180. <Button type="text" icon={<MoreOutlined />} />
  181. </Dropdown>
  182. </div>
  183. <div>
  184. {showRecent ? (
  185. <GrammarRecent
  186. onClick={(value: IGrammarRecent) => {
  187. console.debug("grammar book recent click", value);
  188. setTermId(value.wordId);
  189. setTermSearch(value.word);
  190. setShowRecent(false);
  191. }}
  192. />
  193. ) : termId || termSearch ? (
  194. <TermSearch
  195. wordId={termId}
  196. word={termSearch}
  197. onIdChange={(value: string) => {
  198. setTermId(value);
  199. setTermSearch(undefined);
  200. pushRecent({ title: value, description: value, wordId: value });
  201. }}
  202. />
  203. ) : (
  204. <List
  205. size="small"
  206. dataSource={result}
  207. renderItem={(item) => {
  208. const description =
  209. item.term.meaning +
  210. (item.term.other_meaning ? "," + item.term.other_meaning : "");
  211. return (
  212. <List.Item
  213. key={item.term.guid}
  214. style={{ cursor: "pointer" }}
  215. onClick={() => {
  216. setTermId(item.term.guid);
  217. setTermSearch(undefined);
  218. pushRecent({
  219. title: item.term.word,
  220. description: description,
  221. wordId: item.term.guid,
  222. });
  223. }}
  224. >
  225. <List.Item.Meta
  226. title={item.term.word}
  227. description={description}
  228. />
  229. </List.Item>
  230. );
  231. }}
  232. />
  233. )}
  234. </div>
  235. <TermModal
  236. parentChannelId={grammarChannel?.uid}
  237. tags={[":collection:"]}
  238. open={create}
  239. onClose={() => setCreate(false)}
  240. onUpdate={() => {
  241. //获取语法术语表
  242. grammarTermFetch();
  243. }}
  244. />
  245. </div>
  246. );
  247. };
  248. export default GrammarBookWidget;