Browse Source

更新用户管理界面UI

CaIon 2 năm trước cách đây
mục cha
commit
b140b326b8

+ 0 - 43
web/src/App.js

@@ -8,7 +8,6 @@ import LoginForm from './components/LoginForm';
 import NotFound from './pages/NotFound';
 import Setting from './pages/Setting';
 import EditUser from './pages/User/EditUser';
-import AddUser from './pages/User/AddUser';
 import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
 import PasswordResetForm from './components/PasswordResetForm';
 import GitHubOAuth from './components/GitHubOAuth';
@@ -17,10 +16,8 @@ import { UserContext } from './context/User';
 import { StatusContext } from './context/Status';
 import Channel from './pages/Channel';
 import Token from './pages/Token';
-import EditToken from './pages/Token/EditToken';
 import EditChannel from './pages/Channel/EditChannel';
 import Redemption from './pages/Redemption';
-import EditRedemption from './pages/Redemption/EditRedemption';
 import TopUp from './pages/TopUp';
 import Log from './pages/Log';
 import Chat from './pages/Chat';
@@ -131,22 +128,6 @@ function App() {
                         </PrivateRoute>
                     }
                 />
-                <Route
-                    path='/token/edit/:id'
-                    element={
-                        <Suspense fallback={<Loading></Loading>}>
-                            <EditToken />
-                        </Suspense>
-                    }
-                />
-                <Route
-                    path='/token/add'
-                    element={
-                        <Suspense fallback={<Loading></Loading>}>
-                            <EditToken />
-                        </Suspense>
-                    }
-                />
                 <Route
                     path='/redemption'
                     element={
@@ -155,22 +136,6 @@ function App() {
                         </PrivateRoute>
                     }
                 />
-                <Route
-                    path='/redemption/edit/:id'
-                    element={
-                        <Suspense fallback={<Loading></Loading>}>
-                            <EditRedemption />
-                        </Suspense>
-                    }
-                />
-                <Route
-                    path='/redemption/add'
-                    element={
-                        <Suspense fallback={<Loading></Loading>}>
-                            <EditRedemption />
-                        </Suspense>
-                    }
-                />
                 <Route
                     path='/user'
                     element={
@@ -195,14 +160,6 @@ function App() {
                         </Suspense>
                     }
                 />
-                <Route
-                    path='/user/add'
-                    element={
-                        <Suspense fallback={<Loading></Loading>}>
-                            <AddUser />
-                        </Suspense>
-                    }
-                />
                 <Route
                     path='/user/reset'
                     element={

+ 0 - 2
web/src/components/RedemptionsTable.js

@@ -1,12 +1,10 @@
 import React, {useEffect, useState} from 'react';
-import {Link} from 'react-router-dom';
 import {API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string} from '../helpers';
 
 import {ITEMS_PER_PAGE} from '../constants';
 import {renderQuota} from '../helpers/render';
 import {Button, Modal, Popconfirm, Popover, Table, Tag, Form} from "@douyinfe/semi-ui";
 import EditRedemption from "../pages/Redemption/EditRedemption";
-import editRedemption from "../pages/Redemption/EditRedemption";
 
 function renderTimestamp(timestamp) {
     return (

+ 294 - 313
web/src/components/UsersTable.js

@@ -1,338 +1,319 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, showError, showSuccess } from '../helpers';
-
-import { ITEMS_PER_PAGE } from '../constants';
-import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render';
+import React, {useEffect, useState} from 'react';
+import {API, isAdmin, showError, showSuccess} from '../helpers';
+import {Button, Modal, Popconfirm, Popover, Table, Tag, Form, Tooltip, Space} from "@douyinfe/semi-ui";
+import {ITEMS_PER_PAGE} from '../constants';
+import {renderGroup, renderNumber, renderQuota, renderText, stringToColor} from '../helpers/render';
+import AddUser from "../pages/User/AddUser";
+import EditUser from "../pages/User/EditUser";
 
 function renderRole(role) {
-  switch (role) {
-    case 1:
-      return <Label>普通用户</Label>;
-    case 10:
-      return <Label color='yellow'>管理员</Label>;
-    case 100:
-      return <Label color='orange'>超级管理员</Label>;
-    default:
-      return <Label color='red'>未知身份</Label>;
-  }
+    switch (role) {
+        case 1:
+            return <Tag size='large'>普通用户</Tag>;
+        case 10:
+            return <Tag color='yellow' size='large'>管理员</Tag>;
+        case 100:
+            return <Tag color='orange' size='large'>超级管理员</Tag>;
+        default:
+            return <Tag color='red' size='large'>未知身份</Tag>;
+    }
 }
 
 const UsersTable = () => {
-  const [users, setUsers] = useState([]);
-  const [loading, setLoading] = useState(true);
-  const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searching, setSearching] = useState(false);
+    const columns = [{
+        title: 'ID', dataIndex: 'id',
+    }, {
+        title: '用户名', dataIndex: 'username',
+    }, {
+        title: '分组', dataIndex: 'group', render: (text, record, index) => {
+            return (<div>
+                {renderGroup(text)}
+            </div>);
+        },
+    }, {
+        title: '统计信息', dataIndex: 'info', render: (text, record, index) => {
+            return (<div>
+                <Space spacing={1}>
+                    <Tooltip content={'剩余额度'}>
+                        <Tag color='white' size='large'>{renderQuota(record.quota)}</Tag>
+                    </Tooltip>
+                    <Tooltip content={'已用额度'}>
+                        <Tag color='white' size='large'>{renderQuota(record.used_quota)}</Tag>
+                    </Tooltip>
+                    <Tooltip content={'调用次数'}>
+                        <Tag color='white' size='large'>{renderNumber(record.request_count)}</Tag>
+                    </Tooltip>
+                </Space>
+            </div>);
+        }
+    }, {
+        title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => {
+            return (<div>
+                <Space spacing={1}>
+                    <Tooltip content={'邀请人数'}>
+                        <Tag color='white' size='large'>{renderNumber(record.aff_count)}</Tag>
+                    </Tooltip>
+                    <Tooltip content={'邀请总收益'}>
+                        <Tag color='white' size='large'>{renderQuota(record.aff_history_quota)}</Tag>
+                    </Tooltip>
+                    <Tooltip content={'邀请人ID'}>
+                        {record.inviter_id === 0 ? <Tag color='white' size='large'>无</Tag> :
+                            <Tag color='white' size='large'>{record.inviter_id}</Tag>}
+                    </Tooltip>
+                </Space>
+            </div>);
+        }
+    }, {
+        title: '角色', dataIndex: 'role', render: (text, record, index) => {
+            return (<div>
+                {renderRole(text)}
+            </div>);
+        },
+    }, {
+        title: '状态', dataIndex: 'status', render: (text, record, index) => {
+            return (<div>
+                {renderStatus(text)}
+            </div>);
+        },
+    }, {
+        title: '', dataIndex: 'operate', render: (text, record, index) => (<div>
+            <Popconfirm
+                title="确定?"
+                okType={'warning'}
+                position={'left'}
+                onConfirm={() => {
+                    manageUser(record.username, 'promote', record)
+                }}
+            >
+                <Button theme='light' type='warning' style={{marginRight: 1}}>提升</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="确定?"
+                okType={'warning'}
+                position={'left'}
+                onConfirm={() => {
+                    manageUser(record.username, 'demote', record)
+                }}
+            >
+                <Button theme='light' type='secondary' style={{marginRight: 1}}>降级</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="确定是否要删除此用户?"
+                content="此修改将不可逆"
+                okType={'danger'}
+                position={'left'}
+                onConfirm={() => {
+                    manageUser(record.username, 'delete', record).then(() => {
+                        removeRecord(record.id);
+                    })
+                }}
+            >
+                <Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
+            </Popconfirm>
+            {record.status === 1 ?
+                <Button theme='light' type='warning' style={{marginRight: 1}} onClick={async () => {
+                    manageUser(record.username, 'disable', record)
+                }}>禁用</Button> :
+                <Button theme='light' type='secondary' style={{marginRight: 1}} onClick={async () => {
+                    manageUser(record.username, 'enable', record);
+                }} disabled={record.status === 3}>启用</Button>}
+            <Button theme='light' type='tertiary' style={{marginRight: 1}} onClick={() => {
+                setEditingUser(record);
+                setShowEditUser(true);
+            }}>编辑</Button>
+        </div>),
+    },];
 
-  const loadUsers = async (startIdx) => {
-    const res = await API.get(`/api/user/?p=${startIdx}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      if (startIdx === 0) {
-        setUsers(data);
-      } else {
-        let newUsers = users;
-        newUsers.push(...data);
-        setUsers(newUsers);
-      }
-    } else {
-      showError(message);
+    const [users, setUsers] = useState([]);
+    const [loading, setLoading] = useState(true);
+    const [activePage, setActivePage] = useState(1);
+    const [searchKeyword, setSearchKeyword] = useState('');
+    const [searching, setSearching] = useState(false);
+    const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
+    const [showAddUser, setShowAddUser] = useState(false);
+    const [showEditUser, setShowEditUser] = useState(false);
+    const [editingUser, setEditingUser] = useState({
+        id: undefined,
+    });
+
+    const setCount = (data) => {
+        if (data.length >= (activePage) * ITEMS_PER_PAGE) {
+            setUserCount(data.length + 1);
+        } else {
+            setUserCount(data.length);
+        }
     }
-    setLoading(false);
-  };
 
-  const onPaginationChange = (e, { activePage }) => {
-    (async () => {
-      if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
-        // In this case we have to load more data and then append them.
-        await loadUsers(activePage - 1);
-      }
-      setActivePage(activePage);
-    })();
-  };
+    const removeRecord = key => {
+        console.log(key);
+        let newDataSource = [...users];
+        if (key != null) {
+            let idx = newDataSource.findIndex(data => data.id === key);
 
-  useEffect(() => {
-    loadUsers(0)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-  }, []);
+            if (idx > -1) {
+                newDataSource.splice(idx, 1);
+                setUsers(newDataSource);
+            }
+        }
+    };
 
-  const manageUser = (username, action, idx) => {
-    (async () => {
-      const res = await API.post('/api/user/manage', {
-        username,
-        action
-      });
-      const { success, message } = res.data;
-      if (success) {
-        showSuccess('操作成功完成!');
-        let user = res.data.data;
-        let newUsers = [...users];
-        let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
-        if (action === 'delete') {
-          newUsers[realIdx].deleted = true;
+    const loadUsers = async (startIdx) => {
+        const res = await API.get(`/api/user/?p=${startIdx}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            if (startIdx === 0) {
+                setUsers(data);
+                setCount(data);
+            } else {
+                let newUsers = users;
+                newUsers.push(...data);
+                setUsers(newUsers);
+                setCount(newUsers);
+            }
         } else {
-          newUsers[realIdx].status = user.status;
-          newUsers[realIdx].role = user.role;
+            showError(message);
         }
-        setUsers(newUsers);
-      } else {
-        showError(message);
-      }
-    })();
-  };
+        setLoading(false);
+    };
 
