topfans/frontend/utils/guideConfig.js
2026-04-07 23:08:49 +08:00

652 lines
18 KiB
JavaScript
Raw 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.

/**
* 引导配置文件
* 定义所有引导步骤的配置数据
*
* 调试模式:
* - 开启调试:访问 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
}
}