Selaa lähdekoodia

feat: 添加登录守卫和当前用户管理功能,优化用户认证流程

xieyong 1 viikko sitten
vanhempi
commit
a249cb6cbb

+ 6 - 1
src/App.vue

@@ -1,15 +1,20 @@
 <script>
-import { isLoggedIn } from './utils/storage'
+import { hasServerAuth, isLoggedIn } from './utils/storage'
+import { getServerConfig } from './utils/auth'
 
 function guardLogin() {
   const pages = getCurrentPages()
   const currentRoute = pages[pages.length - 1]?.route || ''
   const onLoginPage = currentRoute === 'pages/login/index'
+  const config = getServerConfig()
 
   if (!isLoggedIn() && !onLoginPage) {
     uni.reLaunch({ url: '/pages/login/index' })
   } else if (isLoggedIn() && onLoginPage) {
     uni.reLaunch({ url: '/pages/home/index' })
+  } else if (config.mode === 'server' && !hasServerAuth()) {
+    // 服务器模式下没有真实 token(demo token 残留 / 从未登录),强制回登录页
+    uni.reLaunch({ url: '/pages/login/index' })
   }
 }
 

+ 28 - 0
src/hooks/useAuthGuard.js

@@ -0,0 +1,28 @@
+import { onShow } from '@dcloudio/uni-app'
+import { getServerConfig } from '@/utils/auth'
+import { hasServerAuth, isLoggedIn } from '@/utils/storage'
+
+// 判断当前会话是否需要回登录页
+function needsLogin() {
+  const config = getServerConfig()
+  // 服务器模式:必须有真实 token(demo token 不算)
+  if (config.mode === 'server') return !hasServerAuth()
+  // 其他模式:本地有 token / userInfo 就算已登录
+  return !isLoggedIn()
+}
+
+// 页面级登录守卫:在 setup 中调用一次即可
+// - 首次渲染前检查一次(防直接跳转 / deep link)
+// - 注册 onShow 回调,每次回到页面都重检(防会话被踢 / token 过期)
+export function useAuthGuard() {
+  function check() {
+    const pages = getCurrentPages()
+    const currentRoute = pages[pages.length - 1]?.route || ''
+    if (currentRoute === 'pages/login/index') return
+    if (needsLogin()) {
+      uni.reLaunch({ url: '/pages/login/index' })
+    }
+  }
+  check()
+  onShow(check)
+}

+ 14 - 0
src/hooks/useCurrentUser.js

@@ -0,0 +1,14 @@
+import { reactive } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getCurrentUser } from '../utils/storage'
+
+// 提供响应式的当前登录人资料:
+// - demo 模式返回 mock,服务器模式返回接口数据;都没有时是 reactive({}),模板 v-if 隐藏
+// - 每次 onShow 用 Object.assign 覆盖,登录/切账号/切模式后自动刷新
+export function useCurrentUser() {
+  const currentUser = reactive(getCurrentUser() || {})
+  onShow(() => {
+    Object.assign(currentUser, getCurrentUser() || {})
+  })
+  return currentUser
+}

+ 5 - 1
src/pages/apply/form.vue

@@ -108,7 +108,9 @@
 import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
 import { onLoad, onShow } from "@dcloudio/uni-app";
 import ModuleIcon from "@/components/ModuleIcon.vue";