-  const renderStatus = (status) => {
-    switch (status) {
-      case 1:
-        return <Label basic>已激活</Label>;
-      case 2:
-        return (
-          <Label basic color='red'>
-            已封禁
-          </Label>
-        );
-      default:
-        return (
-          <Label basic color='grey'>
-            未知状态
-          </Label>
-        );
-    }
-  };
+    const onPaginationChange = (e, {activePage}) => {
+        (async () => {
+            if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
+                // In this case we have to load more data and then append them.
+                await loadUsers(activePage - 1);
+            }
+            setActivePage(activePage);
+        })();
+    };
 
-  const searchUsers = async () => {
-    if (searchKeyword === '') {
-      // if keyword is blank, load files instead.
-      await loadUsers(0);
-      setActivePage(1);
-      return;
-    }
-    setSearching(true);
-    const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      setUsers(data);
-      setActivePage(1);
-    } else {
-      showError(message);
-    }
-    setSearching(false);
-  };
+    useEffect(() => {
+        loadUsers(0)
+            .then()
+            .catch((reason) => {
+                showError(reason);
+            });
+    }, []);
 
-  const handleKeywordChange = async (e, { value }) => {
-    setSearchKeyword(value.trim());
-  };
+    const manageUser = async (username, action, record) => {
+        const res = await API.post('/api/user/manage', {
+            username, action
+        });
+        const {success, message} = res.data;
+        if (success) {
+            showSuccess('操作成功完成!');
+            let user = res.data.data;
+            let newUsers = [...users];
+            if (action === 'delete') {
 
-  const sortUser = (key) => {
-    if (users.length === 0) return;
-    setLoading(true);
-    let sortedUsers = [...users];
-    sortedUsers.sort((a, b) => {
-      return ('' + a[key]).localeCompare(b[key]);
-    });
-    if (sortedUsers[0].id === users[0].id) {
-      sortedUsers.reverse();
+            } else {
+                record.status = user.status;
+                record.role = user.role;
+            }
+            setUsers(newUsers);
+        } else {
+            showError(message);
+        }
+    };
+
+    const renderStatus = (status) => {
+        switch (status) {
+            case 1:
+                return <Tag size='large'>已激活</Tag>;
+            case 2:
+                return (<Tag size='large' color='red'>
+                    已封禁
+                </Tag>);
+            default:
+                return (<Tag size='large' color='grey'>
+                    未知状态
+                </Tag>);
+        }
+    };
+
+    const searchUsers = async () => {
+        if (searchKeyword === '') {
+            // if keyword is blank, load files instead.
+            await loadUsers(0);
+            setActivePage(1);
+            return;
+        }
+        setSearching(true);
+        const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            setUsers(data);
+            setActivePage(1);
+        } else {
+            showError(message);
+        }
+        setSearching(false);
+    };
+
+    const handleKeywordChange = async (value) => {
+        setSearchKeyword(value.trim());
+    };
+
+    const sortUser = (key) => {
+        if (users.length === 0) return;
+        setLoading(true);
+        let sortedUsers = [...users];
+        sortedUsers.sort((a, b) => {
+            return ('' + a[key]).localeCompare(b[key]);
+        });
+        if (sortedUsers[0].id === users[0].id) {
+            sortedUsers.reverse();
+        }
+        setUsers(sortedUsers);
+        setLoading(false);
+    };
+
+    const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
+
+    const closeAddUser = () => {
+        setShowAddUser(false);
     }
-    setUsers(sortedUsers);
-    setLoading(false);
-  };
 
