首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:42:05 +08:00
commit ecca407485
84 changed files with 24158 additions and 0 deletions
+658
View File
@@ -0,0 +1,658 @@
<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>