Преглед изворни кода

:sparkles: 逐词解析显示和编辑

visuddhinanda пре 3 година
родитељ
комит
4e908abaad

+ 51 - 17
dashboard/src/components/nut/Home.tsx

@@ -1,27 +1,61 @@
 import ReactMarkdown from "react-markdown";
 import code_png from "../../assets/nut/code.png";
+import ChannelPicker from "../channel/ChannelPicker";
 import MdView from "../template/MdView";
+import { IWbw } from "../template/Wbw/WbwWord";
+import WbwSent from "../template/WbwSent";
+
 import MarkdownForm from "./MarkdownForm";
 import MarkdownShow from "./MarkdownShow";
 
 const Widget = () => {
-	return (
-		<div>
-			<h1>Home</h1>
-			<h2>mardown test</h2>
-			<MdView html="<h1 name='h1'>hello<MdTpl name='term'/></h1>" />
-			<br />
-			<MarkdownShow body="- Hello, **《mint》**!" />
-			<br />
-			<h3>Form</h3>
-			<MarkdownForm />
-			<br />
-			<img alt="code" src={code_png} />
-			<div>
-				<ReactMarkdown>*This* is text with `quote`</ReactMarkdown>
-			</div>
-		</div>
-	);
+  let wbwData: IWbw[] = [];
+  const valueMake = (value: string) => {
+    return { value: value, status: 3 };
+  };
+  for (let index = 0; index < 20; index++) {
+    wbwData.push({
+      word: valueMake("Word" + index),
+      real: valueMake("word" + index),
+      meaning: { value: ["意思" + index], status: 3 },
+      factors: valueMake("word+word"),
+      factorMeaning: valueMake("mean+mean"),
+      type: valueMake(".n."),
+      grammar: valueMake(".m.$.sg.$.nom."),
+      confidence: 1,
+    });
+  }
+  return (
+    <div>
+      <h1>Home</h1>
+      <h2>wbw</h2>
+      <div style={{ width: 700 }}>
+        <WbwSent
+          data={wbwData}
+          display="inline"
+          fields={{ meaning: true, factors: false, case: false }}
+        />
+      </div>
+      <h2>channel picker</h2>
+      <div style={{ width: 1000 }}>
+        <ChannelPicker type="chapter" articleId="168-915" />
+      </div>
+      <h2>MdView test</h2>
+      <MdView html="<h1 name='h1'>hello<MdTpl name='term'/></h1>" />
+      <br />
+      <MarkdownShow body="- Hello, **《mint》**!" />
+      <br />
+      <h3>Form</h3>
+      <MarkdownForm />
+      <br />
+      <img alt="code" src={code_png} />
+      <div>
+        <ReactMarkdown>*This* is text with `quote`</ReactMarkdown>
+      </div>
+
+      <div style={{ height: 600 }}></div>
+    </div>
+  );
 };
 
 export default Widget;

+ 234 - 86
dashboard/src/components/studio/SelectCase.tsx

@@ -2,95 +2,243 @@ import { Cascader } from "antd";
 import { useIntl } from "react-intl";
 
 interface CascaderOption {
-	value: string | number;
-	label: string;
-	children?: CascaderOption[];
+  value: string | number;
+  label: string;
+  children?: CascaderOption[];
 }
