fix: 优化watch 修改编辑用户弹窗样式
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive, watch } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
import { Modal, Form, Input, Button, message } from 'ant-design-vue';
|
import { Modal, Form, Input, Button, message } from 'ant-design-vue';
|
||||||
import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller';
|
import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller';
|
||||||
import styles from './style.module.less';
|
import styles from './style.module.less';
|
||||||
@@ -30,16 +31,12 @@ export default defineComponent({
|
|||||||
const formData = reactive(getDefaultForm());
|
const formData = reactive(getDefaultForm());
|
||||||
|
|
||||||
/** 监听 visible 重置表单 */
|
/** 监听 visible 重置表单 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => props.visible,
|
if (props.visible) {
|
||||||
(val) => {
|
Object.assign(formData, getDefaultForm());
|
||||||
if (val) {
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
Object.assign(formData, getDefaultForm());
|
}
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
}, [() => props.visible]);
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 提交 */
|
/** 提交 */
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/hooks/useAuth.ts
|
// src/hooks/useAuth.ts
|
||||||
import { useState } from './useState';
|
import { useState } from './useState';
|
||||||
import { watch } from 'vue';
|
import { useEffect } from './useEffect';
|
||||||
import { useMenuStore } from '@/stores/menuStore';
|
import { useMenuStore } from '@/stores/menuStore';
|
||||||
import { usePermissionStore } from '@/stores/permissionStore';
|
import { usePermissionStore } from '@/stores/permissionStore';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
@@ -10,13 +10,14 @@ const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
|
|||||||
function createAuth() {
|
function createAuth() {
|
||||||
const [token, setToken] = useState<string | null>(window.localStorage.getItem(TOKEN_KEY));
|
const [token, setToken] = useState<string | null>(window.localStorage.getItem(TOKEN_KEY));
|
||||||
|
|
||||||
watch(token, (newToken) => {
|
useEffect(() => {
|
||||||
|
const newToken = token.value;
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
window.localStorage.setItem(TOKEN_KEY, newToken);
|
window.localStorage.setItem(TOKEN_KEY, newToken);
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem(TOKEN_KEY);
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
}
|
}
|
||||||
});
|
}, [token]);
|
||||||
|
|
||||||
const isLoggedIn = () => !!token.value;
|
const isLoggedIn = () => !!token.value;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/hooks/useDebounce.ts
|
// src/hooks/useDebounce.ts
|
||||||
import { watch, onUnmounted, Ref } from 'vue';
|
import { onUnmounted, Ref } from 'vue';
|
||||||
import { useState } from './useState';
|
import { useState } from './useState';
|
||||||
|
import { useEffect } from './useEffect';
|
||||||
|
|
||||||
export interface UseDebounceOptions {
|
export interface UseDebounceOptions {
|
||||||
delay?: number;
|
delay?: number;
|
||||||
@@ -40,7 +41,7 @@ export function useDebounce<T>(source: Ref<T>, options: number | UseDebounceOpti
|
|||||||
isFirstCall = true;
|
isFirstCall = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(source, () => {
|
useEffect(() => {
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
|
|
||||||
// 处理 maxWait
|
// 处理 maxWait
|
||||||
@@ -61,7 +62,9 @@ export function useDebounce<T>(source: Ref<T>, options: number | UseDebounceOpti
|
|||||||
updateValue();
|
updateValue();
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return clearTimers;
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
// 卸载
|
// 卸载
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
+16
-19
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
// src/hooks/usePagination.ts
|
// src/hooks/usePagination.ts
|
||||||
import { computed, ref, watch, Ref, triggerRef } from 'vue';
|
import { computed, ref, Ref, triggerRef } from 'vue';
|
||||||
|
import { useEffect } from './useEffect';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页配置项
|
* 分页配置项
|
||||||
@@ -77,24 +78,20 @@ export function usePagination(options: UsePaginationOptions = {}): UsePagination
|
|||||||
const pageSizeRef = ref(options.pageSize ?? defaultPageSize);
|
const pageSizeRef = ref(options.pageSize ?? defaultPageSize);
|
||||||
const totalRef = ref(options.total ?? 0);
|
const totalRef = ref(options.total ?? 0);
|
||||||
|
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => options.current,
|
const val = options.current;
|
||||||
(val) => {
|
if (val !== undefined) currentRef.value = val;
|
||||||
if (val !== undefined) currentRef.value = val;
|
}, [() => options.current]);
|
||||||
},
|
|
||||||
);
|
useEffect(() => {
|
||||||
watch(
|
const val = options.pageSize;
|
||||||
() => options.pageSize,
|
if (val !== undefined) pageSizeRef.value = val;
|
||||||
(val) => {
|
}, [() => options.pageSize]);
|
||||||
if (val !== undefined) pageSizeRef.value = val;
|
|
||||||
},
|
useEffect(() => {
|
||||||
);
|
const val = options.total;
|
||||||
watch(
|
if (val !== undefined) totalRef.value = val;
|
||||||
() => options.total,
|
}, [() => options.total]);
|
||||||
(val) => {
|
|
||||||
if (val !== undefined) totalRef.value = val;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalPages = computed(() => {
|
const totalPages = computed(() => {
|
||||||
const total = totalRef.value;
|
const total = totalRef.value;
|
||||||
|
|||||||
+22
-23
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { ref, Ref, watch, WatchSource, onUnmounted, computed } from 'vue';
|
import { ref, Ref, WatchSource, onUnmounted, computed } from 'vue';
|
||||||
|
import { useEffect } from './useEffect';
|
||||||
|
|
||||||
export interface UseRequestReturn<T> {
|
export interface UseRequestReturn<T> {
|
||||||
/** 纯净的数据,不包含后端外层壳 */
|
/** 纯净的数据,不包含后端外层壳 */
|
||||||
@@ -128,31 +129,29 @@ export function useRequest<T>(
|
|||||||
const shouldAutoRun = computed(() => !manual && ready.value);
|
const shouldAutoRun = computed(() => !manual && ready.value);
|
||||||
let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发
|
let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发
|
||||||
|
|
||||||
watch(
|
useEffect(() => {
|
||||||
shouldAutoRun,
|
const val = shouldAutoRun.value;
|
||||||
(val) => {
|
if (val && !hasAutoRun) {
|
||||||
if (val && !hasAutoRun) {
|
hasAutoRun = true;
|
||||||
hasAutoRun = true;
|
run();
|
||||||
run();
|
} else if (!val) {
|
||||||
} else if (!val) {
|
// 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行
|
||||||
// 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行
|
hasAutoRun = false;
|
||||||
hasAutoRun = false;
|
}
|
||||||
}
|
}, [shouldAutoRun]);
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// refreshDeps 变化时重新请求(初始化时不再重复触发)
|
// refreshDeps 变化时重新请求(初始化时不再重复触发)
|
||||||
if (refreshDeps && refreshDeps.length > 0) {
|
if (refreshDeps && refreshDeps.length > 0) {
|
||||||
watch(
|
let skipInitialRefresh = true;
|
||||||
refreshDeps,
|
useEffect(() => {
|
||||||
() => {
|
if (skipInitialRefresh) {
|
||||||
if (shouldAutoRun.value) {
|
skipInitialRefresh = false;
|
||||||
run();
|
return;
|
||||||
}
|
}
|
||||||
},
|
if (shouldAutoRun.value) {
|
||||||
{ deep: true },
|
run();
|
||||||
);
|
}
|
||||||
|
}, refreshDeps);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSignal = () => abortController?.signal;
|
const getSignal = () => abortController?.signal;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, watch, onBeforeUnmount, toValue } from 'vue';
|
import { ref, onBeforeUnmount, toValue } from 'vue';
|
||||||
import type { Ref, MaybeRefOrGetter } from 'vue';
|
import type { Ref, MaybeRefOrGetter } from 'vue';
|
||||||
|
import { useEffect } from './useEffect';
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
export type WebSocketStatus = 'connecting' | 'open' | 'closed' | 'error';
|
export type WebSocketStatus = 'connecting' | 'open' | 'closed' | 'error';
|
||||||
@@ -216,14 +217,16 @@ export function useWebSocket(
|
|||||||
// --- 监听与副作用 ---
|
// --- 监听与副作用 ---
|
||||||
|
|
||||||
// 监听 URL 变化,如果变了则重新连接
|
// 监听 URL 变化,如果变了则重新连接
|
||||||
watch(
|
let skipInitialUrlWatch = true;
|
||||||
() => toValue(url),
|
useEffect(() => {
|
||||||
() => {
|
if (skipInitialUrlWatch) {
|
||||||
if (status.value !== 'closed') {
|
skipInitialUrlWatch = false;
|
||||||
connect();
|
return;
|
||||||
}
|
}
|
||||||
},
|
if (status.value !== 'closed') {
|
||||||
);
|
connect();
|
||||||
|
}
|
||||||
|
}, [() => toValue(url)]);
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
// 组件卸载时清理资源
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive, watch } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
@@ -119,33 +120,27 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 监听 visible 变化重置表单 */
|
/** 监听 visible 变化重置表单 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => props.visible,
|
if (props.visible) {
|
||||||
(val) => {
|
initFormFromRecord(props.record);
|
||||||
if (val) {
|
cropperVisible.value = false;
|
||||||
initFormFromRecord(props.record);
|
uploadImageUrl.value = '';
|
||||||
cropperVisible.value = false;
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
uploadImageUrl.value = '';
|
}
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
}, [() => props.visible]);
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 监听 jumpType 变化时清理联动字段 */
|
/** 监听 jumpType 变化时清理联动字段 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => formData.jumpType,
|
const newType = formData.jumpType;
|
||||||
(newType) => {
|
if (newType !== 'event_detail') formData.relatedEventId = '';
|
||||||
if (newType !== 'event_detail') formData.relatedEventId = '';
|
if (newType !== 'miniprogram_page') formData.pagePath = '';
|
||||||
if (newType !== 'miniprogram_page') formData.pagePath = '';
|
if (newType !== 'custom_link') formData.customUrl = '';
|
||||||
if (newType !== 'custom_link') formData.customUrl = '';
|
const fieldsToValidate: string[] = [];
|
||||||
const fieldsToValidate: string[] = [];
|
if (newType === 'event_detail') fieldsToValidate.push('relatedEventId');
|
||||||
if (newType === 'event_detail') fieldsToValidate.push('relatedEventId');
|
if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath');
|
||||||
if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath');
|
if (newType === 'custom_link') fieldsToValidate.push('customUrl');
|
||||||
if (newType === 'custom_link') fieldsToValidate.push('customUrl');
|
if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate);
|
||||||
if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate);
|
}, [() => formData.jumpType]);
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 触发隐藏的文件选择器 */
|
/** 触发隐藏的文件选择器 */
|
||||||
const triggerFilePicker = () => {
|
const triggerFilePicker = () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive, watch } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
import { Modal, Form, Input, Tree, Button } from 'ant-design-vue';
|
import { Modal, Form, Input, Tree, Button } from 'ant-design-vue';
|
||||||
import type { TreeProps } from 'ant-design-vue';
|
import type { TreeProps } from 'ant-design-vue';
|
||||||
import { roleNameRules, PERMISSION_TREE } from '../controller';
|
import { roleNameRules, PERMISSION_TREE } from '../controller';
|
||||||
@@ -60,16 +61,12 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 监听 visible 重置 */
|
/** 监听 visible 重置 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => props.visible,
|
if (props.visible) {
|
||||||
(val) => {
|
initFormFromRecord(props.record);
|
||||||
if (val) {
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
initFormFromRecord(props.record);
|
}
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
}, [() => props.visible]);
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Tree 勾选回调(受控) */
|
/** Tree 勾选回调(受控) */
|
||||||
const handleCheck: TreeProps['onCheck'] = (checked) => {
|
const handleCheck: TreeProps['onCheck'] = (checked) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineComponent, ref, reactive, computed, watch } from 'vue';
|
import { defineComponent, ref, reactive, computed } from 'vue';
|
||||||
import { Modal, Form, Input, Table, Button, Space, Pagination } from 'ant-design-vue';
|
import { Modal, Form, Input, Table, Button, Space, Pagination } from 'ant-design-vue';
|
||||||
import { useState } from '@/hooks';
|
import { useEffect, useState } from '@/hooks';
|
||||||
import styles from './UserListModal.module.less';
|
import styles from './UserListModal.module.less';
|
||||||
|
|
||||||
interface UserListModalProps {
|
interface UserListModalProps {
|
||||||
@@ -35,12 +35,9 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** 监听 visible/users 变化重置分页 */
|
/** 监听 visible/users 变化重置分页 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => [props.visible, props.users.length],
|
setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length });
|
||||||
() => {
|
}, [() => props.visible, () => props.users.length]);
|
||||||
setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 });
|
setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 });
|
||||||
|
|||||||
@@ -20,25 +20,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 编辑模式密码区 ===== */
|
/* ===== 密码区重置链接 ===== */
|
||||||
.passwordEditRow {
|
.resetLink {
|
||||||
display: flex;
|
font-size: 14px;
|
||||||
align-items: center;
|
color: #1677ff;
|
||||||
gap: 8px;
|
cursor: pointer;
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.passwordMask {
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: rgba(0, 0, 0, 0.65);
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
|
||||||
|
|
||||||
.resetBtn {
|
&:hover {
|
||||||
flex-shrink: 0;
|
color: #4096ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 取消重置行 ===== */
|
/* ===== 取消重置行 ===== */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineComponent, ref, reactive, watch } from 'vue';
|
import { defineComponent, ref, reactive } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
import { Modal, Form, Input, Select, Button } from 'ant-design-vue';
|
import { Modal, Form, Input, Select, Button } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
ROLE_FORM_OPTIONS,
|
ROLE_FORM_OPTIONS,
|
||||||
@@ -62,17 +63,13 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 监听 visible 变化重置表单 */
|
/** 监听 visible 变化重置表单 */
|
||||||
watch(
|
useEffect(() => {
|
||||||
() => props.visible,
|
if (props.visible) {
|
||||||
(val) => {
|
initFormFromRecord(props.record);
|
||||||
if (val) {
|
isResetting.value = false;
|
||||||
initFormFromRecord(props.record);
|
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||||
isResetting.value = false;
|
}
|
||||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
}, [() => props.visible]);
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 点击"重置密码" */
|
/** 点击"重置密码" */
|
||||||
const handleResetPassword = () => {
|
const handleResetPassword = () => {
|
||||||
@@ -146,19 +143,28 @@ export default defineComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ====== 密码区 ====== */}
|
{/* ====== 密码区 + 角色 ====== */}
|
||||||
{isEdit && !isResetting.value ? (
|
{isEdit && !isResetting.value ? (
|
||||||
/* 编辑模式 - 密码占位 + 重置密码按钮 */
|
/* 编辑模式未重置:密码占位 + 角色 同一行 */
|
||||||
<div class={styles.row}>
|
<div class={styles.row}>
|
||||||
<Form.Item label="密码" class={styles.col}>
|
<Form.Item label="密码" class={styles.col}>
|
||||||
<div class={styles.passwordEditRow}>
|
<Input
|
||||||
<span class={styles.passwordMask}>{PASSWORD_PLACEHOLDER}</span>
|
value={PASSWORD_PLACEHOLDER}
|
||||||
<Button size="small" class={styles.resetBtn} onClick={handleResetPassword}>
|
readonly
|
||||||
重置密码
|
suffix={
|
||||||
</Button>
|
<a class={styles.resetLink} onClick={handleResetPassword}>
|
||||||
</div>
|
重置密码
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="角色" name="role" rules={roleRules} class={styles.col}>
|
||||||
|
<Select
|
||||||
|
options={ROLE_FORM_OPTIONS as any}
|
||||||
|
placeholder="请选择角色"
|
||||||
|
v-model:value={formData.role}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div class={styles.col} />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -189,18 +195,17 @@ export default defineComponent({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 角色:单独一行 */}
|
||||||
|
<Form.Item label="角色" name="role" rules={roleRules}>
|
||||||
|
<Select
|
||||||
|
options={ROLE_FORM_OPTIONS as any}
|
||||||
|
placeholder="请选择角色"
|
||||||
|
v-model:value={formData.role}
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 角色:单独一行,固定宽度 */}
|
|
||||||
<Form.Item label="角色" name="role" rules={roleRules}>
|
|
||||||
<Select
|
|
||||||
options={ROLE_FORM_OPTIONS as any}
|
|
||||||
placeholder="请选择角色"
|
|
||||||
v-model:value={formData.role}
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* 底部按钮 */}
|
{/* 底部按钮 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user