diff --git a/docs/.vuepress/.temp/internal/routes.js b/docs/.vuepress/.temp/internal/routes.js index 4c15038..4273b03 100644 --- a/docs/.vuepress/.temp/internal/routes.js +++ b/docs/.vuepress/.temp/internal/routes.js @@ -1,6 +1,7 @@ export const redirects = JSON.parse("{}") export const routes = Object.fromEntries([ + ["/login.html", { loader: () => import(/* webpackChunkName: "login.html" */"D:/xue/dma_handbook/docs/.vuepress/.temp/pages/login.html.js"), meta: {"title":"DMA手册 - 登录"} }], ["/", { loader: () => import(/* webpackChunkName: "index.html" */"D:/xue/dma_handbook/docs/.vuepress/.temp/pages/index.html.js"), meta: {"title":"首页"} }], ["/posts/administrative.html", { loader: () => import(/* webpackChunkName: "posts_administrative.html" */"D:/xue/dma_handbook/docs/.vuepress/.temp/pages/posts/administrative.html.js"), meta: {"title":"行政"} }], ["/posts/agent.html", { loader: () => import(/* webpackChunkName: "posts_agent.html" */"D:/xue/dma_handbook/docs/.vuepress/.temp/pages/posts/agent.html.js"), meta: {"title":"代理商添加方式说明"} }], diff --git a/docs/.vuepress/.temp/internal/siteData.js b/docs/.vuepress/.temp/internal/siteData.js index dc8aa6c..ba416ff 100644 --- a/docs/.vuepress/.temp/internal/siteData.js +++ b/docs/.vuepress/.temp/internal/siteData.js @@ -1,4 +1,4 @@ -export const siteData = JSON.parse("{\"base\":\"/dma_handbook/\",\"lang\":\"zh-CN\",\"title\":\"DMA服务人员服务操作手册\",\"description\":\"DMA服务人员服务操作手册\",\"head\":[[\"meta\",{\"name\":\"og:type\",\"content\":\"website\"}],[\"meta\",{\"property\":\"og:title\",\"content\":\"DMA服务人员操作手册\"}],[\"meta\",{\"name\":\"description\",\"content\":\"DMA服务操作手册\"}],[\"meta\",{\"property\":\"og:description\",\"content\":\"DMA服务全流程操作指南\"}],[\"meta\",{\"property\":\"og:image\",\"content\":\"https://images.health.ufutx.com/202503/12/1f227399ffc2ddbf6c58eafa80627d19.png?v=1769757244438\"}],[\"link\",{\"rel\":\"icon\",\"href\":\"https://images.health.ufutx.com/202503/12/1f227399ffc2ddbf6c58eafa80627d19.png?v=1769757244438\"}]],\"locales\":{}}") +export const siteData = JSON.parse("{\"base\":\"/dma_handbook/\",\"lang\":\"zh-CN\",\"title\":\"DMA服务人员服务操作手册\",\"description\":\"DMA服务人员服务操作手册\",\"head\":[[\"meta\",{\"name\":\"og:type\",\"content\":\"website\"}],[\"meta\",{\"property\":\"og:title\",\"content\":\"DMA服务人员操作手册\"}],[\"meta\",{\"name\":\"description\",\"content\":\"DMA服务操作手册\"}],[\"meta\",{\"property\":\"og:description\",\"content\":\"DMA服务全流程操作指南\"}],[\"meta\",{\"property\":\"og:image\",\"content\":\"https://images.health.ufutx.com/202503/12/1f227399ffc2ddbf6c58eafa80627d19.png?v=1770021719831\"}],[\"link\",{\"rel\":\"icon\",\"href\":\"https://images.health.ufutx.com/202503/12/1f227399ffc2ddbf6c58eafa80627d19.png?v=1770021719831\"}]],\"locales\":{}}") if (import.meta.webpackHot) { import.meta.webpackHot.accept() diff --git a/docs/.vuepress/.temp/pages/index.html.vue b/docs/.vuepress/.temp/pages/index.html.vue index 9c1e926..54aaa2e 100644 --- a/docs/.vuepress/.temp/pages/index.html.vue +++ b/docs/.vuepress/.temp/pages/index.html.vue @@ -215,6 +215,8 @@ +
+
diff --git a/docs/.vuepress/client.js b/docs/.vuepress/client.js index e6ff649..9583ab5 100644 --- a/docs/.vuepress/client.js +++ b/docs/.vuepress/client.js @@ -1,17 +1,108 @@ +// 导入自定义布局 +import LoginLayout from './layouts/LoginLayout.vue' +// 导入默认布局(必须保留,否则其他页面会404) +import DefaultLayout from '@vuepress/theme-default/layouts/Layout.vue' + import { defineClientConfig } from 'vuepress/client' import { createPinia } from 'pinia' +// 1. 补全所有缺失的核心导入(适配docs根目录的store/utils) +import { useUserStore } from './store/modules/user' +import { showToast, getUserInfo } from '../utils/request' // 新增getUserInfo +// 核心修改:导入全局常量SITE_BASE +import { SITE_BASE } from './constants.js'; +// 原有组件导入(位置不变,无需修改) import WithAuth from './components/WithAuth.vue' import Login from './components/Login.vue' import helperHTML from './components/helperHTML.vue' import longPic from './components/longPic.vue' export default defineClientConfig({ - enhance({ app }) { + // 新增:注册布局 + layouts: { + Layout: DefaultLayout, // 默认布局 + LoginLayout: LoginLayout // 登录页布局 + }, + // 2. 解构出router(路由守卫必须用) + enhance({ app, router }) { const pinia = createPinia() app.use(pinia) + // 全局注册组件(原有逻辑,无问题) app.component('WithAuth', WithAuth) app.component('Login', Login) app.component('helperHTML', helperHTML) app.component('longPic', longPic) + + // 权限指令v-auth:修复this指向+Pinia实例化问题 + app.directive('auth', { + mounted(el, binding) { + // 3. Pinia仓库必须执行实例化(核心修复) + const userStore = useUserStore() + const requiredRoles = binding.value + if (!requiredRoles) return + // 未登录/无权限直接隐藏 + if (!userStore.isLogin) { + el.style.display = 'none' + return + } + let hasPermission = false + if (typeof requiredRoles === 'string') { + hasPermission = userStore.hasRole(requiredRoles) + } else if (Array.isArray(requiredRoles)) { + hasPermission = requiredRoles.some(role => userStore.hasRole(role)) + } + if (!hasPermission) el.style.display = 'none' + }, + updated(el, binding) { + // 4. 修复this指向问题:直接复用mounted逻辑,不用this + this.mounted.call(this, el, binding) + } + }) + + // 全局路由守卫:VuePress 2.x 适配版(白名单+未登录跳登录) + const whiteList = ['/login.html'] // 无需登录的页面 + // 隐藏登录功能 + // router.beforeEach(async (to, from, next) => { + // const userStore = useUserStore() + // const isLogin = userStore.isLogin + // + // // 白名单页面,直接放行 + // if (whiteList.includes(to.path)) { + // next() + // return + // } + // + // // 未登录:跳登录页并记录跳转前地址(适配base: /dma_handbook/) + // // 替换client.js中路由守卫的未登录跳转代码 + // if (!isLogin) { + // showToast('请先登录后访问') + // // 核心修复:直接使用to.fullPath(已包含SITE_BASE),无需手动拼接 + // let redirectPath = to.fullPath + // // 兜底校验:极端情况若fullPath未带SITE_BASE,手动拼接(防止地址栏手动修改) + // if (!redirectPath.startsWith(SITE_BASE)) { + // redirectPath = `${SITE_BASE}${redirectPath.replace(/^\//, '')}` // 去掉开头的/,避免双斜杠 + // } + // // 编码后拼接登录页地址 + // const redirect = encodeURIComponent(redirectPath) + // window.location.href = `${SITE_BASE}login.html?redirect=${redirect}` + // return + // } + // // 核心修改:仅【已登录 + 未拉取过用户信息】时,才调用接口 + // if (!userStore.isUserInfoFetched) { + // await getUserInfo(userStore) + // // 拉取成功后,更新标记为true(本次会话不再重复调用) + // userStore.setUserInfoFetched(true) + // } + // // 已登录,正常放行 + // next() + // }) + + // 全局退出登录方法:6. 挂载到window,所有环境(Markdown/组件)都能调用 + window.$logout = () => { + const userStore = useUserStore() + userStore.resetStore() // 清除Pinia+localStorage状态 + showToast('退出登录成功') + window.location.href = `${SITE_BASE}login.html` + } }, + setup() {}, }) diff --git a/docs/.vuepress/components/Login.vue b/docs/.vuepress/components/Login.vue index 0b53c35..b51554e 100644 --- a/docs/.vuepress/components/Login.vue +++ b/docs/.vuepress/components/Login.vue @@ -1,96 +1,158 @@ - - + + diff --git a/docs/.vuepress/components/WithAuth.vue b/docs/.vuepress/components/WithAuth.vue index a6e7d04..4b680ed 100644 --- a/docs/.vuepress/components/WithAuth.vue +++ b/docs/.vuepress/components/WithAuth.vue @@ -11,7 +11,7 @@ diff --git a/docs/.vuepress/store/modules/user.js b/docs/.vuepress/store/modules/user.js new file mode 100644 index 0000000..392ca7d --- /dev/null +++ b/docs/.vuepress/store/modules/user.js @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia' +export const useUserStore = defineStore('user', { + state: () => ({ + token: localStorage.getItem('rt_token') || '', + userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'), + roles: JSON.parse(localStorage.getItem('roles') || '[]'), + // 新增:会话级标记(是否已拉取过用户信息,不持久化,刷新自动重置) + isUserInfoFetched: false + }), + getters: { + isLogin: (state) => !!state.token, // 必须有 + hasRole: (state) => (role) => state.roles.includes(role), // 必须有 + }, + actions: { + // 确保该方法存在:设置用户信息 + setUserInfo(userInfo) { + this.userInfo = userInfo; + localStorage.setItem('userInfo', JSON.stringify(userInfo)); // 持久化到本地 + }, + // 确保该方法存在:设置权限角色 + setRoles(roleList) { + this.roles = roleList; + localStorage.setItem('roles', JSON.stringify(roleList)); // 持久化到本地 + }, + // 新增:更新用户信息拉取标记 + setUserInfoFetched(status) { + this.isUserInfoFetched = status; + }, + resetStore() { // 必须有:退出登录清除状态 + this.token = '' + this.userInfo = {} + this.roles = [] + localStorage.removeItem('rt_token') + localStorage.removeItem('userInfo') + localStorage.removeItem('roles') + }, + setToken(token) { + this.token = token + localStorage.setItem('rt_token', token) + }, + // 其他setUserInfo/setRoles方法... + }, +}) diff --git a/docs/README.md b/docs/README.md index 3da93e9..b78ab8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -154,3 +154,5 @@ | 第一次对用户的评估报告生成 | 方案结束后第 3 天 18:00 前完成 | | 用户上传复检报告提醒 | 方案结束后第 23 天发送 ` ` | | 第二次对用户的评估报告生成 | 方案结束后第 60 天 18:00 前完成 | +
+
diff --git a/docs/login.md b/docs/login.md new file mode 100644 index 0000000..3fb9fed --- /dev/null +++ b/docs/login.md @@ -0,0 +1,4 @@ +--- +title: DMA手册 - 登录 +layout: LoginLayout +--- diff --git a/docs/.vuepress/utils/auth.js b/docs/utils/auth.js similarity index 92% rename from docs/.vuepress/utils/auth.js rename to docs/utils/auth.js index c500954..3f290f2 100644 --- a/docs/.vuepress/utils/auth.js +++ b/docs/utils/auth.js @@ -1,4 +1,4 @@ -import { PERMISSIONS } from '../roles'; +import { PERMISSIONS } from '../.vuepress/roles'; import axios from 'axios' export function getCurrentUserRole() { console.log('32-') @@ -12,7 +12,7 @@ export function checkPermission(currentRole, requiredPath) { // console.log(rolePermissions,'rolePermissions=') // return rolePermissions.includes('*') || rolePermissions.includes(requiredPath); const allowedPaths = PERMISSIONS[currentRole] || []; - return allowedPaths.some(path => + return allowedPaths.some(path => requiredPath.startsWith(path) || path === '*' ); } diff --git a/docs/utils/httpEnum.js b/docs/utils/httpEnum.js new file mode 100644 index 0000000..3a71606 --- /dev/null +++ b/docs/utils/httpEnum.js @@ -0,0 +1,9 @@ +// 请求头Content-Type枚举 +export const ContentTypeEnum = { + // 表单提交 + FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8', + // 文件上传 + FORM_DATA: 'multipart/form-data;charset=UTF-8', + // JSON提交 + JSON: 'application/json;charset=UTF-8' +}; diff --git a/docs/utils/public.js b/docs/utils/public.js new file mode 100644 index 0000000..0b05f91 --- /dev/null +++ b/docs/utils/public.js @@ -0,0 +1,15 @@ +/** + * 登录成功后回填数据到Pinia仓库 + * @param {object} res - 登录接口返回的用户数据 + * @param {object} userStore - Pinia用户仓库实例 + */ +export function backFillLoginData(res, userStore) { + if (res.token) { + userStore.setToken(res.token); + localStorage.setItem('rt_token', res.token); + } + if (res.userInfo) { + userStore.setUserInfo(res.userInfo); + userStore.setRoles(res.userInfo.roles || []); + } +} diff --git a/docs/utils/request.js b/docs/utils/request.js new file mode 100644 index 0000000..04b86b2 --- /dev/null +++ b/docs/utils/request.js @@ -0,0 +1,159 @@ +import { ref } from 'vue'; +import axios from 'axios'; +import { ContentTypeEnum } from './httpEnum'; +import { useUserStore } from '../.vuepress/store/modules/user'; + +// 适配VuePress服务端渲染,仅在浏览器环境执行 +const isBrowser = typeof window !== 'undefined'; +// 协议自动适配 +const baseHref = isBrowser ? (window.location.href.includes('https:') ? 'https:' : 'http:') : 'http:'; +// 创建axios实例 +const service = axios.create({ + baseURL: 'https://health.ufutx.com/', + withCredentials: false, + timeout: 50000, +}); + +// 自定义请求配置(仅保留hideLoading扩展) +export const requestConfig = { + hideLoading: false +}; + +// 原生加载提示(极简版,适配VuePress) +let loadingInstance = null; +const showLoading = (msg = '加载中...') => { + if (!isBrowser || loadingInstance) return; + loadingInstance = document.createElement('div'); + loadingInstance.style.cssText = `position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:8px 16px;background:rgba(0,0,0,0.7);color:#fff;border-radius:4px;z-index:99999;font-size:14px;`; + loadingInstance.innerText = msg; + document.body.appendChild(loadingInstance); +}; +const closeLoading = () => { + if (!isBrowser || !loadingInstance) return; + document.body.removeChild(loadingInstance); + loadingInstance = null; +}; + +// 原生全局提示(极简版) +export const showToast = (msg = '操作失败', duration = 2000) => { + if (!isBrowser) return; + const toast = document.createElement('div'); + toast.style.cssText = `position:fixed;top:80%;left:50%;transform:translate(-50%,-50%);padding:8px 16px;background:rgba(0,0,0,0.7);color:#fff;border-radius:4px;z-index:99999;font-size:14px;pointer-events:none;`; + toast.innerText = msg; + document.body.appendChild(toast); + setTimeout(() => document.body.removeChild(toast), duration); +}; + +// 请求拦截器:核心保留Token、Content-Type、版本号 +service.interceptors.request.use( + (config) => { + // 加载提示控制 + if (!config.hideLoading) showLoading(); + // 初始化请求头 + if (!config.headers) config.headers = {}; + // 默认Content-Type为JSON + config.headers['Content-Type'] = config.headers['Content-Type'] || ContentTypeEnum.JSON; + // 携带版本号 + if (isBrowser) config.headers['Version'] = import.meta.env.VITE_BASE_API_VERSION; + // 携带Token(仅保留核心rt_token,联动Pinia) + if (isBrowser) { + const userStore = useUserStore(); + const token = userStore.token || localStorage.getItem('rt_token') || ''; + if (token) config.headers['Authorization'] = `Bearer ${token}`; + } + // 处理POST表单参数 + if (config.method?.toLocaleUpperCase() === 'POST' && config.data) { + const contentType = config.headers['Content-Type']; + if (contentType === ContentTypeEnum.FORM_URLENCODED) { + config.data = qs.stringify(config.data); + } + } + return config; + }, + (error) => { + closeLoading(); + console.log('请求错误:', error); + return Promise.reject(error); + } +); + +// 响应拦截器:精简状态码,仅保留核心业务逻辑 +service.interceptors.response.use( + (response) => { + closeLoading(); + const res = response.data; + // 成功码:仅保留0(后端统一成功标识,剔除冗余兼容) + if (res.code === 0) return Promise.resolve(res.data); + // 业务错误:code!==0统一提示 + showToast(res.msg || res.message || '请求失败'); + return Promise.reject(res); + }, + (error) => { + closeLoading(); + // 网络/超时错误 + if (error.message?.includes('timeout')) { + showToast('网络超时,请稍后重试'); + } + // 401未授权:核心鉴权逻辑,跳登录页(适配VuePress base路径) + else if (error.response?.status === 401 && isBrowser) { + showToast('登录已过期,请重新登录'); + const userStore = useUserStore(); + // 清除状态,跳登录页并记录跳转地址 + userStore.resetStore(); + localStorage.removeItem('rt_token'); + const redirect = encodeURIComponent(window.location.href); + window.location.href = `/dma_handbook/login?redirect=${redirect}`; + } + // 其他错误 + else { + showToast(error.message || '网络异常'); + } + console.log('响应错误:', error); + return Promise.reject(error); + } +); + +// 统一请求方法:极简封装,适配VuePress +const request = (config) => { + return new Promise((resolve, reject) => { + service + .request(config) + .then((res) => resolve(res)) + .catch((err) => reject(err)); + }); +}; + + +// 放在request.js文件末尾,showToast导出后、request默认导出前 +/** + * 全局获取用户信息接口(每次进入页面调用) + * @param {object} userStore - Pinia用户仓库实例(传入避免重复实例化) + * @returns {Promise} - 是否获取成功 + */ +export const getUserInfo = async (userStore) => { + // 非浏览器环境/未登录,直接返回失败 + if (typeof window === 'undefined' || !userStore.token) return false; + try { + const res = await request({ + url: '/api/app/get/service/user/info', // 你的用户信息接口地址,baseURL会自动拼接 + method: 'get', + hideLoading: true, // 隐藏加载提示,避免页面闪烁 + }); + // 按你的接口返回格式更新仓库 + if (res && res.service_user_info) { + userStore.setUserInfo(res.service_user_info); // 更新用户信息 + } + if (res && res.role_list) { + userStore.setRoles(res.role_list); // 更新权限角色 + } + return true; + } catch (err) { + // 接口失败轻量提示,不阻塞页面 + showToast('用户信息同步失败', 1500); + console.log('获取用户信息失败:', err); + return false; + } +}; + + +export default request;