实现refreshToken与accessToken

This commit is contained in:
qumeng039@126.com 2025-08-15 14:40:59 +08:00
parent 423ee48c85
commit 6dbbddcf5e
10 changed files with 105 additions and 53 deletions

BIN
dist.zip

Binary file not shown.

View File

@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from "vue-router" import { createRouter, createWebHistory } from "vue-router"
import { useLoginStore } from "@/store/authentication/index" import { useLoginStore } from "@/store/authentication/index"
import cache from "@/utils/cache"
const routes = [ const routes = [
{ path: "/", redirect: "/home", meta: { requiresAuth: true } }, { path: "/", redirect: "/home", meta: { requiresAuth: true } },
{ {
@ -77,14 +78,13 @@ const router = createRouter({
// // 全局前置守卫 // // 全局前置守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const loginStore = useLoginStore() const token = cache.getItem("token")
// 如果目标路由需要登录且用户未登录 // 如果目标路由需要登录且用户未登录
if (to.meta.requiresAuth && !loginStore.token) { if (to.meta.requiresAuth && !token) {
next("/login") // 跳转到登录页 next("/login") // 跳转到登录页
} }
// 如果用户已登录但尝试访问登录页 // 如果用户已登录但尝试访问登录页
else if (to.path === "/login" && loginStore.token) { else if (to.path === "/login" && token) {
next("/home") // 跳转到主页 next("/home") // 跳转到主页
} }
// 其他情况正常放行 // 其他情况正常放行

View File

@ -1,5 +1,9 @@
import http from "@/utils/http" import http from "@/utils/http"
export function login(userInfo) { export function login(userInfo) {
return http.post("/auth/login", userInfo) return http.post("/auth/login", userInfo, { withCredentials: true })
}
export function refreshToken() {
return http.post("/auth/refresh", {}, { withCredentials: true })
} }

View File

@ -69,10 +69,7 @@ export function getSocialCommunityList() {
} }
// 社交紧密团体中社团内的社团内部节点 从用户组中点击,只显示这两个用户的关系 // 社交紧密团体中社团内的社团内部节点 从用户组中点击,只显示这两个用户的关系
export function getSocialCommunityDetailFromUserGroup( export function getSocialCommunityDetailFromUserGroup(relationId, time = "2024-05-16 16:56:04") {
relationId,
time = "2024-05-16 16:56:04"
) {
return http.get(`/linkPrediction/user_detail?relationId=${relationId}&time=${time}`) return http.get(`/linkPrediction/user_detail?relationId=${relationId}&time=${time}`)
} }
// 社交紧密团体的社团内部节点 // 社交紧密团体的社团内部节点
@ -110,3 +107,8 @@ export function getCharacterSocialCommunityDetailNodes(
return http.get(`/linkPrediction/social/community_detail?groupIds=${ids}&dateTime=${time}`) return http.get(`/linkPrediction/social/community_detail?groupIds=${ids}&dateTime=${time}`)
} }
} }
//点击边获取两用户交互的次数
export function getInteractionCount(source, target) {
return http.get(`/linkPrediction/interactionCount?source=${source}&target=${target}`)
}

View File

@ -1,21 +1,34 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { login } from "@/service/api/authentication" import { login, refreshToken } from "@/service/api/authentication"
import cache from "@/utils/cache" import cache from "@/utils/cache"
import router from "@/router" import router from "@/router"
import { ElMessage } from "element-plus" import { ElMessage } from "element-plus"
const TOKEN_KEY = "token"
export const useLoginStore = defineStore("loginStore", { export const useLoginStore = defineStore("loginStore", {
state: () => ({ state: () => ({
token: cache.getItem(TOKEN_KEY) ?? "" token: cache.getItem("token") ?? ""
}), }),
actions: { actions: {
setToken(accessToken) {
this.token = accessToken
cache.setItem("token", accessToken)
},
async loginForAccessToken(userInfo) { async loginForAccessToken(userInfo) {
const res = await login(userInfo) const res = await login(userInfo)
if (res.code != 200) return if (res.code != 200) return
this.token = res.data.accessToken this.setToken(res.data.accessToken)
cache.setItem(TOKEN_KEY, res.data.accessToken)
ElMessage.success("登录成功!") ElMessage.success("登录成功!")
router.push("/navigation") router.push("/navigation")
},
async loginForRefreshToken() {
const res = await refreshToken()
console.log(res)
if (res.code == 200) {
this.setToken(res.data.accessToken)
return true // 刷新成功
} else {
return false
}
} }
} }
}) })

View File

@ -13,7 +13,8 @@ import {
getSocialPostListByRelationId, getSocialPostListByRelationId,
getCharacterSocialCommunityNodes, getCharacterSocialCommunityNodes,
getCharacterSocialCommunityStatistics, getCharacterSocialCommunityStatistics,
getCharacterSocialCommunityDetailNodes getCharacterSocialCommunityDetailNodes,
getInteractionCount
} from "@/service/api/linkPrediction" } from "@/service/api/linkPrediction"
import defaultAvatar from "@/assets/images/avatar/default.png" import defaultAvatar from "@/assets/images/avatar/default.png"
@ -209,6 +210,11 @@ export const useCharacterInteractionStore = defineStore("characterInteraction",
...item, ...item,
count: res.data[item.key] count: res.data[item.key]
})) }))
},
async initInteractionCount(source, target) {
const res = await getInteractionCount(source, target)
if (res.code != 200) return
return res.data
} }
}, },
persist: true // 开启持久化 persist: true // 开启持久化
@ -415,7 +421,7 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
{ text: "立委", top: 57.5, left: 72.5, width: 49, height: 19, fontSize: 12, opacity: 0.6 }, { text: "立委", top: 57.5, left: 72.5, width: 49, height: 19, fontSize: 12, opacity: 0.6 },
{ text: "國會", top: 60.5, left: 265.5, width: 49, height: 19, fontSize: 12, opacity: 0.7 }, { text: "國會", top: 60.5, left: 265.5, width: 49, height: 19, fontSize: 12, opacity: 0.7 },
{ text: "两岸", top: 170.5, left: 287.5, width: 49, height: 19, fontSize: 12, opacity: 0.8 } { text: "两岸", top: 170.5, left: 287.5, width: 49, height: 19, fontSize: 12, opacity: 0.8 }
], ]
}), }),
actions: { actions: {
// 互动行为相似列表数据 // 互动行为相似列表数据
@ -470,8 +476,8 @@ export const useSocialGroupsStore = defineStore("socialGroups", {
) { ) {
customStatisticsObj.hiddenInteractionCount = 1 customStatisticsObj.hiddenInteractionCount = 1
// 创建一个Set来获取不重复的ids // 创建一个Set来获取不重复的ids
const uniqueIds = new Set(ids); const uniqueIds = new Set(ids)
customStatisticsObj.groupCount = uniqueIds.size; customStatisticsObj.groupCount = uniqueIds.size
} }
this.statisticsDetailList = this.statisticsDetailList.map((item) => ({ this.statisticsDetailList = this.statisticsDetailList.map((item) => ({
@ -681,6 +687,12 @@ export const useCharacterHiddenStore = defineStore("characterHidden", {
this.communityDetailNodeList = res.data.userList this.communityDetailNodeList = res.data.userList
this.timeList = Array.from(new Set(res.data.timeList)) this.timeList = Array.from(new Set(res.data.timeList))
this.predictionUserIds = res.data.predictNodes this.predictionUserIds = res.data.predictNodes
},
async initInteractionCount(source, target) {
const res = await getInteractionCount(source, target)
if (res.code != 200) return
return res.data
} }
}, },
persist: true // 开启持久化 persist: true // 开启持久化

View File

@ -2,16 +2,16 @@ import axios from "axios"
import cache from "./cache" import cache from "./cache"
import { ElMessage } from "element-plus" import { ElMessage } from "element-plus"
import router from "@/router" import router from "@/router"
import { useLoginStore } from "@/store/authentication/index"
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量获取基础URL baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量获取基础URL
timeout: 10000 // 请求超时时间 timeout: 10000, // 请求超时时间
withCredentials: true
}) })
//白名单 //白名单
const excludePath = new Set(["/login"]) const excludePath = new Set(["/auth/login", "/auth/refresh"])
// 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config) => { (config) => {
const token = cache.getItem("token") const token = cache.getItem("token")
@ -21,17 +21,14 @@ service.interceptors.request.use(
return config return config
}, },
(error) => { (error) => {
// 对请求错误做些什么
return Promise.reject(error) return Promise.reject(error)
} }
) )
// 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(response) => { async (response) => {
const res = response.data const res = response.data
const loginStore = useLoginStore()
// 根据业务状态码处理
if (res.code !== 200) { if (res.code !== 200) {
ElMessage({ ElMessage({
message: res.message || "Error", message: res.message || "Error",
@ -39,29 +36,32 @@ service.interceptors.response.use(
duration: 5 * 1000 duration: 5 * 1000
}) })
// 特殊状态码处理
if (res.code === 401 || res.code === 403) { if (res.code === 401 || res.code === 403) {
// 若token过期或者token无效或者未找到token对页面中需要认证的接口都有效使得重新登录 try {
import("@/store/authentication/index").then(({ useLoginStore }) => { // 尝试刷新token
useLoginStore().token = "" const refreshed = await loginStore.loginForRefreshToken()
if (refreshed) {
// 刷新成功更新请求头中的accessToken并重新发送请求
response.config.headers.Authorization = `Bearer ${loginStore.token}`
return service(response.config)
}
} catch (refreshError) {
ElMessage({
message: "会话已过期,请重新登录!",
type: "error",
duration: 5 * 1000
}) })
cache.removeItem("token") cache.removeItem("token")
router.push("/login") router.push("/login")
return Promise.reject(new Error("Refresh token failed"))
} }
return res }
// return Promise.reject(new Error(res.message || "Error")) return Promise.reject(new Error(res.message || "Error")) //500会抛出该错
} else { } else {
return res return res
} }
}, },
(error) => { (error) => {
if (error.status == 401 || error.status == 403) {
cache.removeItem("token")
import("@/store/authentication/index").then(({ useLoginStore }) => {
useLoginStore().token = ""
})
router.push("/login")
}
ElMessage({ ElMessage({
message: error.message, message: error.message,
type: "error", type: "error",

View File

@ -53,10 +53,31 @@ const handleGoback = () => {
emit("click:goback", "CommunityNode") emit("click:goback", "CommunityNode")
} }
const handleClickNode = () => { const handleClickNodeOrLink = () => {
chart.on("click", function (params) { chart.on("click", async function (params) {
if (params.dataType == "node" && predictionUserIds.value.includes(params.data.id)) { if (params.dataType == "node" && predictionUserIds.value.includes(params.data.id)) {
//
emit("click:openDialog", params.data) emit("click:openDialog", params.data)
} else if (params.dataType == "edge") {
//线
const { source, target } = params.data //id
const linkIndex = params.dataIndex
const option = chart.getOption()
const currentInteractionCount = await props.interactionStore.initInteractionCount(
source,
target
)
option.series[0].links[linkIndex].edge = currentInteractionCount
chart.setOption(
{
series: [
{
links: option.series[0].links
}
]
},
false
)
} }
}) })
} }
@ -157,8 +178,7 @@ const initChart = async () => {
links.push({ links.push({
source: parentId, source: parentId,
target: child.id, target: child.id,
edge: child.isHidden ? 1 : 0, edge: 0,
interactionTimes: child.interactionTime,
lineStyle: { lineStyle: {
width: child.isHidden ? 7 : edgeWidth(child.interactionTime), width: child.isHidden ? 7 : edgeWidth(child.interactionTime),
color: child.isHidden ? props.interactionStore.predictionLineColor : "#37ACD7", // == color: child.isHidden ? props.interactionStore.predictionLineColor : "#37ACD7", // ==
@ -267,7 +287,7 @@ const initChart = async () => {
show: false, show: false,
position: "middle", position: "middle",
formatter: function (params) { formatter: function (params) {
return `${params.data.interactionTimes}次互动` return `${params.data.edge}次互动`
}, },
fontSize: 14 fontSize: 14
}, },
@ -381,7 +401,7 @@ const highLightUserNodes = (userIds) => {
onMounted(() => { onMounted(() => {
initChart() initChart()
highLightUserNodes() highLightUserNodes()
handleClickNode() handleClickNodeOrLink()
}) })
</script> </script>

View File

@ -5,7 +5,7 @@
:model="ruleForm" :model="ruleForm"
status-icon status-icon
:rules="rules" :rules="rules"
label-width="auto" :label-width="null"
class="demo-ruleForm" class="demo-ruleForm"
@submit.native.prevent="submitForm(ruleFormRef)" @submit.native.prevent="submitForm(ruleFormRef)"
> >
@ -71,7 +71,6 @@ const submitForm = (formEl) => {
if (valid) { if (valid) {
const userInfo = toRaw(ruleForm) const userInfo = toRaw(ruleForm)
loginStore.loginForAccessToken(userInfo) loginStore.loginForAccessToken(userInfo)
} else {
} }
}) })
} }
@ -84,6 +83,7 @@ const submitForm = (formEl) => {
border-radius: 7px; border-radius: 7px;
background-color: #fff; background-color: #fff;
padding: 0 40px; padding: 0 40px;
.demo-ruleForm { .demo-ruleForm {
margin-top: 100px; margin-top: 100px;
height: 50%; height: 50%;

View File

@ -10,7 +10,7 @@
</template> </template>
<script setup> <script setup>
import LoginPanel from "./components/loginPanel.vue" import LoginPanel from "@/views/Login/components/loginPanel.vue"
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -37,9 +37,10 @@ import LoginPanel from "./components/loginPanel.vue"
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
// 1350px // 1350px
@media (max-width: 1350px) { @media (max-width: 1350px) {
width: 650px; // width: 750px; //
} }
// 750px // 750px