Просмотр исходного кода

Merge pull request #2039 from visuddhinanda/agile

添加注册支持
visuddhinanda 2 лет назад
Родитель
Сommit
9a654c9a1b
39 измененных файлов с 673 добавлено и 246 удалено
  1. 13 0
      dashboard/src/Router.tsx
  2. 4 0
      dashboard/src/components/admin/LeftSider.tsx
  3. 1 1
      dashboard/src/components/api/Article.ts
  4. 54 0
      dashboard/src/components/api/Auth.ts
  5. 21 2
      dashboard/src/components/article/AnthologyInfoEdit.tsx
  6. 13 1
      dashboard/src/components/article/ArticleEdit.tsx
  7. 1 1
      dashboard/src/components/auth/Avatar.tsx
  8. 1 1
      dashboard/src/components/auth/SignInAvatar.tsx
  9. 26 0
      dashboard/src/components/auth/SoftwareEdition.tsx
  10. 1 0
      dashboard/src/components/auth/Studio.tsx
  11. 26 2
      dashboard/src/components/channel/Edit.tsx
  12. 1 1
      dashboard/src/components/course/CourseMemberList.tsx
  13. 1 1
      dashboard/src/components/general/LangSelect.tsx
  14. 7 16
      dashboard/src/components/invite/InviteCreate.tsx
  15. 156 0
      dashboard/src/components/invite/InviteList.tsx
  16. 1 1
      dashboard/src/components/nut/users/NonSignInSharedLinks.tsx
  17. 11 17
      dashboard/src/components/nut/users/SignUp.tsx
  18. 4 1
      dashboard/src/components/studio/HeadBar.tsx
  19. 7 0
      dashboard/src/components/studio/PublicitySelect.tsx
  20. 17 11
      dashboard/src/components/studio/table.ts
  21. 205 0
      dashboard/src/components/users/SignUp.tsx
  22. 1 0
      dashboard/src/locales/en-US/auth/index.ts
  23. 5 0
      dashboard/src/locales/en-US/error.ts
  24. 3 0
      dashboard/src/locales/en-US/forms.ts
  25. 0 2
      dashboard/src/locales/en-US/index.ts
  26. 2 0
      dashboard/src/locales/en-US/label.ts
  27. 0 16
      dashboard/src/locales/en-US/tables.ts
  28. 1 0
      dashboard/src/locales/zh-Hans/auth/index.ts
  29. 5 0
      dashboard/src/locales/zh-Hans/error.ts
  30. 3 0
      dashboard/src/locales/zh-Hans/forms.ts
  31. 3 2
      dashboard/src/locales/zh-Hans/index.ts
  32. 2 0
      dashboard/src/locales/zh-Hans/label.ts
  33. 0 16
      dashboard/src/locales/zh-Hans/tables.ts
  34. 15 0
      dashboard/src/pages/admin/invite/index.tsx
  35. 7 0
      dashboard/src/pages/admin/invite/list.tsx
  36. 6 1
      dashboard/src/pages/studio/course/list.tsx
  37. 2 153
      dashboard/src/pages/studio/invite/list.tsx
  38. 25 0
      dashboard/src/pages/users/index.tsx
  39. 22 0
      dashboard/src/pages/users/sign-up.tsx

+ 13 - 0
dashboard/src/Router.tsx

@@ -3,6 +3,9 @@ import { Route, Routes } from "react-router-dom";
 import Anonymous from "./layouts/anonymous";
 import Dashboard from "./layouts/dashboard";
 
+import Users from "./pages/users";
+import UsersSignUp from "./pages/users/sign-up";
+
 import NutUsersSignIn from "./pages/nut/users/sign-in";
 import NutUsersSignUp from "./pages/nut/users/sign-up";
 import NutUsersUnlockNew from "./pages/nut/users/unlock/new";
@@ -31,6 +34,9 @@ import AdminUsers from "./pages/admin/users";
 import AdminUsersList from "./pages/admin/users/list";
 import AdminUsersShow from "./pages/admin/users/show";
 
+import AdminInvite from "./pages/admin/invite";
+import AdminInviteList from "./pages/admin/invite/list";
+
 import LibraryHome from "./pages/library";
 
 import LibraryCommunity from "./pages/library/community";
@@ -160,6 +166,13 @@ const Widget = () => {
             <Route path="list" element={<AdminUsersList />} />
             <Route path="show/:id" element={<AdminUsersShow />} />
           </Route>
+          <Route path="invite" element={<AdminInvite />}>
+            <Route path="list" element={<AdminInviteList />} />
+          </Route>
+        </Route>
+
+        <Route path="users" element={<Users />}>
+          <Route path="sign-up" element={<UsersSignUp />} />
         </Route>
         <Route path="anonymous" element={<Anonymous />}>
           <Route path="users">

+ 4 - 0
dashboard/src/components/admin/LeftSider.tsx

@@ -47,6 +47,10 @@ const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
           label: <Link to="/admin/users/list">users</Link>,
           key: "users",
         },
