Files

659 lines
21 KiB
Vue
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.
<script>
import { getToken, setToken } from '@/utils/auth'
import { login } from './api'
import { FA_SOLID_DATA, FA_BRANDS_DATA, FA_SOLID_FAMILY, FA_BRANDS_FAMILY } from '@/static/utils/fontData'
// 1. 新增:导入用户配置接口
import { getUserConfig, editUserConfig } from './api'
// 1. 引入主题工具方法 + 预设常量(补充缺失的导入)
import {
getThemeFromServer,
switchTheme,
injectTheme,
THEME_PRESETS,
DEFAULT_THEME_KEY,
THEME_CHANGE_EVENT // 补充导入事件常量,统一事件名
} from './utils/theme'
// 引入全局主题适配器(导航栏、TabBar等)
import {
initGlobalTheme,
registerGlobalThemeListener,
applyGlobalTheme,
applyNavigationBarTheme
} from './utils/themeGlobal'
// 激励机制事件常量
const GROWTH_UPDATE_EVENT = 'growthUpdate'
function hexToRgb(hex, fallback = '198,142,63') {
if (!hex || hex[0] !== '#') return fallback
let h = hex.slice(1)
if (h.length === 3) h = h.split('').map(c => c + c).join('')
const n = parseInt(h, 16)
return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`
}
export default {
// 初始化全局数据(小程序全局状态核心)
globalData: {
themeKey: DEFAULT_THEME_KEY,
themeConfig: THEME_PRESETS[DEFAULT_THEME_KEY],
token: '',
lastToken: '',
fontReady: false
},
data() {
return {
themeConfig: THEME_PRESETS[DEFAULT_THEME_KEY]
}
},
computed: {
// 将主题色注入为全局 CSS 变量,供 .btn 等统一按钮类使用
rootVars() {
const c = this.themeConfig || {}
const pRGB = hexToRgb(c.primaryColor)
const dRGB = hexToRgb(c.dangerColor || '#F56C6C')
const sRGB = hexToRgb(c.successColor || '#67C23A')
const isDark = c.isDark || false
const secondaryBg = isDark ? 'rgba(255,255,255,0.16)' : 'rgba(0,0,0,0.06)'
const secondaryText = isDark ? 'rgba(255,255,255,0.9)' : '#333'
return {
'--primaryColor': c.primaryColor,
'--primaryColorRGB': pRGB,
'--secondaryColor': c.secondaryColor,
'--dangerColor': c.dangerColor || '#F56C6C',
'--dangerColorRGB': dRGB,
'--successColor': c.successColor || '#67C23A',
'--successColorRGB': sRGB,
'--warningColor': c.warningColor || '#E6A23C',
'--incomeColor': c.incomeColor || '#67C23A',
'--expendColor': c.expendColor || '#F56C6C',
'--textPrimary': c.textPrimary,
'--textSecondary': c.textSecondary,
'--textTertiary': c.textTertiary || c.textDisabled || '#909399',
'--textDisabled': c.textDisabled || '#C0C4CC',
'--bgColor': c.bgColor,
'--cardBgColor': c.cardBgColor,
'--borderColor': c.borderColor,
'--borderLight': c.borderLight,
'--inputBgColor': c.inputBgColor || c.cardBgColor,
'--gradientSoft': c.gradientSoft,
'--shadowColor': c.shadowColor,
'--btn-secondary-bg': secondaryBg,
'--btn-secondary-text': secondaryText
}
}
},
/**
* 小程序启动入口(优先初始化主题,再处理登录)
*/
onLaunch: async function() {
try {
await this.loadFonts()
console.log('✅ App启动 - 字体加载完成')
// ========== 第一步:优先初始化主题(确保样式先加载,避免闪屏) ==========
await this.initTheme()
this.themeConfig = this.globalData.themeConfig
// 注册全局主题变更监听:同步 CSS 变量驱动的统一按钮样式
uni.$on(THEME_CHANGE_EVENT, (payload) => {
const cfg = (payload && payload.config) || this.globalData.themeConfig || THEME_PRESETS[DEFAULT_THEME_KEY]
this.themeConfig = cfg
})
console.log('✅ App启动 - 主题初始化完成')
// ========== 第二步:注册激励机制全局事件监听 ==========
this.registerGrowthListener()
console.log(' 激励机制事件监听注册完成')
// ========== 第三步:处理登录逻辑(原有逻辑加固) ==========
this.globalData.token = getToken() // 同步token到全局
this.globalData.lastToken = this.globalData.token // 初始化lastToken,用于检测登录状态变化
const token = this.globalData.token
// 已登录 → 先同步配置再跳首页
if (token) {
console.log('✅ 已登录,同步用户配置')
await this.syncUserConfig()
console.log('✅ 跳转到首页')
uni.switchTab({
url: '/pages/index/index',
fail: (err) => {
console.error('跳转到首页失败:', err)
// 兜底:如果tabBar跳转失败,直接打开首页
uni.redirectTo({ url: '/pages/index/index' })
}
})
return
}
// 未登录 → 执行自动登录(保留注释,逻辑加固)
// await this.handleAutoLogin()
} catch (err) {
console.error('❌ App启动流程异常:', err)
// 终极兜底:无论启动失败原因,确保进入基础页面
uni.redirectTo({ url: '/pages/login/login' })
}
},
/**
* 小程序切前台时触发(兜底更新主题,防止样式不一致)
*/
onShow: function() {
// 重新应用全局主题到导航栏(确保切回前台时样式一致)
if (this.globalData.themeConfig) {
applyNavigationBarTheme(this.globalData.themeConfig)
}
// 检测登录状态变化(token从无到有)
const currentToken = getToken()
if (currentToken && currentToken !== this.globalData.lastToken) {
console.log('【App】检测到登录状态变化,同步用户配置')
this.syncUserConfig()
this.globalData.lastToken = currentToken
}
// 触发全局主题事件,通知所有页面更新
uni.$emit(THEME_CHANGE_EVENT, {
key: this.globalData.themeKey,
config: this.globalData.themeConfig
})
console.log('✅ 小程序切前台 - 触发主题同步')
},
/**
* 方法集合(主题 + 登录)
*/
methods: {
/**
* 加载 Font Awesome 字体(微信小程序专用 wx.loadFontFace API
* 注意:wx.loadFontFace 的 source 参数必须为 url("...") 格式,
* 且仅支持 https 网络资源或 base64 data URI,不支持本地静态路径。
*/
loadFonts() {
return new Promise((resolve) => {
const fontConfigs = [
{ family: FA_SOLID_FAMILY, data: FA_SOLID_DATA },
{ family: FA_BRANDS_FAMILY, data: FA_BRANDS_DATA }
]
let loaded = 0
fontConfigs.forEach((config) => {
uni.loadFontFace({
family: config.family,
source: `url("${config.data}")`,
global: true,
success: () => {
loaded++
console.log(`✅ 字体加载成功 [${config.family}] via base64`)
if (loaded === fontConfigs.length) {
this.globalData.fontReady = true
uni.$emit('fontReady')
resolve()
}
},
fail: (err) => {
console.warn(`字体加载失败 [${config.family}]:`, err.errMsg || err)
loaded++
if (loaded === fontConfigs.length) {
this.globalData.fontReady = loaded > 0
uni.$emit('fontReady')
resolve()
}
}
})
})
setTimeout(() => {
if (loaded < fontConfigs.length) {
console.warn('字体加载超时,部分字体可能不可用')
}
this.globalData.fontReady = loaded > 0
uni.$emit('fontReady')
resolve()
}, 10000)
})
},
/**
* 初始化主题(兼容后端/本地/默认,强容错)
*/
async initTheme() {
try {
// 1. 从后端/本地获取合法的主题key
const themeKey = await getThemeFromServer()
// 2. 注入全局(更新storage + 全局事件 + globalData
injectTheme(themeKey)
// 3. 强制同步globalData(小程序getApp()依赖此数据)
this.globalData.themeKey = themeKey
this.globalData.themeConfig = THEME_PRESETS[themeKey] || THEME_PRESETS[DEFAULT_THEME_KEY]
// 4. 应用全局主题到导航栏和TabBar
applyGlobalTheme(this.globalData.themeConfig)
// 5. 注册全局主题变更监听
registerGlobalThemeListener()
console.log(`【主题】初始化完成,当前主题:${this.globalData.themeKey}`)
} catch (error) {
console.error('【主题】初始化失败,使用默认主题:', error)
// 兜底:强制注入默认主题
injectTheme(DEFAULT_THEME_KEY)
this.globalData.themeKey = DEFAULT_THEME_KEY
this.globalData.themeConfig = THEME_PRESETS[DEFAULT_THEME_KEY]
applyGlobalTheme(this.globalData.themeConfig)
registerGlobalThemeListener()
}
},
/**
* 全局切换主题(供任意页面调用)
* @param {string} themeKey 目标主题key
*/
async updateTheme(themeKey, showToast = true) {
if (!themeKey || themeKey === this.globalData.themeKey) return
try {
// 调用核心切换方法(后端+本地+全局事件)
await switchTheme(themeKey, showToast)
// 同步更新globalData(关键:保证getApp()获取最新值)
this.globalData.themeKey = themeKey
this.globalData.themeConfig = THEME_PRESETS[themeKey] || THEME_PRESETS[DEFAULT_THEME_KEY]
// 应用全局主题到导航栏和TabBar
applyGlobalTheme(this.globalData.themeConfig)
console.log(`【主题】全局切换完成:${themeKey}`)
} catch (error) {
console.error(`【主题】切换失败:${themeKey}`, error)
// 兜底:本地强制切换
injectTheme(themeKey)
this.globalData.themeKey = themeKey
this.globalData.themeConfig = THEME_PRESETS[themeKey] || THEME_PRESETS[DEFAULT_THEME_KEY]
applyGlobalTheme(this.globalData.themeConfig)
}
},
/**
* 自动登录逻辑(抽离为独立方法,便于维护)
*/
async handleAutoLogin() {
try {
const res = await uni.login()
if (!res.code) {
throw new Error('获取微信code失败')
}
const tokenData = await login(res.code)
if (tokenData?.token) {
setToken(tokenData.token)
this.globalData.token = tokenData.token // 同步到全局
console.log('✅ 自动登录成功')
await this.syncUserConfig()
console.log('✅ 跳转到首页')
uni.switchTab({
url: '/pages/index/index',
fail: () => uni.redirectTo({ url: '/pages/index/index' })
})
} else {
console.log('❌ 自动登录失败 - 未获取到token')
uni.navigateTo({ url: '/pages/login/login' })
}
} catch (err) {
console.log('❌ 自动登录失败', err)
uni.navigateTo({ url: '/pages/login/login' })
}
},
/**
* 同步用户配置(登录成功后调用,从后端获取主题等配置)
*/
async syncUserConfig() {
try {
const res = await getUserConfig()
if (res?.theme) {
await this.updateTheme(res.theme, false)
}
console.log('✅ 用户配置同步完成')
} catch (error) {
console.warn('❌ 用户配置同步失败:', error)
}
},
/**
* 注册激励机制全局事件监听
*/
registerGrowthListener() {
console.log('【激励机制】开始注册全局事件监听:', GROWTH_UPDATE_EVENT)
uni.$on(GROWTH_UPDATE_EVENT, (growth) => {
console.log('【激励机制】✅ 收到全局事件触发,growth数据:', JSON.stringify(growth))
this.handleGrowthIncentives(growth)
})
console.log('【激励机制】✅ 全局事件监听注册成功')
},
/**
* 处理成长激励(升级、成就、特权)
* @param {Object} growth 成长数据
*/
handleGrowthIncentives(growth) {
console.log('【激励机制】进入 handleGrowthIncentivesgrowth:', growth)
if (!growth) {
console.log('【激励机制】❌ growth为空,直接返回')
return
}
console.log('【激励机制】✅ growth数据有效')
console.log('【激励机制】levelUp:', growth.levelUp, 'newAchievements长度:', growth.newAchievements?.length || 0, 'newPrivileges长度:', growth.newPrivileges?.length || 0)
// 优先级:升级 > 成就 > 特权
// 1. 检测升级
if (growth.levelUp) {
console.log('【激励机制】✅ 检测到升级,levelUp:', growth.levelUp)
this.showLevelUpCelebration(growth)
return
}
// 2. 检测新成就
if (growth.newAchievements && growth.newAchievements.length > 0) {
console.log('【激励机制】✅ 检测到新成就,数量:', growth.newAchievements.length)
this.showAchievementCelebration(growth.newAchievements)
return
}
// 3. 检测新特权
// if (growth.newPrivileges && growth.newPrivileges.length > 0) {
// console.log('【激励机制】✅ 检测到新特权,数量:', growth.newPrivileges.length)
// this.showPrivilegeNotification(growth.newPrivileges)
// }
},
/**
* 显示升级庆祝弹窗
*/
showLevelUpCelebration(growth) {
console.log('【激励机制】进入 showLevelUpCelebration,准备显示升级弹窗')
const privilegesText = growth.newPrivileges && growth.newPrivileges.length
? '\n特权:' + growth.newPrivileges.join('、')
: ''
const content = `等级:Lv.${growth.oldLevel || growth.currentLevel - 1} → Lv.${growth.currentLevel}\n称号:${growth.levelTitle || '暂无'}\n获得经验:+${growth.expGained || 0} EXP${privilegesText}`
uni.showModal({
title: '🎉 恭喜升级!',
content: content,
showCancel: false,
confirmText: '太棒了!',
confirmColor: this.globalData.themeConfig?.primaryColor || '#6366F1',
success: () => {
console.log('【激励机制】升级弹窗已关闭')
}
})
console.log('【激励机制】✅ 升级弹窗已显示')
},
/**
* 显示成就获得弹窗(支持连续展示多个)
*/
showAchievementCelebration(achievements) {
if (!achievements || achievements.length === 0) return
console.log('【激励机制】进入 showAchievementCelebration,成就数量:', achievements.length)
const current = achievements[0]
const content = `${current.name || ''}\n${current.description || current.desc || ''}\n稀有度:${current.rarityName || current.rarity || '初级'}\n获得经验:+${current.expReward || current.exp || 0} EXP`
const remainingAchievements = achievements.length > 1 ? achievements.slice(1) : []
uni.showModal({
title: '🏆 获得成就!',
content: content,
showCancel: false,
confirmText: '太棒了!',
confirmColor: this.globalData.themeConfig?.primaryColor || '#6366F1',
success: () => {
console.log('【激励机制】成就弹窗已关闭')
if (remainingAchievements.length > 0) {
setTimeout(() => {
this.showAchievementCelebration(remainingAchievements)
}, 500)
}
}
})
console.log('【激励机制】✅ 成就弹窗已显示')
},
/**
* 显示特权解锁提示
*/
showPrivilegeNotification(privileges) {
const privilegeText = privileges.join('、')
uni.showToast({
title: `解锁新特权: ${privilegeText}`,
icon: 'success',
duration: 2500
})
}
}
}
</script>
<template>
<view class="app-root" :style="rootVars">
<slot></slot>
</view>
</template>
<style>
/* 字体加载:仅通过 JS 层 uni.loadFontFace() 加载(base64 data URI */
/* 微信小程序不支持 CSS @font-face 本地路径,已在 loadFonts() 中用 wx.loadFontFace 处理 */
/* ========== 主题过渡动画(小程序全兼容) ========== */
.theme-transition {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease,
opacity 0.25s ease,
transform 0.25s ease;
}
/* ========== 页面过渡动画关键帧 ========== */
@keyframes pageFadeIn {
0% { opacity: 0; transform: translateY(20rpx); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes cardEnter {
0% { opacity: 0; transform: translateY(16rpx); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* ========== 动画工具类 ========== */
.page-enter {
animation: pageFadeIn 0.3s ease-out both;
}
.card-enter {
animation: cardEnter 0.25s ease-out both;
}
.content-fade {
transition: opacity 0.25s ease;
}
.shimmer-bg {
background: linear-gradient(90deg,
rgba(0, 0, 0, 0.04) 25%,
rgba(0, 0, 0, 0.08) 50%,
rgba(0, 0, 0, 0.04) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.app-root {
min-height: 100vh;
}
/* ========== 全局基础样式(动态值由 :root CSS 变量决定) ========== */
page {
background: var(--bgColor, #F7F3ED);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 28rpx;
line-height: 1.6;
}
/* 卡片基础样式 */
.card {
background: var(--cardBgColor, #FFFFFF);
border-radius: 12rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
padding: 30rpx;
margin-bottom: 20rpx;
}
/* 旧版主按钮基础样式(保留兼容,优先使用 styles/buttons.scss 中的 .btn--primary */
.btn-primary {
background: var(--gradientSoft, linear-gradient(135deg, #6366F1 0%, #818CF8 100%));
color: #fff;
border-radius: 14rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 32rpx;
border: none;
}
/* 文本样式 - 跟随主题 */
.text-primary {
color: var(--textPrimary, #2c3e50);
}
.text-secondary {
color: var(--textSecondary, #7f8c8d);
}
.text-tertiary {
color: var(--textTertiary, #909399);
}
/* 边框样式 - 跟随主题 */
.border {
border: 1rpx solid var(--borderLight, #F2EDE6);
}
/* 金额颜色(收入/支出)- 跟随主题 */
.income-color {
color: var(--incomeColor, #67C23A);
}
.expend-color {
color: var(--expendColor, #F56C6C);
}
/* ========== 清除默认样式(小程序通用优化) ========== */
button {
padding: 0;
margin: 0;
}
button::after {
border: none;
}
view, text, image {
box-sizing: border-box;
}
/* ========== 全局点击交互动效(丝滑手感) ========== */
/* 通用按压反馈:轻微缩放 + 透明度 */
.tap-feedback {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.tap-feedback:active {
transform: scale(0.96);
opacity: 0.78;
}
/* 卡片按压反馈:轻微缩放 + 阴影减弱 */
.tap-card {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.tap-card:active {
transform: scale(0.98);
opacity: 0.92;
}
/* 涟漪波纹效果 */
.ripple {
position: relative;
overflow: hidden;
}
.ripple::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.32);
transform: translate(-50%, -50%);
transition: width 0.45s ease, height 0.45s ease, opacity 0.6s ease;
opacity: 0;
pointer-events: none;
}
.ripple:active::after {
width: 220rpx;
height: 220rpx;
opacity: 1;
transition: 0s;
}
/* ========== 滑动进场动画(列表/卡片用) ========== */
@keyframes slideUpFade {
0% { opacity: 0; transform: translateY(24rpx); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes slideRightFade {
0% { opacity: 0; transform: translateX(-24rpx); }
100% { opacity: 1; transform: translateX(0); }
}
@keyframes scaleFade {
0% { opacity: 0; transform: scale(0.92); }
100% { opacity: 1; transform: scale(1); }
}
/* 主题色脉冲光晕 - 通过 CSS 变量驱动 */
@keyframes pulseGlow {
0%, 100% { box-shadow: 0 0 0 0 rgba(var(--primaryColorRGB, 99, 102, 241), 0.4); }
50% { box-shadow: 0 0 0 16rpx rgba(var(--primaryColorRGB, 99, 102, 241), 0); }
}
@keyframes rotate360 {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-6rpx); }
75% { transform: translateX(6rpx); }
}
.slide-up { animation: slideUpFade 0.35s cubic-bezier(0.4, 0, 0.2, 1) both; }
.slide-right { animation: slideRightFade 0.35s cubic-bezier(0.4, 0, 0.2, 1) both; }
.scale-fade { animation: scaleFade 0.3s cubic-bezier(0.4, 0, 0.2, 1) both; }
.pulse-glow { animation: pulseGlow 2s ease-in-out infinite; }
.spin-slow { animation: rotate360 12s linear infinite; }
.shake-anim { animation: shake 0.4s ease; }
/* 交错延迟(用于列表元素错开进场) */
.delay-1 { animation-delay: 0.05s; }
.delay-2 { animation-delay: 0.1s; }
.delay-3 { animation-delay: 0.15s; }
.delay-4 { animation-delay: 0.2s; }
.delay-5 { animation-delay: 0.25s; }
/* ========== 文字交互态 ========== */
.text-link {
transition: opacity 0.2s ease, color 0.2s ease;
cursor: pointer;
}
.text-link:active { opacity: 0.6; }
/* ========== 主题渐变文字工具(CSS 变量驱动) ========== */
.text-gradient {
background: var(--gradientSoft, linear-gradient(135deg, #6366F1 0%, #818CF8 100%));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
</style>