Files
offerpai_web/src/stores/index.ts
T
2026-06-02 14:21:13 +08:00

352 lines
11 KiB
TypeScript

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<RootState>({
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<any>[] = []
// 按需发起请求,保持顺序以便后续取值
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: {},
})