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

Merge pull request #1987 from visuddhinanda/agile

修正意思不输入没有下拉菜单
visuddhinanda пре 2 година
родитељ
комит
792ac01f18

+ 11 - 3
dashboard/src/components/api/Group.ts

@@ -1,5 +1,6 @@
 import { IStudio } from "../auth/Studio";
 import { IStudio } from "../auth/Studio";
-import { IUserRequest, TRole } from "./Auth";
+import { IUser } from "../auth/User";
+import { TRole } from "./Auth";
 
 
 export interface IGroupRequest {
 export interface IGroupRequest {
   id?: string;
   id?: string;
@@ -31,7 +32,14 @@ export interface IGroupListResponse {
     count: number;
     count: number;
   };
   };
 }
 }
-
+export interface IGroupMemberRequest {
+  id?: number;
+  user_id: string;
+  group_id: string;
+  power?: number;
+  level?: number;
+  status?: number;
+}
 export interface IGroupMemberData {
 export interface IGroupMemberData {
   id?: number;
   id?: number;
   user_id: string;
   user_id: string;
@@ -39,7 +47,7 @@ export interface IGroupMemberData {
   power?: number;
   power?: number;
   level?: number;
   level?: number;
   status?: number;
   status?: number;
-  user?: IUserRequest;
+  user: IUser;
   created_at?: string;
   created_at?: string;
   updated_at?: string;
   updated_at?: string;
 }
 }

+ 13 - 11
dashboard/src/components/auth/Studio.tsx

@@ -2,7 +2,18 @@ import { Avatar, Space } from "antd";
 
 
 import StudioCard from "./StudioCard";
 import StudioCard from "./StudioCard";
 
 
-const avatarColor = ["indianred", "blueviolet", "#87d068", "#108ee9"];
+export const getAvatarColor = (name?: string) => {
+  const avatarColor = ["indianred", "blueviolet", "#87d068", "#108ee9"];
+  if (!name) {
+    return undefined;
+  }
+  let char = 0;
+  if (name.length > 1) {
+    char = name.length - 1;
+  }
+  const colorIndex = name.charCodeAt(char) % avatarColor.length;
+  return avatarColor[colorIndex];
+};
 export interface IStudio {
 export interface IStudio {
   id: string;
   id: string;
   nickName?: string;
   nickName?: string;
@@ -24,15 +35,6 @@ const StudioWidget = ({
   popOver,
   popOver,
   onClick,
   onClick,
 }: IWidget) => {
 }: IWidget) => {
-  let colorIndex = 0;
-  if (data?.nickName) {
-    let char = 0;
-    if (data.nickName.length > 1) {
-      char = data.nickName.length - 1;
-    }
-    colorIndex = data.nickName.charCodeAt(char) % avatarColor.length;
-  }
-
   return (
   return (
     <StudioCard popOver={popOver} studio={data}>
     <StudioCard popOver={popOver} studio={data}>
       <Space
       <Space
@@ -48,7 +50,7 @@ const StudioWidget = ({
           <Avatar
           <Avatar
             size="small"
             size="small"
             src={data?.avatar}
             src={data?.avatar}
-            style={{ backgroundColor: avatarColor[colorIndex] }}
+            style={{ backgroundColor: getAvatarColor(data?.nickName) }}
           >
           >
             {data?.nickName?.slice(0, 2)}
             {data?.nickName?.slice(0, 2)}
           </Avatar>
           </Avatar>

+ 6 - 1
dashboard/src/components/auth/User.tsx

@@ -1,4 +1,5 @@
 import { Avatar, Space } from "antd";
 import { Avatar, Space } from "antd";
+import { getAvatarColor } from "./Studio";
 
 
 export interface IUser {
 export interface IUser {
   id: string;
   id: string;
@@ -24,7 +25,11 @@ const UserWidget = ({
   return (
   return (
     <Space>
     <Space>
       {showAvatar ? (
       {showAvatar ? (
-        <Avatar size="small" src={avatar}>
+        <Avatar
+          size="small"
+          src={avatar}
+          style={{ backgroundColor: getAvatarColor(nickName) }}
+        >
           {nickName?.slice(0, 2)}
           {nickName?.slice(0, 2)}
         </Avatar>
         </Avatar>
       ) : undefined}
       ) : undefined}

+ 11 - 0
dashboard/src/components/dict/DictContent.tsx

@@ -10,6 +10,9 @@ import DictList from "./DictList";
 import MyCreate from "./MyCreate";
 import MyCreate from "./MyCreate";
 import { useIntl } from "react-intl";
 import { useIntl } from "react-intl";
 import DictGroupTitle from "./DictGroupTitle";
 import DictGroupTitle from "./DictGroupTitle";
+import UserDictList from "./UserDictList";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 
 export interface IDictWords {
 export interface IDictWords {
   pass: string;
   pass: string;
@@ -37,6 +40,8 @@ interface IWidget {
 
 
 const DictContentWidget = ({ word, data, compact }: IWidget) => {
 const DictContentWidget = ({ word, data, compact }: IWidget) => {
   const intl = useIntl();
   const intl = useIntl();
+  const user = useAppSelector(currentUser);
+
   return (
   return (
     <>
     <>
       <Row>
       <Row>
@@ -103,6 +108,12 @@ const DictContentWidget = ({ word, data, compact }: IWidget) => {
                 key: "my",
                 key: "my",
                 children: (
                 children: (
                   <div>
                   <div>
+                    <UserDictList
+                      studioName={user?.realName}
+                      word={word}
+                      compact={true}
+                    />
+                    <Divider>新建</Divider>
                     <MyCreate word={word} />
                     <MyCreate word={word} />
                   </div>
                   </div>
                 ),
                 ),

+ 131 - 7
dashboard/src/components/dict/UserDictList.tsx

@@ -8,13 +8,16 @@ import {
   message,
   message,
   Modal,
   Modal,
   Typography,
   Typography,
+  Tag,
+  Popover,
 } from "antd";
 } from "antd";
 import {
 import {
   PlusOutlined,
   PlusOutlined,
   ExclamationCircleOutlined,
   ExclamationCircleOutlined,
   DeleteOutlined,
   DeleteOutlined,
+  InfoCircleOutlined,
 } from "@ant-design/icons";
 } from "@ant-design/icons";
-import { ActionType, ProTable } from "@ant-design/pro-components";
+import { ActionType, ProList } from "@ant-design/pro-components";
 
 
 import DictCreate from "../../components/dict/DictCreate";
 import DictCreate from "../../components/dict/DictCreate";
 import {
 import {
@@ -28,6 +31,7 @@ import DictEdit from "../../components/dict/DictEdit";
 import { IDeleteResponse } from "../../components/api/Article";
 import { IDeleteResponse } from "../../components/api/Article";
 import TimeShow from "../general/TimeShow";
 import TimeShow from "../general/TimeShow";
 import { getSorterUrl } from "../../utils";
 import { getSorterUrl } from "../../utils";
+import MdView from "../template/MdView";
 
 
 const { Link } = Typography;
 const { Link } = Typography;
 
 
@@ -54,11 +58,15 @@ interface IWidget {
   studioName?: string;
   studioName?: string;
   view?: "studio" | "all";
   view?: "studio" | "all";
   dictName?: string;
   dictName?: string;
+  word?: string;
+  compact?: boolean;
 }
 }
 const UserDictListWidget = ({
 const UserDictListWidget = ({
   studioName,
   studioName,
   view = "studio",
   view = "studio",
   dictName,
   dictName,
+  word,
+  compact = false,
 }: IWidget) => {
 }: IWidget) => {
   const intl = useIntl();
   const intl = useIntl();
   const [isEditOpen, setIsEditOpen] = useState(false);
   const [isEditOpen, setIsEditOpen] = useState(false);
@@ -110,8 +118,116 @@ const UserDictListWidget = ({
 
 
   return (
   return (
     <>
     <>
-      <ProTable<IWord, IParams>
+      <ProList<IWord, IParams>
         actionRef={ref}
         actionRef={ref}
+        metas={{
+          title: {
+            dataIndex: "word",
+            title: "拼写",
+            search: word ? false : undefined,
+            render: (text, entity, index, action) => {
+              return (
+                <Space>
+                  <span
+                    onClick={() => {
+                      setWordId(entity.wordId);
+                      setDrawerTitle(entity.word);
+                      setIsEditOpen(true);
+                    }}
+                  >
+                    {entity.word}
+                  </span>
+                  {entity.note ? (
+                    <Popover
+                      placement="bottom"
+                      content={<MdView html={entity.note} />}
+                    >
+                      <InfoCircleOutlined color="blue" />
+                    </Popover>
+                  ) : (
+                    <></>
+                  )}
+                </Space>
+              );
+            },
+          },
+          subTitle: {
+            search: false,
+            render: (text, row, index, action) => {
+              return (
+                <Space>
+                  {row.type ? (
+                    <Tag key="type" color="blue">
+                      {intl.formatMessage({
+                        id: `dict.fields.type.${row.type?.replaceAll(
+                          ".",
+                          ""
+                        )}.label`,
+                        defaultMessage: row.type,
+                      })}
+                    </Tag>
+                  ) : (
+                    <></>
+                  )}
+                  {row.grammar ? (
+                    <Tag key="grammar" color="#5BD8A6">
+                      {row.grammar
+                        ?.replaceAll(".", "")
+                        .split("$")
+                        .map((item) =>
+                          intl.formatMessage({
+                            id: `dict.fields.type.${item}.label`,
+                            defaultMessage: item,
+                          })
+                        )
+                        .join(".")}
+                    </Tag>
+                  ) : (
+                    <></>
+                  )}
+                </Space>
+              );
+            },
+          },
+          description: {
+            dataIndex: "meaning",
+            title: "整体意思",
+            search: word ? false : undefined,
+            render(dom, entity, index, action, schema) {
+              return (
+                <div>
+                  <Space>
+                    {entity.meaning}
+
+                    <TimeShow
+                      updatedAt={entity.updated_at}
+                      createdAt={entity.updated_at}
+                      type="secondary"
+                    />
+                  </Space>
+                  {compact ? (
+                    <div>
+                      <div>{entity.factors}</div>
+                    </div>
+                  ) : (
+                    <></>
+                  )}
+                </div>
+              );
+            },
+          },
+          content: compact
+            ? undefined
+            : {
+                render(dom, entity, index, action, schema) {
+                  return (
+                    <div>
+                      <div>{entity.factors}</div>
+                    </div>
+                  );
+                },
+              },
+        }}
         columns={[
         columns={[
           {
           {
             title: intl.formatMessage({
             title: intl.formatMessage({
@@ -346,7 +462,11 @@ const UserDictListWidget = ({
 
 
           url += params.keyword ? "&search=" + params.keyword : "";
           url += params.keyword ? "&search=" + params.keyword : "";
 
 
-          url += params.word ? `&word=${params.word}` : "";
+          url += params.word
+            ? `&word=${params.word}`
+            : word
+            ? `&word=${word}`
+            : "";
           url += params.parent ? `&parent=${params.parent}` : "";
           url += params.parent ? `&parent=${params.parent}` : "";
           url += params.dict ? `&dict=${params.dict}` : "";
           url += params.dict ? `&dict=${params.dict}` : "";
           url += dictName
           url += dictName
@@ -388,11 +508,15 @@ const UserDictListWidget = ({
           showQuickJumper: true,
           showQuickJumper: true,
           showSizeChanger: true,
           showSizeChanger: true,
         }}
         }}
-        search={{
-          filterType: "light",
-        }}
+        search={
+          word
+            ? undefined
+            : {
+                filterType: "light",
+              }
+        }
         options={{
         options={{
-          search: true,
+          search: word ? false : true,
         }}
         }}
         headerTitle=""
         headerTitle=""
         toolBarRender={
         toolBarRender={

+ 450 - 0
dashboard/src/components/dict/UserDictTable.tsx

@@ -0,0 +1,450 @@
+import { useIntl } from "react-intl";
+import {
+  Button,
+  Space,
+  Table,
+  Dropdown,
+  Drawer,
+  message,
+  Modal,
+  Typography,
+} from "antd";
+import {
+  PlusOutlined,
+  ExclamationCircleOutlined,
+  DeleteOutlined,
+} from "@ant-design/icons";
+import { ActionType, ProTable } from "@ant-design/pro-components";
+
+import DictCreate from "./DictCreate";
+import {
+  IApiResponseDictList,
+  IDictInfo,
+  IUserDictDeleteRequest,
+} from "../api/Dict";
+import { delete_2, get } from "../../request";
+import { useRef, useState } from "react";
+import DictEdit from "./DictEdit";
+import { IDeleteResponse } from "../api/Article";
+import TimeShow from "../general/TimeShow";
+import { getSorterUrl } from "../../utils";
+
+const { Link } = Typography;
+
+export interface IWord {
+  sn: number;
+  wordId: string;
+  word: string;
+  type?: string | null;
+  grammar?: string | null;
+  parent?: string | null;
+  meaning?: string | null;
+  note?: string | null;
+  factors?: string | null;
+  dict?: IDictInfo;
+  updated_at?: string;
+  created_at?: string;
+}
+interface IParams {
+  word?: string;
+  parent?: string;
+  dict?: string;
+}
+interface IWidget {
+  studioName?: string;
+  view?: "studio" | "all";
+  dictName?: string;
+}
+const UserDictTableWidget = ({
+  studioName,
+  view = "studio",
+  dictName,
+}: IWidget) => {
+  const intl = useIntl();
+  const [isEditOpen, setIsEditOpen] = useState(false);
+  const [isCreateOpen, setIsCreateOpen] = useState(false);
+  const [wordId, setWordId] = useState<string>();
+  const [drawerTitle, setDrawerTitle] = useState("New Word");
+
+  const showDeleteConfirm = (id: string[], title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.confirm",
+        }) +
+        intl.formatMessage({
+          id: "message.irrevocable",
+        }),
+
+      content: title,
+      okText: intl.formatMessage({
+        id: "buttons.delete",
+      }),
+      okType: "danger",
+      cancelText: intl.formatMessage({
+        id: "buttons.no",
+      }),
+      onOk() {
+        console.log("delete", id);
+        return delete_2<IUserDictDeleteRequest, IDeleteResponse>(
+          `/v2/userdict/${id}`,
+          {
+            id: JSON.stringify(id),
+          }
+        )
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              ref.current?.reload();
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
+
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProTable<IWord, IParams>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 80,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.word.label",
+            }),
+            dataIndex: "word",
+            key: "word",
+            tip: "单词过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.type.label",
+            }),
+            dataIndex: "type",
+            key: "type",
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              n: { text: "名词", status: "Default" },
+              ti: { text: "三性", status: "Processing" },
+              v: { text: "动词", status: "Success" },
+              ind: { text: "不变词", status: "Success" },
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.grammar.label",
+            }),
+            dataIndex: "grammar",
+            key: "grammar",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.parent.label",
+            }),
+            dataIndex: "parent",
+            key: "parent",
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.meaning.label",
+            }),
+            dataIndex: "meaning",
+            key: "meaning",
+            tip: "意思过长会自动收缩",
+            ellipsis: true,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.note.label",
+            }),
+            dataIndex: "note",
+            key: "note",
+            search: false,
+            tip: "注释过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.factors.label",
+            }),
+            dataIndex: "factors",
+            key: "factors",
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.dict.shortname.label",
+            }),
+            dataIndex: "dict",
+            key: "dict",
+            hideInTable: view !== "all",
+            search: view !== "all" ? false : undefined,
+            render: (text, row, index, action) => {
+              return row.dict?.shortname;
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.updated-at.label",
+            }),
+            key: "updated_at",
+            width: 200,
+            search: false,
+            dataIndex: "updated_at",
+            valueType: "date",
+            sorter: true,
+            render: (text, row, index, action) => {
+              return (
+                <TimeShow
+                  updatedAt={row.updated_at}
+                  showIcon={false}
+                  showLabel={false}
+                />
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            hideInTable: view === "all",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button
+                  key={index}
+                  type="link"
+                  menu={{
+                    items: [
+                      {
+                        key: "remove",
+                        label: intl.formatMessage({
+                          id: "buttons.delete",
+                        }),
+                        icon: <DeleteOutlined />,
+                        danger: true,
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "share":
+                          break;
+                        case "remove":
+                          showDeleteConfirm([row.wordId], row.word);
+                          break;
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                >
+                  <Link
+                    onClick={() => {
+                      setWordId(row.wordId);
+                      setDrawerTitle(row.word);
+                      setIsEditOpen(true);
+                    }}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={
+          view === "all"
+            ? undefined
+            : {
+                // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+                // 注释该行则默认不显示下拉选项
+                selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+              }
+        }
+        tableAlertRender={
+          view === "all"
+            ? undefined
+            : ({ selectedRowKeys, selectedRows, onCleanSelected }) => (
+                <Space size={24}>
+                  <span>
+                    {intl.formatMessage({ id: "buttons.selected" })}
+                    {selectedRowKeys.length}
+                    <Button
+                      type="link"
+                      style={{ marginInlineStart: 8 }}
+                      onClick={onCleanSelected}
+                    >
+                      {intl.formatMessage({ id: "buttons.unselect" })}
+                    </Button>
+                  </span>
+                </Space>
+              )
+        }
+        tableAlertOptionRender={
+          view === "all"
+            ? undefined
+            : ({ intl, selectedRowKeys, selectedRows, onCleanSelected }) => {
+                return (
+                  <Space size={16}>
+                    <Button
+                      type="link"
+                      onClick={() => {
+                        console.log(selectedRowKeys);
+                        showDeleteConfirm(
+                          selectedRowKeys.map((item) => item.toString()),
+                          selectedRowKeys.length + "个单词"
+                        );
+                        onCleanSelected();
+                      }}
+                    >
+                      批量删除
+                    </Button>
+                  </Space>
+                );
+              }
+        }
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+
+          let url = "/v2/userdict?";
+          switch (view) {
+            case "studio":
+              url += `view=studio&name=${studioName}`;
+              break;
+            case "all":
+              url += `view=all`;
+              break;
+            default:
+              break;
+          }
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          url += params.word ? `&word=${params.word}` : "";
+          url += params.parent ? `&parent=${params.parent}` : "";
+          url += params.dict ? `&dict=${params.dict}` : "";
+          url += dictName
+            ? dictName !== "all"
+              ? `&dict=${dictName}`
+              : ""
+            : "";
+
+          url += getSorterUrl(sorter);
+
+          console.log(url);
+          const res = await get<IApiResponseDictList>(url);
+          const items: IWord[] = res.data.rows.map((item, id) => {
+            const id2 =
+              ((params.current || 1) - 1) * (params.pageSize || 20) + id + 1;
+            return {
+              sn: id2,
+              wordId: item.id,
+              word: item.word,
+              type: item.type,
+              grammar: item.grammar,
+              parent: item.parent,
+              meaning: item.mean,
+              note: item.note,
+              factors: item.factors,
+              dict: item.dict,
+              updated_at: item.updated_at,
+            };
+          });
+          return {
+            total: res.data.count,
+            success: true,
+            data: items,
+          };
+        }}
+        rowKey="wordId"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={{
+          filterType: "light",
+        }}
+        options={{
+          search: true,
+        }}
+        headerTitle=""
+        toolBarRender={
+          view === "all"
+            ? undefined
+            : () => [
+                <Button
+                  key="button"
+                  icon={<PlusOutlined />}
+                  type="primary"
+                  onClick={() => {
+                    setDrawerTitle("New word");
+                    setIsCreateOpen(true);
+                  }}
+                  disabled={true}
+                >
+                  {intl.formatMessage({ id: "buttons.create" })}
+                </Button>,
+              ]
+        }
+      />
+
+      <Drawer
+        title={drawerTitle}
+        placement="right"
+        open={isCreateOpen}
+        onClose={() => {
+          setIsCreateOpen(false);
+        }}
+        key="create"
+        style={{ maxWidth: "100%" }}
+        contentWrapperStyle={{ overflowY: "auto" }}
+        footer={null}
+      >
+        <DictCreate studio={studioName ? studioName : ""} />
+      </Drawer>
+      <Drawer
+        title={drawerTitle}
+        placement="right"
+        open={isEditOpen}
+        onClose={() => {
+          setIsEditOpen(false);
+        }}
+        key="edit"
+        style={{ maxWidth: "100%" }}
+        contentWrapperStyle={{ overflowY: "auto" }}
+        footer={null}
+      >
+        <DictEdit wordId={wordId} />
+      </Drawer>
+    </>
+  );
+};
+
+export default UserDictTableWidget;

+ 2 - 2
dashboard/src/components/group/AddMember.tsx

@@ -4,7 +4,7 @@ import { Button, message, Popover } from "antd";
 import { UserAddOutlined } from "@ant-design/icons";
 import { UserAddOutlined } from "@ant-design/icons";
 import { get, post } from "../../request";
 import { get, post } from "../../request";
 import { IUserListResponse } from "../api/Auth";
 import { IUserListResponse } from "../api/Auth";
-import { IGroupMemberData, IGroupMemberResponse } from "../api/Group";
+import { IGroupMemberRequest, IGroupMemberResponse } from "../api/Group";
 import { useState } from "react";
 import { useState } from "react";
 
 
 interface IFormData {
 interface IFormData {
@@ -24,7 +24,7 @@ const AddMemberWidget = ({ groupId, onCreated }: IWidget) => {
       onFinish={async (values: IFormData) => {
       onFinish={async (values: IFormData) => {
         console.log(values);
         console.log(values);
         if (typeof groupId !== "undefined") {
         if (typeof groupId !== "undefined") {
-          post<IGroupMemberData, IGroupMemberResponse>("/v2/group-member", {
+          post<IGroupMemberRequest, IGroupMemberResponse>("/v2/group-member", {
             user_id: values.userId,
             user_id: values.userId,
             group_id: groupId,
             group_id: groupId,
           }).then((json) => {
           }).then((json) => {

+ 6 - 0
dashboard/src/components/group/GroupMember.tsx

@@ -9,6 +9,7 @@ import {
   IGroupMemberDeleteResponse,
   IGroupMemberDeleteResponse,
   IGroupMemberListResponse,
   IGroupMemberListResponse,
 } from "../api/Group";
 } from "../api/Group";
+import User, { IUser } from "../auth/User";
 
 
 const { Content } = Layout;
 const { Content } = Layout;
 
 
@@ -20,6 +21,7 @@ interface DataItem {
   id: number;
   id: number;
   userId: string;
   userId: string;
   name?: string;
   name?: string;
+  user: IUser;
   tag: IRoleTag[];
   tag: IRoleTag[];
   image: string;
   image: string;
 }
 }
@@ -83,6 +85,7 @@ const GroupMemberWidget = ({ groupId }: IWidgetGroupFile) => {
                 id: item.id ? item.id : 0,
                 id: item.id ? item.id : 0,
                 userId: item.user_id,
                 userId: item.user_id,
                 name: item.user?.nickName,
                 name: item.user?.nickName,
+                user: item.user,
                 tag: [],
                 tag: [],
                 image: "",
                 image: "",
               };
               };
@@ -123,6 +126,9 @@ const GroupMemberWidget = ({ groupId }: IWidgetGroupFile) => {
           avatar: {
           avatar: {
             dataIndex: "image",
             dataIndex: "image",
             editable: false,
             editable: false,
+            render(dom, entity, index, action, schema) {
+              return <User {...entity.user} showName={false} />;
+            },
           },
           },
           subTitle: {
           subTitle: {
             render: (text, row, index, action) => {
             render: (text, row, index, action) => {

+ 2 - 2
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -98,8 +98,8 @@ interface IDetailsWidget {
 
 
 export const Details = ({ data, isPr }: IDetailsWidget) => (
 export const Details = ({ data, isPr }: IDetailsWidget) => (
   <Space wrap>
   <Space wrap>
-    <Channel {...data.channel} />
-    <User {...data.editor} showAvatar={isPr ? true : false} />
+    {isPr ? <></> : <Channel {...data.channel} />}
+    <User {...data.editor} showAvatar={false} />
     {data.prEditAt ? (
     {data.prEditAt ? (
       <TimeShow
       <TimeShow
         type="secondary"
         type="secondary"

+ 15 - 8
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -27,6 +27,7 @@ import StudioName from "../../auth/Studio";
 import CopyToModal from "../../channel/CopyToModal";
 import CopyToModal from "../../channel/CopyToModal";
 import store from "../../../store";
 import store from "../../../store";
 import { randomString } from "../../../utils";
 import { randomString } from "../../../utils";
+import User from "../../auth/User";
 
 
 interface IWidget {
 interface IWidget {
   initValue?: ISentence;
   initValue?: ISentence;
@@ -289,13 +290,19 @@ const SentCellWidget = ({
         {sentData ? (
         {sentData ? (
           <div style={{ display: "flex" }}>
           <div style={{ display: "flex" }}>
             <div style={{ marginRight: 8 }}>
             <div style={{ marginRight: 8 }}>
-              <StudioName
-                data={sentData.studio}
-                hideName
-                popOver={
-                  compact ? <Details data={sentData} isPr={isPr} /> : undefined
-                }
-              />
+              {isPr ? (
+                <User {...sentData.editor} showName={false} />
+              ) : (
+                <StudioName
+                  data={sentData.studio}
+                  hideName
+                  popOver={
+                    compact ? (
+                      <Details data={sentData} isPr={isPr} />
+                    ) : undefined
+                  }
+                />
+              )}
             </div>
             </div>
             <div
             <div
               style={{
               style={{
@@ -364,7 +371,7 @@ const SentCellWidget = ({
                   flexWrap: "wrap",
                   flexWrap: "wrap",
                 }}
                 }}
               >
               >
-                <EditInfo data={sentData} compact={compact} />
+                <EditInfo data={sentData} isPr={isPr} compact={compact} />
                 <SuggestionToolbar
                 <SuggestionToolbar
                   style={{
                   style={{
                     marginBottom: 0,
                     marginBottom: 0,

+ 1 - 1
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -162,7 +162,7 @@ const WbwCaseWidget = ({ data, display, onSplit, onChange }: IWidget) => {
     } else {
     } else {
       //空白的语法信息在逐词解析模式显示占位字符串
       //空白的语法信息在逐词解析模式显示占位字符串
       caseElement = (
       caseElement = (
-        <span>{intl.formatMessage({ id: "dict.fields.case.label" })}</span>
+        <span>{intl.formatMessage({ id: "forms.fields.case.label" })}</span>
       );
       );
     }
     }
   }
   }

+ 19 - 6
dashboard/src/components/template/Wbw/WbwDetailFm.tsx

@@ -7,6 +7,7 @@ import {
   EditOutlined,
   EditOutlined,
   CheckOutlined,
   CheckOutlined,
   SearchOutlined,
   SearchOutlined,
+  CloseOutlined,
 } from "@ant-design/icons";
 } from "@ant-design/icons";
 import { useAppSelector } from "../../../hooks";
 import { useAppSelector } from "../../../hooks";
 
 
@@ -22,8 +23,9 @@ interface IWFMI {
   meaning?: string;
   meaning?: string;
   onChange?: Function;
   onChange?: Function;
 }
 }
-const WbwFactorMeaningItem = ({ pali, meaning, onChange }: IWFMI) => {
+const WbwFactorMeaningItem = ({ pali, meaning = "", onChange }: IWFMI) => {
   const intl = useIntl();
   const intl = useIntl();
+  console.debug("meaning", meaning);
   const defaultMenu: ItemType[] = [
   const defaultMenu: ItemType[] = [
     {
     {
       key: "_lookup",
       key: "_lookup",
@@ -88,18 +90,29 @@ const WbwFactorMeaningItem = ({ pali, meaning, onChange }: IWFMI) => {
     }
     }
   };
   };
 
 
+  const inputCancel = () => {
+    setEditable(false);
+    setInput(meaning);
+  };
+
   const meaningInner = editable ? (
   const meaningInner = editable ? (
     <Input
     <Input
       defaultValue={meaning}
       defaultValue={meaning}
       size="small"
       size="small"
       addonAfter={
       addonAfter={
-        <CheckOutlined
-          style={{ cursor: "pointer" }}
-          onClick={() => inputOk()}
-        />
+        <>
+          <CheckOutlined
+            style={{ cursor: "pointer", marginRight: 4 }}
+            onClick={() => inputOk()}
+          />
+          <CloseOutlined
+            style={{ cursor: "pointer" }}
+            onClick={() => inputCancel()}
+          />
+        </>
       }
       }
       placeholder="Basic usage"
       placeholder="Basic usage"
-      style={{ width: 100 }}
+      style={{ width: 160 }}
       onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
       onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
         setInput(event.target.value);
         setInput(event.target.value);
       }}
       }}

+ 2 - 2
dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx

@@ -80,14 +80,14 @@ const WbwFactorMeaningWidget = ({
   if (display === "block") {
   if (display === "block") {
     if (
     if (
       typeof data.factorMeaning?.value === "string" &&
       typeof data.factorMeaning?.value === "string" &&
-      data.factorMeaning.value.trim().length > 0
+      data.factorMeaning.value.replaceAll("+", "").trim().length > 0
     ) {
     ) {
       factorMeaning = <span>{data.factorMeaning?.value}</span>;
       factorMeaning = <span>{data.factorMeaning?.value}</span>;
     } else {
     } else {
       //空白的意思在逐词解析模式显示占位字符串
       //空白的意思在逐词解析模式显示占位字符串
       factorMeaning = (
       factorMeaning = (
         <Text type="secondary">
         <Text type="secondary">
-          {intl.formatMessage({ id: "dict.fields.factormeaning.label" })}
+          {intl.formatMessage({ id: "forms.fields.factor.meaning.label" })}
         </Text>
         </Text>
       );
       );
     }
     }

+ 1 - 1
dashboard/src/components/template/Wbw/WbwFactors.tsx

@@ -118,7 +118,7 @@ const WbwFactorsWidget = ({ data, display, onChange }: IWidget) => {
         //空白的意思在逐词解析模式显示占位字符串
         //空白的意思在逐词解析模式显示占位字符串
         factors = (
         factors = (
           <Text type="secondary">
           <Text type="secondary">
-            {intl.formatMessage({ id: "dict.fields.factors.label" })}
+            {intl.formatMessage({ id: "forms.fields.factors.label" })}
           </Text>
           </Text>
         );
         );
       }
       }

+ 1 - 1
dashboard/src/components/template/Wbw/WbwMeaning.tsx

@@ -60,7 +60,7 @@ const WbwMeaningWidget = ({
     //空白的意思在逐词解析模式显示占位字符串
     //空白的意思在逐词解析模式显示占位字符串
     meaning = (
     meaning = (
       <Text type="secondary">
       <Text type="secondary">
-        {intl.formatMessage({ id: "dict.fields.meaning.label" })}
+        {intl.formatMessage({ id: "forms.fields.meaning.label" })}
       </Text>
       </Text>
     );
     );
   }
   }

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

@@ -498,6 +498,7 @@ export const WbwSentCtl = ({
         parent: data.parent?.value,
         parent: data.parent?.value,
         factors: data.factors?.value,
         factors: data.factors?.value,
         factormean: data.factorMeaning?.value,
         factormean: data.factorMeaning?.value,
+        note: data.note?.value,
         confidence: conf,
         confidence: conf,
         language: channelLang,
         language: channelLang,
       });
       });

+ 1 - 1
dashboard/src/pages/admin/dictionary/list.tsx

@@ -1,6 +1,6 @@
 import { ProCard, StatisticCard } from "@ant-design/pro-components";
 import { ProCard, StatisticCard } from "@ant-design/pro-components";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
-import UserDictList from "../../../components/dict/UserDictList";
+import UserDictList from "../../../components/dict/UserDictTable";
 import { get } from "../../../request";
 import { get } from "../../../request";
 
 
 const { Statistic } = StatisticCard;
 const { Statistic } = StatisticCard;