import { createStore } from 'vuex' import type { Router } from 'vue-router' import { fetchUserRoutes } from '@/api/menu' import type { MenuItemRaw } from '@/api/menu' import { buildDynamicRoutes } from '@/router/dynamicRoutes' import { fetchIndustryTree, fetchJobCategoryTree, fetchRegionTree } from '@/api/common' import type { IndustryItem, JobCategoryItem, RegionItem } from '@/api/common' import { fetchJobIntention, saveJobIntention } from '@/api/jobs' import type { JobIntention } from '@/api/jobs' import { fetchUserInfo } from '@/api/auth' import type { UserInfo } from '@/api/auth' /** 职位列表页缓存数据(从详情页返回时恢复用) */ export interface JobListCache { /** 缓存的职位列表 */ list: any[] /** 当前已加载到的页码 */ pageNum: number /** 总记录数 */ total: number /** 列表滚动位置 */ scrollTop: number } export interface RootState { appName: string showLogin: boolean loginRedirect: string /** * 登录状态 — 登录成功设为 true,退出成功设为 false * 因为后端 Cookie 是 HttpOnly 的,前端 JS 无法读取,所以用内存状态管理 */ isAuthenticated: boolean /** * 后端返回的原始菜单数据,SideNav 用它来渲染导航 */ dynamicMenus: MenuItemRaw[] /** * 标记动态路由是否已经加载过,避免重复请求 * 每次登出时重置为 false */ routesLoaded: boolean /** * 记录已注册的动态路由 name,登出时用来逐个 removeRoute */ dynamicRouteNames: string[] /** * 行业树分类数据(一级二级嵌套) * 由 /public/industries/tree 接口返回,进首页和 Jobs 页时刷新 * 其他页面/组件可直接从 store 读取 */ industries: IndustryItem[] /** * 岗位树分类数据(一级二级三级嵌套) * 由 /public/job-categories/tree 接口返回,进首页和 Jobs 页时刷新 * 其他页面/组件可直接从 store 读取 */ jobCategories: JobCategoryItem[] /** * 地区树分类数据(省市区三级嵌套) * 由 /public/regions/tree 接口返回,进首页和 Jobs 页时刷新 * 其他页面/组件可直接从 store 读取 */ regions: RegionItem[] /** * 职位列表页缓存 — 从详情页返回时恢复列表和滚动位置 * 为 null 表示没有缓存,需要重新请求 */ jobListCache: JobListCache | null /** * 求职意向数据 — JobGoalDialog 和 Jobs 页面共享 * 由 loadJobIntention action 从接口加载,saveJobIntention action 保存后更新 */ jobIntention: JobIntention /** * 用户个人信息 — 登录后从 /user/manage/info 接口获取 * 多个页面可直接从 store 读取 */ userInfo: UserInfo | null /** * 设置弹窗 — 控制显示/隐藏及初始 Tab */ showSettings: boolean settingsTab: string /** * 邀请码 — 从 URL 参数 invite_code 中提取 * 登录成功后自动清空,避免重复发送 */ inviteCode: string } export default createStore({ state: { appName: 'JobAssistant', showLogin: false, loginRedirect: '', isAuthenticated: sessionStorage.getItem('isAuthenticated') === 'true', dynamicMenus: [], routesLoaded: false, dynamicRouteNames: [], industries: [], jobCategories: [], regions: [], jobListCache: null, jobIntention: { categoryIds: [], regionCodes: [], industryIds: [], employmentType: 0, }, userInfo: null, showSettings: false, settingsTab: 'account', inviteCode: '', }, getters: { getAppName: (state) => state.appName, }, mutations: { SET_APP_NAME(state, name: string) { state.appName = name }, SET_SHOW_LOGIN(state, show: boolean) { state.showLogin = show }, SET_LOGIN_REDIRECT(state, path: string) { state.loginRedirect = path }, SET_AUTHENTICATED(state, val: boolean) { state.isAuthenticated = val if (val) { sessionStorage.setItem('isAuthenticated', 'true') } else { sessionStorage.removeItem('isAuthenticated') } }, SET_DYNAMIC_MENUS(state, menus: MenuItemRaw[]) { state.dynamicMenus = menus }, SET_ROUTES_LOADED(state, loaded: boolean) { state.routesLoaded = loaded }, SET_DYNAMIC_ROUTE_NAMES(state, names: string[]) { state.dynamicRouteNames = names }, SET_INDUSTRIES(state, data: IndustryItem[]) { state.industries = data }, SET_JOB_CATEGORIES(state, data: JobCategoryItem[]) { state.jobCategories = data }, SET_REGIONS(state, data: RegionItem[]) { state.regions = data }, SET_JOB_LIST_CACHE(state, cache: JobListCache | null) { state.jobListCache = cache }, SET_JOB_INTENTION(state, data: JobIntention) { state.jobIntention = { categoryIds: data.categoryIds ?? [], regionCodes: data.regionCodes ?? [], industryIds: data.industryIds ?? [], employmentType: data.employmentType ?? 0, } }, SET_USER_INFO(state, data: UserInfo | null) { state.userInfo = data }, SET_SHOW_SETTINGS(state, show: boolean) { state.showSettings = show }, SET_SETTINGS_TAB(state, tab: string) { state.settingsTab = tab }, SET_INVITE_CODE(state, code: string) { state.inviteCode = code }, }, actions: { updateAppName({ commit }, name: string) { commit('SET_APP_NAME', name) }, openLogin({ commit }, redirect = '') { commit('SET_LOGIN_REDIRECT', redirect) commit('SET_SHOW_LOGIN', true) }, closeLogin({ commit }) { commit('SET_SHOW_LOGIN', false) commit('SET_LOGIN_REDIRECT', '') }, /** * 核心 action:从后端(或 mock)拉取菜单,转成路由并注册 * * @param router — vue-router 实例,由路由守卫调用时传入 * * 流程: * 1. 调 fetchUserRoutes 拿到后端菜单数据 * 2. 存到 state.dynamicMenus(给 SideNav 渲染用) * 3. 用 buildDynamicRoutes 转成 RouteRecordRaw * 4. 逐条 router.addRoute 注册 * 5. 记录已注册的路由名,标记 routesLoaded = true */ async loadDynamicRoutes({ commit }, router: Router) { const menus = await fetchUserRoutes() commit('SET_DYNAMIC_MENUS', menus) const routes = buildDynamicRoutes(menus) const names: string[] = [] for (const route of routes) { router.addRoute(route) if (route.name) names.push(route.name as string) } commit('SET_DYNAMIC_ROUTE_NAMES', names) commit('SET_ROUTES_LOADED', true) }, /** * 静默刷新路由菜单数据(不重新注册路由,只更新菜单显示和权限状态) * 每次页面切换时由路由守卫调用,用于更新 accessible 权限信息 */ async refreshDynamicMenus({ commit }) { try { const menus = await fetchUserRoutes() commit('SET_DYNAMIC_MENUS', menus) } catch (err) { console.error('[store] 刷新路由菜单数据失败', err) } }, /** * 登出:重置状态(不再操作 localStorage,登录状态由 Cookie 管理) * 注意:动态路由不清除,与登录状态无关 */ logout({ commit }) { commit('SET_AUTHENTICATED', false) commit('SET_SHOW_LOGIN', false) commit('SET_LOGIN_REDIRECT', '') commit('SET_USER_INFO', null) // 清除 Jobs 页面缓存数据 commit('SET_JOB_LIST_CACHE', null) commit('SET_JOB_INTENTION', { categoryIds: [], regionCodes: [], industryIds: [], employmentType: 0, }) }, /** * 加载工具类公共数据(行业分类、岗位分类等) * 进首页和 Jobs 页时调用 * 如果 store 中已有数据则跳过请求,避免重复加载 * 传入 force = true 可强制刷新 */ async loadCommonData({ commit, state }, { force = false } = {}) { const needIndustry = force || state.industries.length === 0 const needJobCategory = force || state.jobCategories.length === 0 const needRegion = force || state.regions.length === 0 // 三个都已缓存,直接返回 if (!needIndustry && !needJobCategory && !needRegion) return try { const promises: Promise[] = [] // 按需发起请求,保持顺序以便后续取值 promises.push(needIndustry ? fetchIndustryTree() : Promise.resolve(null)) promises.push(needJobCategory ? fetchJobCategoryTree() : Promise.resolve(null)) promises.push(needRegion ? fetchRegionTree() : Promise.resolve(null)) const [industryRes, jobCategoryRes, regionRes] = await Promise.all(promises) if (industryRes && industryRes.code === '0' && industryRes.data) { commit('SET_INDUSTRIES', industryRes.data) } if (jobCategoryRes && jobCategoryRes.code === '0' && jobCategoryRes.data) { commit('SET_JOB_CATEGORIES', jobCategoryRes.data) } if (regionRes && regionRes.code === '0' && regionRes.data) { commit('SET_REGIONS', regionRes.data) } } catch (err) { console.error('[store] 加载公共分类数据失败', err) } }, /** * 从接口加载求职意向数据到 store * JobGoalDialog 和 Jobs 页面都可调用 */ async loadJobIntention({ commit }) { try { const res = await fetchJobIntention() if (res.code === '0' && res.data) { commit('SET_JOB_INTENTION', res.data) } } catch (err) { console.error('[store] 加载求职意向失败', err) } }, /** * 加载用户个人信息到 store * 登录状态下调用,多个页面可直接读取 store.state.userInfo */ async loadUserInfo({ commit }) { try { const res = await fetchUserInfo() if (res.code === '0' && res.data) { commit('SET_USER_INFO', res.data) } } catch (err) { console.error('[store] 加载用户信息失败', err) } }, /** * 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store * @param data 求职意向数据 */ async saveJobIntention({ commit, state }, data: JobIntention) { if (state.isAuthenticated) { // 已登录 — 调接口持久化,成功后同步 store const res = await saveJobIntention(data) if (res.code === '0') { commit('SET_JOB_INTENTION', data) } return res } else { // 未登录 — 仅本地更新 store,不请求接口 commit('SET_JOB_INTENTION', data) return { code: '0', message: 'ok' } } }, }, modules: {}, })