Complete main UI #37
@@ -186,14 +186,19 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
padding: 10px;
|
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
font-size: constants.$SIZE_ICON_XS;
|
font-size: constants.$SIZE_ICON_XS;
|
||||||
border: 2px constants.$font-secondary-color solid;
|
border: 2px constants.$font-secondary-color solid;
|
||||||
color: constants.$font-secondary-color;
|
color: constants.$font-secondary-color;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
|||||||
@@ -91,6 +91,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verify {
|
||||||
|
a {
|
||||||
|
color: constants.$production-color;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sign-up, .sign-in, .forget {
|
.sign-up, .sign-in, .forget {
|
||||||
.footer {
|
.footer {
|
||||||
a {
|
a {
|
||||||
|
|||||||
@@ -433,6 +433,9 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>((prop
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reloadScrollbar()
|
||||||
|
}, 500)
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
reloadScrollbar()
|
reloadScrollbar()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import Icon from '@ant-design/icons'
|
import Icon from '@ant-design/icons'
|
||||||
import { COLOR_ERROR } from '@/constants/common.constants'
|
import { COLOR_ERROR } from '@/constants/common.constants'
|
||||||
import { getRedirectUrl } from '@/util/route'
|
import { getRedirectUrl } from '@/util/route'
|
||||||
import { getLoginStatus, getNickname, removeToken } from '@/util/auth'
|
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
|
||||||
import { r_auth_logout } from '@/services/auth'
|
import { r_auth_logout } from '@/services/auth'
|
||||||
|
|
||||||
const SidebarFooter: React.FC = () => {
|
const SidebarFooter: React.FC = () => {
|
||||||
@@ -12,6 +12,7 @@ const SidebarFooter: React.FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [exiting, setExiting] = useState(false)
|
const [exiting, setExiting] = useState(false)
|
||||||
const [nickname, setNickname] = useState('')
|
const [nickname, setNickname] = useState('')
|
||||||
|
const [avatar, setAvatar] = useState('')
|
||||||
|
|
||||||
const handleClickAvatar = () => {
|
const handleClickAvatar = () => {
|
||||||
if (getLoginStatus()) {
|
if (getLoginStatus()) {
|
||||||
@@ -45,6 +46,10 @@ const SidebarFooter: React.FC = () => {
|
|||||||
if (getLoginStatus()) {
|
if (getLoginStatus()) {
|
||||||
void getNickname().then((nickname) => {
|
void getNickname().then((nickname) => {
|
||||||
setNickname(nickname)
|
setNickname(nickname)
|
||||||
|
|
||||||
|
void getAvatar().then((avatar) => {
|
||||||
|
setAvatar(`data:image/png;base64,${avatar}`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [loginStatus])
|
}, [loginStatus])
|
||||||
@@ -56,7 +61,11 @@ const SidebarFooter: React.FC = () => {
|
|||||||
onClick={handleClickAvatar}
|
onClick={handleClickAvatar}
|
||||||
title={getLoginStatus() ? '个人中心' : '登录'}
|
title={getLoginStatus() ? '个人中心' : '登录'}
|
||||||
>
|
>
|
||||||
<Icon component={IconFatwebUser} />
|
{avatar ? (
|
||||||
|
<img src={avatar} alt={'Avatar'} />
|
||||||
|
) : (
|
||||||
|
<Icon viewBox={'-20 0 1024 1024'} component={IconFatwebUser} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span hidden={getLoginStatus()} className={'text'}>
|
<span hidden={getLoginStatus()} className={'text'}>
|
||||||
未
|
未
|
||||||
@@ -64,7 +73,7 @@ const SidebarFooter: React.FC = () => {
|
|||||||
登录
|
登录
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</span>
|
</span>
|
||||||
<span hidden={!getLoginStatus()} className={'text'}>
|
<span hidden={!getLoginStatus()} className={'text'} title={nickname}>
|
||||||
{nickname}
|
{nickname}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
@@ -199,6 +199,7 @@ interface UserAddEditParam {
|
|||||||
id?: string
|
id?: string
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
|
verified: boolean
|
||||||
locking?: boolean
|
locking?: boolean
|
||||||
expiration?: string
|
expiration?: string
|
||||||
credentialsExpiration?: string
|
credentialsExpiration?: string
|
||||||
@@ -413,6 +414,10 @@ interface ActiveInfoVo {
|
|||||||
time: string
|
time: string
|
||||||
count: number
|
count: number
|
||||||
}[]
|
}[]
|
||||||
|
verifyHistory: {
|
||||||
|
time: string
|
||||||
|
count: number
|
||||||
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveInfoGetParam {
|
interface ActiveInfoGetParam {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
PERMISSION_ACCOUNT_NEED_INIT,
|
PERMISSION_ACCOUNT_NEED_INIT,
|
||||||
PERMISSION_LOGIN_SUCCESS,
|
PERMISSION_LOGIN_SUCCESS,
|
||||||
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
|
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
|
||||||
|
PERMISSION_NO_VERIFICATION_REQUIRED,
|
||||||
PERMISSION_REGISTER_SUCCESS,
|
PERMISSION_REGISTER_SUCCESS,
|
||||||
PERMISSION_USER_DISABLE,
|
PERMISSION_USER_DISABLE,
|
||||||
PERMISSION_USERNAME_NOT_FOUND
|
PERMISSION_USERNAME_NOT_FOUND
|
||||||
@@ -201,7 +202,7 @@ const SignUp: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className={'retry'}>
|
<div className={'retry'}>
|
||||||
我们发送了一封包含激活账号链接的邮件到您的邮箱里,如未收到,可能被归为垃圾邮件,请仔细检查。
|
我们发送了一封包含验证账号链接的邮件到您的邮箱里,如未收到,可能被归为垃圾邮件,请仔细检查。
|
||||||
<a onClick={handleOnResend}>重新发送</a>
|
<a onClick={handleOnResend}>重新发送</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -228,6 +229,7 @@ const Verify: React.FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const [hasCode, setHasCode] = useState(true)
|
const [hasCode, setHasCode] = useState(true)
|
||||||
|
const [needVerify, setNeedVerify] = useState(true)
|
||||||
const [isValid, setIsValid] = useState(true)
|
const [isValid, setIsValid] = useState(true)
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
const [isGettingAvatar, setIsGettingAvatar] = useState(false)
|
const [isGettingAvatar, setIsGettingAvatar] = useState(false)
|
||||||
@@ -255,11 +257,17 @@ const Verify: React.FC = () => {
|
|||||||
void r_auth_verify({ code })
|
void r_auth_verify({ code })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const response = res.data
|
const response = res.data
|
||||||
if (response.code === PERMISSION_ACCOUNT_NEED_INIT) {
|
switch (response.code) {
|
||||||
|
case PERMISSION_ACCOUNT_NEED_INIT:
|
||||||
void getUserInfo().then((user) => {
|
void getUserInfo().then((user) => {
|
||||||
setAvatar(user.userInfo.avatar)
|
setAvatar(user.userInfo.avatar)
|
||||||
})
|
})
|
||||||
} else {
|
break
|
||||||
|
case PERMISSION_NO_VERIFICATION_REQUIRED:
|
||||||
|
void message.success('无需验证')
|
||||||
|
setNeedVerify(false)
|
||||||
|
break
|
||||||
|
default:
|
||||||
setIsValid(false)
|
setIsValid(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -347,7 +355,13 @@ const Verify: React.FC = () => {
|
|||||||
<div className={'secondary'}>Verify account</div>
|
<div className={'secondary'}>Verify account</div>
|
||||||
</div>
|
</div>
|
||||||
<AntdForm className={'form'} onFinish={handleOnFinish}>
|
<AntdForm className={'form'} onFinish={handleOnFinish}>
|
||||||
<div className={'verify-process'} hidden={!hasCode || !isValid}>
|
<div className={'no-verify-need'} hidden={needVerify}>
|
||||||
|
账号已验证通过,无需验证,点击 <a href={'/'}>回到首页</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={'verify-process'}
|
||||||
|
hidden={!needVerify || !hasCode || !isValid}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -360,7 +374,7 @@ const Verify: React.FC = () => {
|
|||||||
src={
|
src={
|
||||||
<img
|
<img
|
||||||
src={`data:image/png;base64,${avatar}`}
|
src={`data:image/png;base64,${avatar}`}
|
||||||
alt={'avatar'}
|
alt={'Avatar'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
size={100}
|
size={100}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
TooltipComponentOption,
|
TooltipComponentOption,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
|
LegendComponent,
|
||||||
|
LegendComponentOption,
|
||||||
ToolboxComponentOption,
|
ToolboxComponentOption,
|
||||||
DataZoomComponentOption,
|
DataZoomComponentOption,
|
||||||
ToolboxComponent,
|
ToolboxComponent,
|
||||||
@@ -14,7 +16,7 @@ import {
|
|||||||
import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
|
import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
|
||||||
import { UniversalTransition } from 'echarts/features'
|
import { UniversalTransition } from 'echarts/features'
|
||||||
import { SVGRenderer } from 'echarts/renderers'
|
import { SVGRenderer } from 'echarts/renderers'
|
||||||
import { TopLevelFormatterParams } from 'echarts/types/dist/shared'
|
import { CallbackDataParams } from 'echarts/types/dist/shared'
|
||||||
import '@/assets/css/pages/system/statistics.scss'
|
import '@/assets/css/pages/system/statistics.scss'
|
||||||
import { useUpdatedEffect } from '@/util/hooks'
|
import { useUpdatedEffect } from '@/util/hooks'
|
||||||
import { formatByteSize } from '@/util/common'
|
import { formatByteSize } from '@/util/common'
|
||||||
@@ -38,6 +40,7 @@ echarts.use([
|
|||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
ToolboxComponent,
|
ToolboxComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
DataZoomComponent,
|
DataZoomComponent,
|
||||||
BarChart,
|
BarChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
@@ -48,6 +51,7 @@ type EChartsOption = echarts.ComposeOption<
|
|||||||
| TooltipComponentOption
|
| TooltipComponentOption
|
||||||
| ToolboxComponentOption
|
| ToolboxComponentOption
|
||||||
| GridComponentOption
|
| GridComponentOption
|
||||||
|
| LegendComponentOption
|
||||||
| BarSeriesOption
|
| BarSeriesOption
|
||||||
| DataZoomComponentOption
|
| DataZoomComponentOption
|
||||||
| LineSeriesOption
|
| LineSeriesOption
|
||||||
@@ -94,33 +98,26 @@ const barEChartsBaseOption: EChartsOption = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => {
|
const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => {
|
||||||
return (params: TopLevelFormatterParams) =>
|
return (params: CallbackDataParams[]) =>
|
||||||
`${utcToLocalTime(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
params[0].data[0],
|
`${utcToLocalTime(params[0].data[0], format)}<br>${params
|
||||||
format
|
.map(
|
||||||
)}<br><span style="display: flex; justify-content: space-between"><span>${
|
(param) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
params[0]['marker']
|
`<span style="display: flex; justify-content: space-between;"><span>${param.marker}${param.seriesName}</span><span style="font-weight: bold; margin-left: 16px;">${param.data[1]}</span></span>`
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
)
|
||||||
// @ts-expect-error
|
.join('')}`
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
}${params[0]['seriesName']}</span><span style="font-weight: bold">${
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
|
|
||||||
params[0].data[1]
|
|
||||||
}</span></span> `
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineEChartsBaseOption: EChartsOption = {
|
const lineEChartsBaseOption: EChartsOption = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis'
|
trigger: 'axis'
|
||||||
},
|
},
|
||||||
|
legend: {},
|
||||||
toolbox: {
|
toolbox: {
|
||||||
feature: {
|
feature: {
|
||||||
dataZoom: {
|
dataZoom: {
|
||||||
@@ -380,6 +377,19 @@ const ActiveInfo: React.FC = () => {
|
|||||||
)?.count ?? 0
|
)?.count ?? 0
|
||||||
])
|
])
|
||||||
: []
|
: []
|
||||||
|
const verifyList = data.verifyHistory.length
|
||||||
|
? getTimesBetweenTwoTimes(
|
||||||
|
data.verifyHistory[0].time,
|
||||||
|
data.verifyHistory[data.verifyHistory.length - 1].time,
|
||||||
|
'day'
|
||||||
|
).map((time) => [
|
||||||
|
time,
|
||||||
|
data.verifyHistory.find(
|
||||||
|
(value) =>
|
||||||
|
value.time.substring(0, 10) === time.substring(0, 10)
|
||||||
|
)?.count ?? 0
|
||||||
|
])
|
||||||
|
: []
|
||||||
|
|
||||||
activeInfoEChartsRef.current = echarts.init(
|
activeInfoEChartsRef.current = echarts.init(
|
||||||
activeInfoDivRef.current,
|
activeInfoDivRef.current,
|
||||||
@@ -417,6 +427,14 @@ const ActiveInfo: React.FC = () => {
|
|||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
areaStyle: {},
|
areaStyle: {},
|
||||||
data: loginList
|
data: loginList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '验证账号人数',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
areaStyle: {},
|
||||||
|
data: verifyList
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const User: React.FC = () => {
|
|||||||
<AntdImage
|
<AntdImage
|
||||||
preview={{ mask: <Icon component={IconFatwebEye}></Icon> }}
|
preview={{ mask: <Icon component={IconFatwebEye}></Icon> }}
|
||||||
src={`data:image/png;base64,${value}`}
|
src={`data:image/png;base64,${value}`}
|
||||||
alt={'avatar'}
|
alt={'Avatar'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
style={{ background: COLOR_BACKGROUND }}
|
style={{ background: COLOR_BACKGROUND }}
|
||||||
@@ -161,13 +161,21 @@ const User: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!record.locking &&
|
{!record.verify &&
|
||||||
|
!record.locking &&
|
||||||
(!record.expiration || !isPastTime(record.expiration)) &&
|
(!record.expiration || !isPastTime(record.expiration)) &&
|
||||||
(!record.credentialsExpiration || !isPastTime(record.credentialsExpiration)) &&
|
(!record.credentialsExpiration || !isPastTime(record.credentialsExpiration)) &&
|
||||||
record.enable ? (
|
record.enable ? (
|
||||||
<AntdTag color={'green'}>正常</AntdTag>
|
<AntdTag color={'green'}>正常</AntdTag>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{record.verify ? (
|
||||||
|
<>
|
||||||
|
<AntdPopover content={record.verify} trigger={'click'}>
|
||||||
|
<AntdTag style={{ cursor: 'pointer' }}>未验证</AntdTag>
|
||||||
|
</AntdPopover>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
{record.locking ? <AntdTag>锁定</AntdTag> : undefined}
|
{record.locking ? <AntdTag>锁定</AntdTag> : undefined}
|
||||||
{record.expiration && isPastTime(record.expiration) ? (
|
{record.expiration && isPastTime(record.expiration) ? (
|
||||||
<AntdTag>过期</AntdTag>
|
<AntdTag>过期</AntdTag>
|
||||||
@@ -430,6 +438,7 @@ const User: React.FC = () => {
|
|||||||
form.setFieldValue('id', value.id)
|
form.setFieldValue('id', value.id)
|
||||||
form.setFieldValue('username', value.username)
|
form.setFieldValue('username', value.username)
|
||||||
form.setFieldValue('password', undefined)
|
form.setFieldValue('password', undefined)
|
||||||
|
form.setFieldValue('verified', !value.verify?.length)
|
||||||
form.setFieldValue('locking', value.locking)
|
form.setFieldValue('locking', value.locking)
|
||||||
form.setFieldValue('expiration', value.expiration)
|
form.setFieldValue('expiration', value.expiration)
|
||||||
form.setFieldValue('credentialsExpiration', value.credentialsExpiration)
|
form.setFieldValue('credentialsExpiration', value.credentialsExpiration)
|
||||||
@@ -727,6 +736,7 @@ const User: React.FC = () => {
|
|||||||
if (!isDrawerEdit && formValues) {
|
if (!isDrawerEdit && formValues) {
|
||||||
setNewFormValues({
|
setNewFormValues({
|
||||||
username: formValues.username,
|
username: formValues.username,
|
||||||
|
verified: formValues.verified,
|
||||||
locking: formValues.locking,
|
locking: formValues.locking,
|
||||||
expiration: formValues.expiration,
|
expiration: formValues.expiration,
|
||||||
credentialsExpiration: formValues.credentialsExpiration,
|
credentialsExpiration: formValues.credentialsExpiration,
|
||||||
@@ -771,7 +781,7 @@ const User: React.FC = () => {
|
|||||||
src={`data:image/png;base64,${
|
src={`data:image/png;base64,${
|
||||||
isDrawerEdit ? formValues?.avatar : avatar
|
isDrawerEdit ? formValues?.avatar : avatar
|
||||||
}`}
|
}`}
|
||||||
alt={'avatar'}
|
alt={'Avatar'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
size={100}
|
size={100}
|
||||||
@@ -840,6 +850,9 @@ const User: React.FC = () => {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</AntdForm.Item>
|
</AntdForm.Item>
|
||||||
|
<AntdForm.Item name={'verified'} label={'已验证'}>
|
||||||
|
<AntdSwitch />
|
||||||
|
</AntdForm.Item>
|
||||||
<AntdForm.Item
|
<AntdForm.Item
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
name={'locking'}
|
name={'locking'}
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ export const getNickname = async () => {
|
|||||||
return user.userInfo.nickname
|
return user.userInfo.nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAvatar = async () => {
|
||||||
|
const user = await getUserInfo()
|
||||||
|
|
||||||
|
return user.userInfo.avatar
|
||||||
|
}
|
||||||
|
|
||||||
export const getUsername = async () => {
|
export const getUsername = async () => {
|
||||||
const user = await getUserInfo()
|
const user = await getUserInfo()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user