feat: 打造若依 (RuoYi) 规范的高品质 RBAC 用户权限体系,内置 admin/admin123 超级管理员与测试读者,支持登录注册、个人中心与用户管理
Build and Deploy Blog / deploy (push) Successful in 38s
Build and Deploy Blog / deploy (push) Successful in 38s
This commit is contained in:
+640
-17
@@ -299,13 +299,69 @@
|
||||
<span>{{ backendConnected ? 'Spring Boot 3 + MySQL 8.0 在线' : '本地安全模式' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 写文章按钮 (唤起现代化 Markdown 写作台) -->
|
||||
<button @click="openPublishModal"
|
||||
<!-- 写文章按钮 (仅超级管理员可见) -->
|
||||
<button v-if="isAdmin" @click="openPublishModal"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-semibold bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 text-white shadow-md shadow-sky-600/25 transition-all hover:shadow active:scale-95">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
<span class="hidden sm:inline">写新文章</span>
|
||||
</button>
|
||||
|
||||
<!-- 若依风格用户系统入口 -->
|
||||
<div v-if="isLoggedIn" class="relative">
|
||||
<button @click="showUserDropdown = !showUserDropdown"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<span class="text-base">{{ currentUser.avatar || '👨💻' }}</span>
|
||||
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 max-w-[80px] sm:max-w-[120px] truncate">{{ currentUser.nickname }}</span>
|
||||
<span :class="isAdmin ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border-amber-200' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border-sky-200'"
|
||||
class="px-2 py-0.5 text-[10px] font-bold rounded-full border">
|
||||
{{ isAdmin ? '👑 管理员' : '👤 读者' }}
|
||||
</span>
|
||||
<svg class="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
|
||||
<!-- 下拉菜单抽屉 -->
|
||||
<div v-show="showUserDropdown" @click.outside="showUserDropdown = false"
|
||||
class="absolute right-0 mt-2 w-56 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-2xl py-2 z-50">
|
||||
<div class="px-4 py-2.5 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="font-bold text-xs text-slate-800 dark:text-slate-100 flex items-center gap-1.5">
|
||||
<span>{{ currentUser.avatar || '👨💻' }}</span>
|
||||
<span>{{ currentUser.nickname }}</span>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-400 font-mono">@{{ currentUser.username }}</div>
|
||||
<div class="text-[10px] text-slate-400 mt-0.5 truncate">{{ currentUser.email || '未绑定邮箱' }}</div>
|
||||
</div>
|
||||
|
||||
<button v-if="isAdmin" @click="showUserDropdown = false; openPublishModal()"
|
||||
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||
<span>✍️</span> <span>写新文章</span>
|
||||
</button>
|
||||
|
||||
<button v-if="isAdmin" @click="showUserDropdown = false; openUserMgmtModal()"
|
||||
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||
<span>👥</span> <span>用户管理中心 (RuoYi RBAC)</span>
|
||||
</button>
|
||||
|
||||
<button @click="showUserDropdown = false; openProfileModal()"
|
||||
class="w-full text-left px-4 py-2 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center gap-2">
|
||||
<span>👤</span> <span>个人中心设置</span>
|
||||
</button>
|
||||
|
||||
<div class="my-1 border-t border-slate-100 dark:border-slate-800"></div>
|
||||
|
||||
<button @click="showUserDropdown = false; handleLogout()"
|
||||
class="w-full text-left px-4 py-2 text-xs text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 flex items-center gap-2 font-medium">
|
||||
<span>🚪</span> <span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未登录状态展示登录/注册按钮 -->
|
||||
<button v-else @click="openAuthModal('login')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold bg-sky-50 hover:bg-sky-100 dark:bg-sky-950/60 dark:hover:bg-sky-900/60 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800 transition-all shadow-sm active:scale-95">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path></svg>
|
||||
<span>登录 / 注册</span>
|
||||
</button>
|
||||
|
||||
<!-- 深色/浅色模式切换 -->
|
||||
<button @click="toggleDarkMode"
|
||||
class="p-2 rounded-xl text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-transparent dark:border-slate-800"
|
||||
@@ -608,10 +664,25 @@
|
||||
|
||||
<!-- 撰写评论卡片 -->
|
||||
<div class="bg-slate-50/80 dark:bg-slate-800/40 p-4 sm:p-5 rounded-2xl border border-slate-200/60 dark:border-slate-700/60 mb-6 space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<input v-model="newComment.author" type="text" placeholder="你的技术昵称 (默认: 访客)"
|
||||
class="px-3.5 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-48 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="text-xs text-slate-400">评论将实时持久化写入 MySQL 8.0</span>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<div v-if="isLoggedIn" class="flex items-center gap-2">
|
||||
<span class="text-base">{{ currentUser.avatar || '👨💻' }}</span>
|
||||
<span class="text-slate-600 dark:text-slate-300">正在以</span>
|
||||
<strong class="text-slate-900 dark:text-white">{{ currentUser.nickname }}</strong>
|
||||
<span :class="isAdmin ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300'"
|
||||
class="px-1.5 py-0.2 text-[10px] font-bold rounded-full">
|
||||
{{ isAdmin ? '👑 博主' : '👤 读者' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<input v-model="newComment.author" type="text" placeholder="你的昵称 (默认: 访客)"
|
||||
class="px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-40 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<span class="text-slate-400">或</span>
|
||||
<button @click="openAuthModal('login')" class="text-sky-600 dark:text-sky-400 font-bold hover:underline">
|
||||
👉 登录若依账号
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-slate-400 hidden sm:inline font-mono">直写 MySQL 8.0</span>
|
||||
</div>
|
||||
<textarea v-model="newComment.content" rows="3" placeholder="写下你的技术思考、疑问或独到见解..."
|
||||
class="w-full px-3.5 py-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"></textarea>
|
||||
@@ -630,14 +701,20 @@
|
||||
class="p-4 rounded-2xl bg-slate-50/60 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800 text-xs sm:text-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 font-bold flex items-center justify-center text-xs">
|
||||
{{ c.author ? c.author.charAt(0).toUpperCase() : 'U' }}
|
||||
<div class="w-7 h-7 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-sm shadow-sm">
|
||||
{{ c.avatar || (c.role === 'ADMIN' ? '👑' : '👨💻') }}
|
||||
</div>
|
||||
<span class="font-bold text-slate-900 dark:text-slate-100">{{ c.author || '匿名访客' }}</span>
|
||||
<span v-if="c.role === 'ADMIN'" class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border border-amber-200 dark:border-amber-800">
|
||||
👑 博主/管理员
|
||||
</span>
|
||||
<span v-else-if="c.role === 'USER'" class="px-1.5 py-0.5 rounded text-[10px] font-medium bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border border-sky-200 dark:border-sky-800">
|
||||
读者
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-400 font-mono">{{ formatDate(c.createdAt) }}</span>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed pl-8">{{ c.content }}</p>
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed pl-9">{{ c.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-xs text-slate-400">
|
||||
@@ -949,6 +1026,249 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======================= 若依风格 1:用户认证登录/注册中心模态框 ======================= -->
|
||||
<div v-if="showAuthModal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
@click.self="showAuthModal = false">
|
||||
<div class="w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<!-- 头部 -->
|
||||
<div class="px-6 pt-6 pb-4 text-center border-b border-slate-100 dark:border-slate-800 relative">
|
||||
<button @click="showAuthModal = false" class="absolute right-4 top-4 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-lg">✕</button>
|
||||
<div class="w-12 h-12 mx-auto rounded-2xl bg-gradient-to-tr from-sky-500 via-indigo-500 to-purple-600 flex items-center justify-center text-white text-xl font-black shadow-md shadow-sky-500/20 mb-3">
|
||||
RY
|
||||
</div>
|
||||
<h3 class="text-xl font-black text-slate-900 dark:text-white">小东全栈博客 · 认证中心</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">若依 (RuoYi) 规范 RBAC 权限与用户体系</p>
|
||||
|
||||
<!-- 登录/注册 Tab 切换 -->
|
||||
<div class="flex mt-4 bg-slate-100 dark:bg-slate-800 p-1 rounded-xl text-xs font-bold">
|
||||
<button @click="authTab = 'login'" :class="authTab === 'login' ? 'bg-white dark:bg-slate-700 text-sky-600 dark:text-sky-400 shadow-sm' : 'text-slate-500'" class="flex-1 py-1.5 rounded-lg transition-all">
|
||||
账号登录
|
||||
</button>
|
||||
<button @click="authTab = 'register'" :class="authTab === 'register' ? 'bg-white dark:bg-slate-700 text-sky-600 dark:text-sky-400 shadow-sm' : 'text-slate-500'" class="flex-1 py-1.5 rounded-lg transition-all">
|
||||
读者注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div v-if="authTab === 'login'" class="p-6 space-y-4 text-xs">
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">用户名 *</label>
|
||||
<input v-model="loginForm.username" type="text" placeholder="输入账号 (如 admin)"
|
||||
class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none text-sm font-medium">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">密码 *</label>
|
||||
<input v-model="loginForm.password" type="password" placeholder="输入密码 (如 admin123)" @keyup.enter="handleLogin"
|
||||
class="w-full px-3.5 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none text-sm font-medium">
|
||||
</div>
|
||||
|
||||
<!-- 快捷一键填入账号 (若依经典便民测试功能) -->
|
||||
<div class="pt-2">
|
||||
<div class="text-[11px] text-slate-400 mb-2 font-mono">⚡ 快捷填入内置测试账号:</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="quickFill('admin', 'admin123')"
|
||||
class="flex-1 py-1.5 px-2 rounded-xl bg-amber-50 hover:bg-amber-100 dark:bg-amber-950/40 dark:hover:bg-amber-950/70 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800 font-semibold text-[11px] transition-colors">
|
||||
👑 超级管理员 (admin)
|
||||
</button>
|
||||
<button @click="quickFill('ry_user', 'user123')"
|
||||
class="flex-1 py-1.5 px-2 rounded-xl bg-sky-50 hover:bg-sky-100 dark:bg-sky-950/40 dark:hover:bg-sky-950/70 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800 font-semibold text-[11px] transition-colors">
|
||||
👤 测试读者 (ry_user)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="handleLogin" :disabled="authLoading"
|
||||
class="w-full mt-4 py-2.5 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 disabled:opacity-50 text-white font-bold text-sm shadow-md shadow-sky-600/30 flex items-center justify-center gap-2">
|
||||
<span v-if="authLoading" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||
<span>{{ authLoading ? '验证登录中...' : '立即登录' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div v-else class="p-6 space-y-3.5 text-xs">
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">用户名 *</label>
|
||||
<input v-model="registerForm.username" type="text" placeholder="3-20 位字符,用于登录"
|
||||
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">个性昵称</label>
|
||||
<input v-model="registerForm.nickname" type="text" placeholder="例如: 极客小明"
|
||||
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">登录密码 *</label>
|
||||
<input v-model="registerForm.password" type="password" placeholder="不少于 5 位"
|
||||
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">联系邮箱</label>
|
||||
<input v-model="registerForm.email" type="email" placeholder="name@example.com"
|
||||
class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
||||
</div>
|
||||
|
||||
<button @click="handleRegister" :disabled="authLoading"
|
||||
class="w-full mt-4 py-2.5 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 disabled:opacity-50 text-white font-bold text-sm shadow-md shadow-sky-600/30 flex items-center justify-center gap-2">
|
||||
<span v-if="authLoading" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||
<span>{{ authLoading ? '创建账号中...' : '立即注册读者账号' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======================= 若依风格 2:用户权限管理中心模态框 (RBAC 后台) ======================= -->
|
||||
<div v-if="showUserMgmtModal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"
|
||||
@click.self="showUserMgmtModal = false">
|
||||
<div class="w-full max-w-4xl bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden flex flex-col max-h-[90vh] animate-in fade-in zoom-in-95 duration-200">
|
||||
<div class="px-6 py-4 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl">👥</span>
|
||||
<div>
|
||||
<h3 class="font-bold text-base text-slate-900 dark:text-white">若依风格 · 用户权限管理中心 (RBAC)</h3>
|
||||
<p class="text-xs text-slate-400">管理全站注册用户、启用/停用状态及权限角色</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="showUserMgmtModal = false" class="text-slate-400 hover:text-slate-600 text-lg">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格内容区 -->
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-xs text-slate-500 font-mono">共 {{ userList.length }} 位注册用户</div>
|
||||
<button @click="fetchUserList" class="px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-xs font-semibold flex items-center gap-1.5 transition-colors">
|
||||
<span>🔄</span> <span>刷新列表</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
||||
<table class="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-800 text-slate-500 dark:text-slate-400 font-bold">
|
||||
<th class="p-3.5">ID</th>
|
||||
<th class="p-3.5">用户名</th>
|
||||
<th class="p-3.5">昵称</th>
|
||||
<th class="p-3.5">角色权限</th>
|
||||
<th class="p-3.5">账号状态</th>
|
||||
<th class="p-3.5">最近登录</th>
|
||||
<th class="p-3.5 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<tr v-for="u in userList" :key="u.id" class="hover:bg-slate-50/50 dark:hover:bg-slate-800/40 transition-colors">
|
||||
<td class="p-3.5 font-mono">{{ u.id }}</td>
|
||||
<td class="p-3.5 font-bold text-slate-900 dark:text-slate-100">{{ u.username }}</td>
|
||||
<td class="p-3.5 flex items-center gap-1.5">
|
||||
<span>{{ u.avatar || '👨💻' }}</span>
|
||||
<span>{{ u.nickname }}</span>
|
||||
</td>
|
||||
<td class="p-3.5">
|
||||
<span :class="u.role === 'ADMIN' ? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300 border-amber-200' : 'bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border-sky-200'"
|
||||
class="px-2 py-0.5 rounded-full font-bold text-[10px] border">
|
||||
{{ u.role === 'ADMIN' ? '👑 超级管理员' : '👤 访客读者' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-3.5">
|
||||
<span :class="u.status === '0' ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-400 border-emerald-200' : 'bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-400 border-rose-200'"
|
||||
class="px-2 py-0.5 rounded-full font-bold text-[10px] border">
|
||||
{{ u.status === '0' ? '正常' : '已停用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-3.5 text-slate-400 font-mono">{{ u.loginDate ? formatDate(u.loginDate) : '未登录' }}</td>
|
||||
<td class="p-3.5 text-right font-medium">
|
||||
<template v-if="u.username !== 'admin'">
|
||||
<button @click="toggleUserStatus(u)" class="text-sky-600 hover:underline mr-3">
|
||||
{{ u.status === '0' ? '停用' : '启用' }}
|
||||
</button>
|
||||
<button @click="deleteUser(u)" class="text-rose-600 hover:underline">
|
||||
删除
|
||||
</button>
|
||||
</template>
|
||||
<span v-else class="text-[11px] text-slate-400 font-mono">系统保护</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======================= 若依风格 3:个人中心设置模态框 ======================= -->
|
||||
<div v-if="showProfileModal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
@click.self="showProfileModal = false">
|
||||
<div class="w-full max-w-md bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div class="px-6 py-4 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
|
||||
<h3 class="font-bold text-base text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<span>👤 个人中心设置</span>
|
||||
</h3>
|
||||
<button @click="showProfileModal = false" class="text-slate-400 hover:text-slate-600">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-slate-100 dark:border-slate-800 text-xs font-bold">
|
||||
<button @click="profileTab = 'info'" :class="profileTab === 'info' ? 'border-b-2 border-sky-500 text-sky-600 dark:text-sky-400' : 'text-slate-400'" class="flex-1 py-3 text-center transition-colors">
|
||||
基本资料
|
||||
</button>
|
||||
<button @click="profileTab = 'pwd'" :class="profileTab === 'pwd' ? 'border-b-2 border-sky-500 text-sky-600 dark:text-sky-400' : 'text-slate-400'" class="flex-1 py-3 text-center transition-colors">
|
||||
修改密码
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 修改基本资料 -->
|
||||
<div v-if="profileTab === 'info'" class="p-6 space-y-4 text-xs">
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-400">用户名</label>
|
||||
<input :value="currentUser.username" disabled class="w-full px-3.5 py-2 rounded-xl bg-slate-100 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-slate-400 cursor-not-allowed font-mono">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">修改昵称</label>
|
||||
<input v-model="profileForm.nickname" type="text" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">联系邮箱</label>
|
||||
<input v-model="profileForm.email" type="email" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1.5 text-slate-700 dark:text-slate-300">选择头像 Emoji</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button v-for="av in ['👨💻', '🚀', '💻', '🧠', '⚡', '🐬', '🌟', '🛡️']" :key="av"
|
||||
@click="profileForm.avatar = av"
|
||||
:class="profileForm.avatar === av ? 'ring-2 ring-sky-500 scale-110' : 'opacity-70 hover:opacity-100'"
|
||||
class="w-9 h-9 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-lg transition-all">
|
||||
{{ av }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="handleUpdateProfile" class="w-full mt-2 py-2.5 rounded-xl bg-sky-600 hover:bg-sky-700 text-white font-bold text-xs shadow-md shadow-sky-600/30 transition-all">
|
||||
保存资料修改
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<div v-else class="p-6 space-y-4 text-xs">
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">原密码 *</label>
|
||||
<input v-model="pwdForm.oldPassword" type="password" placeholder="输入当前使用的密码" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">新密码 *</label>
|
||||
<input v-model="pwdForm.newPassword" type="password" placeholder="不少于 5 位" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold mb-1 text-slate-700 dark:text-slate-300">确认新密码 *</label>
|
||||
<input v-model="pwdForm.confirmPassword" type="password" placeholder="再次输入新密码" class="w-full px-3.5 py-2 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500">
|
||||
</div>
|
||||
<button @click="handleUpdatePwd" class="w-full mt-2 py-2.5 rounded-xl bg-sky-600 hover:bg-sky-700 text-white font-bold text-xs shadow-md shadow-sky-600/30 transition-all">
|
||||
确认修改密码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回顶部浮动胶囊 -->
|
||||
<button v-show="showScrollTop" @click="scrollToTop"
|
||||
class="fixed bottom-6 right-6 z-40 p-3 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 shadow-xl hover:scale-110 active:scale-95 transition-all"
|
||||
@@ -1004,6 +1324,253 @@
|
||||
const showScrollTop = ref(false);
|
||||
const toasts = ref([]);
|
||||
|
||||
// ============ 若依 (RuoYi) 规范 RBAC 用户认证与权限体系 ============
|
||||
const token = ref(localStorage.getItem('ry_token') || '');
|
||||
const currentUser = ref(null);
|
||||
const roles = ref([]);
|
||||
const permissions = ref([]);
|
||||
const showUserDropdown = ref(false);
|
||||
|
||||
const showAuthModal = ref(false);
|
||||
const authTab = ref('login'); // 'login' | 'register'
|
||||
const authLoading = ref(false);
|
||||
const loginForm = ref({ username: '', password: '' });
|
||||
const registerForm = ref({ username: '', password: '', nickname: '', email: '' });
|
||||
|
||||
const showUserMgmtModal = ref(false);
|
||||
const userList = ref([]);
|
||||
|
||||
const showProfileModal = ref(false);
|
||||
const profileTab = ref('info'); // 'info' | 'pwd'
|
||||
const profileForm = ref({ nickname: '', email: '', avatar: '👨💻' });
|
||||
const pwdForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' });
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value && !!currentUser.value);
|
||||
const isAdmin = computed(() => currentUser.value && currentUser.value.role === 'ADMIN');
|
||||
const authHeaders = computed(() => {
|
||||
return token.value ? { 'Authorization': 'Bearer ' + token.value } : {};
|
||||
});
|
||||
|
||||
function openAuthModal(tab = 'login') {
|
||||
authTab.value = tab;
|
||||
loginForm.value = { username: '', password: '' };
|
||||
registerForm.value = { username: '', password: '', nickname: '', email: '' };
|
||||
showAuthModal.value = true;
|
||||
}
|
||||
|
||||
function quickFill(u, p) {
|
||||
loginForm.value.username = u;
|
||||
loginForm.value.password = p;
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!loginForm.value.username.trim() || !loginForm.value.password.trim()) {
|
||||
showToast('请输入用户名和密码', 'error');
|
||||
return;
|
||||
}
|
||||
authLoading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(loginForm.value)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
token.value = result.data.token;
|
||||
localStorage.setItem('ry_token', result.data.token);
|
||||
currentUser.value = result.data.user;
|
||||
roles.value = result.data.roles;
|
||||
permissions.value = result.data.permissions;
|
||||
showAuthModal.value = false;
|
||||
showToast(`欢迎回来,${currentUser.value.nickname}!`, 'success');
|
||||
} else {
|
||||
showToast(result.msg || '登录失败,请检查账号密码', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('登录服务不可用,请稍后重试', 'error');
|
||||
}
|
||||
authLoading.value = false;
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
if (!registerForm.value.username.trim() || !registerForm.value.password.trim()) {
|
||||
showToast('用户名和密码不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
authLoading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(registerForm.value)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
showToast('注册成功!正在为您自动登录...', 'success');
|
||||
loginForm.value.username = registerForm.value.username;
|
||||
loginForm.value.password = registerForm.value.password;
|
||||
await handleLogin();
|
||||
} else {
|
||||
showToast(result.msg || '注册失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('注册请求失败', 'error');
|
||||
}
|
||||
authLoading.value = false;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
token.value = '';
|
||||
currentUser.value = null;
|
||||
roles.value = [];
|
||||
permissions.value = [];
|
||||
localStorage.removeItem('ry_token');
|
||||
showToast('已安全退出登录', 'info');
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
if (!token.value) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/info`, {
|
||||
headers: { ...authHeaders.value }
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.code === 200) {
|
||||
currentUser.value = result.data.user;
|
||||
roles.value = result.data.roles;
|
||||
permissions.value = result.data.permissions;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
localStorage.removeItem('ry_token');
|
||||
token.value = '';
|
||||
currentUser.value = null;
|
||||
}
|
||||
|
||||
// 用户管理中心
|
||||
function openUserMgmtModal() {
|
||||
showUserMgmtModal.value = true;
|
||||
fetchUserList();
|
||||
}
|
||||
|
||||
async function fetchUserList() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users`, {
|
||||
headers: { ...authHeaders.value }
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
userList.value = result.data || [];
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('获取用户列表失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUserStatus(u) {
|
||||
const newStatus = u.status === '0' ? '1' : '0';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/${u.id}/status?status=${newStatus}`, {
|
||||
method: 'PUT',
|
||||
headers: { ...authHeaders.value }
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
u.status = newStatus;
|
||||
showToast(`已${newStatus === '1' ? '停用' : '启用'}用户 [${u.username}]`, 'success');
|
||||
} else {
|
||||
showToast(result.msg || '操作失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('网络请求失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(u) {
|
||||
if (!confirm(`确定要永久删除用户 [${u.username}] 吗?`)) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/${u.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...authHeaders.value }
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
showToast(`用户 [${u.username}] 已被删除`, 'success');
|
||||
fetchUserList();
|
||||
} else {
|
||||
showToast(result.msg || '删除失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('网络请求失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 个人中心
|
||||
function openProfileModal() {
|
||||
profileTab.value = 'info';
|
||||
profileForm.value = {
|
||||
nickname: currentUser.value.nickname,
|
||||
email: currentUser.value.email || '',
|
||||
avatar: currentUser.value.avatar || '👨💻'
|
||||
};
|
||||
pwdForm.value = { oldPassword: '', newPassword: '', confirmPassword: '' };
|
||||
showProfileModal.value = true;
|
||||
}
|
||||
|
||||
async function handleUpdateProfile() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/profile`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders.value },
|
||||
body: JSON.stringify(profileForm.value)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
currentUser.value.nickname = profileForm.value.nickname;
|
||||
currentUser.value.email = profileForm.value.email;
|
||||
currentUser.value.avatar = profileForm.value.avatar;
|
||||
showToast('个人资料已成功更新', 'success');
|
||||
showProfileModal.value = false;
|
||||
} else {
|
||||
showToast(result.msg || '修改失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('请求失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdatePwd() {
|
||||
if (!pwdForm.value.oldPassword || !pwdForm.value.newPassword) {
|
||||
showToast('旧密码和新密码不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
if (pwdForm.value.newPassword !== pwdForm.value.confirmPassword) {
|
||||
showToast('两次输入的新密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/password`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders.value },
|
||||
body: JSON.stringify(pwdForm.value)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
showToast('密码修改成功,请使用新密码重新登录', 'success');
|
||||
handleLogout();
|
||||
showProfileModal.value = false;
|
||||
openAuthModal('login');
|
||||
} else {
|
||||
showToast(result.msg || '修改密码失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('请求失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const categories = ref(['全部']);
|
||||
const categoryList = ref([]);
|
||||
const availableCategories = computed(() => categories.value.filter(c => c !== '全部'));
|
||||
@@ -1284,17 +1851,26 @@
|
||||
showToast('评论内容不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
if (isLoggedIn.value && currentUser.value) {
|
||||
newComment.value.author = currentUser.value.nickname;
|
||||
}
|
||||
submittingComment.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts/${activePost.value.id}/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders.value
|
||||
},
|
||||
body: JSON.stringify(newComment.value)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
newComment.value.content = '';
|
||||
showToast('评论已持久化写入 MySQL 8.0', 'success');
|
||||
fetchComments(activePost.value.id);
|
||||
} else {
|
||||
showToast(result.msg || '评论发表失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('评论提交失败,请检查网络连接', 'error');
|
||||
@@ -1304,16 +1880,26 @@
|
||||
|
||||
// 删除文章
|
||||
async function deletePost(post) {
|
||||
if (!isAdmin.value) {
|
||||
showToast('无权删除:仅超级管理员可用', 'error');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`确定要永久删除文章《${post.title}》吗?`)) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts/${post.id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
const res = await fetch(`${API_BASE}/posts/${post.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...authHeaders.value }
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
showToast('文章已从 MySQL 数据库中移除', 'success');
|
||||
navigateView('home');
|
||||
fetchPosts();
|
||||
} else {
|
||||
showToast(result.msg || '删除失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('删除失败', 'error');
|
||||
showToast('删除失败,权限不足或服务器错误', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1372,6 +1958,10 @@
|
||||
|
||||
// 持久化发布文章至 MySQL
|
||||
async function publishPost() {
|
||||
if (!isAdmin.value) {
|
||||
showToast('无权发布:请先使用超级管理员账号登录', 'error');
|
||||
return;
|
||||
}
|
||||
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
|
||||
showToast('文章标题和正文内容不能为空!', 'error');
|
||||
return;
|
||||
@@ -1381,15 +1971,19 @@
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/posts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders.value
|
||||
},
|
||||
body: JSON.stringify(newPost.value)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (res.ok && result.code === 200) {
|
||||
showToast('文章已成功持久化写入 MySQL 8.0!', 'success');
|
||||
showPublishModal.value = false;
|
||||
fetchPosts();
|
||||
} else {
|
||||
showToast('发布失败,请稍后重试', 'error');
|
||||
showToast(result.msg || '发布失败,请稍后重试', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('发布失败,无法连接服务器', 'error');
|
||||
@@ -1500,6 +2094,7 @@
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('hashchange', handleHashRoute);
|
||||
|
||||
await fetchUserInfo();
|
||||
await fetchCategories();
|
||||
await fetchPosts();
|
||||
handleHashRoute();
|
||||
@@ -1516,6 +2111,34 @@
|
||||
backendConnected,
|
||||
currentView,
|
||||
navigateView,
|
||||
token,
|
||||
currentUser,
|
||||
isLoggedIn,
|
||||
isAdmin,
|
||||
showUserDropdown,
|
||||
showAuthModal,
|
||||
authTab,
|
||||
authLoading,
|
||||
loginForm,
|
||||
registerForm,
|
||||
openAuthModal,
|
||||
quickFill,
|
||||
handleLogin,
|
||||
handleRegister,
|
||||
handleLogout,
|
||||
showUserMgmtModal,
|
||||
userList,
|
||||
openUserMgmtModal,
|
||||
fetchUserList,
|
||||
toggleUserStatus,
|
||||
deleteUser,
|
||||
showProfileModal,
|
||||
profileTab,
|
||||
profileForm,
|
||||
pwdForm,
|
||||
openProfileModal,
|
||||
handleUpdateProfile,
|
||||
handleUpdatePwd,
|
||||
posts,
|
||||
stats,
|
||||
categories,
|
||||
|
||||
Reference in New Issue
Block a user