更新用户管理界面UI

This commit is contained in:
CaIon
2023-11-27 22:43:46 +08:00
parent 4475d54c16
commit b140b326b8
8 changed files with 540 additions and 550 deletions

View File

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

View File

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

View File

@@ -1,338 +1,319 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import {API, isAdmin, showError, showSuccess} from '../helpers';
import { Link } from 'react-router-dom'; import {Button, Modal, Popconfirm, Popover, Table, Tag, Form, Tooltip, Space} from "@douyinfe/semi-ui";
import { API, showError, showSuccess } from '../helpers'; import {ITEMS_PER_PAGE} from '../constants';
import {renderGroup, renderNumber, renderQuota, renderText, stringToColor} from '../helpers/render';
import { ITEMS_PER_PAGE } from '../constants'; import AddUser from "../pages/User/AddUser";
import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render'; import EditUser from "../pages/User/EditUser";
function renderRole(role) { function renderRole(role) {
switch (role) { switch (role) {
case 1: case 1:
return <Label>普通用户</Label>; return <Tag size='large'>普通用户</Tag>;
case 10: case 10:
return <Label color='yellow'>管理员</Label>; return <Tag color='yellow' size='large'>管理员</Tag>;
case 100: case 100:
return <Label color='orange'>超级管理员</Label>; return <Tag color='orange' size='large'>超级管理员</Tag>;
default: default:
return <Label color='red'>未知身份</Label>; return <Tag color='red' size='large'>未知身份</Tag>;
} }
} }
const UsersTable = () => { const UsersTable = () => {
const [users, setUsers] = useState([]); const columns = [{
const [loading, setLoading] = useState(true); title: 'ID', dataIndex: 'id',
const [activePage, setActivePage] = useState(1); }, {
const [searchKeyword, setSearchKeyword] = useState(''); title: '用户名', dataIndex: 'username',
const [searching, setSearching] = useState(false); }, {
title: '分组', dataIndex: 'group', render: (text, record, index) => {
const loadUsers = async (startIdx) => { return (<div>
const res = await API.get(`/api/user/?p=${startIdx}`); {renderGroup(text)}
const { success, message, data } = res.data; </div>);
if (success) { },
if (startIdx === 0) { }, {
setUsers(data); title: '统计信息', dataIndex: 'info', render: (text, record, index) => {
} else { return (<div>
let newUsers = users; <Space spacing={1}>
newUsers.push(...data); <Tooltip content={'剩余额度'}>
setUsers(newUsers); <Tag color='white' size='large'>{renderQuota(record.quota)}</Tag>
} </Tooltip>
} else { <Tooltip content={'已用额度'}>
showError(message); <Tag color='white' size='large'>{renderQuota(record.used_quota)}</Tag>
} </Tooltip>
setLoading(false); <Tooltip content={'调用次数'}>
}; <Tag color='white' size='large'>{renderNumber(record.request_count)}</Tag>
</Tooltip>
const onPaginationChange = (e, { activePage }) => { </Space>
(async () => { </div>);
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);
})();
};
useEffect(() => {
loadUsers(0)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
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;
} else {
newUsers[realIdx].status = user.status;
newUsers[realIdx].role = user.role;
} }
setUsers(newUsers); }, {
} else { title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => {
showError(message); 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 renderStatus = (status) => { const [users, setUsers] = useState([]);
switch (status) { const [loading, setLoading] = useState(true);
case 1: const [activePage, setActivePage] = useState(1);
return <Label basic>已激活</Label>; const [searchKeyword, setSearchKeyword] = useState('');
case 2: const [searching, setSearching] = useState(false);
return ( const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
<Label basic color='red'> const [showAddUser, setShowAddUser] = useState(false);
已封禁 const [showEditUser, setShowEditUser] = useState(false);
</Label> const [editingUser, setEditingUser] = useState({
); id: undefined,
default:
return (
<Label basic color='grey'>
未知状态
</Label>
);
}
};
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 (e, { 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(); const setCount = (data) => {
if (data.length >= (activePage) * ITEMS_PER_PAGE) {
setUserCount(data.length + 1);
} else {
setUserCount(data.length);
}
} }
setUsers(sortedUsers);
setLoading(false);
};
return ( const removeRecord = key => {
<> console.log(key);
<Form onSubmit={searchUsers}> let newDataSource = [...users];
<Form.Input if (key != null) {
icon='search' let idx = newDataSource.findIndex(data => data.id === key);
fluid
iconPosition='left'
placeholder='搜索用户的 ID用户名显示名称以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
</Form>
<Table basic compact size='small'> if (idx > -1) {
<Table.Header> newDataSource.splice(idx, 1);
<Table.Row> setUsers(newDataSource);
<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>
<Table.Body> const loadUsers = async (startIdx) => {
{users const res = await API.get(`/api/user/?p=${startIdx}`);
.slice( const {success, message, data} = res.data;
(activePage - 1) * ITEMS_PER_PAGE, if (success) {
activePage * ITEMS_PER_PAGE if (startIdx === 0) {
) setUsers(data);
.map((user, idx) => { setCount(data);
if (user.deleted) return <></>; } else {
return ( let newUsers = users;
<Table.Row key={user.id}> newUsers.push(...data);
<Table.Cell>{user.id}</Table.Cell> setUsers(newUsers);
<Table.Cell> setCount(newUsers);
<Popup }
content={user.email ? user.email : '未绑定邮箱地址'} } else {
key={user.username} showError(message);
header={user.display_name ? user.display_name : user.username} }
trigger={<span>{renderText(user.username, 15)}</span>} setLoading(false);
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>
<Table.Footer> const onPaginationChange = (e, {activePage}) => {
<Table.Row> (async () => {
<Table.HeaderCell colSpan='7'> if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
<Button size='small' as={Link} to='/user/add' loading={loading}> // In this case we have to load more data and then append them.
添加新的用户 await loadUsers(activePage - 1);
</Button> }
<Pagination setActivePage(activePage);
floated='right' })();
activePage={activePage} };
onPageChange={onPaginationChange}
size='small' useEffect(() => {
siblingRange={1} loadUsers(0)
totalPages={ .then()
Math.ceil(users.length / ITEMS_PER_PAGE) + .catch((reason) => {
(users.length % ITEMS_PER_PAGE === 0 ? 1 : 0) showError(reason);
});
}, []);
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') {
} 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);
}
const closeEditUser = () => {
setShowEditUser(false);
setEditingUser({
id: undefined,
});
}
const refresh = async () => {
await loadUsers(activePage - 1);
};
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 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);
} }
/> }>添加用户</Button>
</Table.HeaderCell> </>
</Table.Row> );
</Table.Footer>
</Table>
</>
);
}; };
export default UsersTable; export default UsersTable;

View File

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

View File

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

View File

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

View File

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

View File

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