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

Merge pull request #1948 from visuddhinanda/agile

添加角色支持
visuddhinanda 2 лет назад
Родитель
Сommit
61e0b02faa

+ 8 - 0
dashboard/src/Router.tsx

@@ -27,6 +27,10 @@ import AdminDictionaryList from "./pages/admin/dictionary/list";
 import AdminApi from "./pages/admin/api";
 import AdminApiDashboard from "./pages/admin/api/dashboard";
 
+import AdminUsers from "./pages/admin/users";
+import AdminUsersList from "./pages/admin/users/list";
+import AdminUsersShow from "./pages/admin/users/show";
+
 import LibraryHome from "./pages/library";
 
 import LibraryCommunity from "./pages/library/community";
@@ -152,6 +156,10 @@ const Widget = () => {
           <Route path="dictionary" element={<AdminDictionary />}>
             <Route path="list" element={<AdminDictionaryList />} />
           </Route>
+          <Route path="users" element={<AdminUsers />}>
+            <Route path="list" element={<AdminUsersList />} />
+            <Route path="show/:id" element={<AdminUsersShow />} />
+          </Route>
         </Route>
         <Route path="anonymous" element={<Anonymous />}>
           <Route path="users">

Разница между файлами не показана из-за своего большого размера
+ 11 - 0
dashboard/src/assets/icon/index.tsx


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

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

+ 12 - 1
dashboard/src/components/api/Auth.ts

@@ -16,6 +16,7 @@ export interface IUserRequest {
   nickName?: string;
   email?: string;
   avatar?: string;
+  roles?: string[];
 }
 
 export interface IUserListResponse {
@@ -26,7 +27,14 @@ export interface IUserListResponse {
     count: number;
   };
 }
-
+export interface IUserListResponse2 {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IUserApiData[];
+    count: number;
+  };
+}
 export interface IUserResponse {
   ok: boolean;
   message: string;
@@ -40,6 +48,9 @@ export interface IUserApiData {
   email: string;
   avatar?: string;
   avatarName?: string;
+  role: string[];
+  created_at?: string;
+  updated_at?: string;
 }
 
 export interface IStudioApiResponse {

+ 74 - 0
dashboard/src/components/auth/Account.tsx

@@ -0,0 +1,74 @@
+import {
+  ProForm,
+  ProFormSelect,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { get, put } from "../../request";
+import { IUserApiData, IUserRequest, IUserResponse } from "../api/Auth";
+
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  userId?: string;
+  onLoad?: Function;
+}
+
+const AccountWidget = ({ userId, onLoad }: IWidget) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IUserApiData>
+      onFinish={async (values: IUserApiData) => {
+        console.log(values);
+
+        const url = `/v2/user/${userId}`;
+        const postData = {
+          roles: values.role,
+        };
+        console.log("account put ", url, postData);
+        const res = await put<IUserRequest, IUserResponse>(url, postData);
+
+        if (res.ok) {
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        } else {
+          message.error(res.message);
+        }
+      }}
+      params={{}}
+      request={async () => {
+        const url = `/v2/user/${userId}`;
+        console.log("url", url);
+        const res = await get<IUserResponse>(url);
+        if (res.ok) {
+          if (typeof onLoad !== "undefined") {
+            onLoad(res.data);
+          }
+        }
+        return res.data;
+      }}
+    >
+      <ProFormText width="md" readonly name="userName" label="User Name" />
+      <ProFormText width="md" readonly name="nickName" label="Nick Name" />
+      <ProFormText width="md" readonly name="email" label="Email" />
+      <ProFormSelect
+        options={["administrator", "member", "uploader"].map((item) => {
+          return {
+            value: item,
+            label: item,
+          };
+        })}
+        fieldProps={{
+          mode: "tags",
+        }}
+        width="md"
+        name="role"
+        allowClear={true}
+        label={intl.formatMessage({ id: "forms.fields.role.label" })}
+      />
+    </ProForm>
+  );
+};
+
+export default AccountWidget;

+ 47 - 45
dashboard/src/components/auth/Avatar.tsx

@@ -34,52 +34,54 @@ const AvatarWidget = ({ style, placement = "bottomRight" }: IWidget) => {
     setNickName(user?.nickName);
   }, [user]);
 
