Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
b7ac0b18
authored
Nov 27, 2023
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
更新用户管理界面UI
parent
41e461f3
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
532 additions
and
542 deletions
+532
-542
web/src/App.js
+0
-43
web/src/components/RedemptionsTable.js
+0
-2
web/src/components/UsersTable.js
+294
-313
web/src/helpers/render.js
+9
-4
web/src/pages/Token/EditToken.js
+1
-2
web/src/pages/User/AddUser.js
+90
-69
web/src/pages/User/EditUser.js
+129
-105
web/src/pages/User/index.js
+9
-4
No files found.
web/src/App.js
View file @
b7ac0b18
...
...
@@ -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'
;
...
...
@@ -132,22 +129,6 @@ function App() {
}
/
>
<
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
=
{
<
PrivateRoute
>
...
...
@@ -156,22 +137,6 @@ function App() {
}
/
>
<
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
=
{
<
PrivateRoute
>
...
...
@@ -196,14 +161,6 @@ function App() {
}
/
>
<
Route
path
=
'/user/add'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
AddUser
/>
<
/Suspense
>
}
/
>
<
Route
path
=
'/user/reset'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
web/src/components/RedemptionsTable.js
View file @
b7ac0b18
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
(
...
...
web/src/components/UsersTable.js
View file @
b7ac0b18
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
'../helper
s'
;
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
'../constant
s'
;
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
;
web/src/helpers/render.js
View file @
b7ac0b18
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>
;
})}
<
/>
;
}
...
...
web/src/pages/Token/EditToken.js
View file @
b7ac0b18
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
...
...
web/src/pages/User/AddUser.js
View file @
b7ac0b18
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
;
web/src/pages/User/EditUser.js
View file @
b7ac0b18
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-passwor
d'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的微信账户'
name
=
'wechat_id'
value
=
{
wechat_id
}
autoComplete
=
'new-passwor
d'
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
}
>
提交
<
/Butto
n
>
<
/Form
>
<
/S
egmen
t
>
<
Divider
style
=
{{
marginTop
:
20
}}
>
以下信息不可修改
<
/Divider
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
已绑定的
GitHub
账户
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'github_i
d'
value
=
{
github_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
已绑定的微信账户
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'wechat_i
d'
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
/
>
<
/Spi
n
>
<
/S
ideShee
t
>
<
/
>
);
};
...
...
web/src/pages/User/index.js
View file @
b7ac0b18
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
>
<
/
>
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment