refactor: clean up Member module and update Console
- Remove redundant PointsRule repositories (use single PointsRuleRepository) - Clean up Member migrations and consolidate to single Init migration - Update Console frontend API and components for Tenant - Add H5LinkService for member H5 integration
This commit is contained in:
parent
cd9be23693
commit
5cbfb2866c
@ -71,4 +71,13 @@ export namespace TenantApi {
|
||||
export async function updateTenantSettings(id: number, data: FenglingApi.Tenant.TenantSettings) {
|
||||
return requestClient.put<void, FenglingApi.Tenant.TenantSettings>(`${apiPrefix}/tenants/${id}/settings`, data);
|
||||
}
|
||||
|
||||
export interface H5LinkResult {
|
||||
link: string;
|
||||
qrCodeBase64: string;
|
||||
}
|
||||
|
||||
export async function getH5Link(id: number) {
|
||||
return requestClient.get<H5LinkResult>(`${apiPrefix}/tenants/${id}/h5-link`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,10 @@ export namespace FenglingApi {
|
||||
expiresAt?: string
|
||||
description?: string
|
||||
createdAt: string
|
||||
customDomain?: string
|
||||
basePath?: string
|
||||
logo?: string
|
||||
h5BaseUrl?: string
|
||||
}
|
||||
|
||||
export interface CreateTenantDto {
|
||||
@ -25,6 +29,10 @@ export namespace FenglingApi {
|
||||
expiresAt?: string
|
||||
status?: string
|
||||
description?: string
|
||||
customDomain?: string
|
||||
basePath?: string
|
||||
logo?: string
|
||||
h5BaseUrl?: string
|
||||
}
|
||||
|
||||
export interface UpdateTenantDto {
|
||||
@ -36,6 +44,10 @@ export namespace FenglingApi {
|
||||
expiresAt?: string
|
||||
status?: string
|
||||
description?: string
|
||||
customDomain?: string
|
||||
basePath?: string
|
||||
logo?: string
|
||||
h5BaseUrl?: string
|
||||
}
|
||||
|
||||
export interface TenantSettings {
|
||||
|
||||
@ -114,6 +114,31 @@ const handleSave = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const qrCodeVisible = ref(false);
|
||||
const qrCodeUrl = ref('');
|
||||
const currentTenantName = ref('');
|
||||
|
||||
const handleCopyLink = async (row: FenglingApi.Tenant.Tenant) => {
|
||||
try {
|
||||
const { link } = await TenantApi.getH5Link(row.id);
|
||||
await navigator.clipboard.writeText(link);
|
||||
ElMessage.success('Link copied to clipboard');
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to copy link');
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowQrCode = async (row: FenglingApi.Tenant.Tenant) => {
|
||||
try {
|
||||
const { link, qrCodeBase64 } = await TenantApi.getH5Link(row.id);
|
||||
qrCodeUrl.value = `data:image/png;base64,${qrCodeBase64}`;
|
||||
currentTenantName.value = row.name;
|
||||
qrCodeVisible.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to generate QR code');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTenants();
|
||||
});
|
||||
@ -157,9 +182,11 @@ onMounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column prop="expiresAt" label="Expires At" width="180" />
|
||||
<el-table-column prop="createdAt" label="Created At" width="180" />
|
||||
<el-table-column label="Actions" width="200" fixed="right">
|
||||
<el-table-column label="Actions" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
|
||||
<el-button size="small" type="success" @click="handleCopyLink(row)">Copy Link</el-button>
|
||||
<el-button size="small" type="warning" @click="handleShowQrCode(row)">QR Code</el-button>
|
||||
<el-popconfirm title="Are you sure to delete this tenant?" @confirm="handleDelete(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">Delete</el-button>
|
||||
@ -218,5 +245,14 @@ onMounted(() => {
|
||||
<el-button type="primary" @click="handleSave">Save</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="qrCodeVisible" :title="currentTenantName + ' - QR Code'" width="300px">
|
||||
<div style="text-align: center;">
|
||||
<img :src="qrCodeUrl" alt="QR Code" style="max-width: 250px;" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="qrCodeVisible = false">Close</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
221
apps/web-ele/src/views/member-h5/auth/login.vue
Normal file
221
apps/web-ele/src/views/member-h5/auth/login.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const countdown = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const mode = ref<'login' | 'register'>('login')
|
||||
|
||||
const redirect = computed(() => route.query.redirect as string || '/')
|
||||
|
||||
const phoneRules = [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
]
|
||||
|
||||
const codeRules = [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ pattern: /^\d{4,6}$/, message: '请输入4-6位验证码', trigger: 'blur' }
|
||||
]
|
||||
|
||||
// 发送验证码
|
||||
const sendCode = async () => {
|
||||
if (countdown.value > 0) return
|
||||
if (!phone.value || !/^1[3-9]\d{9}$/.test(phone.value)) {
|
||||
alert('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
|
||||
countdown.value = 60
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// TODO: 调用发送验证码 API
|
||||
console.log('发送验证码到:', phone.value)
|
||||
}
|
||||
|
||||
// 登录/注册
|
||||
const handleSubmit = async () => {
|
||||
if (!phone.value || !code.value) {
|
||||
alert('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
// TODO: 调用登录 API
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
router.push(redirect.value)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
const handleWechatLogin = () => {
|
||||
// TODO: 微信登录逻辑
|
||||
console.log('微信登录')
|
||||
}
|
||||
|
||||
// 支付宝登录
|
||||
const handleAlipayLogin = () => {
|
||||
// TODO: 支付宝登录逻辑
|
||||
console.log('支付宝登录')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex flex-col">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="absolute top-1/4 -left-32 w-64 h-64 bg-purple-500/20 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-1/4 -right-32 w-64 h-64 bg-pink-500/20 rounded-full blur-3xl"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部 Logo 区域 -->
|
||||
<div class="relative z-10 pt-16 px-8 text-center">
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-3xl shadow-2xl shadow-purple-500/30 mb-6">
|
||||
<span class="text-4xl">🔔</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">风铃</h1>
|
||||
<p class="text-slate-400">享受品质生活</p>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<div class="relative z-10 flex-1 mt-12 px-6">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex bg-slate-800/50 rounded-2xl p-1 mb-8">
|
||||
<button
|
||||
@click="mode = 'login'"
|
||||
class="flex-1 py-3 rounded-xl text-sm font-medium transition-all"
|
||||
:class="mode === 'login' ? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg' : 'text-slate-400 hover:text-white'"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
@click="mode = 'register'"
|
||||
class="flex-1 py-3 rounded-xl text-sm font-medium transition-all"
|
||||
:class="mode === 'register' ? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg' : 'text-slate-400 hover:text-white'"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 手机号输入 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-slate-400 mb-2">手机号</label>
|
||||
<input
|
||||
v-model="phone"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
class="w-full bg-slate-800/80 border border-slate-700 rounded-2xl px-5 py-4 text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-slate-400 mb-2">验证码</label>
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
v-model="code"
|
||||
type="tel"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
class="flex-1 bg-slate-800/80 border border-slate-700 rounded-2xl px-5 py-4 text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
|
||||
/>
|
||||
<button
|
||||
@click="sendCode"
|
||||
:disabled="countdown > 0"
|
||||
class="px-4 py-4 bg-slate-800/80 border border-slate-700 rounded-2xl text-sm font-medium transition-all"
|
||||
:class="countdown > 0 ? 'text-slate-500 cursor-not-allowed' : 'text-purple-400 hover:text-purple-300 hover:border-purple-500'"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
@click="handleSubmit"
|
||||
:disabled="isLoading"
|
||||
class="w-full py-4 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-2xl text-white font-medium shadow-lg shadow-purple-500/30 hover:shadow-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="isLoading" class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
处理中...
|
||||
</span>
|
||||
<span v-else>{{ mode === 'login' ? '登 录' : '注 册' }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex-1 h-px bg-slate-700"></div>
|
||||
<span class="text-slate-500 text-sm">其他登录方式</span>
|
||||
<div class="flex-1 h-px bg-slate-700"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-6">
|
||||
<button
|
||||
@click="handleWechatLogin"
|
||||
class="w-14 h-14 bg-slate-800/80 rounded-2xl flex items-center justify-center text-2xl hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
💬
|
||||
</button>
|
||||
<button
|
||||
@click="handleAlipayLogin"
|
||||
class="w-14 h-14 bg-slate-800/80 rounded-2xl flex items-center justify-center text-2xl hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
💳
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<div class="relative z-10 p-6 text-center">
|
||||
<p class="text-slate-500 text-xs">
|
||||
登录即表示同意
|
||||
<a href="#" class="text-purple-400">《用户协议》</a>
|
||||
和
|
||||
<a href="#" class="text-purple-400">《隐私政策》</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 页面进入动画 */
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.relative.z-10 {
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.relative.z-10:nth-child(2) { animation-delay: 0.1s; }
|
||||
.relative.z-10:nth-child(3) { animation-delay: 0.2s; }
|
||||
</style>
|
||||
201
apps/web-ele/src/views/member-h5/home/index.vue
Normal file
201
apps/web-ele/src/views/member-h5/home/index.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 模拟数据 - 游客模式
|
||||
const isLoggedIn = ref(false)
|
||||
|
||||
const userInfo = ref({
|
||||
nickname: '会员用户',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Fengling',
|
||||
level: 5,
|
||||
levelName: '黄金会员',
|
||||
points: 1280,
|
||||
coupons: 3,
|
||||
orders: 12
|
||||
})
|
||||
|
||||
// 功能菜单
|
||||
const menuGroups = ref([
|
||||
{
|
||||
title: '我的资产',
|
||||
items: [
|
||||
{ icon: '🪙', label: '积分中心', path: '/points', requiresAuth: false },
|
||||
{ icon: '👑', label: '会员等级', path: '/level', requiresAuth: false },
|
||||
{ icon: '🎫', label: '我的卡券', path: '/coupons', requiresAuth: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '订单管理',
|
||||
items: [
|
||||
{ icon: '📦', label: '我的订单', path: '/orders', requiresAuth: true },
|
||||
{ icon: '📍', label: '收货地址', path: '/addresses', requiresAuth: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '账户设置',
|
||||
items: [
|
||||
{ icon: '👤', label: '个人资料', path: '/profile', requiresAuth: true },
|
||||
{ icon: '🔒', label: '账户安全', path: '/security', requiresAuth: true },
|
||||
{ icon: '⚙️', label: '设置', path: '/settings', requiresAuth: false },
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const handleMenuClick = (item: any) => {
|
||||
if (item.requiresAuth && !isLoggedIn.value) {
|
||||
router.push('/login?redirect=' + item.path)
|
||||
return
|
||||
}
|
||||
router.push(item.path)
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login?redirect=/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100">
|
||||
<!-- 顶部用户区域 -->
|
||||
<div class="relative bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 pb-24 pt-12">
|
||||
<!-- 装饰元素 -->
|
||||
<div class="absolute top-4 right-4 w-24 h-24 bg-white/10 rounded-full blur-xl"></div>
|
||||
<div class="absolute -bottom-8 -left-8 w-32 h-32 bg-white/10 rounded-full blur-2xl"></div>
|
||||
|
||||
<div class="relative z-10 px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 头像 -->
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="userInfo.avatar"
|
||||
class="w-20 h-20 rounded-full border-4 border-white/30 shadow-lg"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div v-if="isLoggedIn" class="absolute -bottom-1 -right-1 bg-yellow-400 text-yellow-900 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
Lv.{{ userInfo.level }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="flex-1 text-white">
|
||||
<template v-if="isLoggedIn">
|
||||
<h2 class="text-xl font-bold">{{ userInfo.nickname }}</h2>
|
||||
<p class="text-white/80 text-sm">{{ userInfo.levelName }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2 class="text-xl font-bold">欢迎来到风铃</h2>
|
||||
<button
|
||||
@click="goToLogin"
|
||||
class="mt-2 text-sm bg-white/20 hover:bg-white/30 px-4 py-1.5 rounded-full transition-colors"
|
||||
>
|
||||
登录 / 注册
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷数据卡片 -->
|
||||
<div class="relative px-6 -mt-16 z-20">
|
||||
<div class="flex gap-4">
|
||||
<!-- 积分卡片 -->
|
||||
<div
|
||||
@click="handleMenuClick(menuGroups[0].items[0])"
|
||||
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||
🪙
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.points : '---' }}</p>
|
||||
<p class="text-xs text-slate-500">积分</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡券卡片 -->
|
||||
<div
|
||||
@click="handleMenuClick(menuGroups[0].items[2])"
|
||||
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||
🎫
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.coupons : '---' }}</p>
|
||||
<p class="text-xs text-slate-500">卡券</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单卡片 -->
|
||||
<div
|
||||
@click="handleMenuClick(menuGroups[1].items[0])"
|
||||
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||
📦
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.orders : '---' }}</p>
|
||||
<p class="text-xs text-slate-500">订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<div class="px-6 mt-6 pb-8">
|
||||
<template v-for="group in menuGroups" :key="group.title">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-slate-500 mb-3 px-2">{{ group.title }}</h3>
|
||||
<div class="bg-white rounded-2xl overflow-hidden shadow-sm">
|
||||
<template v-for="(item, index) in group.items" :key="item.path">
|
||||
<div
|
||||
@click="handleMenuClick(item)"
|
||||
class="flex items-center gap-4 p-4 hover:bg-slate-50 cursor-pointer transition-colors"
|
||||
:class="{ 'border-t border-slate-100': index > 0 }"
|
||||
>
|
||||
<span class="text-2xl">{{ item.icon }}</span>
|
||||
<span class="flex-1 text-slate-700 font-medium">{{ item.label }}</span>
|
||||
<span v-if="item.requiresAuth && !isLoggedIn" class="text-xs text-slate-400">
|
||||
需登录
|
||||
</span>
|
||||
<span class="text-slate-300">›</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可选:添加一些微动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
.bg-white:nth-child(2) { animation-delay: 0.1s; }
|
||||
.bg-white:nth-child(3) { animation-delay: 0.2s; }
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user