352 lines
11 KiB
TypeScript
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: {},
|
|
})
|