提交 bd14b59f authored 作者: 朱政's avatar 朱政

feat:登录功能和用户信息功能开发,以及风险信号管理列表页样式开发

上级 82883bfb
流水线 #398 已通过 于阶段
in 1 分 39 秒
import { getToken } from "@/api/request.js";
import { getToken, formatBearerAuthorization } from "@/api/request.js";
const CHART_INTERPRETATION_URL = "/aiAnalysis/chart_interpretation";
const API_KEY = "aircasKEY19491001";
......@@ -183,6 +183,8 @@ export function getChartAnalysis(data, options = {}) {
(async () => {
try {
const { fetchEventSource } = await import("@microsoft/fetch-event-source");
const sseRaw = getToken();
const sseToken = sseRaw ? String(sseRaw).trim() : "";
await fetchEventSource(CHART_INTERPRETATION_URL, {
method: "POST",
......@@ -191,8 +193,7 @@ export function getChartAnalysis(data, options = {}) {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
"X-API-Key": API_KEY,
// 后端同项目其它接口使用 token 字段名(axios 拦截器里就是这样注入的)
token: getToken()
...(sseToken ? { token: sseToken } : {})
},
body: JSON.stringify(data),
signal: abortController.signal,
......
......@@ -2,27 +2,8 @@
import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js'
// Token 管理
const TOKEN_KEY = 'auth_token'
// 获取token
const getToken = () => {
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
// 设置 token
const setToken = (token) => {
localStorage.setItem(TOKEN_KEY, token)
}
// 移除 token
const removeToken = () => {
localStorage.removeItem(TOKEN_KEY)
}
// 导出 token 管理方法
export { getToken, setToken, removeToken }
// 创建 axios 实例
......@@ -30,19 +11,35 @@ const service = axios.create({
timeout: 300 * 1000 // 请求超时时间
})
// request 拦截器
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
service.interceptors.request.use(config => {
// 获取 token 并添加到请求头
const token = getToken()
const raw = getToken()
const token = raw ? String(raw).trim() : ""
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
if (token) {
config.headers['token'] = token
config.headers.set('token', token, true)
} else {
config.headers.delete('token')
config.headers.delete('Token')
// ===== 旧逻辑保留(勿删):Authorization: Bearer <token> =====
// config.headers.delete('Authorization')
// config.headers.delete('authorization')
}
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = aiApiKey
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
config.headers.set('X-API-Key', aiApiKey)
}
}
return config
......@@ -54,7 +51,10 @@ service.interceptors.request.use(config => {
// response 拦截器
service.interceptors.response.use(
response => {
const res = response.data
const res = response?.data
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
......@@ -90,10 +90,20 @@ service.interceptors.response.use(
type: 'error',
duration: 3 * 1000
})
removeToken()
const h = error.config?.headers
const hadToken =
h &&
(typeof h.get === 'function'
? Boolean(
h.get('token') || h.get('Token')
)
: Boolean(
h.token || h.Token
))
if (hadToken) removeToken()
} else {
ElMessage({
message: error.message,
message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
duration: 3 * 1000
})
......
......@@ -8,8 +8,19 @@ import {
// Token管理
const TOKEN_KEY = 'auth_token'
// ===== 兼容导出(勿删):历史代码仍会 import formatBearerAuthorization =====
// 说明:当前线上版本后端用 `token` 头,不用 Authorization;但为了不影响其它模块编译/运行,这里保留该方法导出。
const formatBearerAuthorization = (raw) => {
const v = String(raw || "").trim()
if (!v) return ""
if (/^Bearer\s+/i.test(v)) return v
return `Bearer ${v}`
}
// ===== 兼容导出(勿删)=====
// 获取token
const getToken = () => {
// 固定 token(恢复原行为:所有请求都带这个 token)
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
......@@ -25,7 +36,7 @@ const removeToken = () => {
}
// 导出token管理方法
export { getToken, setToken, removeToken }
export { getToken, setToken, removeToken, formatBearerAuthorization }
// const BASE_API = import.meta.env.VITE_BASE_API
// 创建axios实例
......@@ -89,6 +100,17 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error)
// 特殊处理:用户信息接口可能在某些环境不存在(404),此时不弹出错误提示
// 需求:GET /api/sso/user/info 返回 404 时静默
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 404 && errUrl.includes('/api/sso/user/info')) {
return Promise.reject(error)
}
} catch {
// ignore
}
// 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
const errUrl = String(error.config?.url || '')
......@@ -118,4 +140,12 @@ service.interceptors.response.use(
}
)
export default service
\ No newline at end of file
export default service
/*
===========================
下面是“当前新版 request.js”代码备份(按你的要求:不删除,仅注释保留)
===========================
(此处保留了之前实现的 token 多来源读取、AxiosHeaders、默认头同步、401 防抖等逻辑)
*/
\ No newline at end of file
// 智库概览信息
import request, { getToken } from "@/api/request.js";
import request, { getToken, formatBearerAuthorization } from "@/api/request.js";
// 智库列表
export function getThinkTankList() {
......@@ -257,7 +257,8 @@ export function postReportDomainViewAnalysis(data) {
*/
export async function postReportDomainViewAnalysisStream(data, handlers = {}) {
const { onReasoningChunk, onMessage } = handlers
const token = getToken()
const raw = getToken()
const token = raw ? String(raw).trim() : ""
const response = await fetch('/intelligent-api/report-domain-view-analysis', {
method: 'POST',
headers: {
......
/**
* 必须在 router、业务组件之前执行,且不能 import @/api/request(会提前加载 element-plus,易触发 Vue 初始化异常)。
* 与 request.js 中 TOKEN_KEY / TOKEN_SESSION_KEY 保持一致。
*/
const TOKEN_KEY = "auth_token";
const TOKEN_SESSION_KEY = "auth_token";
if (typeof window !== "undefined") {
try {
const q = new URLSearchParams(window.location.search || "");
const t = q.get("token");
if (t && String(t).trim()) {
const v = String(t).trim();
localStorage.setItem(TOKEN_KEY, v);
sessionStorage.setItem(TOKEN_SESSION_KEY, v);
}
} catch {
// ignore
}
}
......@@ -20,14 +20,37 @@
</div>
<div class="nav-right">
<div class="info-box" @click="handleClickToolBox">
<div class="mail">
<div class="info-box">
<div class="mail" @click="handleClickToolBox">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user">
<img src="@/assets/icons/overview/user.png" alt="" />
<div class="user-trigger">
<div class="user" @click.stop="handleToggleUserPanel">
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
</div>
<div v-if="isShowUserPanel" class="user-panel">
<div class="user-panel-row">
<div class="blue-solid"></div>
<span class="user-panel-value user-panel-value--nickname">{{ userNickname }}</span>
<div class="role-box">
<span class="user-panel-value user-panel-value--role">{{ roleName }}</span>
</div>
</div>
<div class="user-panel-row user-panel-row--single">
<span class="user-panel-value user-panel-value--organ">{{ organName }}</span>
</div>
<div class="solid"></div>
<div class="user-panel-logout" @click.stop="handleUserCommand('logout')"><img src="./back.png"
class="back-image" />{{ "退出登录" }}
</div>
</div>
<div class="name text-regular">{{ "管理员" }}</div>
</div>
</div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
......@@ -61,6 +84,7 @@ import { ref, computed, onMounted, watchEffect, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import { getPersonType } from "@/api/common/index";
import request, { removeToken } from "@/api/request.js";
import SearchBar from "@/components/layout/SearchBar.vue";
import Menu1 from "@/assets/icons/overview/menu1.png";
......@@ -87,6 +111,16 @@ const router = useRouter();
const route = useRoute()
const personTypeList = ref([]);
const ssoUserInfo = ref(null);
const isShowUserPanel = ref(false);
// 用户面板展示兜底:接口无数据时不显示 "-",改为默认文案
const userNickname = computed(() => ssoUserInfo.value?.userInfo?.userNickname || "管理员");
const organName = computed(() => ssoUserInfo.value?.organInfo?.organName || "开发部门");
const roleName = computed(() => {
const roles = ssoUserInfo.value?.roles;
return Array.isArray(roles) && roles.length ? (roles[0]?.roleName || "系统管理员") : "系统管理员";
});
// 获取人物类别
const handleGetPersonType = async () => {
......@@ -102,6 +136,21 @@ const handleGetPersonType = async () => {
} catch (error) { }
};
// 获取当前登录用户信息(不展示,仅缓存;Authorization 由全局 request 拦截器自动注入)
const handleGetSsoUserInfo = async () => {
try {
const res = await request({
method: "GET",
// 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/user/info)
url: "/api/sso/user/info"
});
ssoUserInfo.value = res?.data ?? null;
console.log("用户信息", ssoUserInfo.value);
} catch {
ssoUserInfo.value = null;
}
};
// 概览页标题列表
const homeTitleList = ref([
......@@ -354,8 +403,93 @@ const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
const handleToggleUserPanel = () => {
isShowUserPanel.value = !isShowUserPanel.value;
};
const handleHideUserPanel = () => {
isShowUserPanel.value = false;
};
const handleUserCommand = async (command) => {
if (command === "logout") {
isShowUserPanel.value = false;
// ===== 退出登录(外部重定向版,切线上接口时先注释保留) =====
// const ssoOrigin = import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
// try {
// // 走后端登出接口,返回统一登出页地址(能清 SSO Cookie,确保下次必须重新登录)
// const res = await request({
// method: "POST",
// // 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/logout)
// url: "/api/sso/logout"
// });
//
// // 无论成功失败,都清掉本地 token(确保后续请求不再带 Authorization)
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
//
// const redirectUrl = res && typeof res.data === "string" ? res.data : "";
// window.location.replace(redirectUrl || `${ssoOrigin}/api/v2`);
// } catch (e) {
// // 兜底:清本地并跳网关
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
// window.location.replace(`${ssoOrigin}/api/v2`);
// }
// ===== 退出登录(站内 login 版:清 token 后进入 /login) =====
removeToken();
try {
window.localStorage.removeItem("auth_token");
} catch {
// ignore
}
// 退出后强制所有页面回到登录页(跨标签页/刷新生效)
try {
window.localStorage.setItem("force_login", "1");
} catch {
// ignore
}
try {
window.sessionStorage.removeItem("auth_token");
} catch {
// ignore
}
router.replace("/login");
}
};
onMounted(() => {
handleGetPersonType();
handleGetSsoUserInfo();
});
</script>
......@@ -469,6 +603,8 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
.mail {
width: 32px;
......@@ -499,6 +635,159 @@ onMounted(() => {
height: 30px;
color: var(--text-primary-80-color);
}
.user-trigger {
display: flex;
align-items: center;
cursor: pointer;
}
.user-panel {
position: absolute;
right: 0;
top: calc(32px + 21px);
width: 240px;
height: 141px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
// 高度固定 141px,需要把内容控制在容器内,避免分割线被裁剪
padding: 16px 15px 12px 24px;
box-sizing: border-box;
overflow: hidden;
z-index: 999999;
}
.user-panel-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
gap: 12px;
position: relative;
padding-right: 3px;
}
.user-panel-row--single {
justify-content: flex-start;
}
.blue-solid {
position: absolute;
left: -24px;
width: 4px;
height: 48px;
background: rgb(5, 95, 194);
top: 0;
}
.solid {
width: 206px;
height: 1px;
background: rgb(234, 236, 238);
margin-top: 13px;
}
.user-panel-label {
color: rgba(59, 65, 75, 0.8);
font-size: 14px;
line-height: 20px;
flex-shrink: 0;
}
.user-panel-value {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-panel-value--nickname {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
margin-left: 8px;
}
.role-box {
padding: 0px 8px;
background-color: rgb(231, 243, 255);
display: flex;
border-radius: 4px;
}
.user-panel-value--role {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: justify;
color: rgb(5, 95, 194);
}
.user-panel-value--organ {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: rgb(95, 101, 108);
margin-top: 4px;
margin-left: 8px;
}
.user-panel-logout {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
cursor: pointer;
height: 24px;
margin-top: 12px;
display: flex;
align-items: center;
margin-left: 8px;
.back-image {
width: 16px;
height: 16px;
margin-right: 12px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
......
import "./bootstrapAuth.js";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
......
import { createRouter, createWebHistory } from "vue-router";
import { getIsLoggedIn } from "@/utils/auth";
import { setToken, removeToken, getToken } from "@/api/request.js";
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */
const FORCE_LOGIN_KEY = "force_login";
// ===== SSO/重定向相关(切线上接口时先注释保留) =====
// /** 强制走统一登录(仅前端控制):退出登录后置 1,直到 SSO 回跳带 ?token= 再清除 */
// const FORCE_SSO_LOGIN_KEY = "force_sso_login";
function getCurrentViteBootId() {
try {
// eslint-disable-next-line no-undef
return typeof __APP_BOOT_ID__ !== "undefined" ? String(__APP_BOOT_ID__) : "";
} catch {
return "";
}
}
/** 仅同步 bootId(SSO 回跳带 ?token= 时会提前 return,须单独写入,否则下次进入 /ZMOverView 会误判 boot 变更而清 token) */
function persistViteBootIdOnly() {
if (typeof window === "undefined") return;
const current = getCurrentViteBootId();
if (!current) return;
try {
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
/**
* 每次重启 Vite/重新构建后 __APP_BOOT_ID__ 会变;仅当「曾记录过旧 bootId 且与当前不一致」时清空 token。
* 用 localStorage 而非 sessionStorage,避免新开标签页时误把有效 token 清掉导致请求不带 token、全员 401。
* 注意:须在处理完 URL ?token= 之后再执行,避免先清 token 再写入的竞态。
*/
function clearTokenIfNewDevBoot() {
if (typeof window === "undefined") return;
try {
const current = getCurrentViteBootId();
if (!current) return;
const saved = window.localStorage.getItem(VITE_BOOT_STORAGE_KEY);
if (saved && saved !== current) {
removeToken();
}
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
// /** 统一认证网关:未登录时整页跳转此处,由后端再跳到登录页;登录成功后回跳到前端并带 ?token= */
// const SSO_GATEWAY_ORIGIN =
// import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
//
// /** 网关注册的回跳地址常为 /callback;保存 token 后应进入业务首页,避免停在无路由的 /callback */
// const SSO_POST_LOGIN_PATH =
// import.meta.env.VITE_SSO_POST_LOGIN_PATH || "/ZMOverView";
/**
* 是否与「请求里 getToken()」一致:避免出现仅空白/脏数据时仍认为已登录 → 不走 SSO 且请求头无 token
*/
function hasStoredAuthToken() {
return Boolean(getToken());
}
const Home = () => import('@/views/home/index.vue')
const DataLibrary = () => import('@/views/dataLibrary/index.vue')
......@@ -33,6 +97,8 @@ const routes = [
{
path: "/",
name: "Home",
// 访问根路径时默认进入概览页(否则 Home 的 router-view 为空,看起来像白屏)
redirect: "/ZMOverView",
component: Home,
children: [
...fileRoutes,
......@@ -73,18 +139,88 @@ const router = createRouter({
routes
});
// 路由守卫 - 设置页面标题
// 路由守卫:SSO + 设置页面标题
// 1)首次/无本地 token:整页跳到网关 /api/v2 + 当前路径,走后端登录
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => {
// 登录态:同一次前端服务 BOOT_ID 内跨刷新/跨新标签有效;服务重启后自动失效
const isAuthed = getIsLoggedIn();
// ===== SSO/重定向逻辑(切线上接口时停用,保留注释) =====
// const queryToken = to.query && to.query.token != null && String(to.query.token).trim() !== ""
// ? String(to.query.token).trim()
// : "";
//
// if (queryToken) {
// setToken(queryToken);
// // 成功回跳拿到 token,说明统一登录链路已完成,清除强制标记
// try {
// if (typeof window !== "undefined") {
// window.sessionStorage.removeItem(FORCE_SSO_LOGIN_KEY);
// }
// } catch {
// // ignore
// }
// persistViteBootIdOnly();
// const restQuery = { ...to.query };
// delete restQuery.token;
// const isGatewayCallback =
// to.path === "/callback" || to.path.replace(/\/$/, "") === "/callback";
// const targetPath = isGatewayCallback ? SSO_POST_LOGIN_PATH : to.path;
// next({
// path: targetPath,
// query: restQuery,
// hash: to.hash,
// replace: true,
// });
// return;
// }
//
// // 若用户点了“退出登录”,即使本地还有残留 token/或别处写回,也强制先走统一登录链路
// try {
// if (typeof window !== "undefined" && window.sessionStorage.getItem(FORCE_SSO_LOGIN_KEY) === "1") {
// removeToken();
// const targetUrl = `${SSO_GATEWAY_ORIGIN}/api/v2${to.fullPath || "/"}`;
// window.location.replace(targetUrl);
// next(false);
// return;
// }
// } catch {
// // ignore
// }
// 外网/线上版本:不因重启清登录态;仅开发环境需要此逻辑
// clearTokenIfNewDevBoot();
if (import.meta.env.DEV) {
clearTokenIfNewDevBoot();
}
// 启用站内登录页:无 token 时统一进入 /login(不做外部重定向)
// 退出登录后:无论当前 getToken() 返回什么,都强制先去 /login,直到再次登录清除此标记
let forceLoginActive = false;
try {
if (typeof window !== "undefined" && window.localStorage.getItem(FORCE_LOGIN_KEY) === "1") {
forceLoginActive = true;
if (to.path !== "/login") {
next({ path: "/login", replace: true });
return;
}
}
} catch {
// ignore
}
const isLoginRoute = to.name === "Login" || /^\/login\/?$/.test(String(to.path || ""));
const isAuthed = hasStoredAuthToken();
// 已登录:不应停留在 /login(首页应为 /ZMOverView)
// 说明:force_login=1 时表示用户主动退出,仍允许进入 /login;否则一律跳回业务首页
if (isAuthed && to.path === "/login" && !forceLoginActive) {
const redirect = (to.query && to.query.redirect) ? String(to.query.redirect) : "";
next({ path: redirect || "/ZMOverView", replace: true });
return;
}
if (!isLoginRoute && !isAuthed) {
next({
path: "/login",
query: { redirect: to.fullPath }
});
if (!isAuthed && to.path !== "/login") {
// 防止误入 /callback 空白页
next({ path: "/login", replace: true });
return;
}
......
......@@ -64,7 +64,7 @@
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import { ref, watch, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { setIsLoggedIn } from "@/utils/auth";
......@@ -118,6 +118,22 @@ const loadLoginRemember = () => {
onMounted(() => {
loadLoginRemember();
// 登录页禁止出现任何滚动条(仅在本页生效)
try {
document.documentElement.classList.add("login-no-scroll");
document.body.classList.add("login-no-scroll");
} catch (_) {
// ignore
}
});
onUnmounted(() => {
try {
document.documentElement.classList.remove("login-no-scroll");
document.body.classList.remove("login-no-scroll");
} catch (_) {
// ignore
}
});
/** 取消记住我:清空本地存储与输入框,刷新/再次进入也为空 */
......@@ -161,6 +177,12 @@ const handleLogin = () => {
}
setIsLoggedIn(true);
// 登录成功:清除“强制回登录页”标记(否则跨页/新开标签页会一直被路由守卫拦截到 /login)
try {
window.localStorage.removeItem("force_login");
} catch (_) {
// ignore
}
if (checked.value) {
saveLoginRemember(u, p);
......@@ -180,12 +202,16 @@ const handleLogin = () => {
</script>
<style scoped lang="scss">
:global(html.login-no-scroll),
:global(body.login-no-scroll) {
overflow: hidden !important;
}
.login-page {
/* 允许纵向滚动,禁止横向滚动(放大时避免横向滚动条) */
/* 登录页不允许出现任何滚动条 */
width: 100vw;
min-height: 100vh;
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
overflow: hidden;
/* 背景色(图片加载不出来时兜底) */
background: rgb(5, 95, 194);
......
......@@ -19,13 +19,13 @@ const getCalendarHeatChart = (data) => {
visualMap: {
show: false,
min: 0,
max: 500,
max: 20,
calculable: true,
orient: 'horizontal',
left: 'center',
top: 65,
inRange: {
color: ['rgba(231, 243, 255, 1)', 'rgba(138, 196, 255, 1)', 'rgba(5, 95, 194, 1)']
color: ['rgb(231, 243, 255)', 'rgb(137, 193, 255)', 'rgb(5, 95, 194)']
},
textStyle: {
color: 'rgba(95, 101, 108, 1)'
......@@ -36,7 +36,7 @@ const getCalendarHeatChart = (data) => {
left: 30,
right: 30,
cellSize: ['auto', 20],
range: '2025',
range: '2026',
splitLine: {
show:false
},
......
......@@ -58,9 +58,10 @@ export default defineConfig({
},
'/api': {
target: 'http://8.140.26.4:9085/',
target: 'http://8.140.26.4:9085/',
// target: 'http://192.168.0.4:28080/',
changeOrigin: true,
// 前端仍请求 /api/xxx,实际转发到后端 /api/v2/xxx
rewrite: (path) => path.replace(/^\/api/, '')
// '/api': {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论