/** * 引导配置文件 * 定义所有引导步骤的配置数据 * * 调试模式: * - 开启调试:访问 pages/square/square?guide_debug=1 * - 关闭调试:访问 pages/square/square?guide_debug=0(会清除调试状态和 is_new_user,恢复正式流程) */ /** * 引导配置结构 * { * key: 'guide_key', // 引导唯一标识 * name: '引导名称', * desc: '引导描述', * reward: { exp: 10, diamond: 5 }, // 奖励配置 * steps: [ * { * target: '.selector', // 目标元素选择器 * content: '提示文案', // 引导提示内容 * position: 'bottom', // 气泡箭头位置: top/bottom/left/right * offset: { x: 0, y: 0 }, // 偏移量 * buttons: ['next'], // 显示的按钮: prev/next/skip * nextPage: 'path', // 点击后跳转的页面(可选) * mask: true/false, // 是否显示遮罩层(默认 true) * // 高亮区域坐标(可选,写死坐标时使用,优先级高于 target) * highlightRect: { top: 100, left: 200, width: 112, height: 126 }, // 高亮区域坐标(rpx) * // 气泡位置控制 * center: true/false, // 气泡是否居中显示(默认 true) * customPosition: { x: 100, y: 200 }, // 气泡自定义坐标(center 为 false 时使用) * // 图片配置 * image: '/static/guide/image.png', // 背景图片路径 * character: 'character1', // 角色图片:character1/character2/character3/character4 * characterPosition: { x: 100, y: 200 }, // 角色图片自定义坐标(优先于偏移量) * characterOffsetX: 300, // 角色图片相对气泡的X轴偏移(默认300) * characterOffsetY: -100, // 角色图片相对气泡的Y轴偏移(默认-100) * characterSize: 300, // 角色图片尺寸(单位 rpx) * dialogType: 'long', // 气泡背景图片:long(长对话框)/square(方对话框) * imageCenter: true/false, // 图片是否居中(默认 true) * imagePosition: { x: 50, y: 100 }, // 图片自定义坐标(imageCenter 为 false 时使用) * highlightKeywords: ['关键词1', '关键词2'], // 高亮关键词数组,匹配到的词用 #FFA500 颜色显示 * tooltipHeight: 300, // 气泡高度(单位 rpx,默认300) * // 高亮区域点击动作(可选) * action: { // 点击高亮区域时触发的动作 * type: 'navigate', // navigate: 路由跳转 | component: 打开组件 | function: 执行函数 * url: '/pages/xxx', // navigate 类型必填 * name: 'xxx', // component 类型必填 * handler: 'xxx' // function 类型必填 * } * } * ] * } */ export const guideConfig = { // 广场页面引导 square_home: { key: 'square_home', name: '广场首页', desc: '了解轮播图和导航功能', reward: { exp: 10, diamond: 5 }, steps: [ { page: '/pages/square/square', mask: false, target: '.banner-carousel', content: '你的「TOPFANS馆」\n已经准备好啦', highlightKeywords: ['「TOPFANS馆」'], tooltipHeight: 416, center: true, customPosition: { x: 12 * 32, y: 25 * 32 }, character: 'character3', characterPosition: { x: 416, y: 1344 }, characterSize: 896, dialogType: 'square', image: '/static/components/cabin1.png', }, { page: '/pages/square/square', mask: true, target: '.cabin-nickname-mine', highlightRect: { top: 19.5*32, left: 7.25*32, width: 9*32, height: 9*32 }, content: '这就回去铸造你的热爱吧', tooltipHeight: 224, center: false, customPosition: { x: 12 * 32, y: 15 * 32 }, character: 'character2', characterPosition: { x: 96, y: 1280 }, characterSize: 320, dialogType: 'long', // buttons: ['next'], action: { type: 'navigate', url: '/pages/exhibition/exhibition' }, }, { page: '/pages/exhibition/exhibition', mask: true, target: '.nft-cards-container-2', content: '咱们的「展厅」空空如也呢\n我带你第一次「铸爱」吧', highlightKeywords: ['「铸爱」'], tooltipHeight: 224, center: false, customPosition: { x: 12 * 32, y: 7 * 32 }, character: 'character5', characterPosition: { x: 96, y: 16 * 32 }, characterSize: 320, dialogType: 'long', // buttons: ['next'], action: { type: 'navigate', url: '/pages/square/square' }, }, { page: '/pages/square/square', mask: true, target: '.banner-carousel', content: '这是榜单', tooltipHeight: 224, center: false, customPosition: { x: 12 * 32, y: 25 * 32 }, character: 'character2', characterPosition: { x: 96, y: 1280 }, characterSize: 320, dialogType: 'long', // buttons: ['next'], action: { type: 'component', name: 'RankingModal' } }, { mask: true, target: '.bottom-nav-container', content: '点击中间的小怪兽展开导航,可以切换到不同功能页面', center: false, customPosition: { x: 12 * 32, y: 25 * 32 }, character: 'character2', characterPosition: { x: 96, y: 1280 }, characterSize: 320, dialogType: 'long', buttons: ['next'], page: '/pages/square/square', } ] }, // 个人资料引导 profile_edit: { key: 'profile_edit', name: '个人资料', desc: '完善头像和昵称', reward: { exp: 10, diamond: 5 }, steps: [ { page: '/pages/profile/profile', mask: true, target: '.avatar-container', content: '点击头像可以更换头像', center: true, customPosition: { x: 0, y: 0 }, image: '', imageCenter: true, imagePosition: { x: 0, y: 0 }, buttons: ['next'] }, { page: '/pages/profile/profile', mask: true, target: '.user-name', content: '点击这里可以修改昵称', center: true, customPosition: { x: 0, y: 0 }, image: '', imageCenter: true, imagePosition: { x: 0, y: 0 }, buttons: ['next', 'skip'] } ] }, // 星册添加引导 starbook_add: { key: 'starbook_add', name: '星册添加', desc: '在星册中添加图片', reward: { exp: 10, diamond: 5 }, steps: [ { page: '/pages/square/square', mask: true, target: '.nft-border', content: '点击此处可以添加图片到星册', center: true, customPosition: { x: 0, y: 0 }, image: '', imageCenter: true, imagePosition: { x: 0, y: 0 }, buttons: ['next'] } ] }, // 展馆添加引导 exhibition_add: { key: 'exhibition_add', name: '展馆添加', desc: '在展馆中添加图片', reward: { exp: 10, diamond: 5 }, steps: [ { page: '/pages/exhibition/exhibition', mask: true, target: '.nft-border', content: '点击此处可以添加图片到展馆', center: true, customPosition: { x: 0, y: 0 }, image: '', imageCenter: true, imagePosition: { x: 0, y: 0 }, buttons: ['next'] } ] }, // 展馆操作引导 exhibition_operate: { key: 'exhibition_operate', name: '展馆操作', desc: '学习展馆图片操作', reward: { exp: 10, diamond: 5 }, steps: [ { page: '/pages/exhibition/exhibition', mask: true, target: '.clickable', content: '点击此处可以打开操作菜单', center: true, customPosition: { x: 0, y: 0 }, image: '', imageCenter: true, imagePosition: { x: 0, y: 0 }, buttons: ['next'] } ] } } /** * 获取引导配置 * @param {string} key 引导key * @returns {Object|null} */ export function getGuideConfig(key) { return guideConfig[key] || null } /** * 获取所有引导key列表 * @returns {string[]} */ export function getAllGuideKeys() { return Object.keys(guideConfig) } /** * 检查是否需要显示某个引导 * @param {string} key 引导key * @returns {boolean} */ export function shouldShowGuide(key) { // 优先检查已存储的调试模式(由页面 onLoad 主动设置) if (uni.getStorageSync('guide_debug_mode')) { return true } // Fallback: 检查 URL 参数控制调试模式 const pages = getCurrentPages() const currentPage = pages[pages.length - 1] const options = currentPage?.options || {} if ('guide_debug' in options) { const debugValue = options.guide_debug const isDebug = debugValue === '1' || debugValue === 'true' if (isDebug) { // 开启调试模式 uni.setStorageSync('guide_debug_mode', true) } else { // 关闭调试模式 uni.setStorageSync('guide_debug_mode', false) uni.removeStorageSync('is_new_user') // 清除所有引导记录 const keys = getAllGuideKeys() keys.forEach(k => uni.removeStorageSync(`guide_shown_${k}`)) } } // 调试模式:总是显示引导 if (uni.getStorageSync('guide_debug_mode')) { return true } // 只有新用户才能看到引导 const isNewUser = uni.getStorageSync('is_new_user') if (!isNewUser) { console.log(`[Guide] 不是新用户,跳过引导: ${key}`) return false } const storageKey = `guide_shown_${key}` return !uni.getStorageSync(storageKey) } /** * 标记引导已显示 * @param {string} key 引导key */ export function markGuideAsShown(key) { const storageKey = `guide_shown_${key}` uni.setStorageSync(storageKey, true) } /** * 重置引导(用于调试) * @param {string} key 引导key */ export function resetGuide(key) { const storageKey = `guide_shown_${key}` uni.removeStorageSync(storageKey) } /** * 重置所有引导(用于调试) */ export function resetAllGuides() { const keys = getAllGuideKeys() keys.forEach(key => { const storageKey = `guide_shown_${key}` uni.removeStorageSync(storageKey) uni.removeStorageSync(`guide_done_${key}`) uni.removeStorageSync(`guide_step_${key}`) }) uni.removeStorageSync('guide_rewards_claimed') } // ==================== 新增:状态管理函数 ==================== /** * 检查引导是否已完成 * @param {string} key 引导key * @returns {boolean} */ export function isGuideDone(key) { return uni.getStorageSync(`guide_done_${key}`) === true } /** * 检查引导是否已完成并领取奖励 * @param {string} key 引导key * @returns {boolean} */ export function isGuideRewardClaimed(key) { const claimed = uni.getStorageSync('guide_rewards_claimed') || [] return claimed.includes(key) } /** * 检查是否有可领取的奖励 * @returns {boolean} */ export function hasClaimableReward() { const keys = getAllGuideKeys() return keys.some(key => isGuideDone(key) && !isGuideRewardClaimed(key)) } /** * 获取未完成的引导数量 * @returns {number} */ export function getUndoneGuideCount() { const keys = getAllGuideKeys() return keys.filter(key => !isGuideDone(key)).length } /** * 获取已完成引导数量 * @returns {number} */ export function getDoneGuideCount() { const keys = getAllGuideKeys() return keys.filter(key => isGuideDone(key)).length } /** * 获取可领取奖励的引导数量 * @returns {number} */ export function getClaimableRewardCount() { const keys = getAllGuideKeys() return keys.filter(key => isGuideDone(key) && !isGuideRewardClaimed(key)).length } /** * 标记引导已完成 * @param {string} key 引导key */ export function markGuideDone(key) { uni.setStorageSync(`guide_done_${key}`, true) } /** * 领取引导奖励 * @param {string} key 引导key * @returns {Object|null} 奖励内容 */ export function claimGuideReward(key) { const config = getGuideConfig(key) if (!config || !config.reward) return null // 检查是否已领取 if (isGuideRewardClaimed(key)) { console.log(`[Guide] 奖励已领取: ${key}`) return null } // 检查是否已完成 if (!isGuideDone(key)) { console.log(`[Guide] 引导未完成,无法领取: ${key}`) return null } // 标记已领取 const claimed = uni.getStorageSync('guide_rewards_claimed') || [] claimed.push(key) uni.setStorageSync('guide_rewards_claimed', claimed) // 发放奖励(需要调用后端接口,这里先本地记录) console.log(`[Guide] 领取奖励: ${key}`, config.reward) return config.reward } /** * 检查是否需要显示引导开始弹窗 * @returns {boolean} */ export function shouldShowGuideStartModal() { // 优先检查已存储的调试模式(由页面 onLoad 主动设置) if (uni.getStorageSync('guide_debug_mode')) { return true } // Fallback: 检查 URL 参数 const pages = getCurrentPages() const currentPage = pages[pages.length - 1] const options = currentPage?.options || {} if ('guide_debug' in options) { const isDebug = options.guide_debug === '1' || options.guide_debug === 'true' if (isDebug) { uni.setStorageSync('guide_debug_mode', true) uni.setStorageSync('is_new_user', true) } } // 再次检查存储(URL 参数设置的可能还没生效) if (uni.getStorageSync('guide_debug_mode')) { return true } // 检查是否已经关闭过弹窗 const guideFirstShow = uni.getStorageSync('guide_first_show') if (guideFirstShow === false) { return false } // 调试模式或新用户:显示弹窗 if (uni.getStorageSync('guide_debug_mode')) { return true } // 检查是否需要显示弹窗 const isNewUser = uni.getStorageSync('is_new_user') return isNewUser && guideFirstShow !== false } /** * 关闭引导开始弹窗(不再显示) */ export function closeGuideStartModal() { uni.setStorageSync('guide_first_show', false) } /** * 获取所有引导的状态列表 * @returns {Array} */ export function getGuideStatusList() { const keys = getAllGuideKeys() return keys.map(key => { const config = getGuideConfig(key) const done = isGuideDone(key) const claimed = isGuideRewardClaimed(key) return { key, name: config?.name || key, desc: config?.desc || '', reward: config?.reward || null, done, claimed, canClaim: done && !claimed } }) } // ==================== 步骤进度存储 ==================== /** * 获取引导的当前步骤 * @param {string} key 引导key * @returns {number} 当前步骤索引,默认0 */ export function getGuideCurrentStep(key) { return uni.getStorageSync(`guide_step_${key}`) || 0 } /** * 设置引导的当前步骤 * @param {string} key 引导key * @param {number} step 步骤索引 */ export function setGuideCurrentStep(key, step) { uni.setStorageSync(`guide_step_${key}`, step) } /** * 获取下一个未完成的引导key * @returns {string|null} 引导key或null */ export function getNextUndoneGuideKey() { const keys = getAllGuideKeys() return keys.find(key => !isGuideDone(key)) || null } // ==================== 子步骤完成追踪 ==================== /** * 标记指定子步骤为已完成 * @param {string} key 引导key * @param {number} stepIndex 步骤索引 */ export function completeSubStep(key, stepIndex) { const completed = uni.getStorageSync(`guide_completed_steps_${key}`) || [] if (!completed.includes(stepIndex)) { completed.push(stepIndex) completed.sort((a, b) => a - b) uni.setStorageSync(`guide_completed_steps_${key}`, completed) } } /** * 获取已完成的子步骤索引数组 * @param {string} key 引导key * @returns {number[]} */ export function getCompletedSteps(key) { return uni.getStorageSync(`guide_completed_steps_${key}`) || [] } /** * 检查指定子步骤是否已完成 * @param {string} key 引导key * @param {number} stepIndex 步骤索引 * @returns {boolean} */ export function isSubStepCompleted(key, stepIndex) { const completed = getCompletedSteps(key) return completed.includes(stepIndex) } /** * 获取第一个未完成的子步骤索引 * @param {string} key 引导key * @returns {number} 未完成步骤索引,如果全部完成则返回0 */ export function getNextIncompleteStep(key) { const completed = getCompletedSteps(key) const config = getGuideConfig(key) const totalSteps = config?.steps?.length || 0 for (let i = 0; i < totalSteps; i++) { if (!completed.includes(i)) { return i } } return 0 } /** * 获取指定步骤所在的页面路径 * @param {string} key 引导key * @param {number} stepIndex 步骤索引 * @returns {string|null} 页面路径,如果步骤不存在返回null */ export function getStepPage(key, stepIndex) { const config = getGuideConfig(key) if (!config || !config.steps || !config.steps[stepIndex]) { return null } return config.steps[stepIndex].page || null } /** * 检查引导是否有进行中的进度 * @param {string} key 引导key * @returns {boolean} */ export function hasGuideProgress(key) { const completed = getCompletedSteps(key) return completed.length > 0 } /** * 清除引导的子步骤进度 * @param {string} key 引导key */ export function clearSubStepProgress(key) { uni.removeStorageSync(`guide_completed_steps_${key}`) } /** * 获取引导的步骤进度信息 * @param {string} key 引导key * @returns {{ completed: number, total: number, percentage: number }} */ export function getStepProgress(key) { const completed = getCompletedSteps(key) const config = getGuideConfig(key) const total = config?.steps?.length || 0 return { completed: completed.length, total, percentage: total > 0 ? Math.round((completed.length / total) * 100) : 0 } }