-import { applicationTypes, currentUser } from "@/data/mock";
+import { applicationTypes } from "@/data/mock";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
+import { useCurrentUser } from "@/hooks/useCurrentUser";
 import {
   addApplication,
   getDraft,
@@ -116,6 +118,8 @@ import {
   saveDraft as persistDraft,
 } from "@/utils/storage";
 const type = ref("leave");
+useAuthGuard();
+const currentUser = useCurrentUser();
 const form = reactive({
   leaveType: "",
   certificateType: "",

+ 2 - 0
src/pages/apply/list.vue

@@ -47,8 +47,10 @@
 <script setup>
 import { computed, ref } from "vue";
 import { onShow } from "@dcloudio/uni-app";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { getApplications } from "@/utils/storage";
 const tabs = ["全部", "审批中", "已通过", "已驳回"];
+useAuthGuard();
 const tab = ref("全部");
 const typeFilter = ref("全部类型");
 const rangeFilter = ref("近三个月");

+ 2 - 0
src/pages/approval/detail.vue

@@ -64,9 +64,11 @@
 <script setup>
 import { ref } from "vue";
 import { onLoad, onShow } from "@dcloudio/uni-app";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { approvals } from "@/data/mock";
 import { getApprovalResults, setApprovalResult } from "@/utils/storage";
 const item = ref(approvals[0]);
+useAuthGuard();
 const result = ref(null);
 function sync() {
   result.value = getApprovalResults()[item.value.id] || null;

+ 2 - 0
src/pages/approval/index.vue

@@ -61,10 +61,12 @@
 <script setup>
 import { computed, ref } from "vue";
 import { onShow } from "@dcloudio/uni-app";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { approvals } from "@/data/mock";
 import { go } from "@/utils/router";
 import { getApprovalResults, setApprovalResult } from "@/utils/storage";
 const tabs = ["待我审批", "我已审批", "抄送我的"];
+useAuthGuard();
 const tab = ref("待我审批");
 const results = ref({});
 onShow(() => (results.value = getApprovalResults()));

+ 2 - 0
src/pages/attendance/index.vue

@@ -53,8 +53,10 @@
 </template>
 <script setup>
 import { ref } from "vue";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { go } from "@/utils/router";
 const month = ref(8);
+useAuthGuard();
 const weeks = ["日", "一", "二", "三", "四", "五", "六"];
 function changeMonth(step) {
   const next = month.value + step;

+ 3 - 1
src/pages/contacts/index.vue

@@ -49,9 +49,11 @@
 <script setup>
 import { computed, ref } from "vue";
 import AppTabBar from "@/components/AppTabBar.vue";
-import { contacts, currentUser } from "@/data/mock";
+import { contacts } from "@/data/mock";
+import { useCurrentUser } from "@/hooks/useCurrentUser";
 import { go } from "@/utils/router";
 const keyword = ref("");
+const currentUser = useCurrentUser();
 const filtered = computed(() =>
   contacts.filter((x) =>
     `${x.name}${x.dept}${x.position}`.includes(keyword.value),

+ 2 - 0
src/pages/employee/detail.vue

@@ -28,8 +28,10 @@
 <script setup>
 import { computed, ref } from "vue";
 import { onLoad } from "@dcloudio/uni-app";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { contacts } from "@/data/mock";
 const employee = ref(contacts[0]);
+useAuthGuard();
 onLoad(
   (o) => (employee.value = contacts.find((x) => x.id == o.id) || contacts[0]),
 );

+ 3 - 1
src/pages/home/index.vue

@@ -111,10 +111,12 @@ import AppTabBar from "@/components/AppTabBar.vue";
 import ModuleIcon from "@/components/ModuleIcon.vue";
 import { ref } from "vue";
 import { onShow } from "@dcloudio/uni-app";
-import { currentUser, quickActions, notices } from "@/data/mock";
+import { quickActions, notices } from "@/data/mock";
+import { useCurrentUser } from "@/hooks/useCurrentUser";
 import { getApprovalResults } from "@/utils/storage";
 import { go } from "@/utils/router";
 const pendingCount = ref(3);
+const currentUser = useCurrentUser();
 onShow(
   () =>
     (pendingCount.value = Math.max(

+ 2 - 0
src/pages/leave/index.vue

@@ -25,7 +25,9 @@
   >
 </template>
 <script setup>
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { go } from "@/utils/router";
+useAuthGuard();
 const leaves = [
   { name: "年假", left: "4.5", used: "0.5", unit: "天", color: "#1677ff" },
   { name: "调休", left: "6", used: "2", unit: "小时", color: "#07a44d" },

+ 2 - 0
src/pages/manage/index.vue

@@ -100,7 +100,9 @@
 <script setup>
 import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import { onLoad, onShow } from "@dcloudio/uni-app";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 const type = ref("recruit");
+useAuthGuard();
 const keyword = ref("");
 const statusFilter = ref("全部状态");
 const configs = {

+ 2 - 0
src/pages/notice/index.vue

@@ -19,6 +19,8 @@
 </template>
 <script setup>
 import { notices } from "@/data/mock";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
+useAuthGuard();
 function open(item) {
   uni.showModal({
     title: item.title,

+ 2 - 0
src/pages/performance/index.vue

@@ -36,6 +36,8 @@
   >
 </template>
 <script setup>
+import { useAuthGuard } from "@/hooks/useAuthGuard";
+useAuthGuard();
 const metrics = [
   { name: "招聘计划达成率", score: 28, total: 30 },
   { name: "员工服务满意度", score: 24, total: 25 },

+ 6 - 3
src/pages/profile/edit.vue

@@ -26,11 +26,14 @@
   >
 </template>
 <script setup>
-import { ref } from "vue";
-import { currentUser } from "@/data/mock";
+import { computed, ref } from "vue";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
+import { useCurrentUser } from "@/hooks/useCurrentUser";
 import { saveProfileChange } from "@/utils/storage";
+const currentUser = useCurrentUser();
+useAuthGuard();
 const lastRequest = ref(null);
-const groups = ref([
+const groups = computed(() => [
   {
     title: "任职信息",
     rows: [

+ 2 - 1
src/pages/profile/index.vue

@@ -60,10 +60,11 @@
 import { ref } from "vue";
 import { onShow } from "@dcloudio/uni-app";
 import AppTabBar from "@/components/AppTabBar.vue";
-import { currentUser } from "@/data/mock";
+import { useCurrentUser } from "@/hooks/useCurrentUser";
 import { go } from "@/utils/router";
 import { clearDemoStorage, getApplications, logout } from "@/utils/storage";
 const applyingCount = ref(0);
+const currentUser = useCurrentUser();
 onShow(
   () =>
     (applyingCount.value = getApplications().filter(

+ 2 - 0
src/pages/salary/index.vue

@@ -27,6 +27,8 @@
 </template>
 <script setup>
 import { reactive } from "vue";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
+useAuthGuard();
 const visible = reactive({});
 const salaryList = [
   {

+ 2 - 0
src/pages/training/index.vue

@@ -57,8 +57,10 @@
 </template>
 <script setup>
 import { computed, ref } from "vue";
+import { useAuthGuard } from "@/hooks/useAuthGuard";
 import { getTrainingProgress, setTrainingProgress } from "@/utils/storage";
 const tabs = ["待学习", "已完成", "考试"];
+useAuthGuard();
 const tab = ref("待学习");
 const defaults = [
   {

+ 43 - 8
src/utils/auth.js

@@ -1,6 +1,9 @@
 import {
   DEMO_ACCOUNT,
   DEMO_PASSWORD,
+  getAuthCredentials,
+  logout,
+  saveCurrentUser,
   saveLoginSession,
   savePermissionData,
   saveRememberedCredentials,
@@ -34,20 +37,35 @@ export function saveServerConfig(config) {
   return normalized
 }
 
+// H5 dev(pnpm dev:h5)走 Vite 代理避开浏览器 CORS;其他场景仍用配置里的真实地址
+const useDevProxy =
+  typeof window !== 'undefined' &&
+  typeof process !== 'undefined' &&
+  process.env?.NODE_ENV !== 'production'
+
 export function getApiBaseUrl(config = getServerConfig()) {
   if (config.mode !== 'server' || !config.hostname) return ''
+  if (useDevProxy) return '/api'
   const port = config.port ? `:${config.port}` : ''
   return `${config.protocol}${config.hostname}${port}/api`
 }
 
 function request({ url, method = 'GET', data, token, sessionId, timeout = 15000 }) {
   return new Promise((resolve, reject) => {
-    const header = token
+    // 未显式传 token 时自动从登录态读取,登录后所有请求自动带上凭证
+    let effectiveToken = token
+    let effectiveSessionId = sessionId
+    if (!effectiveToken) {
+      const stored = getAuthCredentials()
+      effectiveToken = stored.token
+      if (!effectiveSessionId) effectiveSessionId = stored.sessionId
+    }
+    const header = effectiveToken
       ? {
           'content-type': 'application/json',
-          'zoomwin-token': token,
-          Authorization: token,
-          'zoomwin-sid': sessionId || '',
+          'zoomwin-token': effectiveToken,
+          Authorization: effectiveToken,
+          'zoomwin-sid': effectiveSessionId || '',
         }
       : { 'content-type': 'application/json', platform: 'wxapp' }
 
@@ -59,6 +77,10 @@ function request({ url, method = 'GET', data, token, sessionId, timeout = 15000
       timeout,
       success: (response) => {
         if (response.statusCode === 401) {
+          // 服务端认为凭证过期:清本地会话 + 跳登录页
+          logout()
+          uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 1500 })
+          setTimeout(() => uni.reLaunch({ url: '/pages/login/index' }), 1200)
           reject(new Error('身份验证已过期,请重新登录'))
           return
         }
@@ -106,17 +128,24 @@ function collectAuthorities(tree) {
   return authorities
 }
 
-async function loadPermissionTree(baseUrl, userInfo) {
+async function loadPermissionTree(baseUrl) {
   const response = await request({
     url: `${baseUrl}/system/resources/getResourcesTreePDA`,
-    token: userInfo.token,
-    sessionId: userInfo.sessionId,
   })
   const tree = Array.isArray(response.data) ? response.data : []
   savePermissionData(tree, collectAuthorities(tree))
   return tree
 }
 
+// 拉取当前登录人的扩展资料(部门、岗位、角色等),登录后调用一次写入本地
+export async function fetchCurrentUser(baseUrl) {
+  if (!baseUrl) throw new Error('缺少 baseUrl')
+  const response = await request({
+    url: `${baseUrl}/system/account/getLoginUser`,
+  })
+  return response.data || null
+}
+
 export async function performLogin({ account, password, rememberPassword }) {
   const config = getServerConfig()
   const loginName = String(account || '').trim()
@@ -153,11 +182,17 @@ export async function performLogin({ account, password, rememberPassword }) {
   saveLoginSession(data, loginName)
   saveRememberedCredentials({ account: loginName, password: loginPwd, rememberPassword })
   try {
-    await loadPermissionTree(baseUrl, data)
+    await loadPermissionTree(baseUrl)
   } catch (error) {
     savePermissionData([], [])
     uni.showToast({ title: error.message || '权限加载失败', icon: 'none' })
   }
+  try {
+    const currentUser = await fetchCurrentUser(baseUrl)
+    if (currentUser) saveCurrentUser(currentUser)
+  } catch (error) {
+    uni.showToast({ title: error.message || '用户信息加载失败', icon: 'none' })
+  }
   return data
 }
 

+ 55 - 1
src/utils/storage.js

@@ -1,3 +1,5 @@
+import { currentUser as mockCurrentUser } from '@/data/mock'
+
 const APPLY_KEY = 'aimill_hr_applications'
 const DRAFT_KEY = 'aimill_hr_application_drafts'
 const APPROVAL_KEY = 'aimill_hr_approval_results'
@@ -11,6 +13,8 @@ const TOKEN_KEY = 'token'
 const USER_INFO_KEY = 'userInfo'
 const TREE_KEY = 'treeList'
 const AUTHORITIES_KEY = 'authorities'
+const CURRENT_USER_KEY = 'currentUser'
+const API_INFO_KEY = 'apiInfo'
 
 export const DEMO_ACCOUNT = 'ZY20230128'
 export const DEMO_PASSWORD = '123456'
@@ -48,7 +52,7 @@ export function login(account, password, remember = true) {
 }
 
 export function logout() {
-  [AUTH_KEY, TOKEN_KEY, USER_INFO_KEY, TREE_KEY, AUTHORITIES_KEY].forEach((key) =>
+  [AUTH_KEY, TOKEN_KEY, USER_INFO_KEY, TREE_KEY, AUTHORITIES_KEY, CURRENT_USER_KEY].forEach((key) =>
     uni.removeStorageSync(key),
   )
 }
@@ -57,6 +61,13 @@ export function isLoggedIn() {
   return Boolean(uni.getStorageSync(TOKEN_KEY) && uni.getStorageSync(USER_INFO_KEY))
 }
 
+// 服务器模式下需要的是真实 token(demo 登录的是 demo-token- 开头,残留时视为无效)
+export function hasServerAuth() {
+  const token = uni.getStorageSync(TOKEN_KEY)
+  const userInfo = uni.getStorageSync(USER_INFO_KEY)
+  return Boolean(token && userInfo && !String(token).startsWith('demo-token-'))
+}
+
 export function getRememberedAccount() {
   return getRememberedCredentials().account
 }
@@ -93,6 +104,49 @@ export function getLoginUser() {
   return uni.getStorageSync(USER_INFO_KEY) || null
 }
 
+export function saveCurrentUser(data) {
+  if (!data) return
+  uni.setStorageSync(CURRENT_USER_KEY, data)
+}
+
+// 把 /system/account/getLoginUser 的 CurrentUserVO 映射成页面里使用的字段
+function mapCurrentUser(vo) {
+  if (!vo || typeof vo !== 'object') return null
+  const name = String(vo.name || '')
+  return {
+    id: vo.jobNumber || vo.loginName || '',
+    name,
+    initials: name ? name.charAt(0) : '',
+    department: vo.deptName || '',
+    position: vo.postName || '',
+    company: vo.groupName || '',
+    phone: vo.phone || '',
+    entryDate: vo.entryDate || '',
+    workAge: vo.workAge || '',
+    raw: vo,
+  }
+}
+
+function isServerMode() {
+  const info = uni.getStorageSync(API_INFO_KEY)
+  return Boolean(info && info.mode === 'server')
+}
+
+// 演示模式 → mock 数据;服务器模式 → 接口返回的数据;服务器模式未拉到则返回 null
+export function getCurrentUser() {
+  if (!isServerMode()) return mockCurrentUser
+  const stored = uni.getStorageSync(CURRENT_USER_KEY)
+  return mapCurrentUser(stored)
+}
+
+export function getAuthCredentials() {
+  const userInfo = uni.getStorageSync(USER_INFO_KEY) || {}
+  return {
+    token: userInfo.token || '',
+    sessionId: userInfo.sessionId || '',
+  }
+}
+
 export function clearDemoStorage() {
   [APPLY_KEY, DRAFT_KEY, APPROVAL_KEY, TRAINING_KEY, PROFILE_KEY].forEach((key) =>
     uni.removeStorageSync(key),

+ 12 - 0
vite.config.js

@@ -1,6 +1,18 @@
 import { defineConfig } from 'vite'
 import uni from '@dcloudio/vite-plugin-uni'
 
+// H5 dev: 通过 Vite 代理把 /api 转发到后端,避开浏览器 CORS。
+// 接入新后端时,把 API_TARGET 改成对应环境即可。
+const API_TARGET = 'http://aiot.zoomwin.com.cn:51001'
+
 export default defineConfig({
   plugins: [uni()],
+  server: {
+    proxy: {
+      '/api': {
+        target: API_TARGET,
+        changeOrigin: true,
+      },
+    },
+  },
 })