-  return (
-    <>
-      <Form onSubmit={searchUsers}>
-        <Form.Input
-          icon='search'
-          fluid
-          iconPosition='left'
-          placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
-          value={searchKeyword}
-          loading={searching}
-          onChange={handleKeywordChange}
-        />
-      </Form>
+    const closeEditUser = () => {
+        setShowEditUser(false);
+        setEditingUser({
+            id: undefined,
+        });
+    }
 
-      <Table basic compact size='small'>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('id');
-              }}
-            >
-              ID
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('username');
-              }}
-            >
-              用户名
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('group');
-              }}
-            >
-              分组
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('quota');
-              }}
-            >
-              统计信息
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('role');
-              }}
-            >
-              用户角色
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortUser('status');
-              }}
-            >
-              状态
-            </Table.HeaderCell>
-            <Table.HeaderCell>操作</Table.HeaderCell>
-          </Table.Row>
-        </Table.Header>
+    const refresh = async () => {
+        await loadUsers(activePage - 1);
+    };
 
-        <Table.Body>
-          {users
-            .slice(
-              (activePage - 1) * ITEMS_PER_PAGE,
-              activePage * ITEMS_PER_PAGE
-            )
-            .map((user, idx) => {
-              if (user.deleted) return <></>;
-              return (
-                <Table.Row key={user.id}>
-                  <Table.Cell>{user.id}</Table.Cell>
-                  <Table.Cell>
-                    <Popup
-                      content={user.email ? user.email : '未绑定邮箱地址'}
-                      key={user.username}
-                      header={user.display_name ? user.display_name : user.username}
-                      trigger={<span>{renderText(user.username, 15)}</span>}
-                      hoverable
-                    />
-                  </Table.Cell>
-                  <Table.Cell>{renderGroup(user.group)}</Table.Cell>
-                  {/*<Table.Cell>*/}
-                  {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
-                  {/*</Table.Cell>*/}
-                  <Table.Cell>
-                    <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
-                    <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
-                    <Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} />
-                  </Table.Cell>
-                  <Table.Cell>{renderRole(user.role)}</Table.Cell>
-                  <Table.Cell>{renderStatus(user.status)}</Table.Cell>
-                  <Table.Cell>
-                    <div>
-                      <Button
-                        size={'small'}
-                        positive
-                        onClick={() => {
-                          manageUser(user.username, 'promote', idx);
-                        }}
-                        disabled={user.role === 100}
-                      >
-                        提升
-                      </Button>
-                      <Button
-                        size={'small'}
-                        color={'yellow'}
-                        onClick={() => {
-                          manageUser(user.username, 'demote', idx);
-                        }}
-                        disabled={user.role === 100}
-                      >
-                        降级
-                      </Button>
-                      <Popup
-                        trigger={
-                          <Button size='small' negative disabled={user.role === 100}>
-                            删除
-                          </Button>
-                        }
-                        on='click'
-                        flowing
-                        hoverable
-                      >
-                        <Button
-                          negative
-                          onClick={() => {
-                            manageUser(user.username, 'delete', idx);
-                          }}
-                        >
-                          删除用户 {user.username}
-                        </Button>
-                      </Popup>
-                      <Button
-                        size={'small'}
-                        onClick={() => {
-                          manageUser(
-                            user.username,
-                            user.status === 1 ? 'disable' : 'enable',
-                            idx
-                          );
-                        }}
-                        disabled={user.role === 100}
-                      >
-                        {user.status === 1 ? '禁用' : '启用'}
-                      </Button>
-                      <Button
-                        size={'small'}
-                        as={Link}
-                        to={'/user/edit/' + user.id}
-                      >
-                        编辑
-                      </Button>
-                    </div>
-                  </Table.Cell>
-                </Table.Row>
-              );
-            })}
-        </Table.Body>
+    return (
+        <>
+            <AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser>
+            <EditUser refresh={refresh} visible={showEditUser} handleClose={closeEditUser} editingUser={editingUser}></EditUser>
+            <Form onSubmit={searchUsers}>
+                <Form.Input
+                    label='搜索关键字'
+                    icon='search'
+                    field='keyword'
+                    iconPosition='left'
+                    placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
+                    value={searchKeyword}
+                    loading={searching}
+                    onChange={value => handleKeywordChange(value)}
+                />
+            </Form>
 
-        <Table.Footer>
-          <Table.Row>
-            <Table.HeaderCell colSpan='7'>
-              <Button size='small' as={Link} to='/user/add' loading={loading}>
-                添加新的用户
-              </Button>
-              <Pagination
-                floated='right'
-                activePage={activePage}
-                onPageChange={onPaginationChange}
-                size='small'
-                siblingRange={1}
-                totalPages={
-                  Math.ceil(users.length / ITEMS_PER_PAGE) +
-                  (users.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
+            <Table columns={columns} dataSource={pageData} pagination={{
+                currentPage: activePage,
+                pageSize: ITEMS_PER_PAGE,
+                total: userCount,
+                pageSizeOpts: [10, 20, 50, 100], // onPageChange: handlePageChange,
+            }} loading={loading}/>
+            <Button theme='light' type='primary' style={{marginRight: 8}} onClick={
+                () => {
+                    setShowAddUser(true);
                 }
-              />
-            </Table.HeaderCell>
-          </Table.Row>
-        </Table.Footer>
-      </Table>
-    </>
-  );
+            }>添加用户</Button>
+        </>
+    );
 };
 
 export default UsersTable;