+interface IWidget {
+  defaultValue?: string[];
+}
+const Widget = ({ defaultValue }: IWidget) => {
+  const intl = useIntl();
 
-const Widget = () => {
-	const intl = useIntl();
-	const case8 = [
-		{
-			value: "nom",
-			label: intl.formatMessage({ id: "dict.fields.type.nom.label" }),
-		},
-		{
-			value: "acc",
-			label: intl.formatMessage({ id: "dict.fields.type.acc.label" }),
-		},
-		{
-			value: "gen",
-			label: intl.formatMessage({ id: "dict.fields.type.gen.label" }),
-		},
-		{
-			value: "dat",
-			label: intl.formatMessage({ id: "dict.fields.type.dat.label" }),
-		},
-		{
-			value: "inst",
-			label: intl.formatMessage({ id: "dict.fields.type.inst.label" }),
-		},
-		{
-			value: "abl",
-			label: intl.formatMessage({ id: "dict.fields.type.abl.label" }),
-		},
-		{
-			value: "voc",
-			label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
-		},
-	];
-	const case2 = [
-		{
-			value: "sg",
-			label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
-			children: case8,
-		},
-		{
-			value: "pl",
-			label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
-			children: case8,
-		},
-		{
-			value: "base",
-			label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
-		},
-	];
-	const case3 = [
-		{
-			value: "m",
-			label: intl.formatMessage({ id: "dict.fields.type.m.label" }),
-			children: case2,
-		},
-		{
-			value: "nt",
-			label: intl.formatMessage({ id: "dict.fields.type.nt.label" }),
-			children: case2,
-		},
-		{
-			value: "f",
-			label: intl.formatMessage({ id: "dict.fields.type.f.label" }),
-			children: case2,
-		},
-	];
-	const options: CascaderOption[] = [
-		{
-			value: ".n.",
-			label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
-			children: case3,
-		},
-		{
-			value: ".ti.",
-			label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
-			children: case3,
-		},
-		{
-			value: ".v.",
-			label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
-			children: case3,
-		},
-	];
+  const case8 = [
+    {
+      value: "nom",
+      label: intl.formatMessage({ id: "dict.fields.type.nom.label" }),
+    },
+    {
+      value: "acc",
+      label: intl.formatMessage({ id: "dict.fields.type.acc.label" }),
+    },
+    {
+      value: "gen",
+      label: intl.formatMessage({ id: "dict.fields.type.gen.label" }),
+    },
+    {
+      value: "dat",
+      label: intl.formatMessage({ id: "dict.fields.type.dat.label" }),
+    },
+    {
+      value: "inst",
+      label: intl.formatMessage({ id: "dict.fields.type.inst.label" }),
+    },
+    {
+      value: "abl",
+      label: intl.formatMessage({ id: "dict.fields.type.abl.label" }),
+    },
+    {
+      value: "voc",
+      label: intl.formatMessage({ id: "dict.fields.type.voc.label" }),
+    },
+  ];
+  const case2 = [
+    {
+      value: "sg",
+      label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
+      children: case8,
+    },
+    {
+      value: "pl",
+      label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
+      children: case8,
+    },
+    {
+      value: "base",
+      label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+    },
+  ];
+  const case3 = [
+    {
+      value: "m",
+      label: intl.formatMessage({ id: "dict.fields.type.m.label" }),
+      children: case2,
+    },
+    {
+      value: "nt",
+      label: intl.formatMessage({ id: "dict.fields.type.nt.label" }),
+      children: case2,
+    },
+    {
+      value: "f",
+      label: intl.formatMessage({ id: "dict.fields.type.f.label" }),
+      children: case2,
+    },
+  ];
+  const caseVerb3 = [
+    {
+      value: "pres",
+      label: intl.formatMessage({ id: "dict.fields.type.pres.label" }),
+    },
+    {
+      value: "aor",
+      label: intl.formatMessage({ id: "dict.fields.type.aor.label" }),
+    },
+    {
+      value: "fut",
+      label: intl.formatMessage({ id: "dict.fields.type.fut.label" }),
+    },
+    {
+      value: "pf",
+      label: intl.formatMessage({ id: "dict.fields.type.pf.label" }),
+    },
+    {
+      value: "imp",
+      label: intl.formatMessage({ id: "dict.fields.type.imp.label" }),
+    },
+    {
+      value: "cond",
+      label: intl.formatMessage({ id: "dict.fields.type.cond.label" }),
+    },
+    {
+      value: "opt",
+      label: intl.formatMessage({ id: "dict.fields.type.opt.label" }),
+    },
+  ];
+  const caseVerb2 = [
+    {
+      value: "sg",
+      label: intl.formatMessage({ id: "dict.fields.type.sg.label" }),
+      children: caseVerb3,
+    },
+    {
+      value: "pl",
+      label: intl.formatMessage({ id: "dict.fields.type.pl.label" }),
+      children: caseVerb3,
+    },
+  ];
+  const caseVerbInd = [
+    {
+      value: "abs",
+      label: intl.formatMessage({ id: "dict.fields.type.abs.label" }),
+    },
+    {
+      value: "ger",
+      label: intl.formatMessage({ id: "dict.fields.type.ger.label" }),
+    },
+    {
+      value: "inf",
+      label: intl.formatMessage({ id: "dict.fields.type.inf.label" }),
+    },
+  ];
+  const caseInd = [
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+    },
+    {
+      value: "adv",
+      label: intl.formatMessage({ id: "dict.fields.type.adv.label" }),
+    },
+    {
+      value: "conj",
+      label: intl.formatMessage({ id: "dict.fields.type.conj.label" }),
+    },
+    {
+      value: "prep",
+      label: intl.formatMessage({ id: "dict.fields.type.prep.label" }),
+    },
+    {
+      value: "interj",
+      label: intl.formatMessage({ id: "dict.fields.type.interj.label" }),
+    },
+    {
+      value: "pre",
+      label: intl.formatMessage({ id: "dict.fields.type.pre.label" }),
+    },
+    {
+      value: "suf",
+      label: intl.formatMessage({ id: "dict.fields.type.suf.label" }),
+    },
+    {
+      value: "end",
+      label: intl.formatMessage({ id: "dict.fields.type.end.label" }),
+    },
+    {
+      value: "part",
+      label: intl.formatMessage({ id: "dict.fields.type.part.label" }),
+    },
+  ];
+  const caseVerb1 = [
+    {
+      value: "1p",
+      label: intl.formatMessage({ id: "dict.fields.type.1p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "2p",
+      label: intl.formatMessage({ id: "dict.fields.type.2p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "3p",
+      label: intl.formatMessage({ id: "dict.fields.type.3p.label" }),
+      children: caseVerb2,
+    },
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+      children: caseVerbInd,
+    },
+    {
+      value: "base",
+      label: intl.formatMessage({ id: "dict.fields.type.base.label" }),
+    },
+  ];
+  const options: CascaderOption[] = [
+    {
+      value: ".n.",
+      label: intl.formatMessage({ id: "dict.fields.type.n.label" }),
+      children: case3,
+    },
+    {
+      value: ".ti.",
+      label: intl.formatMessage({ id: "dict.fields.type.ti.label" }),
+      children: case3,
+    },
+    {
+      value: ".v.",
+      label: intl.formatMessage({ id: "dict.fields.type.v.label" }),
+      children: caseVerb1,
+    },
+    {
+      value: "ind",
+      label: intl.formatMessage({ id: "dict.fields.type.ind.label" }),
+      children: caseInd,
+    },
+    {
+      value: "un",
+      label: intl.formatMessage({ id: "dict.fields.type.un.label" }),
+    },
+    {
+      value: "adj",
+      label: intl.formatMessage({ id: "dict.fields.type.adj.label" }),
+      children: case3,
+    },
+  ];
+  type SingleValueType = (string | number)[];
+  const onChange = (value: SingleValueType) => {
+    console.log(value);
+  };
 
-	return <Cascader options={options} placeholder="Please select case" />;
+  return (
+    <Cascader
+      options={options}
+      defaultValue={defaultValue}
+      placeholder="Please select case"
+      onChange={onChange}
+    />
+  );
 };
 
 export default Widget;

+ 14 - 0
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -0,0 +1,14 @@
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return (
+    <div>
+      {data.type?.value}-{data.grammar?.value}
+    </div>
+  );
+};
+
+export default Widget;

+ 159 - 0
dashboard/src/components/template/Wbw/WbwDetail.tsx

@@ -0,0 +1,159 @@
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { Dropdown, Tabs, Divider, Button } from "antd";
+import type { MenuProps } from "antd";
+import { SaveOutlined } from "@ant-design/icons";
+
+import { IWbw, IWbwField } from "./WbwWord";
+import WbwDetailBasic, { IWordBasic } from "./WbwDetailBasic";
+import WbwDetailBookMark from "./WbwDetailBookMark";
+import WbwDetailNote from "./WbwDetailNote";
+import WbwDetailAdvance from "./WbwDetailAdvance";
+
+interface IWidget {
+  data: IWbw;
+  onClose?: Function;
+  onChange?: Function;
+  onSave?: Function;
+}
+const Widget = ({ data, onClose, onChange, onSave }: IWidget) => {
+  const intl = useIntl();
+  const [basicSubmit, setBasicSubmit] = useState(false);
+  const [currWbwData, setCurrWbwData] = useState(data);
+  const fieldChanged = (value: IWbwField) => {
+    let mData = currWbwData;
+    switch (value.field) {
+      case "note":
+        mData.note = { value: value.value, status: 5 };
+        break;
+      case "bookMarkColor":
+        mData.bookMarkColor = { value: value.value, status: 5 };
+        break;
+      case "bookMarkText":
+        mData.bookMarkText = { value: value.value, status: 5 };
+        break;
+      case "word":
+        mData.word = { value: value.value, status: 5 };
+        break;
+      case "real":
+        mData.real = { value: value.value, status: 5 };
+        break;
+      default:
+        break;
+    }
+    setCurrWbwData(mData);
+  };
+  const onMenuClick: MenuProps["onClick"] = (e) => {
+    console.log("click", e);
+  };
+
+  const items = [
+    {
+      key: "user-dict",
+      label: intl.formatMessage({ id: "buttons.save.publish" }),
+    },
+  ];
+  return (
+    <div
+      style={{
+        minWidth: 450,
+      }}
+    >
+      <Tabs
+        size="small"
+        type="card"
+        items={[
+          {
+            label: `basic`,
+            key: "basic",
+            children: (
+              <div>
+                <WbwDetailBasic
+                  data={data}
+                  submit={basicSubmit}
+                  onSubmit={(e: IWordBasic) => {
+                    console.log(e);
+                    if (typeof onChange !== "undefined") {
+                      onChange(currWbwData);
+                    }
+                    setBasicSubmit(false);
+                  }}
+                />
+              </div>
+            ),
+          },
+          {
+            label: `bookmark`,
+            key: "bookmark",
+            children: (
+              <WbwDetailBookMark
+                data={data}
+                onChange={(e: IWbwField) => {
+                  fieldChanged(e);
+                }}
+              />
+            ),
+          },
+          {
+            label: `Note`,
+            key: "note",
+            children: (
+              <WbwDetailNote
+                data={data}
+                onChange={(e: IWbwField) => {
+                  fieldChanged(e);
+                }}
+              />
+            ),
+          },
+          {
+            label: `advance`,
+            key: "advance",
+            children: (
+              <div>
+                <WbwDetailAdvance
+                  data={currWbwData}
+                  onChange={(e: IWbwField) => {
+                    fieldChanged(e);
+                  }}
+                />
+              </div>
+            ),
+          },
+        ]}
+      />
+      <Divider style={{ margin: "8px 0" }}></Divider>
+      <div style={{ display: "flex", justifyContent: "space-between" }}>
+        <div>
+          <Button
+            danger
+            onClick={() => {
+              if (typeof onClose !== "undefined") {
+                onClose();
+              }
+            }}
+          >
+            {intl.formatMessage({ id: "buttons.cancel" })}
+          </Button>
+        </div>
+        <Dropdown.Button
+          style={{ width: "unset" }}
+          type="primary"
+          menu={{ items, onClick: onMenuClick }}
+          onClick={() => {
+            setBasicSubmit(true);
+            console.log("data", currWbwData);
+            if (typeof onSave !== "undefined") {
+              onSave(currWbwData);
+            }
+          }}
+        >
+          <SaveOutlined />
+          {intl.formatMessage({ id: "buttons.save" })}
+        </Dropdown.Button>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 54 - 0
dashboard/src/components/template/Wbw/WbwDetailAdvance.tsx

@@ -0,0 +1,54 @@
+import { useState, useEffect } from "react";
+import { useIntl } from "react-intl";
+import type { RadioChangeEvent } from "antd";
+import { Radio } from "antd";
+import { Input } from "antd";
+
+import { IWbw } from "./WbwWord";
+
+const { TextArea } = Input;
+
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+}
+const Widget = ({ data, onChange }: IWidget) => {
+  const intl = useIntl();
+
+  const onWordChange = (
+    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+  ) => {
+    console.log("onWordChange:", e.target.value);
+    if (typeof onChange !== "undefined") {
+      onChange({ field: "word", value: e.target.value });
+    }
+  };
+  const onRealChange = (
+    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+  ) => {
+    console.log("onRealChange:", e.target.value);
+    if (typeof onChange !== "undefined") {
+      onChange({ field: "real", value: e.target.value });
+    }
+  };
+  return (
+    <>
+      <div>显示</div>
+      <Input
+        showCount
+        maxLength={512}
+        defaultValue={data.word.value}
+        onChange={onWordChange}
+      />
+      <div>拼写</div>
+      <Input
+        showCount
+        maxLength={512}
+        defaultValue={data.real?.value}
+        onChange={onRealChange}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 196 - 0
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -0,0 +1,196 @@
+import { useRef, useState, useEffect } from "react";
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormText,
+  ProFormSelect,
+  ProFormInstance,
+} from "@ant-design/pro-components";
+import { Divider, message, Form, Select } from "antd";
+import { Collapse, Tag } from "antd";
+
+import SelectCase from "../../studio/SelectCase";
+import { IWbw } from "./WbwWord";
+
+const { Panel } = Collapse;
+
+const handleChange = (value: string | string[]) => {
+  console.log(`Selected: ${value}`);
+};
+
+export interface IWordBasic {
+  meaning?: string[];
+  case?: string;
+  factors?: string;
+  factorMeaning?: string;
+  parent?: string;
+}
+interface IWidget {
+  data: IWbw;
+  submit?: boolean;
+  onSubmit?: Function;
+}
+const Widget = ({ data, submit = false, onSubmit }: IWidget) => {
+  const formRef = useRef<ProFormInstance>();
+  const intl = useIntl();
+  const [items, setItems] = useState(["jack", "lucy"]);
+  const formItemLayout = {
+    labelCol: { span: 4 },
+    wrapperCol: { span: 14 },
+  };
+  useEffect(() => {
+    if (submit) {
+      if (typeof onSubmit !== "undefined") {
+        onSubmit(formRef.current?.getFieldFormatValue?.());
+      }
+    }
+  }, [submit]);
+  return (
+    <ProForm<IWordBasic>
+      formRef={formRef}
+      {...formItemLayout}
+      layout="horizontal"
+      submitter={{
+        render: (props, doms) => {
+          return <></>;
+        },
+      }}
+      onFinish={async (values) => {
+        console.log(values);
+        message.success("提交成功");
+      }}
+      params={{}}
+      request={async () => {
+        return {
+          meaning: data.meaning?.value,
+          factors: data.factors?.value,
+          parent: data.parent?.value,
+          case: data.type?.value,
+        };
+      }}
+    >
+      <Select
+        mode="tags"
+        placeholder="Please select"
+        defaultValue={data.meaning?.value}
+        onChange={handleChange}
+        style={{ width: "100%" }}
+        options={items.map((item) => ({ label: item, value: item }))}
+      />
+      <ProFormSelect
+        width="md"
+        name="meaning"
+        dependencies={["meaning"]}
+        label={intl.formatMessage({ id: "forms.fields.meaning.label" })}
+        tooltip={intl.formatMessage({ id: "forms.fields.meaning.tooltip" })}
+        placeholder={intl.formatMessage({ id: "forms.fields.meaning.label" })}
+        options={items.map((item) => ({ label: item, value: item }))}
+        fieldProps={{
+          mode: "tags",
+          optionItemRender(item) {
+            return item.label + " - " + item.value;
+          },
+          dropdownRender(menu) {
+            return (
+              <>
+                {menu}
+                <Divider style={{ margin: "8px 0" }}>更多</Divider>
+                <Collapse defaultActiveKey={["1"]}>
+                  <Panel
+                    header="This is panel header 1"
+                    style={{ padding: 0 }}
+                    key="1"
+                  >
+                    <Tag
+                      onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
+                        e.preventDefault();
+
+                        const it =
+                          formRef.current?.getFieldValue("meaning") || [];
+                        console.log(it);
+                        if (!items.includes("hello")) {
+                          setItems([...items, "hello"]);
+                        }
+                        if (!it.includes("hello")) {
+                          it.push("hello");
+                          console.log("it push", it);
+                        }
+                        formRef.current?.setFieldsValue({ meaning: it });
+                      }}
+                    >
+                      意思
+                    </Tag>
+                  </Panel>
+                  <Panel header="This is panel header 2" key="2">
+                    <Tag>意思</Tag>
+                  </Panel>
+                  <Panel header="This is panel header 3" key="3">
+                    <Tag>意思</Tag>
+                  </Panel>
+                </Collapse>
+              </>
+            );
+          },
+        }}
+      />
+      <ProFormText
+        width="md"
+        name="factors"
+        label={intl.formatMessage({ id: "forms.fields.factors.label" })}
+        tooltip={intl.formatMessage({ id: "forms.fields.factors.tooltip" })}
+        placeholder={intl.formatMessage({ id: "forms.fields.factors.label" })}
+      />
+      <Form.Item
+        label={intl.formatMessage({ id: "forms.fields.case.label" })}
+        tooltip={intl.formatMessage({ id: "forms.fields.case.tooltip" })}
+        name="case"
+      >
+        <SelectCase />
+      </Form.Item>
+      <ProFormText
+        name="parent"
+        width="md"
+        label={intl.formatMessage({ id: "forms.fields.parent.label" })}
+        tooltip={intl.formatMessage({ id: "forms.fields.parent.tooltip" })}
+        placeholder={intl.formatMessage({ id: "forms.fields.parent.label" })}
+      />
+      <Collapse bordered={false}>
+        <Panel header="词源" key="1">
+          <ProFormText
+            name="parent1"
+            width="md"
+            label={intl.formatMessage({ id: "forms.fields.parent.label" })}
+            tooltip={intl.formatMessage({ id: "forms.fields.parent.tooltip" })}
+            placeholder={intl.formatMessage({
+              id: "forms.fields.parent.label",
+            })}
+          />
+          <ProFormSelect
+            width="md"
+            name="grammar"
+            label={intl.formatMessage({ id: "forms.fields.meaning.label" })}
+            tooltip={intl.formatMessage({ id: "forms.fields.meaning.tooltip" })}
+            placeholder={intl.formatMessage({
+              id: "forms.fields.meaning.label",
+            })}
+            options={[
+              { label: "过去分词", value: "pp" },
+              { label: "现在分词", value: "prp" },
+              { label: "未来分词", value: "fpp" },
+            ]}
+            fieldProps={{
+              optionItemRender(item) {
+                return item.label + " - " + item.value;
+              },
+            }}
+          />
+        </Panel>
+        <Panel header="关系" key="2">
+          关系语法
+        </Panel>
+      </Collapse>
+    </ProForm>
+  );
+};
+
+export default Widget;

+ 101 - 0
dashboard/src/components/template/Wbw/WbwDetailBookMark.tsx

@@ -0,0 +1,101 @@
+import { useState, useEffect } from "react";
+import { useIntl } from "react-intl";
+import type { RadioChangeEvent } from "antd";
+import { Radio } from "antd";
+import { Input } from "antd";
+
+import { IWbw } from "./WbwWord";
+
+const { TextArea } = Input;
+
+const onTextChange = (
+  e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+) => {
+  console.log("Change:", e.target.value);
+};
+
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+}
+const Widget = ({ data, onChange }: IWidget) => {
+  const intl = useIntl();
+  const [value, setValue] = useState("none");
+
+  const styleColor: React.CSSProperties = {
+    display: "inline-block",
+    width: 28,
+    height: 18,
+  };
+  const options = [
+    {
+      label: (
+        <span
+          style={{
+            ...styleColor,
+            backgroundColor: "white",
+          }}
+        >
+          none
+        </span>
+      ),
+      value: "unset",
+    },
+    {
+      label: (
+        <span
+          style={{
+            ...styleColor,
+            backgroundColor: "blue",
+          }}
+        ></span>
+      ),
+      value: "blue",
+    },
+    {
+      label: (
+        <span
+          style={{
+            ...styleColor,
+            backgroundColor: "yellow",
+          }}
+        ></span>
+      ),
+      value: "yellow",
+    },
+  ];
+  const onColorChange = ({ target: { value } }: RadioChangeEvent) => {
+    console.log("radio3 checked", value);
+    setValue(value);
+    if (typeof onChange !== "undefined") {
+      onChange({ field: "bookMarkColor", value: value });
+    }
+  };
+  const onTextChange = (
+    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+  ) => {
+    console.log("Change:", e.target.value);
+    if (typeof onChange !== "undefined") {
+      onChange({ field: "bookMarkText", value: e.target.value });
+    }
+  };
+  return (
+    <>
+      <Radio.Group
+        options={options}
+        defaultValue={data.bookMarkColor?.value}
+        onChange={onColorChange}
+        value={value}
+      />
+      <TextArea
+        defaultValue={data.bookMarkText?.value}
+        showCount
+        maxLength={512}
+        autoSize={{ minRows: 6, maxRows: 8 }}
+        onChange={onTextChange}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 35 - 0
dashboard/src/components/template/Wbw/WbwDetailNote.tsx

@@ -0,0 +1,35 @@
+import { useIntl } from "react-intl";
+import { Input } from "antd";
+
+import { IWbw } from "./WbwWord";
+
+const { TextArea } = Input;
+
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+}
+const Widget = ({ data, onChange }: IWidget) => {
+  const intl = useIntl();
+  const onTextChange = (
+    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+  ) => {
+    console.log("Change:", e.target.value);
+    if (typeof onChange !== "undefined") {
+      onChange({ field: "note", value: e.target.value });
+    }
+  };
+  return (
+    <>
+      <TextArea
+        defaultValue={data.note?.value}
+        showCount
+        maxLength={512}
+        autoSize={{ minRows: 8, maxRows: 10 }}
+        onChange={onTextChange}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 10 - 0
dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx

@@ -0,0 +1,10 @@
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return <div>{data.factorMeaning?.value}</div>;
+};
+
+export default Widget;

+ 10 - 0
dashboard/src/components/template/Wbw/WbwFactors.tsx

@@ -0,0 +1,10 @@
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return <div>{data.factors?.value}</div>;
+};
+
+export default Widget;

+ 10 - 0
dashboard/src/components/template/Wbw/WbwMeaning.tsx

@@ -0,0 +1,10 @@
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+}
+const Widget = ({ data }: IWidget) => {
+  return <div>{data.meaning?.value}</div>;
+};
+
+export default Widget;

+ 80 - 0
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -0,0 +1,80 @@
+import { useState } from "react";
+import { Popover } from "antd";
+import { TagFilled, InfoCircleOutlined } from "@ant-design/icons";
+
+import WbwDetail from "./WbwDetail";
+import { IWbw } from "./WbwWord";
+
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+  onSave?: Function;
+}
+const Widget = ({ data, onChange, onSave }: IWidget) => {
+  const [open, setOpen] = useState(false);
+  const [paliColor, setPaliColor] = useState("unset");
+  const wbwDetail = (
+    <WbwDetail
+      data={data}
+      onClose={() => {
+        setPaliColor("unset");
+        setOpen(false);
+      }}
+      onChange={(e: IWbw) => {
+        if (typeof onChange !== "undefined") {
+          onChange(e);
+        }
+      }}
+      onSave={(e: IWbw) => {
+        if (typeof onSave !== "undefined") {
+          onSave(e);
+          setOpen(false);
+          setPaliColor("unset");
+        }
+      }}
+    />
+  );
+  const handleClickChange = (open: boolean) => {
+    setOpen(open);
+    if (open) {
+      setPaliColor("lightblue");
+    } else {
+      setPaliColor("unset");
+    }
+  };
+  const noteIcon = data.note ? (
+    <Popover content={data.note.value} placement="bottom">
+      <InfoCircleOutlined style={{ color: "blue" }} />
+    </Popover>
+  ) : (
+    <></>
+  );
+  const bookMarkIcon = data.bookMarkText ? (
+    <Popover content={data.bookMarkText.value} placement="bottom">
+      <TagFilled style={{ color: data.bookMarkColor?.value }} />
+    </Popover>
+  ) : (
+    <></>
+  );
+  return (
+    <div>
+      <Popover
+        content={wbwDetail}
+        placement="bottom"
+        trigger="click"
+        open={open}
+        onOpenChange={handleClickChange}
+      >
+        <span
+          style={{ backgroundColor: paliColor, padding: 4, borderRadius: 5 }}
+        >
+          {data.word.value}
+        </span>
+      </Popover>
+      {noteIcon}
+      {bookMarkIcon}
+    </div>
+  );
+};
+
+export default Widget;

