管理员为用户添加临时权限,但是,如加,添加临时之后权限没变化

This commit is contained in:
Kunagisa 2025-07-03 23:03:05 +08:00
parent 3fa236ca11
commit ddfc8e98c6
6 changed files with 449 additions and 38 deletions

View File

@ -81,3 +81,38 @@ export const adminChangeUserPrivilege = async (uuid, privilege) => {
}
}
/**
* 用户申请临时权限
* 需要登录
* @param {string} privilege - 申请的权限
* @returns {Promise<void>} 无返回值成功即为修改成功
*/
export const requestTempPrivilege = async (privilege) => {
try {
console.log('申请的权限【requestTempPrivilege】privilege:', privilege);
await axiosInstance.post('/user/temp_privilege_request', { privilege });
} catch (error) {
throw error;
}
}
/**
* 管理员为用户添加临时权限
* 需要admin权限
* @param {string} uuid - 用户uuid
* @param {string} privilege - 权限
* @param {number} exp_time - 过期时间(分钟)
* @returns {Promise<void>} 无返回值成功即为修改成功
*/
export const adminAddTempPrivilege = async (uuid, privilege, exp_time) => {
try {
const payload = { uuid, privilege };
if (exp_time !== undefined && exp_time !== null && exp_time !== '') {
payload.exp_time = exp_time;
}
await axiosInstance.post('/admin/add_temp_privilege', payload);
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,151 @@
<template>
<div v-if="visible" class="privilege-dialog-overlay" @click.self="handleClose">
<div class="privilege-dialog">
<div class="privilege-dialog-header">
<h3>权限申请</h3>
</div>
<div class="privilege-dialog-content">
<div class="privilege-type privilege-type-highlight">
申请权限<span class="privilege-name" :class="privilegeColorClass[privilegeName]">{{ privilegeName }}</span>
</div>
<p>如需访问该功能请点击下方按钮提交申请</p>
</div>
<div class="privilege-dialog-footer">
<button class="cancel-button" @click="handleClose">取消</button>
<button class="apply-button" @click="handleApply">提交申请</button>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
visible: { type: Boolean, default: false },
privilegeName: { type: String, default: '' }
})
const emit = defineEmits(['close', 'apply'])
function handleClose() {
emit('close')
}
function handleApply() {
emit('apply')
}
// class
const privilegeColorClass = {
'管理员': 'privilege-admin',
'模组': 'privilege-mod',
'竞技': 'privilege-competitor',
'地图': 'privilege-map',
'用户': 'privilege-user'
}
</script>
<style scoped>
.privilege-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1100;
}
.privilege-dialog {
background: white;
border-radius: 8px;
width: 90%;
max-width: 420px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
animation: dialog-fade-in 0.3s ease;
}
.privilege-dialog-header {
padding: 16px 20px 0 20px;
text-align: center;
}
.privilege-dialog-header h3 {
margin: 0;
color: #416bdf;
font-size: 20px;
font-weight: 600;
}
.privilege-dialog-content {
padding: 18px 20px 0 20px;
color: #606266;
font-size: 15px;
line-height: 1.5;
text-align: left;
}
.privilege-type {
margin-bottom: 10px;
font-size: 15px;
color: #333;
}
.privilege-name {
color: #416bdf;
font-weight: 600;
font-size: 16px;
margin-left: 4px;
}
.privilege-dialog-footer {
padding: 18px 20px 20px 20px;
text-align: right;
}
.cancel-button, .apply-button {
background: #f5f5f5;
color: #333;
border: none;
padding: 7px 20px;
border-radius: 4px;
font-size: 15px;
cursor: pointer;
margin-left: 8px;
transition: background 0.2s;
}
.apply-button {
background: #416bdf;
color: #fff;
}
.cancel-button:hover {
background: #e0e0e0;
}
.apply-button:hover {
background: #274bb5;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.privilege-type-highlight {
background: #eaf3ff;
border-radius: 6px;
padding: 6px 12px;
margin-bottom: 14px;
display: inline-block;
}
.privilege-name.privilege-admin {
color: #ff7675;
}
.privilege-name.privilege-mod {
color: #6c5ce7;
}
.privilege-name.privilege-competitor {
color: #00b894;
}
.privilege-name.privilege-map {
color: #0984e3;
}
.privilege-name.privilege-user {
color: #636e72;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div class="temp-privilege-form">
<h2>添加临时权限</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="uuid">用户UUID</label>
<input id="uuid" v-model="uuid" type="text" placeholder="请输入用户UUID" required />
</div>
<div class="form-group">
<label for="privilege">权限</label>
<select id="privilege" v-model="privilege" required>
<option value="" disabled>请选择权限</option>
<option v-for="(label, key) in privilegeDisplayName" :key="key" :value="key">{{ label }}</option>
</select>
</div>
<div class="form-group">
<label for="exp_time">过期时间</label>
<select id="exp_time" v-model="exp_timeOption">
<option value="">30分钟默认</option>
<option value="60">1小时</option>
<option value="180">3小时</option>
<option value="1440">1</option>
<option value="other">其他</option>
</select>
<input v-if="exp_timeOption === 'other'" v-model="exp_timeCustom" type="number" min="1" placeholder="请输入分钟数" class="custom-exp-input" />
</div>
<button class="submit-btn" type="submit" :disabled="loading">提交</button>
</form>
<div v-if="msg" :class="{'success-msg': msgType==='success', 'error-msg': msgType==='error'}">{{ msg }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { adminAddTempPrivilege } from '@/api/login.js'
const uuid = ref('')
const privilege = ref('lv-map')
const exp_timeOption = ref('')
const exp_timeCustom = ref('')
const loading = ref(false)
const msg = ref('')
const msgType = ref('')
const privilegeDisplayName = {
'lv-mod': '模组',
'lv-map': '地图',
'lv-competitor': '竞技'
}
function getExpTime() {
if (exp_timeOption.value === 'other') {
return exp_timeCustom.value ? Number(exp_timeCustom.value) : ''
}
return exp_timeOption.value ? Number(exp_timeOption.value) : ''
}
async function handleSubmit() {
msg.value = ''
msgType.value = ''
loading.value = true
try {
await adminAddTempPrivilege(uuid.value, privilege.value, getExpTime())
msg.value = '添加成功!'
msgType.value = 'success'
uuid.value = ''
privilege.value = ''
exp_timeOption.value = ''
exp_timeCustom.value = ''
} catch (e) {
msg.value = '添加失败,请检查输入或重试。'
msgType.value = 'error'
}
loading.value = false
}
</script>
<style scoped>
.temp-privilege-form {
background: #fff;
border-radius: 8px;
padding: 32px 24px 24px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
max-width: 420px;
margin: 40px auto 0 auto;
}
.temp-privilege-form h2 {
margin-bottom: 24px;
color: #2563eb;
text-align: center;
}
.form-group {
margin-bottom: 18px;
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 6px;
color: #333;
font-weight: 500;
}
.form-group input,
.form-group select {
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 15px;
outline: none;
transition: border 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #2563eb;
}
.custom-exp-input {
margin-top: 8px;
}
.submit-btn {
width: 100%;
background: #2563eb;
color: #fff;
border: none;
border-radius: 4px;
padding: 10px 0;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.submit-btn:disabled {
background: #a5b4fc;
cursor: not-allowed;
}
.success-msg {
color: #22c55e;
text-align: center;
margin-top: 18px;
}
.error-msg {
color: #ef4444;
text-align: center;
margin-top: 18px;
}
</style>

View File

@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router'
import { hasValidToken, getUserInfo, logoutUser, getStoredUser } from '../utils/jwt';
import { justLoggedIn } from '../utils/authSessionState';
import EditorsMaps from '@/views/index/EditorsMaps.vue'
import mitt from 'mitt'
import { hasPrivilege } from '@/utils/privilege'
const routes = [
{
@ -20,7 +22,7 @@ const routes = [
path: 'demands',
name: 'DemandList',
component: () => import('@/views/index/DemandList.vue'),
meta: { requiresAuth: true }
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
},
{
path: 'maps',
@ -46,13 +48,13 @@ const routes = [
path: 'weapon-match',
name: 'WeaponMatch',
component: () => import('@/views/index/WeaponMatch.vue'),
meta: { requiresAuth: true }
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
},
{
path: 'competition',
name: 'Competition',
component: () => import('@/views/index/Competition.vue'),
meta: { requiresAuth: true }
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-competitor'] }
},
{
path: 'competition/add',
@ -85,12 +87,14 @@ const routes = [
{
path: 'PIC2TGA',
name: 'PIC2TGA',
component: () => import('@/views/index/PIC2TGA.vue')
component: () => import('@/views/index/PIC2TGA.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
},
{
path: 'terrainGenerate',
name: 'TerrainGenerate',
component: () => import('@/views/index/TerrainGenerate.vue')
component: () => import('@/views/index/TerrainGenerate.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
}
]
},
@ -124,17 +128,20 @@ const router = createRouter({
linkExactActiveClass: 'router-link-exact-active'
})
export const eventBus = mitt()
// 路由守卫
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
const requiredPrivilege = to.matched.find(record => record.meta.requiredPrivilege)?.meta.requiredPrivilege;
if (!requiresAuth) {
return next(); // 如果页面不需要认证,直接放行
if (!requiresAuth && !requiredPrivilege) {
return next(); // 如果页面不需要认证和权限,直接放行
}
// 方案1: 检查SessionStorage中是否已有用户信息 (刷新后最先检查这里)
if (getStoredUser()) {
return next();
// 继续检查权限
}
// 方案2: 如果刚登录过,直接放行
@ -143,14 +150,13 @@ router.beforeEach(async (to, from, next) => {
// 此时token肯定有效但可能还没来得及请求用户信息
// 为确保用户信息被存储,可以调用一次,但不阻塞导航
getUserInfo().catch(err => console.error("Error fetching user info after login:", err));
return next();
}
// 方案3: 检查localStorage中是否有token (刷新后SessionStorage没有用户信息时)
if (hasValidToken()) {
try {
await getUserInfo(); // 尝试获取用户信息该函数会把用户信息存入SessionStorage
return next(); // 获取成功,放行
// 继续检查权限
} catch (error) {
// 获取失败 (token无效, 网络问题等)
logoutUser(); // 清除所有凭证
@ -161,11 +167,17 @@ router.beforeEach(async (to, from, next) => {
}
}
// 方案4: 如果以上条件都不满足,跳转登录页
return next({
path: '/backend/login',
query: { redirect: to.fullPath }
});
// 权限校验
if (requiredPrivilege) {
const user = getStoredUser();
if (!user || !hasPrivilege(user.privilege, requiredPrivilege)) {
// 通过事件总线通知弹窗
eventBus.emit('no-privilege')
return next(false)
}
}
return next();
});
export default router

View File

@ -19,7 +19,7 @@
</div>
<ul v-show="dropdownOpen" style="list-style:none;padding-left:10px;">
<li :class="{active: currentAdminView === 'permission-review'}">
<a @click="selectAdminView('permission-review')">权限审核</a>
<a @click="selectAdminView('permission-review')">临时权限申请</a>
</li>
<li :class="{active: currentAdminView === 'user-management'}">
<a @click="selectAdminView('user-management')">用户管理</a>
@ -70,6 +70,7 @@
<div class="admin-main-content">
<AdminEditUserPrivilege v-if="currentAdminView === 'admin-edit-user-privilege'" />
<AffairManagement v-if="currentAdminView === 'affair-management'" />
<TempPrivilegeReview v-if="currentAdminView === 'permission-review'" />
</div>
</div>
</div>
@ -81,6 +82,7 @@ import { useRouter } from 'vue-router'
import { getUserInfo } from '@/utils/jwt'
import AdminEditUserPrivilege from '@/components/backend/AdminEditUserPrivilege.vue'
import AffairManagement from '@/components/backend/AffairManagement.vue'
import TempPrivilegeReview from '@/components/backend/TempPrivilegeReview.vue'
const router = useRouter()
const hasToken = ref(false)

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { computed, ref, onMounted, onUnmounted, defineAsyncComponent } from 'vue'
import { getUserInfo } from '@/utils/jwt'
import { useRouter } from 'vue-router'
import { hasPrivilege } from '@/utils/privilege'
import { eventBus } from '@/router/index.js'
import { requestTempPrivilege } from '@/api/login.js'
const PrivilegeRequestDialog = defineAsyncComponent(() => import('@/components/PrivilegeRequestDialog.vue'))
const isLoggedIn = computed(() => {
return !!localStorage.getItem('access_token') && !!currentUserData.value
@ -130,6 +133,63 @@ const showTerrainMenu = computed(() => showTerrainList || showTerrainGenerate.va
const showOnlineToolsMenu = computed(() => showWeaponMatch.value || showPic2Tga.value)
const showPublicMenu = computed(() => showDemands.value)
const showCompetitionMenu = computed(() => showCompetition.value)
const errorDialogVisible = ref(false)
const errorDialogMessage = ref('')
const privilegeDialogVisible = ref(false)
const privilegeDialogName = ref('')
const privilegeDialogKey = ref('')
const successDialog = ref({ visible: false, message: '' })
const privilegeDisplayNames = {
'lv-admin': '管理员',
'lv-mod': '模组',
'lv-map': '地图',
'lv-user': '用户',
'lv-competitor': '竞技'
}
function showNoPrivilegeDialog(name = '', key = '') {
privilegeDialogName.value = name
privilegeDialogKey.value = key
privilegeDialogVisible.value = true
}
async function handlePrivilegeApply() {
privilegeDialogVisible.value = false
if (!isLoggedIn.value) {
successDialog.value = { visible: true, message: '请先登录后再申请权限!' }
return
}
try {
await requestTempPrivilege(privilegeDialogKey.value)
successDialog.value = { visible: true, message: '权限申请已提交,请等待审核。' }
} catch (e) {
successDialog.value = { visible: true, message: '权限申请失败,请重试或联系管理员。' }
}
}
function handleNavClick(route, privilegeList) {
if (!isLoggedIn.value || !currentUserData.value || !hasPrivilege(currentUserData.value.privilege, privilegeList)) {
// lv-admin
const order = ['lv-mod', 'lv-competitor', 'lv-map', 'lv-user']
let privilegeKey = ''
for (const key of order) {
if (privilegeList && privilegeList.includes(key)) {
privilegeKey = key
break
}
}
// lv-admin
if (!privilegeKey && privilegeList && privilegeList.includes('lv-admin')) {
privilegeKey = 'lv-admin'
}
const privilegeName = privilegeDisplayNames[privilegeKey] || privilegeKey
showNoPrivilegeDialog(privilegeName, privilegeKey)
return
}
router.push(route)
}
</script>
<template>
@ -151,35 +211,38 @@ const showCompetitionMenu = computed(() => showCompetition.value)
</div>
</div>
<!-- 地形 一级菜单 -->
<div v-if="showTerrainMenu" class="nav-dropdown">
<div class="nav-dropdown">
<span class="nav-link">地形与纹理</span>
<div class="dropdown-content">
<router-link to="/terrain" class="nav-link">地形图列表</router-link>
<router-link v-if="showTerrainGenerate" to="/terrainGenerate" class="nav-link">地形纹理合成工具</router-link>
<router-link v-if="isLoggedIn" to="/terrainGenerate" class="nav-link" @click.prevent="handleNavClick('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">地形纹理合成工具</router-link>
</div>
</div>
<!-- 在线工具 一级菜单 -->
<div v-if="showOnlineToolsMenu" class="nav-dropdown">
<span class="nav-link">在线工具</span>
<div class="dropdown-content">
<router-link v-if="showWeaponMatch" to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
<router-link v-if="showPic2Tga" to="/PIC2TGA" class="nav-link">在线转tga工具</router-link>
<!-- 仅登录后显示的菜单项 -->
<template v-if="isLoggedIn">
<!-- 在线工具 一级菜单 -->
<div class="nav-dropdown">
<span class="nav-link">在线工具</span>
<div class="dropdown-content">
<router-link to="/weapon-match" class="nav-link" @click.prevent="handleNavClick('/weapon-match', ['lv-admin','lv-mod'])">Weapon 匹配</router-link>
<router-link to="/PIC2TGA" class="nav-link" @click.prevent="handleNavClick('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">在线转tga工具</router-link>
</div>
</div>
</div>
<!-- 赛事信息 一级菜单 -->
<div v-if="showCompetitionMenu" class="nav-dropdown">
<span class="nav-link">赛事信息</span>
<div class="dropdown-content">
<router-link v-if="showCompetition" to="/competition" class="nav-link">赛程信息</router-link>
<!-- 赛事信息 一级菜单 -->
<div class="nav-dropdown">
<span class="nav-link">赛事信息</span>
<div class="dropdown-content">
<router-link to="/competition" class="nav-link" @click.prevent="handleNavClick('/competition', ['lv-admin','lv-competitor'])">赛程信息</router-link>
</div>
</div>
</div>
<!-- 公共信息区 一级菜单 -->
<div v-if="showPublicMenu" class="nav-dropdown">
<span class="nav-link">公共信息区</span>
<div class="dropdown-content">
<router-link v-if="showDemands" to="/demands" class="nav-link">办事大厅</router-link>
<!-- 公共信息区 一级菜单 -->
<div class="nav-dropdown">
<span class="nav-link">公共信息区</span>
<div class="dropdown-content">
<router-link to="/demands" class="nav-link" @click.prevent="handleNavClick('/demands', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">办事大厅</router-link>
</div>
</div>
</div>
</template>
</div>
<div class="nav-right" :class="{ active: showMobileMenu }">
<router-link v-if="!isLoggedIn" to="/backend/login" class="nav-link login-btn">
@ -223,6 +286,12 @@ const showCompetitionMenu = computed(() => showCompetition.value)
</p>
</div>
</footer>
<PrivilegeRequestDialog
:visible="privilegeDialogVisible"
:privilegeName="privilegeDialogName"
@close="privilegeDialogVisible = false"
@apply="handlePrivilegeApply"
/>
</div>
</template>