+        {
+          label: <Link to="/admin/invite/list">invite</Link>,
+          key: "invite",
+        },
       ],
     },
     {

+ 1 - 1
dashboard/src/components/api/Article.ts

@@ -25,7 +25,7 @@ export interface IAnthologyDataResponse {
   subtitle: string;
   summary: string;
   article_list: IArticleListApiResponse[];
-  studio: IStudioApiResponse;
+  studio: IStudio;
   default_channel?: IChannel;
   lang: string;
   status: number;

+ 54 - 0
dashboard/src/components/api/Auth.ts

@@ -10,6 +10,24 @@ export type TRole =
   | "assistant"
   | "unknown";
 
+export interface ISignUpRequest {
+  token: string;
+  username: string;
+  nickname: string;
+  email: string;
+  password: string;
+  lang: string;
+}
+export interface ISignUpVerifyResponse {
+  ok: boolean;
+  message: string | { email: boolean; username: boolean };
+  data: string;
+}
+export interface ISignInResponse {
+  ok: boolean;
+  message: string;
+  data: string;
+}
 export interface IUserRequest {
   id?: string;
   userName?: string;
@@ -61,3 +79,39 @@ export interface IStudioApiResponse {
   avatar?: string;
   owner: IUser;
 }
+
+export interface IInviteRequest {
+  email: string;
+  lang: string;
+  studio: string;
+  dashboard?: string;
+}
+export interface IInviteResponse {
+  ok: boolean;
+  message: string;
+  data: IInviteData;
+}
+
+export interface IInviteData {
+  id: string;
+  user_uid: string;
+  email: string;
+  status: string;
+  created_at: string;
+  updated_at: string;
+}
+export interface IInviteListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IInviteData[];
+    count: number;
+  };
+}
+export interface IInviteResponse {
+  ok: boolean;
+  message: string;
+  data: IInviteData;
+}
+
+export type TSoftwareEdition = "basic" | "pro";

+ 21 - 2
dashboard/src/components/article/AnthologyInfoEdit.tsx

@@ -9,12 +9,18 @@ import {
 import MDEditor from "@uiw/react-md-editor";
 
 import { get, put } from "../../request";
-import { IAnthologyDataRequest, IAnthologyResponse } from "../api/Article";
+import {
+  IAnthologyDataRequest,
+  IAnthologyDataResponse,
+  IAnthologyResponse,
+} from "../api/Article";
 import LangSelect from "../general/LangSelect";
 import PublicitySelect from "../studio/PublicitySelect";
 import { useState } from "react";
 import { DefaultOptionType } from "antd/lib/select";
 import { IApiResponseChannelList } from "../api/Channel";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 interface IFormData {
   title: string;
@@ -38,6 +44,9 @@ const AnthologyInfoEditWidget = ({
   const intl = useIntl();
   const [channelOption, setChannelOption] = useState<DefaultOptionType[]>([]);
   const [currChannel, setCurrChannel] = useState<RequestOptionsType>();
+  const [data, setData] = useState<IAnthologyDataResponse>();
+
+  const user = useAppSelector(currentUser);
 
   return anthologyId ? (
     <ProForm<IFormData>
@@ -73,6 +82,7 @@ const AnthologyInfoEditWidget = ({
         const res = await get<IAnthologyResponse>(url);
         console.log("文集get", res);
         if (res.ok) {
+          setData(res.data);
           if (typeof onLoad !== "undefined") {
             onLoad(res.data);
           }
@@ -133,7 +143,16 @@ const AnthologyInfoEditWidget = ({
 
       <ProForm.Group>
         <LangSelect width="md" />
-        <PublicitySelect width="md" disable={["public_no_list"]} />
+        <PublicitySelect
+          width="md"
+          disable={["public_no_list"]}
+          readonly={
+            user?.roles?.includes("basic") ||
+            data?.studio.roles?.includes("basic")
+              ? true
+              : false
+          }
+        />
       </ProForm.Group>
       <ProForm.Group>
         <ProFormSelect

+ 13 - 1
dashboard/src/components/article/ArticleEdit.tsx

@@ -23,6 +23,8 @@ import MDEditor from "@uiw/react-md-editor";
 import ArticlePrevDrawer from "../../components/article/ArticlePrevDrawer";
 import { IStudio } from "../auth/Studio";
 import ArticleEditTools from "./ArticleEditTools";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 interface IFormData {
   uid: string;
@@ -66,6 +68,7 @@ const ArticleEditWidget = ({
   const [owner, setOwner] = useState<IStudio>();
   const formRef = useRef<ProFormInstance>();
   const [title, setTitle] = useState<string>();
+  const user = useAppSelector(currentUser);
 
   return unauthorized ? (
     <Result
@@ -197,6 +200,7 @@ const ArticleEditWidget = ({
             content_type: res.data.content_type,
             lang: res.data.lang,
             status: res.data.status,
+            studio: res.data.studio,
           };
         }}
       >
@@ -227,7 +231,15 @@ const ArticleEditWidget = ({
         </ProForm.Group>
         <ProForm.Group>
           <LangSelect width="md" />
-          <PublicitySelect width="md" disable={["public_no_list"]} />
+          <PublicitySelect
+            width="md"
+            disable={["public_no_list"]}
+            readonly={
+              user?.roles?.includes("basic") || owner?.roles?.includes("basic")
+                ? true
+                : false
+            }
+          />
         </ProForm.Group>
         <ProForm.Group>
           <ProFormTextArea

+ 1 - 1
dashboard/src/components/auth/Avatar.tsx

@@ -82,7 +82,7 @@ const AvatarWidget = ({ style, placement = "bottomRight" }: IWidget) => {
       </ProCard>
     );
   };
-  const Login = () => <Link to="/anonymous/users/sign-in">登录</Link>;
+  const Login = () => <Link to="/anonymous/users/sign-in">登录/注册</Link>;
   return (
     <>
       <Popover content={user ? <UserCard /> : <Login />} placement={placement}>

+ 1 - 1
dashboard/src/components/auth/SignInAvatar.tsx

@@ -39,7 +39,7 @@ const SignInAvatarWidget = ({ style, placement = "bottomRight" }: IWidget) => {
   }, [user]);
 
   if (typeof user === "undefined") {
-    return <Link to="/anonymous/users/sign-in">登录</Link>;
+    return <Link to="/anonymous/users/sign-in">登录/注册</Link>;
   } else {
     return (
       <>

+ 26 - 0
dashboard/src/components/auth/SoftwareEdition.tsx

@@ -0,0 +1,26 @@
+import { useIntl } from "react-intl";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
+import { TSoftwareEdition } from "../api/Auth";
+
+interface IWidget {
+  style?: React.CSSProperties;
+}
+const SoftwareEdition = ({ style }: IWidget) => {
+  const intl = useIntl();
+  const user = useAppSelector(currentUser);
+  let edition: TSoftwareEdition = "pro";
+  if (user?.roles?.includes("basic")) {
+    edition = "basic";
+  }
+  console.info("edition", edition);
+  return (
+    <span style={style}>
+      {intl.formatMessage({
+        id: `labels.software.edition.${edition}`,
+      })}
+    </span>
+  );
+};
+
+export default SoftwareEdition;

+ 1 - 0
dashboard/src/components/auth/Studio.tsx

@@ -20,6 +20,7 @@ export interface IStudio {
   studioName?: string;
   realName?: string;
   avatar?: string;
+  roles?: string[];
 }
 interface IWidget {
   data?: IStudio;

+ 26 - 2
dashboard/src/components/channel/Edit.tsx

@@ -6,12 +6,17 @@ import {
 } from "@ant-design/pro-components";
 import { Alert, message } from "antd";
 
-import { IApiResponseChannel } from "../../components/api/Channel";
+import {
+  IApiResponseChannel,
+  IApiResponseChannelData,
+} from "../../components/api/Channel";
 import { get, put } from "../../request";
 import ChannelTypeSelect from "../../components/channel/ChannelTypeSelect";
 import LangSelect from "../../components/general/LangSelect";
 import PublicitySelect from "../../components/studio/PublicitySelect";
 import { useState } from "react";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 interface IFormData {
   name: string;
@@ -30,6 +35,9 @@ interface IWidget {
 const EditWidget = ({ studioName, channelId, onLoad }: IWidget) => {
   const intl = useIntl();
   const [isSystem, setIsSystem] = useState<Boolean>();
+  const [data, setData] = useState<IApiResponseChannelData>();
+
+  const user = useAppSelector(currentUser);
   return (
     <>
       {isSystem ? (
@@ -53,6 +61,18 @@ const EditWidget = ({ studioName, channelId, onLoad }: IWidget) => {
           const res = await get<IApiResponseChannel>(
             `/v2/channel/${channelId}`
           );
+          if (res.ok === false) {
+            return {
+              name: "",
+              type: "",
+              lang: "",
+              summary: "",
+              status: 0,
+              studio: "",
+              isSystem: true,
+            };
+          }
+          setData(res.data);
           if (typeof onLoad !== "undefined") {
             onLoad(res.data);
           }
@@ -89,7 +109,11 @@ const EditWidget = ({ studioName, channelId, onLoad }: IWidget) => {
         </ProForm.Group>
         <ProForm.Group>
           <PublicitySelect
-            readonly={isSystem ? true : false}
+            readonly={
+              isSystem || user?.roles?.includes("basic") || data?.status === 5
+                ? true
+                : false
+            }
             disable={["public_no_list"]}
           />
         </ProForm.Group>

+ 1 - 1
dashboard/src/components/course/CourseMemberList.tsx

@@ -216,7 +216,7 @@ const CourseMemberListWidget = ({ courseId, onSelect }: IWidget) => {
             valueEnum: {
               all: {
                 text: intl.formatMessage({
-                  id: "tables.publicity.all",
+                  id: "forms.fields.publicity.all.label",
                 }),
                 status: "Default",
               },

+ 1 - 1
dashboard/src/components/general/LangSelect.tsx

@@ -6,7 +6,7 @@ export const LangValueEnum = () => {
   return {
     all: {
       text: intl.formatMessage({
-        id: "tables.publicity.all",
+        id: "forms.fields.publicity.all.label",
       }),
       status: "Default",
     },

+ 7 - 16
dashboard/src/components/invite/InviteCreate.tsx

@@ -7,21 +7,10 @@ import {
 import { message } from "antd";
 import { post } from "../../request";
 import { useRef } from "react";
-import { IInviteData } from "../../pages/studio/invite/list";
 import LangSelect from "../general/LangSelect";
 import { dashboardBasePath } from "../../utils";
+import { IInviteRequest, IInviteResponse } from "../api/Auth";
 
-interface IInviteRequest {
-  email: string;
-  lang: string;
-  studio: string;
-  dashboard?: string;
-}
-interface IInviteResponse {
-  ok: boolean;
-  message: string;
-  data: IInviteData;
-}
 interface IFormData {
   email: string;
   lang: string;
@@ -42,14 +31,16 @@ const InviteCreateWidget = ({ studio, onCreate }: IWidget) => {
         if (typeof studio === "undefined") {
           return;
         }
-        console.log(values);
-        const res = await post<IInviteRequest, IInviteResponse>(`/v2/invite`, {
+        const url = `/v2/invite`;
+        const data: IInviteRequest = {
           email: values.email,
           lang: values.lang,
           studio: studio,
           dashboard: dashboardBasePath(),
-        });
-        console.log(res);
+        };
+        console.info("api request", values);
+        const res = await post<IInviteRequest, IInviteResponse>(url, data);
+        console.debug("api response", res);
         if (res.ok) {
           message.success(intl.formatMessage({ id: "flashes.success" }));
           if (typeof onCreate !== "undefined") {

+ 156 - 0
dashboard/src/components/invite/InviteList.tsx

@@ -0,0 +1,156 @@
+import { useIntl } from "react-intl";
+import { Button, Popover } from "antd";
+import { ActionType, ProTable } from "@ant-design/pro-components";
+import { UserAddOutlined } from "@ant-design/icons";
+
+import { get } from "../../request";
+import { RoleValueEnum } from "../../components/studio/table";
+
+import { useRef, useState } from "react";
+import InviteCreate from "../../components/invite/InviteCreate";
+import { getSorterUrl } from "../../utils";
+import { IInviteListResponse } from "../../components/api/Auth";
+
+interface DataItem {
+  sn: number;
+  id: string;
+  email: string;
+  status: string;
+  created_at: string;
+}
+
+interface IWidget {
+  studioName?: string;
+}
+
+const InviteListWidget = ({ studioName }: IWidget) => {
+  const intl = useIntl(); //i18n
+  const [openCreate, setOpenCreate] = useState(false);
+
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProTable<DataItem>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.email.label",
+            }),
+            dataIndex: "email",
+            key: "email",
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.status.label",
+            }),
+            dataIndex: "status",
+            key: "status",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: RoleValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created_at",
+            width: 100,
+            search: false,
+            dataIndex: "created_at",
+            valueType: "date",
+          },
+        ]}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          let url = `/v2/invite?`;
+          if (studioName) {
+            url += `view=studio&studio=${studioName}`;
+          } else {
+            url += `view=all`;
+          }
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          url += getSorterUrl(sorter);
+
+          console.log(url);
+          const res = await get<IInviteListResponse>(url);
+          const items: DataItem[] = res.data.rows.map((item, id) => {
+            return {
+              sn: id + offset + 1,
+              id: item.id,
+              email: item.email,
+              status: item.status,
+              created_at: item.created_at,
+            };
+          });
+          console.log(items);
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={
+          studioName
+            ? () => [
+                <Popover
+                  content={
+                    <InviteCreate
+                      studio={studioName}
+                      onCreate={() => {
+                        setOpenCreate(false);
+                        ref.current?.reload();
+                      }}
+                    />
+                  }
+                  placement="bottomRight"
+                  trigger="click"
+                  open={openCreate}
+                  onOpenChange={(open: boolean) => {
+                    setOpenCreate(open);
+                  }}
+                >
+                  <Button
+                    key="button"
+                    icon={<UserAddOutlined />}
+                    type="primary"
+                  >
+                    {intl.formatMessage({ id: "buttons.invite" })}
+                  </Button>
+                </Popover>,
+              ]
+            : undefined
+        }
+      />
+    </>
+  );
+};
+
+export default InviteListWidget;

+ 1 - 1
dashboard/src/components/nut/users/NonSignInSharedLinks.tsx

@@ -9,7 +9,7 @@ const Widget = () => {
         <FormattedMessage id="nut.users.sign-in.title" />
       </Link>
       <Divider type="vertical" />
-      <Link to="/anonymous/users/sign-up">
+      <Link to="/users/sign-up">
         <FormattedMessage id="nut.users.sign-up.title" />
       </Link>
       <Divider type="vertical" />

+ 11 - 17
dashboard/src/components/nut/users/SignUp.tsx

@@ -10,9 +10,14 @@ import { useNavigate } from "react-router-dom";
 import { EyeInvisibleOutlined, EyeTwoTone } from "@ant-design/icons";
 
 import { get, post } from "../../../request";
-import { IInviteResponse } from "../../../pages/studio/invite/list";
+
 import LangSelect from "../../general/LangSelect";
 import { useRef, useState } from "react";
+import {
+  IInviteResponse,
+  ISignInResponse,
+  ISignUpRequest,
+} from "../../api/Auth";
 
 interface IFormData {
   email: string;
@@ -22,20 +27,6 @@ interface IFormData {
   password2: string;
   lang: string;
 }
-interface ISignInResponse {
-  ok: boolean;
-  message: string;
-  data: string;
-}
-
-interface ISignUpRequest {
-  token: string;
-  username: string;
-  nickname: string;
-  email: string;
-  password: string;
-  lang: string;
-}
 
 interface IWidget {
   token?: string;
@@ -89,8 +80,10 @@ const SignUpWidget = ({ token }: IWidget) => {
         }
       }}
       request={async () => {
-        const res = await get<IInviteResponse>(`/v2/invite/${token}`);
-        console.log(res.data);
+        const url = `/v2/invite/${token}`;
+        console.info("api request", url);
+        const res = await get<IInviteResponse>(url);
+        console.debug("api response", res.data);
         return {
           id: res.data.id,
           username: "",
@@ -193,6 +186,7 @@ const SignUpWidget = ({ token }: IWidget) => {
           }}
         </ProFormDependency>
       </ProForm.Group>
+
       <ProForm.Group>
         <LangSelect label="常用的译文语言" />
       </ProForm.Group>

+ 4 - 1
dashboard/src/components/studio/HeadBar.tsx

@@ -7,6 +7,7 @@ import SignInAvatar from "../auth/SignInAvatar";
 import ToLibrary from "../auth/ToLibrary";
 import ThemeSelect from "../general/ThemeSelect";
 import NotificationIcon from "../notification/NotificationIcon";
+import SoftwareEdition from "../auth/SoftwareEdition";
 
 const { Search } = Input;
 const { Header } = Layout;
@@ -28,13 +29,15 @@ const HeadBarWidget = () => {
         style={{
           display: "flex",
           width: "100%",
+          height: "100%",
           justifyContent: "space-between",
         }}
       >
-        <div style={{ width: 80 }}>
+        <div style={{ display: "flex" }}>
           <Link to="/">
             <img alt="code" style={{ height: 36 }} src={img_banner} />
           </Link>
+          <SoftwareEdition style={{ color: "white" }} />
         </div>
         <div style={{ width: 500, lineHeight: 44 }}>
           <Search

+ 7 - 0
dashboard/src/components/studio/PublicitySelect.tsx

@@ -19,6 +19,13 @@ const PublicitySelectWidget = ({ width, disable = [], readonly }: IWidget) => {
       }),
       disable: disable.includes("disable"),
     },
+    {
+      value: 5,
+      label: intl.formatMessage({
+        id: "forms.fields.publicity.blocked.label",
+      }),
+      disable: true,
+    },
     {
       value: 10,
       label: intl.formatMessage({

+ 17 - 11
dashboard/src/components/studio/table.ts

@@ -5,37 +5,43 @@ export const PublicityValueEnum = () => {
   return {
     all: {
       text: intl.formatMessage({
-        id: "tables.publicity.all",
+        id: "forms.fields.publicity.all.label",
       }),
       status: "Default",
     },
     0: {
       text: intl.formatMessage({
-        id: "tables.publicity.disable",
+        id: "forms.fields.publicity.disable.label",
+      }),
+      status: "Default",
+    },
+    5: {
+      text: intl.formatMessage({
+        id: "forms.fields.publicity.blocked.label",
       }),
       status: "Default",
     },
     10: {
       text: intl.formatMessage({
-        id: "tables.publicity.private",
+        id: "forms.fields.publicity.private.label",
       }),
       status: "Success",
     },
     20: {
       text: intl.formatMessage({
-        id: "tables.publicity.public.bylink",
+        id: "forms.fields.publicity.public_no_list.label",
       }),
       status: "Processing",
     },
     30: {
       text: intl.formatMessage({
-        id: "tables.publicity.public",
+        id: "forms.fields.publicity.public.label",
       }),
       status: "Processing",
     },
     40: {
       text: intl.formatMessage({
-        id: "tables.publicity.public.edit",
+        id: "forms.fields.publicity.public.edit.label",
       }),
       status: "Processing",
     },
@@ -47,27 +53,27 @@ export const RoleValueEnum = () => {
   return {
     all: {
       text: intl.formatMessage({
-        id: "tables.role.all",
+        id: "auth.role.all",
       }),
     },
     owner: {
       text: intl.formatMessage({
-        id: "tables.role.owner",
+        id: "auth.role.owner",
       }),
     },
     manager: {
       text: intl.formatMessage({
-        id: "tables.role.manager",
+        id: "auth.role.manager",
       }),
     },
     editor: {
       text: intl.formatMessage({
-        id: "tables.role.editor",
+        id: "auth.role.editor",
       }),
     },
     member: {
       text: intl.formatMessage({
-        id: "tables.role.member",
+        id: "auth.role.member",
       }),
     },
   };

+ 205 - 0
dashboard/src/components/users/SignUp.tsx

@@ -0,0 +1,205 @@
+import { useRef, useState } from "react";
+import { useIntl } from "react-intl";
+import { Alert, Button, Result, message } from "antd";
+import type { ProFormInstance } from "@ant-design/pro-components";
+import {
+  CheckCard,
+  ProForm,
+  ProFormCheckbox,
+  ProFormText,
+  StepsForm,
+} from "@ant-design/pro-components";
+
+import { post } from "../../request";
+import { IInviteRequest, IInviteResponse } from "../api/Auth";
+import { dashboardBasePath } from "../../utils";
+import { get as getUiLang } from "../../locales";
+
+interface IFormData {
+  email: string;
+  lang: string;
+}
+
+const SingUpWidget = () => {
+  const intl = useIntl();
+  const formRef = useRef<ProFormInstance>();
+  const [error, setError] = useState<string>();
+  const [agree, setAgree] = useState(false);
+  return (
+    <StepsForm<IFormData>
+      formRef={formRef}
+      onFinish={async (values: IFormData) => {}}
+      formProps={{
+        validateMessages: {
+          required: "此项为必填项",
+        },
+      }}
+      submitter={{
+        render(props, dom) {
+          if (props.step === 0) {
+            return (
+              <Button
+                type="primary"
+                disabled={!agree}
+                onClick={() => props.onSubmit?.()}
+              >
+                {"下一步"}
+              </Button>
+            );
+          } else if (props.step === 2) {
+            return <></>;
+          } else {
+            return dom;
+          }
+        },
+      }}
+    >
+      <StepsForm.StepForm<{
+        name: string;
+      }>
+        name="welcome"
+        title="注册"
+        stepProps={{
+          description: "注册wikipali教育版",
+        }}
+        onFinish={async () => {
+          return true;
+        }}
+      >
+        <Alert
+          message={"wikipali的阅读,字典,搜索功能无需注册就能使用。"}
+          style={{ marginBottom: 8 }}
+        />
+        <CheckCard.Group
+          onChange={(value) => {
+            console.log("value", value);
+          }}
+          defaultValue="A"
+          style={{ width: "100%" }}
+          size="small"
+        >
+          <CheckCard
+            title="未注册"
+            description={
+              <div>
+                <div>✅经文阅读</div>
+                <div>✅字典</div>
+                <div>✅经文搜索</div>
+                <div>❌课程</div>
+                <div>❌翻译</div>
+              </div>
+            }
+            value="B"
+            disabled
+          />
+          <CheckCard
+            title={intl.formatMessage({ id: "labels.software.edition.basic" })}
+            description={
+              <div>
+                <div>✅逐词解析</div>
+                <div>✅翻译</div>
+                <div>✅参加课程</div>
+                <div>❌公开发布译文和逐词解析</div>
+                <div>❌公开发布用户字典和术语</div>
+                <div>❌建立课程</div>
+                <div>❌建立群组</div>
+              </div>
+            }
+            value="A"
+          />
+
+          <CheckCard
+            title={intl.formatMessage({ id: "labels.software.edition.pro" })}
+            disabled
+            description={
+              <div>
+                <div>✅逐词解析</div>
+                <div>✅翻译</div>
+                <div>✅参加课程</div>
+                <div>✅公开发布译文和逐词解析</div>
+                <div>✅公开发布用户字典和术语</div>
+                <div>✅建立课程</div>
+                <div>✅建立群组</div>
+              </div>
+            }
+            value="C"
+          />
+        </CheckCard.Group>
+        <ProFormCheckbox.Group
+          name="checkbox"
+          layout="horizontal"
+          options={["我已经了解教育版的功能限制"]}
+          fieldProps={{
+            onChange(checkedValue) {
+              if (checkedValue.includes("我已经了解教育版的功能限制")) {
+                setAgree(true);
+              } else {
+                setAgree(false);
+              }
+            },
+          }}
+        />
+      </StepsForm.StepForm>
+
+      <StepsForm.StepForm<{
+        checkbox: string;
+      }>
+        name="checkbox"
+        title="邮箱验证"
+        stepProps={{
+          description: "填入您的注册邮箱",
+        }}
+        onFinish={async () => {
+          const values = formRef.current?.getFieldsValue();
+          const url = `/v2/invite`;
+          const data: IInviteRequest = {
+            email: values.email,
+            lang: getUiLang(),
+            studio: "",
+            dashboard: dashboardBasePath(),
+          };
+          console.info("api request", values);
+          try {
+            const res = await post<IInviteRequest, IInviteResponse>(url, data);
+            console.debug("api response", res);
+            if (res.ok) {
+              message.success(intl.formatMessage({ id: "flashes.success" }));
+            } else {
+              setError(intl.formatMessage({ id: `error.${res.message}` }));
+            }
+            return res.ok;
+          } catch (error) {
+            setError(error as string);
+            return false;
+          }
+        }}
+      >
+        {error ? <Alert type="error" message={error} /> : undefined}
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="email"
+            required
+            label={intl.formatMessage({ id: "forms.fields.email.label" })}
+            rules={[
+              {
+                required: true,
+                type: "email",
+              },
+            ]}
+          />
+        </ProForm.Group>
+      </StepsForm.StepForm>
+
+      <StepsForm.StepForm name="finish" title="完成注册">
+        <Result
+          status="success"
+          title="验证码已经成功发送"
+          subTitle="验证邮件已经发送到您的邮箱。请查收邮件,根据提示完成注册。"
+        />
+      </StepsForm.StepForm>
+    </StepsForm>
+  );
+};
+
+export default SingUpWidget;

+ 1 - 0
dashboard/src/locales/en-US/auth/index.ts

@@ -1,5 +1,6 @@
 const items = {
   "auth.role.label": "role",
+  "auth.role.all": "all",
   "auth.role.owner": "owner",
   "auth.role.manager": "manager",
   "auth.role.editor": "editor",

+ 5 - 0
dashboard/src/locales/en-US/error.ts

@@ -0,0 +1,5 @@
+const items = {
+  "error.email.exists": "该邮箱已经存在",
+};
+
+export default items;

+ 3 - 0
dashboard/src/locales/en-US/forms.ts

@@ -19,10 +19,13 @@ const items = {
   "forms.fields.power.label": "power",
   "forms.fields.type.label": "type",
   "forms.fields.publicity.label": "publicity",
+  "forms.fields.publicity.all.label": "all",
   "forms.fields.publicity.disable.label": "disable",
+  "forms.fields.publicity.blocked.label": "blocked",
   "forms.fields.publicity.private.label": "private",
   "forms.fields.publicity.public_no_list.label": "public no list",
   "forms.fields.publicity.public.label": "public",
+  "forms.fields.publicity.public.edit.label": "public edit",
   "forms.fields.teacher.label": "teacher",
   "forms.fields.studentsassistant.label": "students / assistant",
   "forms.fields.student.label": "student",

+ 0 - 2
dashboard/src/locales/en-US/index.ts

@@ -1,6 +1,5 @@
 import forms from "./forms";
 import buttons from "./buttons";
-import tables from "./tables";
 import nut from "./nut";
 import channel from "./channel";
 import dict from "./dict";
@@ -50,7 +49,6 @@ const items = {
   "columns.studio.attachment.title": "Attachment",
   ...buttons,
   ...forms,
-  ...tables,
   ...nut,
   ...channel,
   ...dict,

+ 2 - 0
dashboard/src/locales/en-US/label.ts

@@ -36,6 +36,8 @@ const items = {
   "labels.loading": "loading",
   "labels.empty": "empty",
   "labels.curr.paragraph.cart.tpl": "Add to Cart",
+  "labels.software.edition.basic": "basic",
+  "labels.software.edition.pro": "pro",
 };
 
 export default items;

+ 0 - 16
dashboard/src/locales/en-US/tables.ts

@@ -1,16 +0,0 @@
-const items = {
-  "tables.publicity.all": "all",
-  "tables.publicity.disable": "disable",
-  "tables.publicity.private": "private",
-  "tables.publicity.public.bylink": "public in link",
-  "tables.publicity.public": "public",
-  "tables.publicity.public.edit": "public editable",
-  "tables.role.all": "all",
-  "tables.role.owner": "owner",
-  "tables.role.manager": "manager",
-  "tables.role.editor": "editor",
-  "tables.role.member": "member",
-  "tables.progress.label": "progress",
-};
-
-export default items;

+ 1 - 0
dashboard/src/locales/zh-Hans/auth/index.ts

@@ -1,5 +1,6 @@
 const items = {
   "auth.role.label": "角色",
+  "auth.role.all": "全部角色",
   "auth.role.owner": "拥有者",
   "auth.role.manager": "管理员",
   "auth.role.editor": "编辑者",

+ 5 - 0
dashboard/src/locales/zh-Hans/error.ts

@@ -0,0 +1,5 @@
+const items = {
+  "error.email.exists": "该邮箱已经存在",
+};
+
+export default items;

+ 3 - 0
dashboard/src/locales/zh-Hans/forms.ts

@@ -19,10 +19,13 @@ const items = {
   "forms.fields.power.label": "权限",
   "forms.fields.type.label": "类型",
   "forms.fields.publicity.label": "公开性",
+  "forms.fields.publicity.all.label": "全部",
   "forms.fields.publicity.disable.label": "禁用",
+  "forms.fields.publicity.blocked.label": "锁定",
   "forms.fields.publicity.private.label": "私有",
   "forms.fields.publicity.public_no_list.label": "公开不列出",
   "forms.fields.publicity.public.label": "公开",
+  "forms.fields.publicity.public.edit.label": "公开可编辑",
   "forms.fields.teacher.label": "主讲人",
   "forms.fields.studentsassistant.label": "学生与助教",
   "forms.fields.student.label": "学生",

+ 3 - 2
dashboard/src/locales/zh-Hans/index.ts

@@ -1,6 +1,5 @@
 import forms from "./forms";
 import buttons from "./buttons";
-import tables from "./tables";
 import nut from "./nut";
 import channel from "./channel";
 import dict from "./dict";
@@ -14,6 +13,8 @@ import auth from "./auth";
 import course from "./course";
 import message from "./message";
 import label from "./label";
+import error from "./error";
+
 const items = {
   "columns.library.title": "藏经阁",
   "columns.library.community.title": "社区",
@@ -50,7 +51,6 @@ const items = {
   "columns.studio.attachment.title": "网盘",
   ...buttons,
   ...forms,
-  ...tables,
   ...nut,
   ...channel,
   ...dict,
@@ -64,6 +64,7 @@ const items = {
   ...course,
   ...message,
   ...label,
+  ...error,
 };
 
 export default items;

+ 2 - 0
dashboard/src/locales/zh-Hans/label.ts

@@ -41,6 +41,8 @@ const items = {
   "labels.dict.pass.2": "查词干",
   "labels.dict.pass.3": "查衍生",
   "labels.dict.pass.4": "查二次衍生",
+  "labels.software.edition.basic": "基本版",
+  "labels.software.edition.pro": "增强版",
 };
 
 export default items;

+ 0 - 16
dashboard/src/locales/zh-Hans/tables.ts

@@ -1,16 +0,0 @@
-const items = {
-  "tables.publicity.all": "全部",
-  "tables.publicity.disable": "禁用",
-  "tables.publicity.private": "私有",
-  "tables.publicity.public.bylink": "链接公开",
-  "tables.publicity.public": "公开",
-  "tables.publicity.public.edit": "公开可编辑",
-  "tables.role.all": "全部",
-  "tables.role.owner": "拥有者",
-  "tables.role.manager": "管理员",
-  "tables.role.editor": "编辑",
-  "tables.role.member": "成员",
-  "tables.progress.label": "进度",
-};
-
-export default items;

+ 15 - 0
dashboard/src/pages/admin/invite/index.tsx

@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+import { styleStudioContent } from "../../studio/style";
+
+const { Content } = Layout;
+
+const Widget = () => {
+  return (
+    <Content style={styleStudioContent}>
+      <Outlet />
+    </Content>
+  );
+};
+
+export default Widget;

+ 7 - 0
dashboard/src/pages/admin/invite/list.tsx

@@ -0,0 +1,7 @@
+import InviteList from "../../../components/invite/InviteList";
+
+const Widget = () => {
+  return <InviteList />;
+};
+
+export default Widget;

+ 6 - 1
dashboard/src/pages/studio/course/list.tsx

@@ -39,6 +39,8 @@ import {
   studentCanDo,
 } from "../../../components/course/RolePower";
 import { ISetStatus, setStatus } from "../../../components/course/UserAction";
+import { useAppSelector } from "../../../hooks";
+import { currentUser } from "../../../reducers/current-user";
 
 interface DataItem {
   sn: number;
@@ -86,6 +88,7 @@ const Widget = () => {
   const [studyNumber, setStudyNumber] = useState<number>(0);
   const ref = useRef<ActionType>();
   const [openCreate, setOpenCreate] = useState(false);
+  const user = useAppSelector(currentUser);
 
   useEffect(() => {
     /**
@@ -455,7 +458,9 @@ const Widget = () => {
             }}
           >
             <Button
-              disabled={activeKey !== "create"}
+              disabled={
+                activeKey !== "create" || user?.roles?.includes("basic")
+              }
               key="button"
               icon={<PlusOutlined />}
               type="primary"

+ 2 - 153
dashboard/src/pages/studio/invite/list.tsx

@@ -1,162 +1,11 @@
 import { useParams } from "react-router-dom";
-import { useIntl } from "react-intl";
-import { Button, Popover } from "antd";
-import { ActionType, ProTable } from "@ant-design/pro-components";
-import { UserAddOutlined } from "@ant-design/icons";
 
-import { get } from "../../../request";
-import { RoleValueEnum } from "../../../components/studio/table";
-
-import { useRef, useState } from "react";
-import InviteCreate from "../../../components/invite/InviteCreate";
-import { getSorterUrl } from "../../../utils";
-
-export interface IInviteData {
-  id: string;
-  user_uid: string;
-  email: string;
-  status: string;
-  created_at: string;
-  updated_at: string;
-}
-interface IInviteListResponse {
-  ok: boolean;
-  message: string;
-  data: {
-    rows: IInviteData[];
-    count: number;
-  };
-}
-export interface IInviteResponse {
-  ok: boolean;
-  message: string;
-  data: IInviteData;
-}
-
-interface DataItem {
-  sn: number;
-  id: string;
-  email: string;
-  status: string;
-  created_at: string;
-}
+import InviteList from "../../../components/invite/InviteList";
 
 const Widget = () => {
-  const intl = useIntl(); //i18n
   const { studioname } = useParams(); //url 参数
-  const [openCreate, setOpenCreate] = useState(false);
-
-  const ref = useRef<ActionType>();
-
-  return (
-    <>
-      <ProTable<DataItem>
-        actionRef={ref}
-        columns={[
-          {
-            title: intl.formatMessage({
-              id: "dict.fields.sn.label",
-            }),
-            dataIndex: "sn",
-            key: "sn",
-            width: 50,
-            search: false,
-          },
-          {
-            title: intl.formatMessage({
-              id: "forms.fields.email.label",
-            }),
-            dataIndex: "email",
-            key: "email",
-          },
-          {
-            title: intl.formatMessage({
-              id: "forms.fields.status.label",
-            }),
-            dataIndex: "status",
-            key: "status",
-            width: 100,
-            search: false,
-            filters: true,
-            onFilter: true,
-            valueEnum: RoleValueEnum(),
-          },
-          {
-            title: intl.formatMessage({
-              id: "forms.fields.created-at.label",
-            }),
-            key: "created_at",
-            width: 100,
-            search: false,
-            dataIndex: "created_at",
-            valueType: "date",
-          },
-        ]}
-        request={async (params = {}, sorter, filter) => {
-          console.log(params, sorter, filter);
-          let url = `/v2/invite?view=studio&studio=${studioname}`;
-          const offset =
-            ((params.current ? params.current : 1) - 1) *
-            (params.pageSize ? params.pageSize : 20);
-          url += `&limit=${params.pageSize}&offset=${offset}`;
-          url += params.keyword ? "&search=" + params.keyword : "";
-
-          url += getSorterUrl(sorter);
 
-          console.log(url);
-          const res = await get<IInviteListResponse>(url);
-          const items: DataItem[] = res.data.rows.map((item, id) => {
-            return {
-              sn: id + offset + 1,
-              id: item.id,
-              email: item.email,
-              status: item.status,
-              created_at: item.created_at,
-            };
-          });
-          console.log(items);
-          return {
-            total: res.data.count,
-            succcess: true,
-            data: items,
-          };
-        }}
-        rowKey="id"
-        bordered
-        pagination={{
-          showQuickJumper: true,
-          showSizeChanger: true,
-        }}
-        search={false}
-        options={{
-          search: true,
-        }}
-        toolBarRender={() => [
-          <Popover
-            content={
-              <InviteCreate
-                studio={studioname}
-                onCreate={() => {
-                  setOpenCreate(false);
-                  ref.current?.reload();
-                }}
-              />
-            }
-            placement="bottomRight"
-            trigger="click"
-            open={openCreate}
-            onOpenChange={(open: boolean) => {
-              setOpenCreate(open);
-            }}
-          >
-            <Button key="button" icon={<UserAddOutlined />} type="primary">
-              {intl.formatMessage({ id: "buttons.invite" })}
-            </Button>
-          </Popover>,
-        ]}
-      />
-    </>
-  );
+  return <InviteList studioName={studioname} />;
 };
 
 export default Widget;

+ 25 - 0
dashboard/src/pages/users/index.tsx

@@ -0,0 +1,25 @@
+import { Outlet } from "react-router-dom";
+import UiLangSelect from "../../components/general/UiLangSelect";
+import { Footer } from "antd/lib/layout/layout";
+
+const Widget = () => {
+  return (
+    <div style={{ minHeight: "100vh" }}>
+      <div style={{ textAlign: "right", backgroundColor: "#3e3e3e" }}>
+        <UiLangSelect />
+      </div>
+      <div
+        style={{
+          minHeight: "100vh",
+          paddingTop: "1em",
+          backgroundColor: "#3e3e3e",
+        }}
+      >
+        <Outlet />
+      </div>
+      <Footer />
+    </div>
+  );
+};
+
+export default Widget;

+ 22 - 0
dashboard/src/pages/users/sign-up.tsx

@@ -0,0 +1,22 @@
+import { Card } from "antd";
+
+import SignUp from "../../components/users/SignUp";
+
+const Widget = () => {
+  return (
+    <div
+      style={{
+        width: 1000,
+        maxWidth: "100%",
+        marginLeft: "auto",
+        marginRight: "auto",
+      }}
+    >
+      <Card title="注册">
+        <SignUp />
+      </Card>
+    </div>
+  );
+};
+
+export default Widget;