+ 112 - 0
dashboard/src/components/template/Wbw/WbwWord.tsx

@@ -0,0 +1,112 @@
+import { useState } from "react";
+import { onChange } from "../../../reducers/setting";
+import WbwCase from "./WbwCase";
+import WbwFactorMeaning from "./WbwFactorMeaning";
+import WbwFactors from "./WbwFactors";
+import WbwMeaning from "./WbwMeaning";
+import WbwPali from "./WbwPali";
+
+type FieldName =
+  | "word"
+  | "real"
+  | "meaning"
+  | "type"
+  | "grammar"
+  | "case"
+  | "parent"
+  | "factors"
+  | "factorMeaning"
+  | "relation"
+  | "note"
+  | "bookMarkColor"
+  | "bookMarkText"
+  | "confidence";
+
+export interface IWbwField {
+  field: FieldName;
+  value: string;
+}
+enum WbwStatus {
+  initiate = 0,
+  auto = 3,
+  manual = 5,
+}
+interface WbwElement {
+  value: string;
+  status: WbwStatus;
+}
+interface WbwElement2 {
+  value: string[];
+  status: WbwStatus;
+}
+export interface IWbw {
+  word: WbwElement;
+  real?: WbwElement;
+  meaning?: WbwElement2;
+  type?: WbwElement;
+  grammar?: WbwElement;
+  case?: WbwElement;
+  parent?: WbwElement;
+  factors?: WbwElement;
+  factorMeaning?: WbwElement;
+  relation?: WbwElement;
+  note?: WbwElement;
+  bookMarkColor?: WbwElement;
+  bookMarkText?: WbwElement;
+  confidence: number;
+}
+export interface IWbwFields {
+  meaning?: boolean;
+  factors?: boolean;
+  factorMeaning?: boolean;
+  case?: boolean;
+}
+interface IWidget {
+  data: IWbw;
+  display?: "block" | "inline";
+  fields?: IWbwFields;
+  onChange?: Function;
+}
+const Widget = ({
+  data,
+  display = "block",
+  fields = { meaning: true, factors: true, factorMeaning: true, case: true },
+  onChange,
+}: IWidget) => {
+  const [wordData, setWordData] = useState(data);
+
+  const styleWbw: React.CSSProperties = {
+    display: display === "block" ? "block" : "flex",
+  };
+  return (
+    <div style={styleWbw}>
+      <WbwPali
+        data={wordData}
+        onChange={(e: IWbw) => {
+          //setWordData(e);
+          if (typeof onChange !== "undefined") {
+            // onChange(e);
+          }
+        }}
+        onSave={(e: IWbw) => {
+          console.log("save", e);
+          const newData: IWbw = JSON.parse(JSON.stringify(e));
+          setWordData(newData);
+          if (typeof onChange !== "undefined") {
+            //onChange(e);
+          }
+        }}
+      />
+      <div style={{ backgroundColor: wordData.bookMarkColor?.value }}>
+        {fields?.meaning ? <WbwMeaning data={wordData} /> : undefined}
+        {fields?.factors ? <WbwFactors data={wordData} /> : undefined}
+        {fields?.factorMeaning ? (
+          <WbwFactorMeaning data={wordData} />
+        ) : undefined}
+        {fields?.case ? <WbwCase data={wordData} /> : undefined}
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 0 - 0
dashboard/src/components/template/Wbw/wbw.less


+ 27 - 0
dashboard/src/components/template/WbwSent.tsx

@@ -0,0 +1,27 @@
+import WbwWord, { IWbw, IWbwFields } from "./Wbw/WbwWord";
+
+interface IWidget {
+  data: IWbw[];
+  display?: "block" | "inline";
+  fields?: IWbwFields;
+}
+const Widget = ({ data, display, fields }: IWidget) => {
+  const wbwSent = data.map((item, id) => {
+    return (
+      <WbwWord
+        data={item}
+        key={id}
+        display={display}
+        fields={fields}
+        onChange={(e: IWbw) => {
+          console.log("word changed", e);
+          console.log("word id", id);
+          //TODO update
+        }}
+      />
+    );
+  });
+  return <div style={{ display: "flex", flexWrap: "wrap" }}>{wbwSent}</div>;
+};
+
+export default Widget;