v1.8.0 - Add actual Taskbar component
This commit is contained in:
@@ -59,6 +59,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
enableTaskListContextMenu: true,
|
||||
enableTaskBarContextMenu: true,
|
||||
enableLinkAnchor: true,
|
||||
showActualTaskbar: false,
|
||||
enableTaskBarTooltip: true,
|
||||
fullscreen: false,
|
||||
expandAll: true,
|
||||
locale: 'zh-CN',
|
||||
@@ -201,6 +203,21 @@ interface Props {
|
||||
// 是否启用 LinkAnchor 连接触点功能(默认为 true)
|
||||
// 当设置为 false 时,TaskBar 上不显示前置/后置任务的连接触点
|
||||
enableLinkAnchor?: boolean
|
||||
// 是否启用 TaskBar 气泡提示框(默认为 true)
|
||||
// 当设置为 false 时,TaskBar 上不显示悬停气泡提示框
|
||||
enableTaskBarTooltip?: boolean
|
||||
// 是否显示实际任务条(默认为 false)
|
||||
// 当设置为 true 且任务存在 actualStartDate 时,会在计划任务条下方显示实际任务条
|
||||
showActualTaskbar?: boolean
|
||||
// 自定义任务状态背景色(优先级高于默认配色,低于Task.barColor)
|
||||
// 待处理任务背景色:任务未开始且未逾期时使用
|
||||
pendingTaskBackgroundColor?: string
|
||||
// 逾期任务背景色:当前日期晚于任务结束日期时使用
|
||||
delayTaskBackgroundColor?: string
|
||||
// 已完成任务背景色:任务进度达到100%时使用
|
||||
completeTaskBackgroundColor?: string
|
||||
// 进行中任务背景色:任务已开始但未完成且未逾期时使用
|
||||
ongoingTaskBackgroundColor?: string
|
||||
// 全屏状态控制(响应式)
|
||||
fullscreen?: boolean
|
||||
// 展开/收起所有任务(响应式)
|
||||
@@ -1201,6 +1218,8 @@ const milestonesForTimeline = computed((): Milestone[] => {
|
||||
startDate: task.startDate, // 此时已确保非空
|
||||
endDate: task.endDate,
|
||||
assignee: task.assignee,
|
||||
assigneeName: task.assigneeName,
|
||||
avatar: task.avatar,
|
||||
type: task.type || 'milestone',
|
||||
icon: task.icon,
|
||||
description: task.description,
|
||||
@@ -2808,6 +2827,12 @@ defineExpose({
|
||||
:working-hours="props.workingHours"
|
||||
:task-bar-config="props.taskBarConfig"
|
||||
:allow-drag-and-resize="props.allowDragAndResize"
|
||||
:show-actual-taskbar="props.showActualTaskbar"
|
||||
:enable-task-bar-tooltip="props.enableTaskBarTooltip"
|
||||
:pending-task-background-color="props.pendingTaskBackgroundColor"
|
||||
:delay-task-background-color="props.delayTaskBackgroundColor"
|
||||
:complete-task-background-color="props.completeTaskBackgroundColor"
|
||||
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
:use-default-milestone-dialog="props.useDefaultMilestoneDialog"
|
||||
:on-milestone-save="handleMilestoneSave"
|
||||
@@ -2861,6 +2886,10 @@ defineExpose({
|
||||
:task="taskDrawerTask"
|
||||
:is-edit="taskDrawerEditMode"
|
||||
:assignee-options="props.assigneeOptions"
|
||||
:pending-task-background-color="props.pendingTaskBackgroundColor"
|
||||
:delay-task-background-color="props.delayTaskBackgroundColor"
|
||||
:complete-task-background-color="props.completeTaskBackgroundColor"
|
||||
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
|
||||
@submit="handleTaskDrawerSubmit"
|
||||
@close="taskDrawerVisible = false"
|
||||
@start-timer="handleStartTimer"
|
||||
|
||||
@@ -509,7 +509,7 @@ defineExpose({
|
||||
top: `${offsetTop}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
zIndex: highlightedTaskId !== null ? 1001 : 25,
|
||||
zIndex: highlightedTaskId !== null ? 2000 : 500,
|
||||
pointerEvents: 'none',
|
||||
}"
|
||||
/>
|
||||
|
||||
+740
-83
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,10 @@ interface Props {
|
||||
isEdit?: boolean
|
||||
onDelete?: (task: Task) => void
|
||||
assigneeOptions?: AssigneeOption[]
|
||||
pendingTaskBackgroundColor?: string
|
||||
delayTaskBackgroundColor?: string
|
||||
completeTaskBackgroundColor?: string
|
||||
ongoingTaskBackgroundColor?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -35,6 +39,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
{ value: 'zhaoliu', label: '赵六' },
|
||||
{ value: 'qianqi', label: '钱七' },
|
||||
],
|
||||
pendingTaskBackgroundColor: '#c0c4cc',
|
||||
delayTaskBackgroundColor: '#f56c6c',
|
||||
completeTaskBackgroundColor: '#67c23a',
|
||||
ongoingTaskBackgroundColor: '#409eff',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -146,6 +154,8 @@ const formData = reactive<Task>({
|
||||
assignee: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
actualStartDate: '',
|
||||
actualEndDate: '',
|
||||
predecessor: [],
|
||||
estimatedHours: 0,
|
||||
actualHours: 0,
|
||||
@@ -334,6 +344,8 @@ const errors = reactive({
|
||||
type: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
actualStartDate: '',
|
||||
actualEndDate: '',
|
||||
})
|
||||
|
||||
// 监听 visible 属性变化
|
||||
@@ -397,6 +409,8 @@ const resetForm = () => {
|
||||
assignee: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
actualStartDate: '',
|
||||
actualEndDate: '',
|
||||
predecessor: [],
|
||||
estimatedHours: 0,
|
||||
actualHours: 0,
|
||||
@@ -408,6 +422,9 @@ const resetForm = () => {
|
||||
timerEndTime: undefined,
|
||||
timerElapsedTime: 0,
|
||||
children: undefined,
|
||||
bgColor: undefined,
|
||||
avatar: undefined,
|
||||
barColor: undefined,
|
||||
})
|
||||
|
||||
// 清除错误信息
|
||||
@@ -434,14 +451,19 @@ const validateForm = (): boolean => {
|
||||
isValid = false
|
||||
}
|
||||
if (!formData.startDate) {
|
||||
errors.startDate = t.value.startDateRequired
|
||||
errors.startDate = t.value.plannedStartDateRequired
|
||||
isValid = false
|
||||
}
|
||||
if (!formData.endDate) {
|
||||
errors.endDate = t.value.endDateRequired
|
||||
errors.endDate = t.value.plannedEndDateRequired
|
||||
isValid = false
|
||||
} else if (formData.startDate && new Date(formData.endDate) < new Date(formData.startDate)) {
|
||||
errors.endDate = t.value.endDateInvalid
|
||||
errors.endDate = t.value.plannedEndDateInvalid
|
||||
isValid = false
|
||||
}
|
||||
// 验证实际日期(可选字段)
|
||||
if (formData.actualStartDate && formData.actualEndDate && new Date(formData.actualEndDate) < new Date(formData.actualStartDate)) {
|
||||
errors.actualEndDate = t.value.actualEndDateInvalid
|
||||
isValid = false
|
||||
}
|
||||
return isValid
|
||||
@@ -654,6 +676,71 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
formData.assigneeName = selected.label
|
||||
}
|
||||
}
|
||||
|
||||
// 计算任务状态
|
||||
const taskStatus = computed(() => {
|
||||
if (!props.task) return { text: '', color: '', bgColor: '', borderColor: '' }
|
||||
|
||||
const task = props.task
|
||||
const progress = task.progress || 0
|
||||
const now = new Date()
|
||||
const endDate = task.endDate ? new Date(task.endDate) : null
|
||||
const startDate = task.startDate ? new Date(task.startDate) : null
|
||||
const actualStartDate = task.actualStartDate
|
||||
|
||||
// 待处理:没有实际开始日期并且进度为0
|
||||
if (!actualStartDate && progress === 0) {
|
||||
const color = props.pendingTaskBackgroundColor
|
||||
return {
|
||||
text: t.value.statusPending,
|
||||
color: color,
|
||||
bgColor: color + '20', // 添加20%透明度
|
||||
borderColor: color,
|
||||
}
|
||||
}
|
||||
|
||||
// 已完成
|
||||
if (progress >= 100) {
|
||||
const color = props.completeTaskBackgroundColor
|
||||
return {
|
||||
text: t.value.statusCompleted,
|
||||
color: color,
|
||||
bgColor: color + '20',
|
||||
borderColor: color,
|
||||
}
|
||||
}
|
||||
|
||||
// 已逾期:当前时间已超过结束日期,但未完成
|
||||
if (endDate && now > endDate && progress < 100) {
|
||||
const color = props.delayTaskBackgroundColor
|
||||
return {
|
||||
text: t.value.statusDelayed,
|
||||
color: color,
|
||||
bgColor: color + '20',
|
||||
borderColor: color,
|
||||
}
|
||||
}
|
||||
|
||||
// 进行中:当前时间在开始日期之后,且未完成,且未逾期
|
||||
if (startDate && now >= startDate && progress < 100) {
|
||||
const color = props.ongoingTaskBackgroundColor
|
||||
return {
|
||||
text: t.value.statusOngoing,
|
||||
color: color,
|
||||
bgColor: color + '20',
|
||||
borderColor: color,
|
||||
}
|
||||
}
|
||||
|
||||
// 默认待处理
|
||||
const color = props.pendingTaskBackgroundColor
|
||||
return {
|
||||
text: t.value.statusPending,
|
||||
color: color,
|
||||
bgColor: color + '20',
|
||||
borderColor: color,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -763,6 +850,18 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
</span>
|
||||
</div>
|
||||
<div style="flex: 1"></div>
|
||||
<!-- Status Badge -->
|
||||
<div
|
||||
v-if="isEdit && props.task"
|
||||
class="status-badge"
|
||||
:style="{
|
||||
backgroundColor: taskStatus.bgColor,
|
||||
color: taskStatus.color,
|
||||
border: `1px solid ${taskStatus.borderColor}`,
|
||||
}"
|
||||
>
|
||||
{{ taskStatus.text }}
|
||||
</div>
|
||||
<button class="drawer-close-btn" type="button" @click="handleClose">
|
||||
<svg
|
||||
class="close-icon"
|
||||
@@ -844,14 +943,14 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-start-date">
|
||||
{{ t.startDate }} <span class="required">*</span></label
|
||||
{{ t.plannedStartDate }} <span class="required">*</span></label
|
||||
>
|
||||
<DatePicker
|
||||
id="task-start-date"
|
||||
v-model="formData.startDate"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.startDateRequired"
|
||||
:placeholder="t.plannedStartDateRequired"
|
||||
:class="{ error: errors.startDate }"
|
||||
/>
|
||||
<span v-if="errors.startDate" class="error-text">{{ errors.startDate }}</span>
|
||||
@@ -859,20 +958,52 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-end-date">
|
||||
{{ t.endDate }} <span class="required">*</span></label
|
||||
{{ t.plannedEndDate }} <span class="required">*</span></label
|
||||
>
|
||||
<DatePicker
|
||||
id="task-end-date"
|
||||
v-model="formData.endDate"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.endDateRequired"
|
||||
:placeholder="t.plannedEndDateRequired"
|
||||
:class="{ error: errors.endDate }"
|
||||
/>
|
||||
<span v-if="errors.endDate" class="error-text">{{ errors.endDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-actual-start-date">
|
||||
{{ t.actualStartDate }}</label
|
||||
>
|
||||
<DatePicker
|
||||
id="task-actual-start-date"
|
||||
v-model="formData.actualStartDate"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.actualStartDate"
|
||||
:class="{ error: errors.actualStartDate }"
|
||||
/>
|
||||
<span v-if="errors.actualStartDate" class="error-text">{{ errors.actualStartDate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-actual-end-date">
|
||||
{{ t.actualEndDate }}</label
|
||||
>
|
||||
<DatePicker
|
||||
id="task-actual-end-date"
|
||||
v-model="formData.actualEndDate"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.actualEndDate"
|
||||
:class="{ error: errors.actualEndDate }"
|
||||
/>
|
||||
<span v-if="errors.actualEndDate" class="error-text">{{ errors.actualEndDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MultiSelectPredecessor
|
||||
v-model="formData.predecessor"
|
||||
:tasks="allTasks"
|
||||
@@ -1068,6 +1199,15 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
color: var(--gantt-text-primary, #303133);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.drawer-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -1075,6 +1215,7 @@ const handleAssigneeChanged = (event: Event) => {
|
||||
padding: 4px;
|
||||
color: var(--gantt-text-muted, #909399);
|
||||
transition: color 0.2s;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.drawer-close-btn:hover {
|
||||
|
||||
@@ -226,6 +226,59 @@ const slotPayload = computed(() => ({
|
||||
daysText: daysText.value,
|
||||
progressClass: progressClass.value,
|
||||
}))
|
||||
|
||||
// 计算左侧边框颜色 - 支持barColor自定义
|
||||
const leftBorderColor = computed(() => {
|
||||
// 如果设置了barColor,优先使用
|
||||
if (props.task.barColor) {
|
||||
return props.task.barColor
|
||||
}
|
||||
// 否则返回null,使用CSS默认样式
|
||||
return null
|
||||
})
|
||||
|
||||
// 计算自定义边框样式
|
||||
const customBorderStyle = computed(() => {
|
||||
if (leftBorderColor.value) {
|
||||
return {
|
||||
borderLeftColor: leftBorderColor.value
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
// 处理assignee列的显示数据
|
||||
const assigneeDisplayData = computed(() => {
|
||||
const task = props.task
|
||||
const avatar = task.avatar
|
||||
const assignee = task.assignee
|
||||
|
||||
// 处理avatar列表
|
||||
let avatarList: string[] = []
|
||||
if (avatar) {
|
||||
avatarList = Array.isArray(avatar) ? avatar : [avatar]
|
||||
}
|
||||
|
||||
// 处理assignee列表
|
||||
let assigneeList: string[] = []
|
||||
if (assignee) {
|
||||
assigneeList = Array.isArray(assignee) ? assignee : [assignee]
|
||||
}
|
||||
|
||||
// 如果没有avatar,从assignee生成文字头像
|
||||
const displayAvatars = avatarList.length > 0
|
||||
? avatarList.map(url => ({ type: 'image', url }))
|
||||
: assigneeList.map(name => ({ type: 'text', name }))
|
||||
|
||||
// 生成显示的名称文本(换行拼接)
|
||||
const nameText = assigneeList.join('\n') || '-'
|
||||
|
||||
return {
|
||||
avatars: displayAvatars,
|
||||
nameText,
|
||||
hasMultiple: displayAvatars.length > 1
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -243,7 +296,7 @@ const slotPayload = computed(() => ({
|
||||
'task-type-milestone': isMilestoneTask,
|
||||
[customRowClass]: customRowClass,
|
||||
}"
|
||||
:style="customRowStyle"
|
||||
:style="[customRowStyle, customBorderStyle]"
|
||||
@click="handleRowClick"
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@mousedown="handleMouseDown"
|
||||
@@ -373,10 +426,25 @@ const slotPayload = computed(() => ({
|
||||
<!-- 负责人列 -->
|
||||
<template v-else-if="column.key === 'assignee'">
|
||||
<div class="assignee-info">
|
||||
<div class="avatar">
|
||||
{{ props.task.assignee ? props.task.assignee.charAt(0) : '-' }}
|
||||
<!-- 多头像容器 -->
|
||||
<div class="assignee-avatars-container">
|
||||
<div
|
||||
v-for="(avatarItem, idx) in assigneeDisplayData.avatars"
|
||||
:key="idx"
|
||||
class="avatar"
|
||||
:style="{
|
||||
zIndex: idx + 1,
|
||||
marginLeft: idx > 0 ? '-8px' : '0'
|
||||
}"
|
||||
>
|
||||
<!-- 图片头像 -->
|
||||
<img v-if="avatarItem.type === 'image'" :src="(avatarItem as any).url" :alt="`avatar-${idx}`" />
|
||||
<!-- 文字头像 -->
|
||||
<span v-else class="avatar-text">{{ (avatarItem as any).name.charAt(0).toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="assignee-name">{{ props.task.assignee || '-' }}</span>
|
||||
<!-- 名称显示,支持换行和超出显示... -->
|
||||
<span class="assignee-name" :title="assigneeDisplayData.nameText">{{ assigneeDisplayData.nameText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -614,9 +682,29 @@ const slotPayload = computed(() => ({
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 多头像容器 */
|
||||
.assignee-avatars-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.assignee-avatars-container .avatar {
|
||||
position: relative;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assignee-avatars-container .avatar:hover {
|
||||
transform: translateY(-2px) scale(1.1);
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
min-width: 25px;
|
||||
min-height: 25px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
background: var(--gantt-primary);
|
||||
color: var(--gantt-text-white);
|
||||
@@ -627,11 +715,31 @@ const slotPayload = computed(() => ({
|
||||
font-weight: 500;
|
||||
border: 2px solid var(--gantt-border-medium);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar .avatar-text {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assignee-name {
|
||||
font-size: 14px;
|
||||
color: var(--gantt-text-secondary);
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
|
||||
@@ -37,6 +37,15 @@ interface Props {
|
||||
taskBarConfig?: TaskBarConfig
|
||||
// 是否允许拖拽和拉伸(默认为 true)
|
||||
allowDragAndResize?: boolean
|
||||
// 是否显示实际任务条(默认为 false)
|
||||
showActualTaskbar?: boolean
|
||||
// 是否启用 TaskBar 气泡提示框(默认为 true)
|
||||
enableTaskBarTooltip?: boolean
|
||||
// 自定义任务状态背景色
|
||||
pendingTaskBackgroundColor?: string
|
||||
delayTaskBackgroundColor?: string
|
||||
completeTaskBackgroundColor?: string
|
||||
ongoingTaskBackgroundColor?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -55,6 +64,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
}),
|
||||
taskBarConfig: undefined,
|
||||
allowDragAndResize: true,
|
||||
showActualTaskbar: false,
|
||||
enableTaskBarTooltip: true,
|
||||
pendingTaskBackgroundColor: undefined,
|
||||
delayTaskBackgroundColor: undefined,
|
||||
completeTaskBackgroundColor: undefined,
|
||||
ongoingTaskBackgroundColor: undefined,
|
||||
})
|
||||
|
||||
// 定义emits
|
||||
@@ -1049,11 +1064,6 @@ const hasCircularDependency = (taskId: number, targetId: number): boolean => {
|
||||
|
||||
// 创建连接
|
||||
const createLink = (sourceTask: Task, targetTask: Task, mode: 'predecessor' | 'successor') => {
|
||||
console.log('----> hello:', {
|
||||
mode,
|
||||
sourceTask,
|
||||
targetTask,
|
||||
})
|
||||
if (mode === 'predecessor') {
|
||||
// mode='predecessor' 表示从左侧 anchor 拖出
|
||||
// 从左侧 anchor 拖到右侧:sourceTask 依赖 targetTask
|
||||
@@ -4478,6 +4488,12 @@ const handleAddSuccessor = (task: Task) => {
|
||||
:current-time-scale="currentTimeScale"
|
||||
:task-bar-config="props.taskBarConfig"
|
||||
:allow-drag-and-resize="props.allowDragAndResize && !isInHighlightMode"
|
||||
:show-actual-taskbar="props.showActualTaskbar"
|
||||
:enable-task-bar-tooltip="props.enableTaskBarTooltip"
|
||||
:pending-task-background-color="props.pendingTaskBackgroundColor"
|
||||
:delay-task-background-color="props.delayTaskBackgroundColor"
|
||||
:complete-task-background-color="props.completeTaskBackgroundColor"
|
||||
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
|
||||
:is-highlighted="highlightedTaskIds.has(task.id)"
|
||||
:is-primary-highlight="highlightedTaskId === task.id"
|
||||
:is-in-highlight-mode="isInHighlightMode"
|
||||
|
||||
Reference in New Issue
Block a user