+ 9 - 4
web/src/helpers/render.js

@@ -1,4 +1,5 @@
 import { Label } from 'semantic-ui-react';
+import {Tag} from "@douyinfe/semi-ui";
 
 export function renderText(text, limit) {
   if (text.length > limit) {
@@ -9,18 +10,22 @@ export function renderText(text, limit) {
 
 export function renderGroup(group) {
   if (group === '') {
-    return <Label>default</Label>;
+    return <Tag size='large'>default</Tag>;
   }
   let groups = group.split(',');
   groups.sort();
   return <>
     {groups.map((group) => {
       if (group === 'vip' || group === 'pro') {
-        return <Label color='yellow'>{group}</Label>;
+        return <Tag size='large' color='yellow'>{group}</Tag>;
       } else if (group === 'svip' || group === 'premium') {
-        return <Label color='red'>{group}</Label>;
+        return <Tag size='large' color='red'>{group}</Tag>;
+      }
+      if (group === 'default') {
+        return <Tag size='large'>{group}</Tag>;
+      } else {
+        return <Tag size='large' color={stringToColor(group)}>{group}</Tag>;
       }
-      return <Label>{group}</Label>;
     })}
   </>;
 }

+ 1 - 2
web/src/pages/Token/EditToken.js

@@ -1,5 +1,4 @@
 import React, {useEffect, useRef, useState} from 'react';
-// import {Button, Form, Header, Message, Segment} from 'semantic-ui-react';
 import {useParams, useNavigate} from 'react-router-dom';
 import {API, isMobile, showError, showSuccess, timestamp2string} from '../../helpers';
 import {renderQuota, renderQuotaWithPrompt} from '../../helpers/render';
@@ -247,7 +246,7 @@ const EditToken = (props) => {
                         disabled={unlimited_quota}
                     />
                     <div style={{marginTop: 20}}>
-                        <Typography.Text>{`新建数量`}</Typography.Text>
+                        <Typography.Text>新建数量</Typography.Text>
                     </div>
                     {!isEdit && (
                         <AutoComplete

+ 90 - 69
web/src/pages/User/AddUser.js

@@ -1,77 +1,98 @@
-import React, { useState } from 'react';
-import { Button, Form, Header, Segment } from 'semantic-ui-react';
-import { API, showError, showSuccess } from '../../helpers';
+import React, {useState} from 'react';
+import {API, isMobile, showError, showSuccess} from '../../helpers';
+import Title from "@douyinfe/semi-ui/lib/es/typography/title";
+import {Button, SideSheet, Space, Input, Spin} from "@douyinfe/semi-ui";
 
-const AddUser = () => {
-  const originInputs = {
-    username: '',
-    display_name: '',
-    password: '',
-  };
-  const [inputs, setInputs] = useState(originInputs);
-  const { username, display_name, password } = inputs;
+const AddUser = (props) => {
+    const originInputs = {
+        username: '',
+        display_name: '',
+        password: '',
+    };
+    const [inputs, setInputs] = useState(originInputs);
+    const [loading, setLoading] = useState(false);
+    const {username, display_name, password} = inputs;
 
-  const handleInputChange = (e, { name, value }) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
-  };
+    const handleInputChange = (name, value) => {
+        setInputs((inputs) => ({...inputs, [name]: value}));
+    };
 
-  const submit = async () => {
-    if (inputs.username === '' || inputs.password === '') return;
-    const res = await API.post(`/api/user/`, inputs);
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess('用户账户创建成功!');
-      setInputs(originInputs);
-    } else {
-      showError(message);
+    const submit = async () => {
+        setLoading(true);
+        if (inputs.username === '' || inputs.password === '') return;
+        const res = await API.post(`/api/user/`, inputs);
+        const {success, message} = res.data;
+        if (success) {
+            showSuccess('用户账户创建成功!');
+            setInputs(originInputs);
+            props.refresh();
+            props.handleClose();
+        } else {
+            showError(message);
+        }
+        setLoading(false);
+    };
+
+    const handleCancel = () => {
+        props.handleClose();
     }
-  };
 
-  return (
-    <>
-      <Segment>
-        <Header as="h3">创建新用户账户</Header>
-        <Form autoComplete="off">
-          <Form.Field>
-            <Form.Input
-              label="用户名"
-              name="username"
-              placeholder={'请输入用户名'}
-              onChange={handleInputChange}
-              value={username}
-              autoComplete="off"
-              required
-            />
-          </Form.Field>
-          <Form.Field>
-            <Form.Input
-              label="显示名称"
-              name="display_name"
-              placeholder={'请输入显示名称'}
-              onChange={handleInputChange}
-              value={display_name}
-              autoComplete="off"
-            />
-          </Form.Field>
-          <Form.Field>
-            <Form.Input
-              label="密码"
-              name="password"
-              type={'password'}
-              placeholder={'请输入密码'}
-              onChange={handleInputChange}
-              value={password}
-              autoComplete="off"
-              required
-            />
-          </Form.Field>
-          <Button positive type={'submit'} onClick={submit}>
-            提交
-          </Button>
-        </Form>
-      </Segment>
-    </>
-  );
+    return (
+        <>
+            <SideSheet
+                placement={'left'}
+                title={<Title level={3}>{'添加用户'}</Title>}
+                headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+                bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+                visible={props.visible}
+                footer={
+                    <div style={{display: 'flex', justifyContent: 'flex-end'}}>
+                        <Space>
+                            <Button theme='solid' size={'large'} onClick={submit}>提交</Button>
+                            <Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
+                        </Space>
+                    </div>
+                }
+                closeIcon={null}
+                onCancel={() => handleCancel()}
+                width={isMobile() ? '100%' : 600}
+            >
+                <Spin spinning={loading}>
+                    <Input
+                        style={{marginTop: 20}}
+                        label="用户名"
+                        name="username"
+                        addonBefore={'用户名'}
+                        placeholder={'请输入用户名'}
+                        onChange={value => handleInputChange('username', value)}
+                        value={username}
+                        autoComplete="off"
+                    />
+                    <Input
+                        style={{marginTop: 20}}
+                        addonBefore={'显示名'}
+                        label="显示名称"
+                        name="display_name"
+                        autoComplete="off"
+                        placeholder={'请输入显示名称'}
+                        onChange={value => handleInputChange('display_name', value)}
+                        value={display_name}
+                    />
+                    <Input
+                        style={{marginTop: 20}}
+                        label="密 码"
+                        name="password"
+                        type={'password'}
+                        addonBefore={'密码'}
+                        placeholder={'请输入密码'}
+                        onChange={value => handleInputChange('password', value)}
+                        value={password}
+                        autoComplete="off"
+                    />
+                </Spin>
+            </SideSheet>
+        </>
+    );
 };
 
 export default AddUser;

+ 129 - 105
web/src/pages/User/EditUser.js

@@ -1,12 +1,12 @@
 import React, { useEffect, useState } from 'react';
-import { Button, Form, Header, Segment } from 'semantic-ui-react';
 import { useParams, useNavigate } from 'react-router-dom';
-import { API, showError, showSuccess } from '../../helpers';
+import {API, isMobile, showError, showSuccess} from '../../helpers';
 import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
+import Title from "@douyinfe/semi-ui/lib/es/typography/title";
+import {SideSheet, Space, Button, Spin, Input, Typography, Select, Divider} from "@douyinfe/semi-ui";
 
-const EditUser = () => {
-  const params = useParams();
-  const userId = params.id;
+const EditUser = (props) => {
+  const userId = props.editingUser.id;
   const [loading, setLoading] = useState(true);
   const [inputs, setInputs] = useState({
     username: '',
@@ -21,15 +21,14 @@ const EditUser = () => {
   const [groupOptions, setGroupOptions] = useState([]);
   const { username, display_name, password, github_id, wechat_id, email, quota, group } =
       inputs;
-  const handleInputChange = (e, { name, value }) => {
+  const handleInputChange = (name, value) => {
     setInputs((inputs) => ({ ...inputs, [name]: value }));
   };
   const fetchGroups = async () => {
     try {
       let res = await API.get(`/api/group/`);
       setGroupOptions(res.data.data.map((group) => ({
-        key: group,
-        text: group,
+        label: group,
         value: group,
       })));
     } catch (error) {
@@ -38,9 +37,10 @@ const EditUser = () => {
   };
   const navigate = useNavigate();
   const handleCancel = () => {
-    navigate("/setting");
+    props.handleClose();
   }
   const loadUser = async () => {
+    setLoading(true);
     let res = undefined;
     if (userId) {
       res = await API.get(`/api/user/${userId}`);
@@ -56,14 +56,16 @@ const EditUser = () => {
     }
     setLoading(false);
   };
+
   useEffect(() => {
     loadUser().then();
     if (userId) {
       fetchGroups().then();
     }
-  }, []);
+  }, [props.editingUser.id]);
 
   const submit = async () => {
+    setLoading(true);
     let res = undefined;
     if (userId) {
       let data = { ...inputs, id: parseInt(userId) };
@@ -77,112 +79,134 @@ const EditUser = () => {
     const { success, message } = res.data;
     if (success) {
       showSuccess('用户信息更新成功!');
+      props.refresh();
+      props.handleClose();
     } else {
       showError(message);
     }
+    setLoading(false);
   };
 
   return (
       <>
-        <Segment loading={loading}>
-          <Header as='h3'>更新用户信息</Header>
-          <Form autoComplete='new-password'>
-            <Form.Field>
-              <Form.Input
-                  label='用户名'
-                  name='username'
-                  placeholder={'请输入新的用户名'}
-                  onChange={handleInputChange}
-                  value={username}
-                  autoComplete='new-password'
-              />
-            </Form.Field>
-            <Form.Field>
-              <Form.Input
-                  label='密码'
-                  name='password'
-                  type={'password'}
-                  placeholder={'请输入新的密码,最短 8 位'}
-                  onChange={handleInputChange}
-                  value={password}
-                  autoComplete='new-password'
-              />
-            </Form.Field>
-            <Form.Field>
-              <Form.Input
-                  label='显示名称'
-                  name='display_name'
-                  placeholder={'请输入新的显示名称'}
-                  onChange={handleInputChange}
-                  value={display_name}
-                  autoComplete='new-password'
-              />
-            </Form.Field>
+        <SideSheet
+            placement={'right'}
+            title={<Title level={3}>{'编辑用户'}</Title>}
+            headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+            bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+            visible={props.visible}
+            footer={
+              <div style={{display: 'flex', justifyContent: 'flex-end'}}>
+                <Space>
+                  <Button theme='solid' size={'large'} onClick={submit}>提交</Button>
+                  <Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
+                </Space>
+              </div>
+            }
+            closeIcon={null}
+            onCancel={() => handleCancel()}
+            width={isMobile() ? '100%' : 600}
+        >
+          <Spin spinning={loading}>
+            <div style={{marginTop: 20}}>
+              <Typography.Text>用户名</Typography.Text>
+            </div>
+            <Input
+                label='用户名'
+                name='username'
+                placeholder={'请输入新的用户名'}
+                onChange={value => handleInputChange('username', value)}
+                value={username}
+                autoComplete='new-password'
+            />
+            <div style={{marginTop: 20}}>
+              <Typography.Text>密码</Typography.Text>
+            </div>
+            <Input
+                label='密码'
+                name='password'
+                type={'password'}
+                placeholder={'请输入新的密码,最短 8 位'}
+                onChange={value => handleInputChange('password', value)}
+                value={password}
+                autoComplete='new-password'
+            />
+            <div style={{marginTop: 20}}>
+              <Typography.Text>显示名称</Typography.Text>
+            </div>
+            <Input
+                label='显示名称'
+                name='display_name'
+                placeholder={'请输入新的显示名称'}
+                onChange={value => handleInputChange('display_name', value)}
+                value={display_name}
+                autoComplete='new-password'
+            />
             {
                 userId && <>
-                  <Form.Field>
-                    <Form.Dropdown
-                        label='分组'
-                        placeholder={'请选择分组'}
-                        name='group'
-                        fluid
-                        search
-                        selection
-                        allowAdditions
-                        additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
-                        onChange={handleInputChange}
-                        value={inputs.group}
-                        autoComplete='new-password'
-                        options={groupOptions}
-                    />
-                  </Form.Field>
-                  <Form.Field>
-                    <Form.Input
-                        label={`剩余额度${renderQuotaWithPrompt(quota)}`}
-                        name='quota'
-                        placeholder={'请输入新的剩余额度'}
-                        onChange={handleInputChange}
-                        value={quota}
-                        type={'number'}
-                        autoComplete='new-password'
-                    />
-                  </Form.Field>
+                  <div style={{marginTop: 20}}>
+                    <Typography.Text>分组</Typography.Text>
+                  </div>
+                  <Select
+                      placeholder={'请选择分组'}
+                      name='group'
+                      fluid
+                      search
+                      selection
+                      allowAdditions
+                      additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
+                      onChange={value => handleInputChange('group', value)}
+                      value={inputs.group}
+                      autoComplete='new-password'
+                      optionList={groupOptions}
+                  />
+                  <div style={{marginTop: 20}}>
+                    <Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
+                  </div>
+                  <Input
+                      name='quota'
+                      placeholder={'请输入新的剩余额度'}
+                      onChange={value => handleInputChange('quota', value)}
+                      value={quota}
+                      type={'number'}
+                      autoComplete='new-password'
+                  />
                 </>
             }
-            <Form.Field>
-              <Form.Input
-                  label='已绑定的 GitHub 账户'
-                  name='github_id'
-                  value={github_id}
-                  autoComplete='new-password'
-                  placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
-                  readOnly
-              />
-            </Form.Field>
-            <Form.Field>
-              <Form.Input
-                  label='已绑定的微信账户'
-                  name='wechat_id'
-                  value={wechat_id}
-                  autoComplete='new-password'
-                  placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
-                  readOnly
-              />
-            </Form.Field>
-            <Form.Field>
-              <Form.Input
-                  label='已绑定的邮箱账户'
-                  name='email'
-                  value={email}
-                  autoComplete='new-password'
-                  placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
-                  readOnly
-              />
-            </Form.Field>
-            <Button onClick={handleCancel}>取消</Button>
-            <Button positive onClick={submit}>提交</Button>
-          </Form>
-        </Segment>
+            <Divider style={{marginTop: 20}}>以下信息不可修改</Divider>
+            <div style={{marginTop: 20}}>
+              <Typography.Text>已绑定的 GitHub 账户</Typography.Text>
+            </div>
+            <Input
+                name='github_id'
+                value={github_id}
+                autoComplete='new-password'
+                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
+                readonly
+            />
+            <div style={{marginTop: 20}}>
+              <Typography.Text>已绑定的微信账户</Typography.Text>
+            </div>
+            <Input
+                name='wechat_id'
+                value={wechat_id}
+                autoComplete='new-password'
+                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
+                readonly
+            />
+            <div style={{marginTop: 20}}>
+              <Typography.Text>已绑定的邮箱账户</Typography.Text>
+            </div>
+            <Input
+                name='email'
+                value={email}
+                autoComplete='new-password'
+                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
+                readonly
+            />
+          </Spin>
+
+        </SideSheet>
       </>
   );
 };

+ 9 - 4
web/src/pages/User/index.js

@@ -1,13 +1,18 @@
 import React from 'react';
 import { Segment, Header } from 'semantic-ui-react';
 import UsersTable from '../../components/UsersTable';
+import {Layout} from "@douyinfe/semi-ui";
 
 const User = () => (
   <>
-    <Segment>
-      <Header as='h3'>管理用户</Header>
-      <UsersTable/>
-    </Segment>
+    <Layout>
+        <Layout.Header>
+            <h3>管理用户</h3>
+        </Layout.Header>
+        <Layout.Content>
+            <UsersTable/>
+        </Layout.Content>
+    </Layout>
   </>
 );