forked from mt-fe-group/mt-yd-ui
Compare commits
2 Commits
c1e152405d
...
0ddba21e1c
Author | SHA1 | Date | |
---|---|---|---|
0ddba21e1c | |||
535cb490b1 |
154
AddOrUpdateConfig.md
Normal file
154
AddOrUpdateConfig.md
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# Add Or Update Dialog Configs
|
||||||
|
|
||||||
|
> 通过传入合理的配置项来使用 addOrUpdate Dialog
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
```js
|
||||||
|
const addOrUpdateConfigs = {
|
||||||
|
type: 'dialog', // dialog | drawer | page
|
||||||
|
infoUrl: '/monitoring/product',
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
api: '/monitoring/product/getCode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'processTime',
|
||||||
|
label: '加工时间',
|
||||||
|
placeholder: '请输入加工时间',
|
||||||
|
type: 'number', // type: number(input+number) | default(input) | textarea | select(options在父组件里获取) | datetime
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
// 除了required之外的验证规则
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
trigger: 'blur',
|
||||||
|
transform: val => Number(val),
|
||||||
|
message: '必须输入数字'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'remark',
|
||||||
|
'specifications',
|
||||||
|
{
|
||||||
|
name: 'typeDictValue',
|
||||||
|
rules: [{ required: true, trigger: 'blur' }],
|
||||||
|
label: '产品类型', // 对于非常见属性,最好自己指定label
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
// 动态获取
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unitDictValue',
|
||||||
|
label: '单位',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择单位',
|
||||||
|
options: [
|
||||||
|
// 动态获取
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
operations: [
|
||||||
|
// { name: 'reset', url: true },
|
||||||
|
{ name: 'cancel', url: true, showAlways: true },
|
||||||
|
{ name: 'save', url: '/monitoring/product', permission: '', showOnEdit: false },
|
||||||
|
{ name: 'update', url: '/monitoring/product', permission: '', showOnEdit: true }
|
||||||
|
],
|
||||||
|
subtable: {
|
||||||
|
// for i18n
|
||||||
|
title: '动态属性',
|
||||||
|
url: '/monitoring/productArrt',
|
||||||
|
tableConfigs: [
|
||||||
|
{ type: 'index', name: '序号' },
|
||||||
|
{ prop: 'createTime', name: '添加时间', filter: val => (val ? moment(val).format('YYYY-MM-DD hh:mm:ss') : '-') },
|
||||||
|
{ prop: 'name', name: '属性名', formField: true, rules: [{ required: true, message: '必填', trigger: 'blur' }] },
|
||||||
|
{ prop: 'code', name: '属性值', formField: true },
|
||||||
|
{ prop: 'operations', name: '操作', fixed: 'right', width: 180, subcomponent: TableOperateComponent, options: ['edit', 'delete'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extraComponents: [
|
||||||
|
{
|
||||||
|
name: 'CompName',
|
||||||
|
label: 'markdown编辑器',
|
||||||
|
component: () => import('xx.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### type
|
||||||
|
|
||||||
|
类型: string
|
||||||
|
值:dialog | drawer | page
|
||||||
|
含义:对话框、抽屉、新页面
|
||||||
|
|
||||||
|
### infoUrl
|
||||||
|
|
||||||
|
类型:string
|
||||||
|
含义:详情的接口,如 `/monitoring/product`
|
||||||
|
|
||||||
|
### fields
|
||||||
|
|
||||||
|
含义:设置新增、编辑时的字段
|
||||||
|
类型:`Array<string | object>`
|
||||||
|
|
||||||
|
- 当类型为 string 时,默认渲染 `<input>`
|
||||||
|
- 当类型为 object 时,有如下选项:
|
||||||
|
- name: 字段名
|
||||||
|
- label: 字段的 label
|
||||||
|
- api: 如果设置了该属性,则该字段会自动从服务器获取值,一般为 code 字段需要
|
||||||
|
- placeholder
|
||||||
|
- type: 渲染何种类型的组件,默认值: 'input'
|
||||||
|
- options: 当上一条 type 值为 'select' 时,需要自行动态获取并加载 options 列表
|
||||||
|
- required: 是否是必须填写的字段(或可用过 rules 做更加具体的设定,设定方式参考 async-validator )
|
||||||
|
- rules: 验证规则数组,如果只有"必填"的需求,可直接用上一条
|
||||||
|
|
||||||
|
### operations
|
||||||
|
|
||||||
|
含义:设置对话框等组件里,需要哪些按钮
|
||||||
|
类型:`Array<object>`
|
||||||
|
属性:
|
||||||
|
|
||||||
|
- name,按钮的类型,决定按钮的文字和颜色
|
||||||
|
- url,按钮操作的接口地址,不需要的可以给 null 或 true
|
||||||
|
- permission,该操作需要的权限,如 "sys:xxx:add" 形式
|
||||||
|
- showOnEdit: boolean,是否编辑页面展示,不设置则始终展示
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```js
|
||||||
|
operations: [
|
||||||
|
{ name: 'reset', url: true },
|
||||||
|
{ name: 'cancel', url: true, showAlways: true },
|
||||||
|
{ name: 'save', url: '/monitoring/product', permission: '', showOnEdit: false },
|
||||||
|
{ name: 'update', url: '/monitoring/product', permission: '', showOnEdit: true }
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### subtable
|
||||||
|
|
||||||
|
含义:有些对话框里需要额外的表格来展示更深层次的数据,如“产品属性”
|
||||||
|
类型:object
|
||||||
|
选项:
|
||||||
|
- title, 内嵌表格的标题
|
||||||
|
- url, 内嵌表格的数据地址
|
||||||
|
- tableConfigs,内嵌表格的配置选项
|
||||||
|
- 类型:`Array<object>`
|
||||||
|
- 配置:
|
||||||
|
- type: 同 element-ui 的 table 属性的 type
|
||||||
|
- fixed: 同 element-ui 的 table 属性的 fixed
|
||||||
|
- width: 同 element-ui 的 table 属性的 width
|
||||||
|
- name: 表头显示的内容
|
||||||
|
- filter: 一般用于转换时间
|
||||||
|
- prop: 字段
|
||||||
|
- formField: boolean, 是否用于表单的填写
|
||||||
|
- rules: 表单验证规则,详见:async-validator
|
||||||
|
- subcomponent: 同 base-table 的 subcomponent
|
||||||
|
- options: 表格操作列需要哪些操作
|
||||||
|
- 值:`edit` | `delete` | `detail`,需要其他可自行添加(修改 base-table 组件)
|
247
src/components/base-dialog/AttrForm/index.vue
Normal file
247
src/components/base-dialog/AttrForm/index.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<div class="attr-form">
|
||||||
|
<h3>{{ title }} <el-button style="margin-left: 8px;" type="text" @click="showAddAttr = true">添加</el-button></h3>
|
||||||
|
<div v-if="!showAddAttr">
|
||||||
|
<component
|
||||||
|
key="sub-table"
|
||||||
|
:is="require('../../base-table/index.vue').default"
|
||||||
|
:table-head-configs="tableConfigs"
|
||||||
|
:data="dataList"
|
||||||
|
:max-height="500"
|
||||||
|
@operate-event="handleOperations"
|
||||||
|
/>
|
||||||
|
<el-pagination
|
||||||
|
@size-change="sizeChangeHandle"
|
||||||
|
@current-change="currentChangeHandle"
|
||||||
|
:current-page="pageIndex"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="totalPage"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
<div v-else style="background: #eee; border-radius: 8px; padding: 12px;">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="10" :offset="7">
|
||||||
|
<el-form ref="AttrForm" :model="AttrForm" :rules="AttrFormRules" :inline="true">
|
||||||
|
<el-form-item v-for="field in attrFormFields" :key="field.prop" :prop="field.prop" :label="field.name">
|
||||||
|
<el-input v-model="AttrForm[field.prop]" clearable />
|
||||||
|
<!-- add more... -->
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="text-align: center;">
|
||||||
|
<el-button size="small" @click="handleCloseAttrForm">取消</el-button>
|
||||||
|
<el-button type="success" size="small" @click="handleSaveAttrForm">保存</el-button>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseTable from '@/components/base-table'
|
||||||
|
import { pick } from 'lodash/object'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AttrForm',
|
||||||
|
components: { BaseTable },
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/** subtable 需要设置的属性 */
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
tableConfigs: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
/** 表单提交需要的属性 */
|
||||||
|
pId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAddAttr: false,
|
||||||
|
dataList: [],
|
||||||
|
pageIndex: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
totalPage: 0,
|
||||||
|
AttrForm: {}, // 需动态设置
|
||||||
|
AttrFormRules: {} // 需动态设置
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
attrFormFields() {
|
||||||
|
const _ = this.tableConfigs.filter(item => item.formField)
|
||||||
|
/** 顺带配置 AttrForm */
|
||||||
|
_.forEach(item => {
|
||||||
|
this.$set(this.AttrForm, [item.prop], '')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$set(this.AttrForm, 'id', null)
|
||||||
|
return _
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getDataList()
|
||||||
|
/** 设置 AttrForm 的 rules */
|
||||||
|
for(const config of this.tableConfigs) {
|
||||||
|
if (config.rules) {
|
||||||
|
this.$set(this.AttrFormRules, [config.prop], config.rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** init dataform */
|
||||||
|
initAttrForm() {
|
||||||
|
Object.entries(this.AttrForm).forEach(([key, value]) => {
|
||||||
|
if (typeof value === 'object' || typeof value === 'number') {
|
||||||
|
this.AttrForm[key] = null
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
this.AttrForm[key] = []
|
||||||
|
} else {
|
||||||
|
this.AttrForm[key] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** requests */
|
||||||
|
getDataList() {
|
||||||
|
this.dataListLoading = true
|
||||||
|
// 获取动态属性列表
|
||||||
|
this.$http({
|
||||||
|
url: this.$http.adornUrl(`${this.url}/page`),
|
||||||
|
method: 'get',
|
||||||
|
params: this.$http.adornParams({
|
||||||
|
page: this.pageIndex,
|
||||||
|
limit: this.pageSize,
|
||||||
|
productId: this.pId
|
||||||
|
// order: 'asc/desc',
|
||||||
|
// orderField: 'name'
|
||||||
|
})
|
||||||
|
}).then(({ data: res }) => {
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.dataList = res.data.list
|
||||||
|
this.totalPage = res.data.total
|
||||||
|
} else {
|
||||||
|
this.dataList = []
|
||||||
|
this.totalPage = 0
|
||||||
|
}
|
||||||
|
this.dataListLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** handlers */
|
||||||
|
handleOperations({ type, data: id }) {
|
||||||
|
switch (type) {
|
||||||
|
case 'edit':
|
||||||
|
{
|
||||||
|
this.showAddAttr = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$http.get(this.$http.adornUrl(`${this.url}/${id}`)).then(({ data: res }) => {
|
||||||
|
if (res && res.code === 0 && res.data) {
|
||||||
|
const neededFields = [...this.attrFormFields.map(item => item.prop), 'id']
|
||||||
|
const filtered = pick(res.data, neededFields)
|
||||||
|
for (let field of neededFields) {
|
||||||
|
this.AttrForm[field] = filtered[field]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
return this.deleteHandle(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteHandle(id) {
|
||||||
|
var ids = id ? [id] : []
|
||||||
|
|
||||||
|
this.$confirm(`确定对id=${ids.join(',')}进行${id ? '删除' : '批量删除'}操作?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.$http({
|
||||||
|
url: this.$http.adornUrl(this.url),
|
||||||
|
method: 'delete',
|
||||||
|
data: this.$http.adornData(ids, false, 'raw')
|
||||||
|
}).then(({ data }) => {
|
||||||
|
if (data && data.code === 0) {
|
||||||
|
this.$message({
|
||||||
|
message: '操作成功',
|
||||||
|
type: 'success',
|
||||||
|
duration: 1500,
|
||||||
|
onClose: () => {
|
||||||
|
this.getDataList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCloseAttrForm() {
|
||||||
|
this.showAddAttr = false
|
||||||
|
this.initAttrForm()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveAttrForm() {
|
||||||
|
this.$refs['AttrForm'].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.$http({
|
||||||
|
// url: this.$http.adornUrl(`${this.url}/${!this.AttrForm.id ? '' : this.AttrForm.id}`),
|
||||||
|
url: this.$http.adornUrl(this.url),
|
||||||
|
method: this.AttrForm.id ? 'put' : 'post',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
data: JSON.stringify({ ...this.AttrForm, productId: this.pId })
|
||||||
|
}).then(({ data }) => {
|
||||||
|
if (data && data.code === 0) {
|
||||||
|
this.$message({
|
||||||
|
message: '操作成功',
|
||||||
|
type: 'success',
|
||||||
|
duration: 1500,
|
||||||
|
onClose: () => {
|
||||||
|
this.showAddAttr = false
|
||||||
|
this.getDataList()
|
||||||
|
this.initAttrForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 每页数
|
||||||
|
sizeChangeHandle(val) {
|
||||||
|
this.pageSize = val
|
||||||
|
this.pageIndex = 1
|
||||||
|
this.getDataList()
|
||||||
|
},
|
||||||
|
// 当前页
|
||||||
|
currentChangeHandle(val) {
|
||||||
|
this.pageIndex = val
|
||||||
|
this.getDataList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -45,33 +45,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template v-if="dataForm.id && configs.subtable">
|
<template v-if="dataForm.id && configs.subtable">
|
||||||
<h3>{{ configs.subtable.title }} <el-button style="margin-left: 8px;" type="text" @click="showAddAttr = true">添加</el-button></h3>
|
<attr-form :pId="dataForm.id" v-bind="configs.subtable" />
|
||||||
<component
|
|
||||||
v-if="!showAddAttr"
|
|
||||||
key="sub-table"
|
|
||||||
:is="require('../../base-table/index.vue').default"
|
|
||||||
:table-head-configs="configs.subtable.tableConfigs"
|
|
||||||
:data="subtableDataList"
|
|
||||||
:max-height="500"
|
|
||||||
@operate-event="handleOperations"
|
|
||||||
@refreshDataList="getDataList"
|
|
||||||
/>
|
|
||||||
<div v-else style="background: #eee; border-radius: 8px; height: 200px;">
|
|
||||||
<el-row>
|
|
||||||
<el-form ref="AttrForm" :model="AttrForm" :rules="AttrFormRules">
|
|
||||||
<el-form-item v-for="field in attrFormFields" :key="field.prop" :prop="field.prop" :label="field.name">
|
|
||||||
<!-- 子窗口只给input吧,太麻烦了,有特殊需求就不用这个组件了 -->
|
|
||||||
<el-input v-model="AttrForm[field.prop]" clearable />
|
|
||||||
<!-- TODO: mounted里初始化AttrForm,configs里设置AttrFormRules,mounted里设置attrFormFields -->
|
|
||||||
<!-- TODO: 单独用一个组件来展示动态属性这一部分 -->
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-row>
|
|
||||||
<el-row>
|
|
||||||
<el-button size="small" @click="showAddAttr = false">取消</el-button>
|
|
||||||
<el-button type="primary" size="small" @click="handleSaveAttrForm">保存</el-button>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
@ -93,6 +67,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import AttrForm from '../AttrForm'
|
||||||
import { pick } from 'lodash/object'
|
import { pick } from 'lodash/object'
|
||||||
|
|
||||||
// 标题 for i18n
|
// 标题 for i18n
|
||||||
@ -125,7 +100,7 @@ const COLUMN_PER_ROW = 2
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddOrUpdateDialog',
|
name: 'AddOrUpdateDialog',
|
||||||
components: {},
|
components: { AttrForm },
|
||||||
props: {
|
props: {
|
||||||
configs: {
|
configs: {
|
||||||
/**
|
/**
|
||||||
@ -170,18 +145,12 @@ export default {
|
|||||||
},
|
},
|
||||||
defaultPlaceholders: {}, // 自动根据 defaultNames 计算得来
|
defaultPlaceholders: {}, // 自动根据 defaultNames 计算得来
|
||||||
/** 表单相关属性 */
|
/** 表单相关属性 */
|
||||||
|
|
||||||
visible: false,
|
visible: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
isDetail: false,
|
isDetail: false,
|
||||||
isUpdated: false,
|
isUpdated: false,
|
||||||
dataForm: {},
|
dataForm: {},
|
||||||
dataFormRules: {},
|
dataFormRules: {},
|
||||||
/** 子列表相关属性 */
|
|
||||||
subtableDataList: [],
|
|
||||||
showAddAttr: false,
|
|
||||||
AttrForm: {},
|
|
||||||
AttrFormRules: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -369,12 +338,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleOperations() {},
|
|
||||||
|
|
||||||
getDataList() {},
|
|
||||||
|
|
||||||
handleSaveAttrForm() {},
|
|
||||||
|
|
||||||
handleClose() {
|
handleClose() {
|
||||||
if (this.isAdd || this.isUpdated) this.$emit('refreshDataList')
|
if (this.isAdd || this.isUpdated) this.$emit('refreshDataList')
|
||||||
this.visible = false
|
this.visible = false
|
||||||
|
@ -157,11 +157,11 @@ const addOrUpdateConfigs = {
|
|||||||
subtable: {
|
subtable: {
|
||||||
// for i18n
|
// for i18n
|
||||||
title: '动态属性',
|
title: '动态属性',
|
||||||
url: '/monitoring/productArrt/page',
|
url: '/monitoring/productArrt',
|
||||||
tableConfigs: [
|
tableConfigs: [
|
||||||
{ type: 'index', name: '序号' },
|
{ type: 'index', name: '序号' },
|
||||||
{ prop: 'createTime', name: '添加时间', filter: val => (val ? moment(val).format('YYYY-MM-DD hh:mm:ss') : '-') },
|
{ prop: 'createTime', name: '添加时间', filter: val => (val ? moment(val).format('YYYY-MM-DD hh:mm:ss') : '-') },
|
||||||
{ prop: 'name', name: '属性名', formField: true },
|
{ prop: 'name', name: '属性名', formField: true, rules: [{ required: true, message: '必填', trigger: 'blur' }] },
|
||||||
{ prop: 'code', name: '属性值', formField: true },
|
{ prop: 'code', name: '属性值', formField: true },
|
||||||
{ prop: 'operations', name: '操作', fixed: 'right', width: 180, subcomponent: TableOperateComponent, options: ['edit', 'delete'] }
|
{ prop: 'operations', name: '操作', fixed: 'right', width: 180, subcomponent: TableOperateComponent, options: ['edit', 'delete'] }
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user