-  const UserCard = () => (
-    <ProCard
-      style={{ maxWidth: 500, minWidth: 300 }}
-      actions={[
-        <Tooltip
-          title={intl.formatMessage({
-            id: "buttons.setting",
-          })}
-        >
-          <SettingModal trigger={<SettingOutlined key="setting" />} />
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: "columns.library.blog.label",
-          })}
-        >
-          <Link to={`/blog/${userName}/overview`}>
-            <HomeOutlined key="home" />
-          </Link>
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: "buttons.sign-out",
-          })}
-        >
-          <LogoutOutlined
-            key="logout"
-            onClick={() => {
-              sessionStorage.removeItem("token");
-              localStorage.removeItem("token");
-              navigate("/anonymous/users/sign-in");
-            }}
-          />
-        </Tooltip>,
-      ]}
-    >
-      <div>
-        <Title level={4}>{nickName}</Title>
-        <div style={{ textAlign: "right" }}>
-          {intl.formatMessage({
-            id: "buttons.welcome",
-          })}
+  const UserCard = () => {
+    return (
+      <ProCard
+        style={{ maxWidth: 500, minWidth: 300 }}
+        actions={[
+          <Tooltip
+            title={intl.formatMessage({
+              id: "buttons.setting",
+            })}
+          >
+            <SettingModal trigger={<SettingOutlined key="setting" />} />
+          </Tooltip>,
+          <Tooltip
+            title={intl.formatMessage({
+              id: "columns.library.blog.label",
+            })}
+          >
+            <Link to={`/blog/${userName}/overview`}>
+              <HomeOutlined key="home" />
+            </Link>
+          </Tooltip>,
+          <Tooltip
+            title={intl.formatMessage({
+              id: "buttons.sign-out",
+            })}
+          >
+            <LogoutOutlined
+              key="logout"
+              onClick={() => {
+                sessionStorage.removeItem("token");
+                localStorage.removeItem("token");
+                navigate("/anonymous/users/sign-in");
+              }}
+            />
+          </Tooltip>,
+        ]}
+      >
+        <div>
+          <Title level={4}>{nickName}</Title>
+          <div style={{ textAlign: "right" }}>
+            {intl.formatMessage({
+              id: "buttons.welcome",
+            })}
+          </div>
         </div>
-      </div>
-    </ProCard>
-  );
+      </ProCard>
+    );
+  };
   const Login = () => <Link to="/anonymous/users/sign-in">登录</Link>;
   return (
     <>

+ 36 - 11
dashboard/src/components/auth/SignInAvatar.tsx

@@ -14,16 +14,25 @@ import {
 
 import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
+import { TooltipPlacement } from "antd/lib/tooltip";
+import SettingModal from "./setting/SettingModal";
+import { AdminIcon } from "../../assets/icon";
 
 const { Title, Paragraph } = Typography;
 
-const SignInAvatarWidget = () => {
+interface IWidget {
+  placement?: TooltipPlacement;
+  style?: React.CSSProperties;
+}
+
+const SignInAvatarWidget = ({ style, placement = "bottomRight" }: IWidget) => {
   const intl = useIntl();
   const navigate = useNavigate();
   const [userName, setUserName] = useState<string>();
   const [nickName, setNickName] = useState<string>();
   const user = useAppSelector(_currentUser);
 
+  console.debug("user", user);
   useEffect(() => {
     setUserName(user?.realName);
     setNickName(user?.nickName);
@@ -44,8 +53,22 @@ const SignInAvatarWidget = () => {
                     id: "buttons.setting",
                   })}
                 >
-                  <SettingOutlined key="setting" />
+                  <SettingModal trigger={<SettingOutlined key="setting" />} />
                 </Tooltip>,
+                user.roles?.includes("root") ||
+                user.roles?.includes("administrator") ? (
+                  <Tooltip
+                    title={intl.formatMessage({
+                      id: "buttons.admin",
+                    })}
+                  >
+                    <Link to={`/admin`}>
+                      <AdminIcon />
+                    </Link>
+                  </Tooltip>
+                ) : (
+                  <></>
+                ),
                 <Tooltip
                   title={intl.formatMessage({
                     id: "columns.library.blog.label",
@@ -81,16 +104,18 @@ const SignInAvatarWidget = () => {
               </Paragraph>
             </ProCard>
           }
-          placement="bottomRight"
+          placement={placement}
         >
-          <Avatar
-            style={{ backgroundColor: "#87d068" }}
-            icon={<UserOutlined />}
-            src={user?.avatar}
-            size="small"
-          >
-            {nickName?.slice(0, 2)}
-          </Avatar>
+          <span style={style}>
+            <Avatar
+              style={{ backgroundColor: "#87d068" }}
+              icon={<UserOutlined />}
+              src={user?.avatar}
+              size="small"
+            >
+              {nickName?.slice(0, 2)}
+            </Avatar>
+          </span>
         </Popover>
       </>
     );

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

@@ -24,7 +24,9 @@ const UserWidget = ({
   return (
     <Space>
       {showAvatar ? (
-        <Avatar size="small">{nickName?.slice(0, 1)}</Avatar>
+        <Avatar size="small" src={avatar}>
+          {nickName?.slice(0, 2)}
+        </Avatar>
       ) : undefined}
       {showName ? nickName : undefined}
     </Space>

+ 5 - 0
dashboard/src/components/studio/LeftSider.tsx

@@ -9,6 +9,8 @@ import {
   HomeOutlined,
   TeamOutlined,
 } from "@ant-design/icons";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 const { Sider } = Layout;
 
@@ -21,6 +23,8 @@ type IWidgetHeadBar = {
 };
 const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
   //Library head bar
+  const user = useAppSelector(currentUser);
+
   const intl = useIntl(); //i18n
   const { studioname } = useParams();
   const linkRecent = "/studio/" + studioname + "/recent/list";
@@ -146,6 +150,7 @@ const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
             </Link>
           ),
           key: "attachment",
+          disabled: user?.roles?.includes("uploader") ? false : true,
         },
         {
           label: (

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

@@ -84,6 +84,7 @@ const items = {
   "buttons.open.in.new.tab": "Open in New Tab",
   "buttons.add_to_anthology": "Add to Anthology",
   "buttons.open.in.studio": "Open in Studio",
+  "buttons.admin": "admin",
 };
 
 export default items;

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

@@ -84,6 +84,7 @@ const items = {
   "buttons.open.in.new.tab": "在新标签页中打开",
   "buttons.add_to_anthology": "添加到文集",
   "buttons.open.in.studio": "在Studio中打开",
+  "buttons.admin": "后台管理",
 };
 
 export default items;

+ 15 - 2
dashboard/src/pages/admin/index.tsx

@@ -1,15 +1,28 @@
-import { Layout } from "antd";
+import { Layout, Result } from "antd";
 import { Outlet } from "react-router-dom";
 import LeftSider from "../../components/admin/LeftSider";
 import HeadBar from "../../components/studio/HeadBar";
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
 
 const Widget = () => {
+  const user = useAppSelector(currentUser);
+
   return (
     <div>
       <HeadBar />
       <Layout>
         <LeftSider />
-        <Outlet />
+        {user?.roles?.includes("root") ||
+        user?.roles?.includes("administrator") ? (
+          <Outlet />
+        ) : (
+          <Result
+            status="403"
+            title="403"
+            subTitle="Sorry, you are not authorized to access this page."
+          />
+        )}
       </Layout>
     </div>
   );

+ 15 - 0
dashboard/src/pages/admin/users/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;

+ 176 - 0
dashboard/src/pages/admin/users/list.tsx

@@ -0,0 +1,176 @@
+import { Button, Space, Dropdown, Typography, Tag } from "antd";
+import { MoreOutlined } from "@ant-design/icons";
+
+import { ActionType, ProList } from "@ant-design/pro-components";
+
+import { get } from "../../../request";
+import { useRef } from "react";
+
+import { getSorterUrl } from "../../../utils";
+
+import { IUserApiData, IUserListResponse2 } from "../../../components/api/Auth";
+import { Link } from "react-router-dom";
+import TimeShow from "../../../components/general/TimeShow";
+import User from "../../../components/auth/User";
+
+const { Text } = Typography;
+
+interface IParams {
+  content_type?: string;
+}
+
+const UsersWidget = () => {
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProList<IUserApiData, IParams>
+        actionRef={ref}
+        metas={{
+          title: {
+            dataIndex: "title",
+            search: false,
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <Link to={`/admin/users/show/${entity.id}`}>
+                  {entity.nickName}
+                </Link>
+              );
+            },
+          },
+          description: {
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <Text type="secondary">
+                  <Space>
+                    {entity.userName}
+                    <TimeShow
+                      type="secondary"
+                      createdAt={entity.created_at}
+                      updatedAt={entity.updated_at}
+                    />
+                  </Space>
+                </Text>
+              );
+            },
+            editable: false,
+            search: false,
+          },
+          subTitle: {
+            render: (dom, entity, index, action, schema) => {
+              return entity.role ? (
+                <Space>
+                  {entity.role.map((item, id) => {
+                    return <Tag>{item}</Tag>;
+                  })}
+                </Space>
+              ) : (
+                <></>
+              );
+            },
+          },
+          avatar: {
+            editable: false,
+            search: false,
+            render: (dom, entity, index, action, schema) => {
+              return (
+                <User
+                  avatar={entity.avatar}
+                  userName={entity.userName}
+                  nickName={entity.nickName}
+                  showName={false}
+                />
+              );
+            },
+          },
+          actions: {
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown
+                  menu={{
+                    items: [
+                      { label: "替换", key: "replace" },
+                      { label: "引用模版", key: "tpl" },
+                      { label: "删除", key: "delete", danger: true },
+                    ],
+                    onClick: (e) => {
+                      console.log("click ", e.key);
+                    },
+                  }}
+                  placement="bottomRight"
+                >
+                  <Button
+                    type="link"
+                    size="small"
+                    icon={<MoreOutlined />}
+                    onClick={(e) => e.preventDefault()}
+                  />
+                </Dropdown>,
+              ];
+            },
+          },
+          content_type: {
+            // 自己扩展的字段,主要用于筛选,不在列表中显示
+            title: "类型",
+            valueType: "select",
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              admin: {
+                text: "管理员",
+                status: "Error",
+              },
+              uploader: {
+                text: "上传",
+                status: "Success",
+              },
+              member: {
+                text: "会员",
+                status: "Processing",
+              },
+              user: {
+                text: "用户",
+                status: "Processing",
+              },
+            },
+          },
+        }}
+        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/user?view=all";
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          url += getSorterUrl(sorter);
+
+          console.log(url);
+          const res = await get<IUserListResponse2>(url);
+          return {
+            total: res.data.count,
+            success: res.ok,
+            data: res.data.rows,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={{
+          filterType: "light",
+        }}
+        options={{
+          search: true,
+        }}
+        headerTitle=""
+      />
+    </>
+  );
+};
+
+export default UsersWidget;

+ 39 - 0
dashboard/src/pages/admin/users/show.tsx

@@ -0,0 +1,39 @@
+import { useState } from "react";
+import { useParams } from "react-router-dom";
+import { Card, Tabs } from "antd";
+
+import GoBack from "../../../components/studio/GoBack";
+import Account from "../../../components/auth/Account";
+import { IUserApiData } from "../../../components/api/Auth";
+
+const Widget = () => {
+  const { id } = useParams(); //url 参数
+  const [title, setTitle] = useState("");
+
+  return (
+    <Card title={<GoBack to={`/admin/users/list`} title={title} />}>
+      <Tabs
+        size="small"
+        items={[
+          {
+            label: `基本`,
+            key: "basic",
+            children: (
+              <Account
+                userId={id}
+                onLoad={(value: IUserApiData) => setTitle(value.nickName)}
+              />
+            ),
+          },
+          {
+            label: `统计`,
+            key: "term",
+            children: <></>,
+          },
+        ]}
+      />
+    </Card>
+  );
+};
+
+export default Widget;

+ 3 - 3
dashboard/src/reducers/current-user.ts

@@ -39,7 +39,7 @@ export interface IUser {
   nickName: string;
   realName: string;
   avatar: string;
-  roles: string[];
+  roles: string[] | null;
 }
 
 interface IState {
@@ -70,9 +70,9 @@ export const slice = createSlice({
 export const { signIn, signOut, guest } = slice.actions;
 
 export const isRoot = (state: RootState): boolean =>
-  state.currentUser.payload?.roles.includes(ROLE_ROOT) || false;
+  state.currentUser.payload?.roles?.includes(ROLE_ROOT) || false;
 export const isAdministrator = (state: RootState): boolean =>
-  state.currentUser.payload?.roles.includes(ROLE_ADMINISTRATOR) || false;
+  state.currentUser.payload?.roles?.includes(ROLE_ADMINISTRATOR) || false;
 export const currentUser = (state: RootState): IUser | undefined =>
   state.currentUser.payload;
 export const isGuest = (state: RootState): boolean | undefined =>

Некоторые файлы не были показаны из-за большого количества измененных файлов