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