topfans/frontend/store/modules/guide.js
2026-04-07 23:08:49 +08:00

443 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 引导状态管理模块
*/
import { getGuideConfig, shouldShowGuide, markGuideAsShown, markGuideDone, getGuideCurrentStep, setGuideCurrentStep, completeSubStep, clearSubStepProgress, getNextIncompleteStep } from '@/utils/guideConfig'
// 设置页面实例的辅助函数(保留接口兼容)
// eslint-disable-next-line no-unused-vars
export function setGuidePageInstance(_instance) {
// uni.createSelectorQuery() 不需要传入页面实例
}
const state = {
// 当前激活的引导配置
currentGuide: null,
// 当前步骤索引
currentStep: 0,
// 是否显示引导
isActive: false,
// 目标元素位置信息
targetRect: {
top: 0,
left: 0,
width: 0,
height: 0
},
// 是否正在页面跳转中(保持遮罩显示)
isNavigating: false,
// 待执行的行动(高亮区域点击触发)
pendingAction: null,
// 是否处于组件打开模式(遮罩隐藏)
componentMode: false,
}
const mutations = {
/**
* 开始引导
* @param {Object} guideConfig 引导配置
* @param {number} guideConfig.guideConfig 引导配置对象
* @param {number} [guideConfig.startFromStep] 从指定步骤开始(可选,默认从保存的进度继续)
*/
START_GUIDE(state, { guideConfig, startFromStep }) {
state.currentGuide = guideConfig
// 如果指定了起始步骤,从指定步骤开始;否则恢复之前保存的进度
state.currentStep = startFromStep !== undefined ? startFromStep : (getGuideCurrentStep(guideConfig.key) || 0)
state.isActive = true
state.isNavigating = false
},
/**
* 下一步
*/
NEXT_STEP(state) {
if (state.currentGuide && state.currentStep < state.currentGuide.steps.length - 1) {
state.currentStep++
// 存储当前步骤
setGuideCurrentStep(state.currentGuide.key, state.currentStep)
}
},
/**
* 上一步
*/
PREV_STEP(state) {
if (state.currentStep > 0) {
state.currentStep--
}
},
/**
* 结束引导
*/
END_GUIDE(state) {
// 标记引导已显示
if (state.currentGuide) {
const key = state.currentGuide.key
markGuideAsShown(key)
markGuideDone(key) // 标记为已完成
// 重置步骤进度
setGuideCurrentStep(key, 0)
// 清除子步骤完成记录
clearSubStepProgress(key)
}
state.currentGuide = null
state.currentStep = 0
state.isActive = false
state.isNavigating = false
},
/**
* 标记当前子步骤为已完成
*/
COMPLETE_SUBSTEP(state) {
if (state.currentGuide) {
const key = state.currentGuide.key
const stepIndex = state.currentStep
// 标记子步骤完成
completeSubStep(key, stepIndex)
// 更新存储的当前步骤
setGuideCurrentStep(key, stepIndex)
}
},
/**
* 设置目标元素位置
*/
SET_TARGET_RECT(state, rect) {
state.targetRect = rect
},
/**
* 设置跳转状态
*/
SET_NAVIGATING(state, isNavigating) {
state.isNavigating = isNavigating
},
/**
* 更新当前步骤(用于页面跳转后重新定位)
*/
UPDATE_STEP(state, step) {
state.currentStep = step
},
/**
* 打开组件
*/
OPEN_COMPONENT(state, componentName) {
state.pendingAction = { type: 'component', name: componentName }
},
/**
* 执行函数
*/
EXECUTE_FUNCTION(state, handler) {
state.pendingAction = { type: 'function', handler }
},
/**
* 清除待执行行动
*/
CLEAR_PENDING_ACTION(state) {
state.pendingAction = null
},
/**
* 关闭遮罩进入组件模式
*/
SET_COMPONENT_MODE(state) {
state.componentMode = true
},
/**
* 重置组件模式
*/
RESET_COMPONENT_MODE(state) {
state.componentMode = false
}
}
const getters = {
// 获取当前步骤配置
currentStepConfig: (state) => {
if (!state.currentGuide || !state.currentGuide.steps) return null
return state.currentGuide.steps[state.currentStep]
},
// 是否还有下一步
hasNext: (state) => {
if (!state.currentGuide) return false
return state.currentStep < state.currentGuide.steps.length - 1
},
// 是否是第一步
isFirst: (state) => {
return state.currentStep === 0
},
// 是否是最后一步
isLast: (state) => {
if (!state.currentGuide) return false
return state.currentStep === state.currentGuide.steps.length - 1
}
}
const actions = {
/**
* 初始化引导
* @param {string} guideKey 引导key
*/
initGuide({ commit }, guideKey) {
console.log(`[Guide] 开始初始化引导: ${guideKey}, shouldShowGuide: ${shouldShowGuide(guideKey)}`)
// 检查是否需要显示引导
if (!shouldShowGuide(guideKey)) {
console.log(`[Guide] 引导已显示过,跳过: ${guideKey}`)
return false
}
const guide = getGuideConfig(guideKey)
if (!guide) {
console.warn(`[Guide] 未找到引导配置: ${guideKey}`)
return false
}
console.log(`[Guide] 开始显示引导: ${guideKey}, 步骤数: ${guide.steps.length}`)
commit('START_GUIDE', { guideConfig: guide })
return true
},
/**
* 从头开始引导(清除所有进度)
* @param {string} guideKey 引导key
*/
startGuideFromBeginning({ commit }, guideKey) {
const guide = getGuideConfig(guideKey)
if (!guide) {
console.warn(`[Guide] 未找到引导配置: ${guideKey}`)
return false
}
// 清除子步骤进度
clearSubStepProgress(guideKey)
// 从步骤0开始
commit('START_GUIDE', { guideConfig: guide, startFromStep: 0 })
return true
},
/**
* 从断点继续引导
* @param {string} guideKey 引导key
*/
resumeGuide({ commit }, guideKey) {
const guide = getGuideConfig(guideKey)
if (!guide) {
console.warn(`[Guide] 未找到引导配置: ${guideKey}`)
return false
}
// 获取第一个未完成的步骤
const resumeStep = getNextIncompleteStep(guideKey)
commit('START_GUIDE', { guideConfig: guide, startFromStep: resumeStep })
return true
},
/**
* 计算目标元素位置
* @param {string} selector 元素选择器
*/
calculateTargetPosition({ commit }, selector) {
return new Promise((resolve, reject) => {
if (!selector) {
console.log(`[Guide] selector 为空`)
resolve(null)
return
}
console.log(`[Guide] 开始查询元素: ${selector}`)
// 获取系统信息用于 px 转 rpx
const systemInfo = uni.getSystemInfoSync()
const rpxRatio = 750 / systemInfo.windowWidth
// 不传 in() 参数,使用默认页面实例
const query = uni.createSelectorQuery()
query.select(selector).boundingClientRect((rect) => {
console.log(`[Guide] 元素查询结果:`, rect)
if (rect) {
// 将 px 转换为 rpx
commit('SET_TARGET_RECT', {
top: rect.top * rpxRatio,
left: rect.left * rpxRatio,
width: rect.width * rpxRatio,
height: rect.height * rpxRatio
})
console.log(`[Guide] 转换后的 rpx 坐标:`, {
top: rect.top * rpxRatio,
left: rect.left * rpxRatio,
width: rect.width * rpxRatio,
height: rect.height * rpxRatio
})
resolve(rect)
} else {
console.warn(`[Guide] 未找到目标元素: ${selector}`)
resolve(null)
}
}).exec()
})
},
/**
* 下一步
*/
nextStep({ commit, getters, state }) {
const stepConfig = getters.currentStepConfig
// 检查 action 配置
if (stepConfig && stepConfig.action) {
const { action } = stepConfig
if (action.type === 'navigate' && action.url) {
// 如果 isNavigating 已为 true说明是由 handleHighlightClick 发起的导航
// 只需要完成步骤即可,不需要再次发起导航
if (state.isNavigating) {
commit('COMPLETE_SUBSTEP')
if (getters.hasNext) {
commit('NEXT_STEP')
} else {
commit('END_GUIDE')
}
return
}
commit('SET_NAVIGATING', true)
uni.navigateTo({
url: action.url,
success: () => {
commit('COMPLETE_SUBSTEP')
commit('SET_NAVIGATING', false)
},
fail: (err) => {
console.error('[Guide] 页面跳转失败:', err)
commit('SET_NAVIGATING', false)
}
})
return
} else if (action.type === 'component' && action.name) {
store.dispatch('guide/openComponent', action.name)
commit('COMPLETE_SUBSTEP')
if (getters.hasNext) {
commit('NEXT_STEP')
} else {
commit('END_GUIDE')
}
return
} else if (action.type === 'function' && action.handler) {
store.dispatch('guide/executeFunction', action.handler)
commit('COMPLETE_SUBSTEP')
if (getters.hasNext) {
commit('NEXT_STEP')
} else {
commit('END_GUIDE')
}
return
}
}
// 检查是否正在跳转中(由 handleHighlightClick 设置)
// 只有在非 action 流程中才检查此标志
if (state.isNavigating) {
commit('COMPLETE_SUBSTEP')
if (getters.hasNext) {
commit('NEXT_STEP')
} else {
commit('END_GUIDE')
}
return
}
if (stepConfig && stepConfig.nextPage) {
// 需要跳转页面
commit('SET_NAVIGATING', true)
uni.navigateTo({
url: stepConfig.nextPage,
success: () => {
// 跳转成功后不立即关闭遮罩,等待目标页面 onReady 后重新定位
},
fail: (err) => {
console.error('[Guide] 页面跳转失败:', err)
commit('SET_NAVIGATING', false)
}
})
} else if (getters.hasNext) {
// 先标记当前步骤完成,再前进
commit('COMPLETE_SUBSTEP')
commit('NEXT_STEP')
} else {
// 最后一步:标记完成并结束
commit('COMPLETE_SUBSTEP')
commit('END_GUIDE')
}
},
/**
* 上一步
*/
prevStep({ commit, getters }) {
if (!getters.isFirst) {
commit('PREV_STEP')
}
},
/**
* 跳过引导
*/
skipGuide({ commit }) {
commit('END_GUIDE')
},
/**
* 页面跳转后重新定位引导
*/
relocateOnPageReady({ commit, state, dispatch }) {
if (!state.isNavigating) return
// 延迟执行,确保 DOM 已渲染
setTimeout(() => {
const stepConfig = state.currentGuide?.steps[state.currentStep]
if (stepConfig && stepConfig.target) {
dispatch('calculateTargetPosition', stepConfig.target).then(() => {
commit('SET_NAVIGATING', false)
})
}
}, 100)
},
/**
* 打开组件
*/
openComponent({ commit }, componentName) {
console.log(`[Guide] 打开组件: ${componentName}`)
uni.$emit('guide:openComponent', componentName)
commit('OPEN_COMPONENT', componentName)
},
/**
* 执行函数
*/
executeFunction({ commit }, handler) {
console.log(`[Guide] 执行函数: ${handler}`)
commit('EXECUTE_FUNCTION', handler)
},
/**
* 清除待执行行动
*/
clearPendingAction({ commit }) {
commit('CLEAR_PENDING_ACTION')
}
}
export default {
namespaced: true,
state,
mutations,
getters,
actions
}