diff --git a/package-lock.json b/package-lock.json index 796e8ee..3e1ad42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "axios": "^1.9.0", + "blueimp-md5": "^2.19.0", "bootstrap": "^5.3.6", "core-js": "^3.8.3", "jquery": "^3.7.1", @@ -3417,6 +3418,12 @@ "dev": true, "license": "MIT" }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmmirror.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", diff --git a/package.json b/package.json index cb562d0..c1c0eb4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "axios": "^1.9.0", + "blueimp-md5": "^2.19.0", "bootstrap": "^5.3.6", "core-js": "^3.8.3", "jquery": "^3.7.1", diff --git a/public/index.html b/public/index.html index 3e5a139..ed215dd 100644 --- a/public/index.html +++ b/public/index.html @@ -5,6 +5,7 @@ + <%= htmlWebpackPlugin.options.title %> diff --git a/src/App.vue b/src/App.vue index e1e9125..cc3d219 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,13 +1,24 @@ diff --git a/src/components/Alert.vue b/src/components/Alert.vue new file mode 100644 index 0000000..bcbfb3d --- /dev/null +++ b/src/components/Alert.vue @@ -0,0 +1,95 @@ + + + + + + diff --git a/src/main.js b/src/main.js index 20d0cd8..4bf5105 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,9 @@ import Vue from 'vue' import App from './App.vue' import router from './router' +import store from './store' +import api from './request/api' +import Alert from '@/plugins/alert' import 'jquery' import 'bootstrap/dist/js/bootstrap.bundle.min.js' import 'bootstrap/dist/css/bootstrap.min.css' @@ -11,7 +14,22 @@ import "perfect-scrollbar/css/perfect-scrollbar.css" Vue.config.productionTip = false -new Vue({ +//挂载后端请求函数到全局 +Vue.prototype.$api = api +Vue.use(Alert) + +/** + * 初始化系统 + */ +async function initApp() { + //初始化请求系统配置信息 + await store.dispatch('config/fetch_config') + + new Vue({ router, + store, render: h => h(App) }).$mount('#app') +} + +initApp() \ No newline at end of file diff --git a/src/plugins/alert.js b/src/plugins/alert.js new file mode 100644 index 0000000..0bba5cb --- /dev/null +++ b/src/plugins/alert.js @@ -0,0 +1,24 @@ +import Alert from "@/components/Alert.vue" +import Vue from "vue" + +const AlertConstructor = Vue.extend(Alert) + +function showAlert(message,type,duration = 5000){ + const instance = new AlertConstructor({ + propsData:{message,type,duration} + }) + + const vm = instance.$mount() + document.body.appendChild(vm.$el) + instance.$on('close',() => { + document.body.removeChild(vm.$el) + instance.$destroy() + }) + return instance +} + +export default { + install(){ + Vue.prototype.$alert = showAlert + } +} \ No newline at end of file diff --git a/src/request/api.js b/src/request/api.js index 0d91d7e..6f7dd14 100644 --- a/src/request/api.js +++ b/src/request/api.js @@ -1,9 +1,19 @@ import request from "./request" // 导出一个名为login_api的函数,该函数使用request.post方法发送post请求到/api/auth/login路径 -export const login_api =(param)=> request.post('/api/auth/login',param) +const login = async (param)=> await request.post('/api/auth/login',param) // 导出一个名为registered_api的函数,该函数接收一个参数param,并使用request.post方法发送一个POST请求到/api/Auth/Register接口,请求体为param -export const registered_api =(param)=> request.post('/api/Auth/Register',param) +const register = async (param)=> await request.post('/api/Auth/Register',param) // 导出一个名为SendValidateCode_api的函数,该函数使用request.post方法发送POST请求,请求的URL为'/api/Auth/SendValidateCode' -export const SendValidateCode_api = (param)=> request.post(`/api/Auth/SendValidateCode?email=${param}`) \ No newline at end of file + const SendValidateCode = async (param)=> await request.post(`/api/Auth/SendValidateCode?email=${param}`) + + //获取所有系统配置 + const getAllConfig = async ()=> await request.get('/api/systemconfig/getallsystemconfig') + +export default { + login, + register, + SendValidateCode, + getAllConfig +} \ No newline at end of file diff --git a/src/request/request.js b/src/request/request.js index cda0db7..66702b4 100644 --- a/src/request/request.js +++ b/src/request/request.js @@ -1,24 +1,27 @@ import axios from "axios" +import {attachAccessToken,authRequestError} from '@/utils/intercaptors/request/auth' +import { handlerefresherror } from "@/utils/intercaptors/response/refresh" + const request = axios.create({ baseURL:'http://192.168.5.100:2088', - timeout:10000 + timeout:10000, + // ✅ 只把 status >= 500 视为错误,401/402/403 等都当作正常返回 + validateStatus: function (status) { + return status < 500 // 500以上才视为 error,其他都进 then + } }) -// request.interceptors.request.use(config=>{ -// config.headers.Authorization = localStorage.getItem('token') -// return config -// },err=>{ -// Promise.reject(err) -// } -// ) +request.interceptors.request.use(attachAccessToken,authRequestError) + +request.interceptors.response.use( + handlerefresherror, + undefined +) request.interceptors.response.use(res=>{ - alert(res.data.message) return res.data -},err=>{ - alert(err.response.data.message) -} +},undefined ) export default request \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 870b058..15199bd 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -12,23 +12,30 @@ const routes = [ name: 'home', component: HomeView } - ,{ - path:'', - redirect:'/home' + , { + path: '', + redirect: '/home' }, { - path:'/auth', + path: '/auth', component: Login, children: [ { - path:'login', + path: 'login', component: Login }, { - path:'register', + path: 'register', component: Register } ] + }, + { + path: '/layout', + component: () => import('@/views/layout/Home.vue'), + children: [ + { path: 'index', component: () => import('@/views/layout/Dashboard.vue') } + ] } ] diff --git a/src/store/index.js b/src/store/index.js index b5dc189..3434cb0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,4 +1,12 @@ import Vue from 'vue' import Vuex from 'vuex' +import config from './modules/config' Vue.use(Vuex) + + +export default new Vuex.Store({ + modules:{ + config + } +}) \ No newline at end of file diff --git a/src/store/modules/config.js b/src/store/modules/config.js new file mode 100644 index 0000000..2f30843 --- /dev/null +++ b/src/store/modules/config.js @@ -0,0 +1,54 @@ +import api from '@/request/api' + +const state = { + //全局系统配置 + systemconfig:[] +} + +const mutations = { + /**设置系统配置 */ + SET_CONFIG(state,config){ + state.systemconfig = config + } +} + +const actions = { + /** + * 初始化配置 + */ + async fetch_config({commit}){ + try{ + const res = await api.getAllConfig() + commit('SET_CONFIG',res.data) + }catch(e){ + console.error('初始化配置失败'.e) + } + } +} + +const getters = { + /** + * 将后端返回的系统配置数组转换为对象格式 + * @returns {object} + */ + configMap: (state) => { + const map = {} + state.systemconfig.forEach(item => { + map[item.configName] = item.configBody + }) + return map + }, + systemname: (state,getters) => getters.configMap.SystemName || '青蓝', + systemdesc: (state,getters) => getters.configMap.SystemDescription || '无', + systemlogo: (state,getters) => getters.configMap.LogoLocation || 'https://img.picui.cn/free/2025/06/11/684900b15ecc4.jpg', + systemphone: (state,getters) => getters.configMap.Phone || '13000000000', + systememail: (state,getters) => getters.configMap.Email || 'admin@admin.com' +} + +export default { + namespaced:true, + state, + mutations, + actions, + getters +} \ No newline at end of file diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..2c99ee1 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,26 @@ + +export default { + getAccessToken(){ + return sessionStorage.getItem('ACCESS_TOKEN') || '' + }, + getRefreshToken(){ + return localStorage.getItem('REFRESH_TOKEN') || '' + }, + getUserId(){ + return localStorage.getItem('USER_ID') || -2 + }, + setAccessToken(token){ + sessionStorage.setItem('ACCESS_TOKEN',token) + }, + setRefreshToken(token){ + localStorage.setItem('REFRESH_TOKEN',token) + }, + setUserId(id){ + localStorage.setItem('USER_ID',id) + }, + clear(){ + sessionStorage.removeItem('ACCESS_TOKEN') + localStorage.removeItem('REFRESH_TOKEN') + localStorage.removeItem('USER_ID') + } +} \ No newline at end of file diff --git a/src/utils/intercaptors/request/auth.js b/src/utils/intercaptors/request/auth.js new file mode 100644 index 0000000..f0dea8d --- /dev/null +++ b/src/utils/intercaptors/request/auth.js @@ -0,0 +1,14 @@ +// src/utils/interceptors/request/auth.js +import auth from '@/utils/auth' + +export function attachAccessToken(config) { + const token = auth.getAccessToken() + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + return config +} + +export function authRequestError(err) { + return Promise.reject(err) +} diff --git a/src/utils/intercaptors/response/refresh.js b/src/utils/intercaptors/response/refresh.js new file mode 100644 index 0000000..7bd8295 --- /dev/null +++ b/src/utils/intercaptors/response/refresh.js @@ -0,0 +1,60 @@ +import request from "@/request/request" +import auth from "@/utils/auth" + +let isrefreshing = false + +//待刷新token任务列表 +let queue = [] + +/** + * 运行任务队列 + * @param {*} token + */ +function runqueque(token) { + queue.forEach(element => element(token)) + queue = [] +} + +/** + * 处理accessToken自动刷新 + * @param {*} response + * @returns + */ +export function handlerefresherror(response) { + const { config, data } = response + if (data.code == 2009 && !config._retry) { + config._retry = true + if (!isrefreshing) { + isrefreshing = true + return request.post('/auth/refreshToken', { + userid: auth.getUserId(), + refreshtoken: auth.getRefreshToken() + }) + .then(r => { + const {Token , RefreshToken} = r.data.data + auth.setAccessToken(Token) + auth.setRefreshToken(RefreshToken) + runqueque(Token) + // 用新 token 重做原始请求 + config.headers['Authorization'] = `Bearer ${Token}` + return request(config) + }) + .catch(err => { + auth.clear() + window.location.href = '/auth/login' + return Promise.reject(err) + }) + .finally(() => isrefreshing = false) + } + + // 刷新中:队列化 + return new Promise((resolve, reject) => { + queue.push(token => { + config.headers['Authorization'] = `Bearer ${token}` + resolve(request(config)) + }) + }) + } else { + return response + } +} \ No newline at end of file diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue index f32b4dc..d8363e4 100644 --- a/src/views/auth/Login.vue +++ b/src/views/auth/Login.vue @@ -1,54 +1,109 @@ - \ No newline at end of file diff --git a/src/views/layout/Dashboard.vue b/src/views/layout/Dashboard.vue new file mode 100644 index 0000000..6c0c1ea --- /dev/null +++ b/src/views/layout/Dashboard.vue @@ -0,0 +1,387 @@ + \ No newline at end of file diff --git a/src/views/layout/Home.vue b/src/views/layout/Home.vue new file mode 100644 index 0000000..57e6b74 --- /dev/null +++ b/src/views/layout/Home.vue @@ -0,0 +1,249 @@ + + + + + \ No newline at end of file