补充普通用户无权限页面

This commit is contained in:
xuans 2026-08-30 20:43:14 +08:00
parent 9ac6f47b5e
commit 85145d67c4
5 changed files with 128 additions and 0 deletions

View File

@ -3,6 +3,7 @@ import { onMounted, ref } from "vue";
export function useAppShell() {
const setupState = ref(null);
const authenticated = ref(null);
const isAdmin = ref(null);
const bootstrapping = ref(true);
const error = ref("");
const routePath = window.location.pathname.replace(/\/+$/, "") || "/";
@ -20,10 +21,12 @@ export function useAppShell() {
await checkAuth();
} else {
authenticated.value = false;
isAdmin.value = false;
}
} catch (err) {
setupState.value = null;
authenticated.value = null;
isAdmin.value = null;
error.value = err.message || "无法连接核心服务,请先启动 scert serve";
} finally {
bootstrapping.value = false;
@ -36,16 +39,19 @@ export function useAppShell() {
});
if (response.status === 401) {
authenticated.value = false;
isAdmin.value = false;
return;
}
const result = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(result.error || "读取登录状态失败");
authenticated.value = result.authenticated === true;
isAdmin.value = result.is_admin === true || ["super_admin", "admin"].includes(result.role);
}
function handleInitialized() {
setupState.value = { ...setupState.value, initialized: true };
authenticated.value = false;
isAdmin.value = false;
}
onMounted(() => {
@ -59,6 +65,7 @@ export function useAppShell() {
return {
setupState,
authenticated,
isAdmin,
bootstrapping,
error,
routePath,

View File

@ -1,5 +1,6 @@
<script setup>
import { useAppShell } from "./App.js";
import AccessDenied from "./views/AccessDenied/AccessDenied.vue";
import Dashboard from "./views/Dashboard/Dashboard.vue";
import Login from "./views/Login/Login.vue";
import PasswordReset from "./views/PasswordReset/PasswordReset.vue";
@ -9,6 +10,7 @@ import SuperAdminReset from "./views/SuperAdminReset/SuperAdminReset.vue";
const {
setupState,
authenticated,
isAdmin,
bootstrapping,
error,
routePath,
@ -44,5 +46,9 @@ const {
<Login v-else-if="setupState && authenticated === false" />
<AccessDenied
v-else-if="setupState && authenticated === true && isAdmin === false"
/>
<Dashboard v-else-if="setupState && authenticated === true" />
</template>

View File

@ -0,0 +1,60 @@
.access-denied-page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 20px;
background:
radial-gradient(circle at top left, rgba(35, 106, 135, 0.18), transparent 42%),
#eef4f6;
}
.access-denied-card {
width: min(100%, 560px);
padding: 48px;
border: 1px solid rgba(31, 65, 78, 0.16);
border-radius: 28px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 24px 70px rgba(31, 65, 78, 0.14);
text-align: center;
}
.access-denied-mark {
width: 72px;
height: 72px;
object-fit: contain;
margin-bottom: 20px;
}
.access-denied-card h1 {
margin: 12px 0 16px;
color: #173746;
font-size: clamp(28px, 5vw, 40px);
line-height: 1.15;
}
.access-denied-description {
margin: 0 auto 24px;
max-width: 440px;
color: #60747d;
line-height: 1.8;
}
.access-denied-error {
margin: 0 0 18px;
padding: 12px 16px;
border: 1px solid #e6b8b8;
border-radius: 12px;
color: #a13b3b;
background: #fff4f4;
}
.access-denied-card .primary-button {
min-width: 180px;
}
@media (max-width: 600px) {
.access-denied-card {
padding: 32px 24px;
border-radius: 22px;
}
}

View File

@ -0,0 +1,29 @@
import { ref } from "vue";
export function useAccessDenied() {
const loggingOut = ref(false);
const error = ref("");
async function logout() {
if (loggingOut.value) return;
loggingOut.value = true;
error.value = "";
try {
const response = await fetch("/api/v1/auth/logout", {
method: "POST",
credentials: "include",
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || "退出登录失败");
}
window.location.assign("/login");
} catch (err) {
error.value = err.message || "退出登录失败";
} finally {
loggingOut.value = false;
}
}
return { loggingOut, error, logout };
}

View File

@ -0,0 +1,26 @@
<template>
<main class="access-denied-page">
<section class="access-denied-card" role="status" aria-live="polite">
<img class="access-denied-mark" src="/Scert.png" alt="Scert 标志" />
<p class="eyebrow">SCERT / 访问控制</p>
<h1>当前账号没有管理权限</h1>
<p class="access-denied-description">
你已经登录成功,但当前账号只能使用普通用户功能,不能访问证书、服务器和系统设置管理页面。
</p>
<p v-if="error" class="access-denied-error">{{ error }}</p>
<button class="primary-button" type="button" :disabled="loggingOut" @click="logout">
{{ loggingOut ? "正在退出……" : "退出并切换账号" }}
</button>
</section>
</main>
</template>
<script setup>
import { useAccessDenied } from "./AccessDenied.js";
const { loggingOut, error, logout } = useAccessDenied();
</script>
<style scoped>
@import "./AccessDenied.css";
</style>