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

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
+58
View File
@@ -0,0 +1,58 @@
import { getEnvConfig } from './env'
const envConfig = getEnvConfig()
const AES_KEY = envConfig.aesKey || '12345678901234567890123456789012'
const AES_IV = envConfig.aesIv || '1234567890123456'
const CryptoJS = require('../static/utils/crypto-js.min.js')
function encrypt(data) {
return new Promise((resolve, reject) => {
try {
let str = typeof data === 'string' ? data : JSON.stringify(data)
let encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(str),
CryptoJS.enc.Utf8.parse(AES_KEY),
{
iv: CryptoJS.enc.Utf8.parse(AES_IV),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
resolve(encrypted.toString())
} catch (e) {
console.error('加密失败', e)
reject(e)
}
})
}
function decrypt(data) {
return new Promise((resolve, reject) => {
try {
let decrypted = CryptoJS.AES.decrypt(
data,
CryptoJS.enc.Utf8.parse(AES_KEY),
{
iv: CryptoJS.enc.Utf8.parse(AES_IV),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
let str = decrypted.toString(CryptoJS.enc.Utf8)
try {
resolve(JSON.parse(str))
} catch {
resolve(str)
}
} catch (e) {
console.error('解密失败', e)
reject(e)
}
})
}
export default {
encrypt,
decrypt
}
+67
View File
@@ -0,0 +1,67 @@
const cache = new Map()
const CACHE_TTL = 30000
const PENDING = new Map()
export const cachedRequest = async (key, requestFn, ttl = CACHE_TTL) => {
const now = Date.now()
const cached = cache.get(key)
if (cached && now - cached.time < ttl) {
return cached.data
}
if (PENDING.has(key)) {
return PENDING.get(key)
}
const promise = requestFn().then(data => {
cache.set(key, { data, time: Date.now() })
PENDING.delete(key)
return data
}).catch(err => {
PENDING.delete(key)
throw err
})
PENDING.set(key, promise)
return promise
}
export const clearCache = (key) => {
if (key) {
cache.delete(key)
} else {
cache.clear()
}
}
export const getCacheKey = (prefix, params) => {
return `${prefix}:${JSON.stringify(params || {})}`
}
export const createCachedRequest = (cachePrefix) => {
return async (url, config = {}) => {
const key = getCacheKey(cachePrefix || url, config.data)
return cachedRequest(key, () => {
if (typeof config.request === 'function') {
return config.request(url, config)
}
return new Promise((resolve, reject) => {
uni.request({
url: url,
method: config.method || 'POST',
data: config.data || {},
header: config.header || {},
timeout: config.timeout || 15000,
success: (res) => resolve(res.data),
fail: (err) => reject(new Error(err.errMsg || '请求失败'))
})
})
}, config.cacheTTL)
}
}
export default {
cachedRequest,
clearCache,
createCachedRequest
}
+112
View File
@@ -0,0 +1,112 @@
import { clearCache } from './apiCache.js'
const TOKEN_KEY = 'user_token'
// 获取token
export function getToken() {
try {
return uni.getStorageSync(TOKEN_KEY) || ''
} catch (err) {
console.error('获取token失败:', err)
return ''
}
}
// 保存token
export function setToken(token) {
try {
if (token && typeof token === 'string') {
uni.setStorageSync(TOKEN_KEY, token)
}
} catch (err) {
console.error('保存token失败:', err)
uni.showToast({
title: '登录状态保存失败',
icon: 'none'
})
}
}
// 删除token
export function removeToken() {
try {
uni.removeStorageSync(TOKEN_KEY)
} catch (err) {
console.error('删除token失败:', err)
}
}
// 判断是否登录
export function isLogin() {
const token = getToken()
return !!token && token.length > 0
}
// 校验登录(未登录跳转)
export function checkLogin() {
if (!isLogin()) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage?.route !== 'pages/login/login') {
uni.navigateTo({
url: '/pages/login/login'
})
}
return false
}
return true
}
// 退出登录
export function logout() {
try {
// 1. 删除token(保留原有核心逻辑)
removeToken()
clearCache()
// 2. 清空用户信息缓存(解决用户名/头像/签名/邮箱残留)
uni.removeStorageSync('userName')
uni.removeStorageSync('userAvatar')
uni.removeStorageSync('userSignature')
uni.removeStorageSync('userEmail')
// 如有其他登录相关缓存,继续补充:
// uni.removeStorageSync('userInfo')
// 3. 重置全局变量(如有)
if (getApp().globalData?.userInfo) {
getApp().globalData.userInfo = null
}
if (getApp().globalData?.token) {
getApp().globalData.token = ''
}
if (getApp().globalData?.lastToken) {
getApp().globalData.lastToken = ''
}
} catch (err) {
console.error('清空登录缓存失败:', err)
uni.showToast({
title: '清空登录状态失败',
icon: 'none'
})
return // 出错则终止,避免错误跳转
}
// 4. 提示(保留原有)
uni.showToast({
title: '已退出登录',
icon: 'success',
duration: 1500
})
// 5. 关键修改:用reLaunch替代switchTab,强制重启小程序
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/login', // 直接跳登录页,而非首页
fail: () => {
// 兜底:重启失败则直接跳转
uni.redirectTo({
url: '/pages/login/login'
})
}
})
}, 1500)
}
+287
View File
@@ -0,0 +1,287 @@
import { getDomain } from './env'
// 头像基础路径(远端)
const AVATAR_BASE_URL = getDomain() + '/static/avatar/'
// 图片基础路径(远端)
const IMAGE_BASE_URL = getDomain() + '/static/images/'
// ========== 记账分类常量 start ==========
export const DEFAULT_CATE_LIST = [
// 支出类(type: 1
{ value: 'food', label: '餐饮', icon: '🍚', color: '#ff6b6b', type: 1 },
{ value: 'transport', label: '交通', icon: '🚇', color: '#4ecdc4', type: 1 },
{ value: 'shopping', label: '购物', icon: '🛍️', color: '#ffe66d', type: 1 },
{ value: 'entertainment', label: '娱乐', icon: '🎮', color: '#a855f7', type: 1 },
{ value: 'home', label: '居家', icon: '🏠', color: '#84b3d9', type: 1 },
{ value: 'medical', label: '医疗', icon: '🏥', color: '#ef4444', type: 1 },
{ value: 'education', label: '教育', icon: '📚', color: '#22c55e', type: 1 },
{ value: 'social', label: '社交', icon: '👥', color: '#f97316', type: 1 },
{ value: 'digital', label: '数码', icon: '📱', color: '#3b82f6', type: 1 },
{ value: 'clothing', label: '服饰', icon: '👔', color: '#ec4899', type: 1 },
{ value: 'beauty', label: '美容', icon: '💄', color: '#d946ef', type: 1 },
{ value: 'sports', label: '运动', icon: '⚽', color: '#10b981', type: 1 },
{ value: 'pet', label: '宠物', icon: '🐶', color: '#fbbf24', type: 1 },
{ value: 'baby', label: '育儿', icon: '🍼', color: '#fb7185', type: 1 },
{ value: 'travel', label: '旅游', icon: '✈️', color: '#06b6d4', type: 1 },
{ value: 'other_expense', label: '其他', icon: '📦', color: '#9ca3af', type: 1 },
// 收入类(type: 2
{ value: 'salary', label: '工资', icon: '💼', color: '#22c55e', type: 2 },
{ value: 'bonus', label: '奖金', icon: '🎁', color: '#fbbf24', type: 2 },
{ value: 'side_hustle', label: '副业', icon: '💻', color: '#3b82f6', type: 2 },
{ value: 'investment', label: '理财', icon: '📈', color: '#a855f7', type: 2 },
{ value: 'transfer', label: '转账', icon: '💳', color: '#84b3d9', type: 2 },
{ value: 'red_packet', label: '红包', icon: '🧧', color: '#ef4444', type: 2 },
{ value: 'other_income', label: '其他', icon: '🎯', color: '#6b7280', type: 2 }
]
// 例如:账单类型常量
export const BILL_TYPE = {
DEFAULT: 0, // 全部
EXPEND: 1, // 支出
INCOME: 2, // 收入
}
// ========== 我的页面菜单图标 ==========
export const MINE_MENU_ICONS = {
GROWTH: '/static/icon/growth.png',
BUDGET: '/static/icon/monthly_budget.png',
THEME: '/static/icon/theme.png',
ABOUT: '/static/icon/about.png',
EMAIL: '/static/icon/email.png',
WECHAT: '/static/icon/wechat.png'
}
//
// 分类颜色选项
export const COLOR_OPTIONS = [
'#84b3d9', '#a0d0e0', '#b8d8e6', '#d0e0e8', '#e8e8e8',
'#f0f0f0', '#f8f0f0', '#f0f8f0', '#f0f0f8', '#e0f0f8'
]
// 滑动按钮宽度(rpx
export const SLIDE_BTN_WIDTH = 240
// 筛选Tab 常量
export const BILL_FILTER_TYPE = {
ALL: 'all',
EXPEND: 'expend',
INCOME: 'income'
}
// 账单类型筛选选项(用于循环渲染)
export const BILL_TYPE_FILTER_OPTIONS = [
{ label: '全部', value: BILL_FILTER_TYPE.ALL },
{ label: '支出', value: BILL_FILTER_TYPE.EXPEND },
{ label: '收入', value: BILL_FILTER_TYPE.INCOME }
]
// 时间筛选标签
export const TIME_TAG = {
ALL: 'all',
TODAY: 'today',
WEEK: 'week',
MONTH: 'month',
LAST_MONTH: 'lastMonth',
THIS_YEAR: 'thisYear',
CUSTOM: 'custom'
}
// 账单时间筛选选项(用于循环渲染)
export const BILL_TIME_FILTER_OPTIONS = [
{ label: '今日', value: TIME_TAG.TODAY },
{ label: '本周', value: TIME_TAG.WEEK },
{ label: '本月', value: TIME_TAG.MONTH },
{ label: '上月', value: TIME_TAG.LAST_MONTH },
{ label: '本年', value: TIME_TAG.THIS_YEAR },
{ label: '自定义', value: TIME_TAG.CUSTOM }
]
// 4. 金额快捷按钮数值
export const QUICK_MONEY_AMOUNTS = [10, 20, 50, 100, 200, 500];
// 5. 备注快捷标签
export const QUICK_NOTE_TAGS = ['午餐', '打车', '地铁', '工资', '购物', '娱乐'];
// 6. 预算进度颜色阈值
export const BUDGET_PROGRESS_COLOR = {
OVER: 100, // 超支(红色)
WARNING: 80, // 警告(橙色)
NORMAL: 0 // 正常(蓝色)
}
// 7. 预算进度颜色值
export const PROGRESS_COLORS = {
OVER: '#F53F3F',
WARNING: '#FFAA33',
NORMAL: '#E8F3FF'
}
// 8. 最近选择分类最大数量
export const RECENT_CATE_MAX_COUNT = 3
// 9. 滑动阈值(触发显示按钮的最小滑动距离)
export const SLIDE_THRESHOLD = 50
// ========== 记账分类常量 end ==========
// ========== 待办(TODO)相关常量 start ==========
// 待办基础分类(对应你原有的 BASE_CATE)
export const TODO_BASE_CATE = Object.freeze([
{
value: 'work',
label: '工作',
color: '#84b3d9'
},
{
value: 'life',
label: '生活',
color: '#a0d0e0'
},
{
value: 'study',
label: '学习',
color: '#b8d8e6'
}
])
// 星期名称映射(从周一开始,对应你原有的 WEEK_DAYS)
export const WEEK_DAYS = Object.freeze(['一', '二', '三', '四', '五', '六', '日'])
// 结构化的筛选常量(替代硬编码)
export const TIME_FILTER_OPTIONS = [
{ label: '今日', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
{ label: '自定义', value: 'custom' }
]
export const STATUS_FILTER_OPTIONS = [
{ label: '全部', value: 'all' },
{ label: '待办', value: 'undone' },
{ label: '已完成', value: 'done' }
]
// 优先级常量(结构化)
export const TODO_PRIORITY_LIST = [
{ label: '高', value: 1, icon: '🔴' },
{ label: '中', value: 2, icon: '🟡' },
{ label: '低', value: 3, icon: '🟢' }
]
// ========== 待办(TODO)相关常量 end ==========
// ========== 收支渠道常量 ==========
export const PAY_CHANNELS = [
// 通用渠道(支持收入和支出)
{ value: 'wechat', label: '微信', types: [1, 2] }, // 1=支出, 2=收入
{ value: 'alipay', label: '支付宝', types: [1, 2] },
{ value: 'unionpay', label: '云闪付', types: [1, 2] }, // 新增
{ value: 'bank', label: '银行卡', types: [1, 2] },
{ value: 'cash', label: '现金', types: [1, 2] },
{ value: 'online_banking', label: '网银', types: [1, 2] }, // 新增
{ value: 'pos', label: 'POS机', types: [1, 2] }, // 新增
{ value: 'other', label: '其他', types: [1, 2] }, // 新增
// 仅收入渠道
{ value: 'salary', label: '工资', types: [2] },
{ value: 'red_packet', label: '红包', types: [2] },
{ value: 'transfer_in', label: '转账', types: [2] },
{ value: 'corporate', label: '对公转账', types: [2] }, // 新增
// 仅支出渠道
{ value: 'credit', label: '信用卡', types: [1] },
{ value: 'huawei', label: '花呗', types: [1] },
{ value: 'white', label: '白条', types: [1] },
{ value: 'jd_pay', label: '京东支付', types: [1] }, // 新增
{ value: 'meituan_pay', label: '美团支付', types: [1] }, // 新增
{ value: 'gift_card', label: '储值卡', types: [1] } // 新增
]
// 获取指定类型的渠道列表
export const getChannelsByType = (type) => {
return PAY_CHANNELS.filter(channel => channel.types.includes(type))
}
// 根据渠道值获取渠道名称
export const getChannelLabel = (value) => {
const channel = PAY_CHANNELS.find(c => c.value === value)
return channel ? channel.label : value
}
// 根据分类值获取分类名称
export const getCateLabel = (value) => {
const cate = DEFAULT_CATE_LIST.find(c => c.value === value)
return cate ? cate.label : value
}
// ========== 可扩展:其他全局常量 ==========
// 远端头像资源配置(根据当前环境动态获取域名)
export const DEFAULT_AVATARS = [
AVATAR_BASE_URL + 'ginger_cat.jpg', // 橘猫
AVATAR_BASE_URL + 'ragdoll.jpg', // 布偶猫
AVATAR_BASE_URL + 'hamster.jpg', // 小仓鼠
AVATAR_BASE_URL + 'dragon.jpg', // 龙
AVATAR_BASE_URL + 'pony.jpg', // 小马
AVATAR_BASE_URL + 'lamb.jpg', // 小羊
AVATAR_BASE_URL + 'corgi.jpg', // 柯基
AVATAR_BASE_URL + 'piglet.jpg', // 小猪
]
// 例如:请求状态码常量
export const HTTP_CODE = {
SUCCESS: 200,
TOKEN_EXPIRE: 401,
SERVER_ERROR: 500
}
// ========== 心情相关常量 ==========
export const MOOD_LIST = [
{ emoji: '😊', label: '开心', phrases: ['今天心情真好', '阳光灿烂的一天', '嘴角不自觉上扬', '满心欢喜'] },
{ emoji: '😌', label: '平静', phrases: ['岁月静好', '内心一片安宁', '享受这份宁静', '平和自在'] },
{ emoji: '😔', label: '低落', phrases: ['有点小忧伤', '心情沉沉的', '需要一点温暖', '静待花开'] },
{ emoji: '😤', label: '生气', phrases: ['有点烦躁呢', '深呼吸冷静一下', '需要冷静片刻', '平复一下心情'] },
{ emoji: '😴', label: '疲惫', phrases: ['有点累了', '好好休息一下', '给自己放个假', '累并快乐着'] },
{ emoji: '😰', label: '焦虑', phrases: ['有点担心', '相信一切会好', '慢慢来不着急', '放宽心'] },
{ emoji: '😄', label: '兴奋', phrases: ['超级开心!', '激动人心的时刻', '迫不及待了', '满心期待'] },
{ emoji: '🤔', label: '思考', phrases: ['正在思考中', '让我想想', '深思熟虑', '思绪万千'] },
{ emoji: '😢', label: '难过', phrases: ['有点难过', '需要一个拥抱', '一切都会过去', '明天会更好'] },
{ emoji: '😎', label: '酷', phrases: ['今天超酷的', '自信满满', '做自己就好', '洒脱自在'] },
{ emoji: '🥰', label: '幸福', phrases: ['被幸福包围', '甜蜜的感觉', '幸福感爆棚', '满心欢喜'] },
{ emoji: '😠', label: '愤怒', phrases: ['真的生气了', '需要冷静一下', '平复一下情绪', '深呼吸'] },
{ emoji: '🥳', label: '庆祝', phrases: ['值得庆祝!', '太棒了!', '举杯庆祝', '喜笑颜开'] },
{ emoji: '😱', label: '惊讶', phrases: ['哇!好惊喜', '太意外了', '不可思议', '令人震惊'] },
{ emoji: '😇', label: '感恩', phrases: ['心怀感恩', '感谢生活', '感恩遇见', '心存感激'] },
{ emoji: '💪', label: '加油', phrases: ['加油加油!', '相信自己', '勇往直前', '全力以赴'] },
{ emoji: '😘', label: '害羞', phrases: ['有点害羞', '不好意思啦', '脸红心跳', '羞涩一笑'] },
{ emoji: '🤩', label: '崇拜', phrases: ['超级崇拜!', '偶像光芒', '闪闪发光', '心生敬意'] },
{ emoji: '😪', label: '困倦', phrases: ['好困呀', '眼皮打架了', '需要小憩', '睡个好觉'] },
{ emoji: '😋', label: '满足', phrases: ['心满意足', '幸福感满满', '知足常乐', '十分满足'] }
]
// ========== Logo 与品牌信息常量 ==========
export const LOGO_CONFIG = {
path: IMAGE_BASE_URL + 'logo.png',
name: '简记memo',
subtitle: '让记录更简单',
slogan: '记录,是对抗遗忘的方式。\n生活的美好藏在点滴细节里,\n用简记memo,写下时光的足迹。',
version: 'v1.0.0',
copyright: `© ${new Date().getFullYear()} 简记memo. All rights reserved.`,
email: 'memo@miaoall.cn',
wechat: {
name: '孙三苗',
account: 'sunsanmiao'
},
brand: {
primaryColor: '#84b3d9',
secondaryColor: '#6a99c2',
gradient: 'linear-gradient(135deg, #84b3d9 0%, #6a99c2 100%)'
}
}
+25
View File
File diff suppressed because one or more lines are too long
+109
View File
@@ -0,0 +1,109 @@
/**
* 环境配置
* 根据编译环境自动切换 API 地址
*/
// 环境列表
export const ENV = {
DEV: 'development',
PROD: 'production',
TEST: 'test'
}
// 当前环境(默认测试环境)
let currentEnv = ENV.TEST
/**
* 设置当前环境
* @param {string} env - 环境名称
*/
export const setEnv = (env) => {
if (Object.values(ENV).includes(env)) {
currentEnv = env
}
}
/**
* 获取当前环境
* @returns {string}
*/
export const getEnv = () => currentEnv
/**
* 环境配置
* 只配置域名,路由前缀在 request.js 中单独配置
*/
const ENV_CONFIG = {
[ENV.DEV]: {
name: '开发环境',
domain: 'https://memo.test.miaoall.cn',
debug: true,
aesKey: '12345678901234567890123456789012',
aesIv: '1234567890123456'
},
[ENV.TEST]: {
name: '测试环境',
domain: 'https://memo.test.miaoall.cn',
debug: true,
aesKey: '12345678901234567890123456789012',
aesIv: '1234567890123456'
},
[ENV.PROD]: {
name: '生产环境',
domain: 'https://memo.miaoall.cn',
debug: false,
aesKey: 'a1s2d3f4g5h6j7k8l9z0xq1w2e3r4t5y6',
aesIv: 'q1w2e3r4t5y6u7i8'
}
}
/**
* 获取当前环境的配置
* @returns {Object}
*/
export const getEnvConfig = () => {
return ENV_CONFIG[currentEnv] || ENV_CONFIG[ENV.TEST]
}
/**
* 获取 API 域名
* @returns {string}
*/
export const getDomain = () => getEnvConfig().domain
/**
* 获取当前环境名称
* @returns {string}
*/
export const getEnvName = () => getEnvConfig().name
/**
* 是否为开发模式
* @returns {boolean}
*/
export const isDev = () => currentEnv === ENV.DEV
/**
* 是否为生产模式
* @returns {boolean}
*/
export const isProd = () => currentEnv === ENV.PROD
/**
* 是否为测试模式
* @returns {boolean}
*/
export const isTest = () => currentEnv === ENV.TEST
// 默认导出配置
export default {
ENV,
getEnv,
setEnv,
getEnvConfig,
getDomain,
getEnvName,
isDev,
isProd,
isTest
}
+98
View File
@@ -0,0 +1,98 @@
import { MOOD_LIST } from './constants.js'
export function formatMoodEmoji(emoji) {
if (!emoji) return ''
const item = MOOD_LIST.find(m => m.emoji === emoji)
return item ? item.emoji : emoji
}
export function getMoodDateRange(range) {
const now = new Date()
let startDate = ''
let endDate = ''
switch (range) {
case 'today':
startDate = formatDate(now)
endDate = formatDate(now)
break
case 'week':
const weekStart = new Date(now)
const dayOfWeek = now.getDay() || 7
weekStart.setDate(now.getDate() - dayOfWeek + 1)
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekStart.getDate() + 6)
startDate = formatDate(weekStart)
endDate = formatDate(weekEnd)
break
case 'month':
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0)
startDate = formatDate(monthStart)
endDate = formatDate(monthEnd)
break
case 'all':
default:
startDate = formatDate(now)
endDate = formatDate(now)
break
}
return { startDate, endDate }
}
export function getMoodTypeName(type) {
const map = {
happy: '开心',
neutral: '平静',
sad: '低落',
angry: '生气',
tired: '疲惫',
anxious: '焦虑',
excited: '兴奋',
thinking: '思考',
sad2: '难过',
cool: '酷',
happy2: '幸福',
angry2: '愤怒',
celebrate: '庆祝',
surprised: '惊讶',
grateful: '感恩',
cheer: '加油',
shy: '害羞',
admire: '崇拜',
sleepy: '困倦',
satisfied: '满足'
}
return map[type] || type || '未知'
}
export function getMoodLabel(emoji) {
const item = MOOD_LIST.find(m => m.emoji === emoji)
return item ? item.label : ''
}
export function getMoodTypeByEmoji(emoji) {
const item = MOOD_LIST.find(m => m.emoji === emoji)
if (!item) return 'neutral'
const label = item.label
if (['开心', '兴奋', '幸福', '庆祝', '满足'].includes(label)) return 'happy'
if (['低落', '难过', '焦虑', '困倦'].includes(label)) return 'sad'
if (['生气', '愤怒'].includes(label)) return 'angry'
if (['疲惫'].includes(label)) return 'tired'
return 'neutral'
}
function formatDate(date) {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
}
export default {
getMoodDateRange,
getMoodTypeName,
getMoodLabel,
getMoodTypeByEmoji
}
+245
View File
@@ -0,0 +1,245 @@
import AES from './aes.js'
import { getToken, checkLogin, removeToken, setToken } from './auth'
import { login } from '../api'
import { getDomain, getEnvName } from './env'
import { cachedRequest, getCacheKey, clearCache } from './apiCache.js'
const API_PREFIX = '/api/app'
const config = {
BASE_URL: getDomain() + API_PREFIX,
API_PREFIX: API_PREFIX,
DOMAIN: getDomain(),
ENV_NAME: getEnvName(),
TIMEOUT: 15000,
LOGIN_PAGE: '/pages/login/login',
CODE: {
SUCCESS: 200,
NEED_LOGIN: 1000,
TOKEN_EXPIRED: 1001,
TOKEN_INVALID: 1002
},
MAX_RETRY: 2
}
let isRefreshing = false
let refreshPromise = null
const isLoginPage = () => {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
return currentPage?.route === 'pages/login/login'
} catch (err) {
return false
}
}
const getCurrentPageInfo = () => {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
return {
route: currentPage?.route || 'unknown',
options: currentPage?.options || {}
}
} catch (err) {
return { route: 'unknown', options: {} }
}
}
const refreshToken = async () => {
if (isRefreshing) {
return refreshPromise
}
const currentPage = getCurrentPageInfo()
console.log(`📋 当前页面: ${currentPage.route}`)
if (isLoginPage()) {
console.log('⚠️ 当前在登录页,不执行自动刷新Token')
return Promise.reject(new Error('当前在登录页'))
}
isRefreshing = true
refreshPromise = new Promise(async (resolve, reject) => {
try {
console.log(`🔄 尝试自动刷新Token... (触发页面: ${currentPage.route})`)
const loginRes = await uni.login()
if (!loginRes.code) {
throw new Error('获取微信code失败')
}
const tokenData = await login(loginRes.code)
if (!tokenData?.token) {
throw new Error('刷新Token失败')
}
setToken(tokenData.token)
console.log(`✅ Token刷新成功 (触发页面: ${currentPage.route})`)
resolve(tokenData.token)
} catch (err) {
console.error(`❌ Token刷新失败(触发页面: ${currentPage.route}):`, err)
removeToken()
clearCache()
reject(err)
} finally {
isRefreshing = false
refreshPromise = null
}
})
return refreshPromise
}
const executeRequest = async (options, retryCount = 0) => {
const { url, data = {}, needLogin = true, showLoading = true, encrypt = true } = options
if (showLoading) {
uni.showLoading({ title: '处理中...', mask: true })
}
try {
if (needLogin && url !== '/user/login' && !checkLogin()) {
const currentPage = getCurrentPageInfo()
console.log(`🔴 未登录检测 - 页面: ${currentPage.route}, 接口: ${url}`)
if (retryCount === 0) {
try {
console.log(`🟡 准备调用 refreshToken - 页面: ${currentPage.route}`)
await refreshToken()
console.log(`🟢 refreshToken 成功,重试请求 - 页面: ${currentPage.route}`)
return await executeRequest(options, retryCount + 1)
} catch (err) {
console.log(`🔴 refreshToken 失败: ${err.message} - 页面: ${currentPage.route}`)
uni.showToast({ title: '请先登录', icon: 'none' })
redirectToLogin()
return Promise.reject(new Error('请先登录'))
}
}
uni.showToast({ title: '请先登录', icon: 'none' })
redirectToLogin()
return Promise.reject(new Error('请先登录'))
}
const token = getToken()
console.log(`🔵 [POST] 请求接口:${url},参数:`, data)
let requestData = {}
if (encrypt) {
requestData.data = await AES.encrypt(data)
} else {
requestData = data
}
const response = await new Promise((resolve, reject) => {
const requestTask = uni.request({
url: config.BASE_URL + url,
method: 'POST',
header: { token: token || '', 'Content-Type': 'application/json' },
data: requestData,
timeout: config.TIMEOUT,
success: (res) => resolve(res),
fail: (err) => reject(new Error(err.errMsg?.includes('timeout') ? '请求超时' : '网络异常'))
})
setTimeout(() => requestTask.abort(), config.TIMEOUT)
})
if (response.statusCode !== 200) {
console.log(`🔵 [POST] 请求接口:${url}Error${response.statusCode}`)
throw new Error(`请求出错啦 o(╥﹏╥)o`)
}
const encryptRes = response.data.data || ''
let result = {}
if (encrypt && encryptRes) {
const decryptStr = await AES.decrypt(encryptRes)
result = typeof decryptStr === 'string' ? JSON.parse(decryptStr) : decryptStr
} else {
result = response.data
}
console.log(`🟢 [POST] 请求接口:${url} 响应结果:`, result)
if (result.code === config.CODE.SUCCESS) {
return result.data ?? true
} else if (result.code === config.CODE.NEED_LOGIN ||
result.code === config.CODE.TOKEN_EXPIRED ||
result.code === config.CODE.TOKEN_INVALID) {
if (retryCount < config.MAX_RETRY) {
console.log(`🔄 Token失效,尝试刷新(第${retryCount + 1}次)`)
try {
await refreshToken()
return await executeRequest(options, retryCount + 1)
} catch (err) {
uni.showToast({ title: '登录已过期', icon: 'none' })
removeToken()
clearCache()
setTimeout(() => redirectToLogin(), 1500)
return Promise.reject(new Error('登录已过期'))
}
}
uni.showToast({ title: '登录已过期', icon: 'none' })
removeToken()
clearCache()
setTimeout(() => redirectToLogin(), 1500)
return Promise.reject(new Error('登录已过期'))
} else {
throw new Error(result.msg || '请求失败')
}
} catch (err) {
console.error(`🔴 [POST] 请求异常:`, err)
if (err.message.includes('网络异常')) {
uni.showToast({ title: '网络连接异常,请检查网络', icon: 'none', duration: 2000 })
} else if (err.message.includes('请求超时')) {
uni.showToast({ title: '请求超时,请稍后重试', icon: 'none', duration: 2000 })
} else {
uni.showToast({ title: err.message || '系统异常', icon: 'none', duration: 2000 })
}
return Promise.reject(err)
} finally {
if (showLoading) uni.hideLoading()
}
}
const request = async (options, retryCount = 0) => {
const { cache } = options
if (cache === true && retryCount === 0) {
const cacheKey = getCacheKey(options.url, options.data)
return cachedRequest(cacheKey, () => executeRequest(options, retryCount), options.cacheTTL)
}
if (cache === 'bypass' && retryCount === 0) {
const cacheKey = getCacheKey(options.url, options.data)
clearCache(cacheKey)
}
return executeRequest(options, retryCount)
}
function redirectToLogin() {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage?.route !== 'pages/login/login') {
uni.redirectTo({ url: config.LOGIN_PAGE })
}
} catch (err) {
uni.reLaunch({ url: config.LOGIN_PAGE })
}
}
request.post = function (url, data = {}, options = {}) {
return this({ url, data, ...options })
}
export const getRequestConfig = () => ({
domain: config.DOMAIN,
apiPrefix: config.API_PREFIX,
baseUrl: config.BASE_URL,
envName: config.ENV_NAME
})
export { clearCache }
export default request
+524
View File
@@ -0,0 +1,524 @@
import { getToken } from '@/utils/auth'
import { getUserConfig, editUserConfig } from '@/api'
import { forceApplyTabBarTheme } from '@/utils/themeGlobal'
// 全新主题预设 - 温暖生活风
// 设计特点:燕麦底色、低饱和、手作感、留白充足、去AI化
export const THEME_PRESETS = {
// 暖陶色 - 默认主主题(温暖、自然、生活感)
default: {
name: '暖陶',
primaryColor: '#C68E3F',
secondaryColor: '#4A9E7F',
accentColor: '#B85C3A',
dangerColor: '#C75B4A',
warningColor: '#D4A24A',
successColor: '#5B9E6E',
successColorRGB: '91, 158, 110',
infoColor: '#5B8DB8',
pendingColor: '#F5EDE4',
bgColor: '#FAF6F0',
bgGradientStart: '#F7F3ED',
bgGradientEnd: '#FAF6F0',
cardBgColor: '#FFFCF8',
cardBgBlur: 'rgba(255, 252, 248, 0.92)',
textPrimary: '#3D3630',
textSecondary: '#7A7066',
textTertiary: '#9A9088',
textDisabled: '#B0A89E',
textLight: '#EDE8E1',
borderColor: '#E8E2DA',
borderLight: '#F2EDE6',
inputBgColor: '#F7F3ED',
shadowColor: 'rgba(198, 142, 63, 0.18)',
shadowLight: 'rgba(61, 54, 48, 0.04)',
shadowMedium: 'rgba(61, 54, 48, 0.08)',
shadowStrong: 'rgba(61, 54, 48, 0.12)',
gradientStart: '#B85C3A',
gradientEnd: '#C68E3F',
gradientSoft: 'linear-gradient(135deg, #B85C3A 0%, #C68E3F 100%)',
gradientVibrant: 'linear-gradient(135deg, #A04A2E 0%, #B85C3A 50%, #C68E3F 100%)',
priorityHigh: '#C75B4A',
priorityMiddle: '#D4A24A',
priorityLow: '#5B9E6E',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A24A',
moodCalm: '#5B9E6E',
moodSad: '#7B9EB8',
moodAngry: '#C75B4A',
moodNeutral: '#B0A89E',
incomeColor: '#5B9E6E',
expendColor: '#C75B4A'
},
// 青苔 - 自然清新
mint: {
name: '青苔',
primaryColor: '#4A9E7F',
secondaryColor: '#6DB89A',
accentColor: '#3D8568',
dangerColor: '#C75B4A',
warningColor: '#D4A24A',
successColor: '#5B9E6E',
infoColor: '#5B8DB8',
pendingColor: '#EEF5F1',
bgColor: '#F5F8F3',
bgGradientStart: '#F0F4ED',
bgGradientEnd: '#F5F8F3',
cardBgColor: '#FAFCF8',
cardBgBlur: 'rgba(250, 252, 248, 0.92)',
textPrimary: '#2D3A30',
textSecondary: '#6B7D6E',
textTertiary: '#8B9A8E',
textDisabled: '#A3B0A6',
textLight: '#E5EBE7',
borderColor: '#D8E2DA',
borderLight: '#E8EDE9',
inputBgColor: '#EEF5F1',
shadowColor: 'rgba(74, 158, 127, 0.1)',
shadowLight: 'rgba(45, 58, 48, 0.04)',
shadowMedium: 'rgba(45, 58, 48, 0.07)',
shadowStrong: 'rgba(45, 58, 48, 0.11)',
gradientStart: '#3D8568',
gradientEnd: '#4A9E7F',
gradientSoft: 'linear-gradient(135deg, #3D8568 0%, #4A9E7F 100%)',
gradientVibrant: 'linear-gradient(135deg, #2D6B53 0%, #3D8568 50%, #4A9E7F 100%)',
priorityHigh: '#C75B4A',
priorityMiddle: '#D4A24A',
priorityLow: '#5B9E6E',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A24A',
moodCalm: '#4A9E7F',
moodSad: '#7B9EB8',
moodAngry: '#C75B4A',
moodNeutral: '#A3B0A6',
incomeColor: '#5B9E6E',
expendColor: '#C75B4A'
},
// 藕粉 - 柔和温婉
cherry: {
name: '藕粉',
primaryColor: '#C4908A',
secondaryColor: '#D5A9A4',
accentColor: '#B07872',
dangerColor: '#C75B4A',
warningColor: '#D4A24A',
successColor: '#5B9E6E',
infoColor: '#5B8DB8',
pendingColor: '#F8F0EF',
bgColor: '#FAF5F3',
bgGradientStart: '#F6F0EE',
bgGradientEnd: '#FAF5F3',
cardBgColor: '#FDF9F8',
cardBgBlur: 'rgba(253, 249, 248, 0.92)',
textPrimary: '#3D3332',
textSecondary: '#7A6C6A',
textTertiary: '#9A8C8A',
textDisabled: '#B0A5A3',
textLight: '#EDE5E3',
borderColor: '#E5DAD7',
borderLight: '#F0E8E5',
inputBgColor: '#F8F0EF',
shadowColor: 'rgba(196, 144, 138, 0.1)',
shadowLight: 'rgba(61, 51, 50, 0.04)',
shadowMedium: 'rgba(61, 51, 50, 0.07)',
shadowStrong: 'rgba(61, 51, 50, 0.11)',
gradientStart: '#B07872',
gradientEnd: '#C4908A',
gradientSoft: 'linear-gradient(135deg, #B07872 0%, #C4908A 100%)',
gradientVibrant: 'linear-gradient(135deg, #9A5E5A 0%, #B07872 50%, #C4908A 100%)',
priorityHigh: '#C75B4A',
priorityMiddle: '#D4A24A',
priorityLow: '#5B9E6E',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A24A',
moodCalm: '#5B9E6E',
moodSad: '#7B9EB8',
moodAngry: '#C75B4A',
moodNeutral: '#B0A5A3',
incomeColor: '#5B9E6E',
expendColor: '#C75B4A'
},
// 暮蓝 - 沉静夜色
sky: {
name: '暮蓝',
primaryColor: '#5B8DB8',
secondaryColor: '#7BA7C6',
accentColor: '#4978A0',
dangerColor: '#C75B4A',
warningColor: '#D4A24A',
successColor: '#5B9E6E',
infoColor: '#5B8DB8',
pendingColor: '#EEF2F6',
bgColor: '#F3F5F8',
bgGradientStart: '#EEF1F5',
bgGradientEnd: '#F3F5F8',
cardBgColor: '#F8FAFB',
cardBgBlur: 'rgba(248, 250, 251, 0.92)',
textPrimary: '#2D343D',
textSecondary: '#6B7580',
textTertiary: '#8B95A0',
textDisabled: '#A3ABB5',
textLight: '#E5E9ED',
borderColor: '#D8DEE4',
borderLight: '#E8ECF0',
inputBgColor: '#EEF2F6',
shadowColor: 'rgba(91, 141, 184, 0.1)',
shadowLight: 'rgba(45, 52, 61, 0.04)',
shadowMedium: 'rgba(45, 52, 61, 0.07)',
shadowStrong: 'rgba(45, 52, 61, 0.11)',
gradientStart: '#4978A0',
gradientEnd: '#5B8DB8',
gradientSoft: 'linear-gradient(135deg, #4978A0 0%, #5B8DB8 100%)',
gradientVibrant: 'linear-gradient(135deg, #355E80 0%, #4978A0 50%, #5B8DB8 100%)',
priorityHigh: '#C75B4A',
priorityMiddle: '#D4A24A',
priorityLow: '#5B9E6E',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A24A',
moodCalm: '#5B9E6E',
moodSad: '#5B8DB8',
moodAngry: '#C75B4A',
moodNeutral: '#A3ABB5',
incomeColor: '#5B9E6E',
expendColor: '#C75B4A'
},
// 深夜模式 - 暗色温暖
dark: {
name: '深夜',
primaryColor: '#D4AA60',
secondaryColor: '#7DB89A',
accentColor: '#C68E3F',
dangerColor: '#D47060',
warningColor: '#D4AA60',
successColor: '#6DB89A',
infoColor: '#7BA7C6',
pendingColor: '#2A2824',
bgColor: '#1E1D1A',
bgGradientStart: '#252420',
bgGradientEnd: '#1E1D1A',
cardBgColor: '#2A2824',
cardBgBlur: 'rgba(42, 40, 36, 0.92)',
textPrimary: '#E8E2DA',
textSecondary: '#9A9088',
textTertiary: '#807870',
textDisabled: '#6B6360',
textLight: '#2A2824',
borderColor: '#3A3632',
borderLight: '#302E2A',
inputBgColor: '#252420',
shadowColor: 'rgba(212, 170, 96, 0.15)',
shadowLight: 'rgba(0, 0, 0, 0.3)',
shadowMedium: 'rgba(0, 0, 0, 0.4)',
shadowStrong: 'rgba(0, 0, 0, 0.5)',
gradientStart: '#D4AA60',
gradientEnd: '#C68E3F',
gradientSoft: 'linear-gradient(135deg, #D4AA60 0%, #C68E3F 100%)',
gradientVibrant: 'linear-gradient(135deg, #C68E3F 0%, #D4AA60 50%, #E0C080 100%)',
priorityHigh: '#D47060',
priorityMiddle: '#D4AA60',
priorityLow: '#6DB89A',
checkboxColor: '#2A2824',
moodHappy: '#D4AA60',
moodCalm: '#6DB89A',
moodSad: '#7BA7C6',
moodAngry: '#D47060',
moodNeutral: '#9A9088',
incomeColor: '#6DB89A',
expendColor: '#D47060'
},
// 薰衣 - 梦幻紫韵
cream: {
name: '薰衣',
primaryColor: '#957BB7',
secondaryColor: '#B099D4',
accentColor: '#7A60A0',
dangerColor: '#C87888',
warningColor: '#D4A060',
successColor: '#7AB884',
infoColor: '#9A8AC8',
pendingColor: '#F3EFF8',
bgColor: '#F6F3FA',
bgGradientStart: '#F0ECF6',
bgGradientEnd: '#F6F3FA',
cardBgColor: '#FBFAFD',
cardBgBlur: 'rgba(251, 250, 253, 0.92)',
textPrimary: '#2E2240',
textSecondary: '#6B5B7A',
textTertiary: '#8B7B9A',
textDisabled: '#B8AEC8',
textLight: '#E8E0F0',
borderColor: '#DDD4E8',
borderLight: '#EBE4F0',
inputBgColor: '#F3EFF8',
shadowColor: 'rgba(149, 123, 183, 0.1)',
shadowLight: 'rgba(46, 34, 64, 0.04)',
shadowMedium: 'rgba(46, 34, 64, 0.07)',
shadowStrong: 'rgba(46, 34, 64, 0.11)',
gradientStart: '#7A60A0',
gradientEnd: '#957BB7',
gradientSoft: 'linear-gradient(135deg, #7A60A0 0%, #957BB7 100%)',
gradientVibrant: 'linear-gradient(135deg, #5A4580 0%, #7A60A0 50%, #957BB7 100%)',
priorityHigh: '#C87888',
priorityMiddle: '#D4A060',
priorityLow: '#7AB884',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A060',
moodCalm: '#7AB884',
moodSad: '#957BB7',
moodAngry: '#C87888',
moodNeutral: '#B8AEC8',
incomeColor: '#7AB884',
expendColor: '#C87888'
},
// 柑橘 - 活力明亮
ocean: {
name: '柑橘',
primaryColor: '#D4824A',
secondaryColor: '#E09A6A',
accentColor: '#C07040',
dangerColor: '#C86060',
warningColor: '#C8A040',
successColor: '#6AA87A',
infoColor: '#B89070',
pendingColor: '#FDF0E8',
bgColor: '#FDF6F0',
bgGradientStart: '#F8F0E8',
bgGradientEnd: '#FDF6F0',
cardBgColor: '#FEFCFA',
cardBgBlur: 'rgba(254, 252, 250, 0.92)',
textPrimary: '#3D281A',
textSecondary: '#7A5E48',
textTertiary: '#9A7E68',
textDisabled: '#C8B8A8',
textLight: '#F0E4D8',
borderColor: '#E8D8CC',
borderLight: '#F2E8E0',
inputBgColor: '#FDF0E8',
shadowColor: 'rgba(212, 130, 74, 0.1)',
shadowLight: 'rgba(61, 40, 26, 0.04)',
shadowMedium: 'rgba(61, 40, 26, 0.07)',
shadowStrong: 'rgba(61, 40, 26, 0.11)',
gradientStart: '#C07040',
gradientEnd: '#D4824A',
gradientSoft: 'linear-gradient(135deg, #C07040 0%, #D4824A 100%)',
gradientVibrant: 'linear-gradient(135deg, #9A5530 0%, #C07040 50%, #D4824A 100%)',
priorityHigh: '#C86060',
priorityMiddle: '#C8A040',
priorityLow: '#6AA87A',
checkboxColor: '#FFFFFF',
moodHappy: '#D4824A',
moodCalm: '#6AA87A',
moodSad: '#7B9EB8',
moodAngry: '#C86060',
moodNeutral: '#C8B8A8',
incomeColor: '#6AA87A',
expendColor: '#C86060'
},
// 薄荷 - 清凉冰爽
matcha: {
name: '薄荷',
primaryColor: '#5BA8A0',
secondaryColor: '#7BC4BC',
accentColor: '#4A968E',
dangerColor: '#C07078',
warningColor: '#C8A860',
successColor: '#5A9B6A',
infoColor: '#7AB8B0',
pendingColor: '#E8F4F2',
bgColor: '#F0F7F6',
bgGradientStart: '#E8F2F0',
bgGradientEnd: '#F0F7F6',
cardBgColor: '#F8FBFB',
cardBgBlur: 'rgba(248, 251, 251, 0.92)',
textPrimary: '#1A3330',
textSecondary: '#4A7A74',
textTertiary: '#6A9A94',
textDisabled: '#A0C8C0',
textLight: '#D8EBE8',
borderColor: '#C8DDD8',
borderLight: '#E0EBE8',
inputBgColor: '#E8F4F2',
shadowColor: 'rgba(91, 168, 160, 0.1)',
shadowLight: 'rgba(26, 51, 48, 0.04)',
shadowMedium: 'rgba(26, 51, 48, 0.07)',
shadowStrong: 'rgba(26, 51, 48, 0.11)',
gradientStart: '#4A968E',
gradientEnd: '#5BA8A0',
gradientSoft: 'linear-gradient(135deg, #4A968E 0%, #5BA8A0 100%)',
gradientVibrant: 'linear-gradient(135deg, #357A72 0%, #4A968E 50%, #5BA8A0 100%)',
priorityHigh: '#C07078',
priorityMiddle: '#C8A860',
priorityLow: '#5A9B6A',
checkboxColor: '#FFFFFF',
moodHappy: '#D4A24A',
moodCalm: '#5BA8A0',
moodSad: '#7B9EB8',
moodAngry: '#C07078',
moodNeutral: '#A0C8C0',
incomeColor: '#5A9B6A',
expendColor: '#C07078'
}
}
// 统一本地存储key(核心存储完整配置,避免冗余)
export const THEME_STORAGE_KEY = 'user_current_theme'
// 默认主题key
export const DEFAULT_THEME_KEY = 'default'
// 全局主题变更事件名(标准化)
export const THEME_CHANGE_EVENT = 'themeChanged'
// ===================== 本地存储方法(统一逻辑 + 强容错) =====================
export const saveThemeConfigToStorage = (themeConfig) => {
try {
const pureConfig = JSON.parse(JSON.stringify(themeConfig))
uni.setStorageSync(THEME_STORAGE_KEY, JSON.stringify(pureConfig))
} catch (e) {
console.error('【主题】保存到本地失败:', e)
}
}
export const getThemeConfigFromStorage = () => {
try {
const themeStr = uni.getStorageSync(THEME_STORAGE_KEY)
if (!themeStr) return THEME_PRESETS[DEFAULT_THEME_KEY]
const config = JSON.parse(themeStr)
const requiredFields = ['primaryColor', 'bgColor', 'textPrimary']
const isValid = requiredFields.every(field => config[field])
return isValid ? config : THEME_PRESETS[DEFAULT_THEME_KEY]
} catch (e) {
console.warn('【主题】读取本地配置失败,使用默认:', e)
return THEME_PRESETS[DEFAULT_THEME_KEY]
}
}
export const saveThemeKeyToStorage = (themeKey) => {
const validKey = THEME_PRESETS[themeKey] ? themeKey : DEFAULT_THEME_KEY
uni.setStorageSync('selectedTheme', validKey)
}
export const getThemeKeyFromStorage = () => {
const themeKey = uni.getStorageSync('selectedTheme')
return THEME_PRESETS[themeKey] ? themeKey : DEFAULT_THEME_KEY
}
// ===================== 后端接口相关(异步容错 + 无token降级) =====================
export const getThemeFromServer = async () => {
if (!getToken()) {
console.log('【主题】未登录,使用本地主题key')
return getThemeKeyFromStorage()
}
try {
const res = await getUserConfig()
const serverThemeKey = res?.theme
console.log('【主题】从后端获取到key', serverThemeKey)
const validKey = THEME_PRESETS[serverThemeKey] ? serverThemeKey : DEFAULT_THEME_KEY
saveThemeKeyToStorage(validKey)
saveThemeConfigToStorage(THEME_PRESETS[validKey])
return validKey
} catch (error) {
console.warn('【主题】从后端获取失败,使用本地缓存:', error)
return getThemeKeyFromStorage()
}
}
export const saveThemeToServer = async (themeKey) => {
if (!getToken() || !THEME_PRESETS[themeKey]) return false
try {
await editUserConfig({ theme: themeKey })
console.log(`【主题】同步到后端成功:${themeKey}`)
return true
} catch (error) {
console.error(`【主题】同步到后端失败:${themeKey}`, error)
return false
}
}
// ===================== 全局主题注入(核心:保证全项目数据一致) =====================
export const injectTheme = (themeKey) => {
const validKey = THEME_PRESETS[themeKey] ? themeKey : DEFAULT_THEME_KEY
const themeConfig = THEME_PRESETS[validKey]
console.log('【主题】注入全局配置:', validKey, themeConfig)
const appInstance = getApp({ allowDefault: true })
if (appInstance) {
appInstance.globalData = appInstance.globalData || {}
appInstance.globalData.themeConfig = JSON.parse(JSON.stringify(themeConfig))
appInstance.globalData.themeKey = validKey
}
saveThemeKeyToStorage(validKey)
saveThemeConfigToStorage(themeConfig)
uni.$emit(THEME_CHANGE_EVENT, {
key: validKey,
config: themeConfig
})
Promise.resolve().then(() => {
forceApplyTabBarTheme(themeConfig)
setTimeout(() => {
forceApplyTabBarTheme(themeConfig)
}, 100)
setTimeout(() => {
forceApplyTabBarTheme(themeConfig)
}, 300)
})
}
// ===================== 对外暴露的核心方法(一站式调用) =====================
export const switchTheme = async (themeKey, showToast = true) => {
const validKey = THEME_PRESETS[themeKey] ? themeKey : DEFAULT_THEME_KEY
try {
const serverPromise = saveThemeToServer(validKey)
injectTheme(validKey)
if (showToast) {
const serverSuccess = await serverPromise
uni.showToast({
title: serverSuccess
? `${THEME_PRESETS[validKey].name}切换成功`
: `${THEME_PRESETS[validKey].name}已生效,云端同步失败`,
icon: 'none',
duration: 1500
})
} else {
await serverPromise
}
return true
} catch (error) {
injectTheme(validKey)
if (showToast) {
uni.showToast({
title: `${THEME_PRESETS[validKey].name}已生效,云端同步失败`,
icon: 'none',
duration: 2000
})
}
return false
}
}
export const initTheme = async () => {
try {
const themeKey = await getThemeFromServer()
injectTheme(themeKey)
return JSON.parse(JSON.stringify(THEME_PRESETS[themeKey]))
} catch (error) {
console.error('【主题】初始化失败,使用默认配置:', error)
injectTheme(DEFAULT_THEME_KEY)
return JSON.parse(JSON.stringify(THEME_PRESETS[DEFAULT_THEME_KEY]))
}
}
+340
View File
@@ -0,0 +1,340 @@
// utils/themeGlobal.js
// 全局主题适配器 - 处理导航栏、TabBar 等全局UI主题
import { THEME_PRESETS, getThemeConfigFromStorage, THEME_CHANGE_EVENT } from './theme'
let isInitializing = false
let currentThemeConfig = null
/**
* 应用全局主题到导航栏和TabBar
* @param {Object} themeConfig 主题配置对象
*/
export const applyGlobalTheme = (themeConfig) => {
if (!themeConfig || isInitializing) return
try {
const app = getApp({ allowDefault: true })
if (app) {
app.globalData = app.globalData || {}
app.globalData.themeConfig = themeConfig
}
currentThemeConfig = themeConfig
setTimeout(() => {
applyNavigationBarTheme(themeConfig)
applyTabBarThemeSafe(themeConfig)
}, 100)
console.log('【全局主题】已应用到导航栏和TabBar')
} catch (e) {
console.error('【全局主题】应用失败:', e)
}
}
/**
* 安全应用TabBar主题(先检查是否在TabBar页面)
* @param {Object} themeConfig 主题配置
*/
const applyTabBarThemeSafe = (themeConfig) => {
try {
const pages = getCurrentPages()
if (pages.length === 0) {
console.warn('【全局主题】当前无页面,跳过TabBar设置')
return
}
const currentPage = pages[pages.length - 1]
const currentRoute = currentPage.route || ''
const tabBarPages = ['pages/index/index', 'pages/todo/todo', 'pages/bill/bill', 'pages/mine/mine']
if (tabBarPages.includes(currentRoute)) {
applyTabBarTheme(themeConfig)
} else {
console.log('【全局主题】当前非TabBar页面,跳过TabBar设置')
}
} catch (e) {
console.error('【全局主题】TabBar安全检测失败:', e)
}
}
/**
* 强制应用TabBar主题(先检查当前页面是否是TabBar页面)
* @param {Object} themeConfig 主题配置
*/
export const forceApplyTabBarTheme = (themeConfig) => {
try {
const pages = getCurrentPages()
if (pages.length === 0) {
console.log('【全局主题】当前无页面,跳过TabBar设置')
return
}
const currentPage = pages[pages.length - 1]
const currentRoute = currentPage.route || ''
const tabBarPages = ['pages/index/index', 'pages/todo/todo', 'pages/bill/bill', 'pages/mine/mine']
if (!tabBarPages.includes(currentRoute)) {
console.log('【全局主题】当前非TabBar页面(' + currentRoute + '),跳过TabBar设置')
return
}
uni.setTabBarStyle({
color: validateColor(themeConfig.textSecondary),
selectedColor: validateColor(themeConfig.primaryColor),
backgroundColor: validateColor(themeConfig.cardBgColor),
borderStyle: themeConfig.bgColor === '#1a202c' ? 'black' : 'white',
success: () => {
console.log('【全局主题】强制刷新TabBar样式成功')
},
fail: (err) => {
console.warn('【全局主题】强制刷新TabBar样式失败:', err)
}
})
} catch (e) {
console.error('【全局主题】强制刷新TabBar失败:', e)
}
}
/**
* 应用导航栏主题
* @param {Object} themeConfig 主题配置
*/
export const applyNavigationBarTheme = (themeConfig) => {
try {
const bgColor = validateColor(themeConfig.bgColor)
const isDark = isDarkColor(bgColor)
console.log('【全局主题】设置导航栏颜色 - bgColor:', bgColor, 'isDark:', isDark)
if (isDark) {
setNavBarWhite(bgColor)
} else {
setNavBarBlack(bgColor)
}
} catch (e) {
console.error('【全局主题】导航栏主题设置失败:', e)
}
}
/**
* 简化版本:直接应用导航栏主题(页面调用)
*/
export const refreshNavBarTheme = () => {
try {
if (!currentThemeConfig) {
currentThemeConfig = getThemeConfigFromStorage() || THEME_PRESETS['default']
}
applyNavigationBarTheme(currentThemeConfig)
} catch (e) {
console.error('【全局主题】刷新导航栏失败:', e)
}
}
/**
* 判断颜色是否为深色
* @param {string} color 颜色值
* @returns {boolean} 是否为深色
*/
const isDarkColor = (color) => {
if (!color || color.length < 7) return false
const hex = color.replace('#', '')
const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance <= 0.5
}
/**
* 设置导航栏为白色前景(用于深色背景)
*/
const setNavBarWhite = (bgColor) => {
try {
console.log('【全局主题】调用 setNavigationBarColor - frontColor: white, #ffffff:', bgColor)
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: bgColor,
success: () => {
console.log('【全局主题】导航栏颜色设置成功(白色前景)')
},
fail: (err) => {
console.warn('【全局主题】导航栏颜色设置失败(白色前景):', err)
}
})
} catch (e) {
console.error('【全局主题】setNavBarWhite 失败:', e)
}
}
/**
* 设置导航栏为黑色前景(用于浅色背景)
*/
const setNavBarBlack = (bgColor) => {
try {
console.log('【全局主题】调用 setNavigationBarColor - frontColor: #000000, bgColor:', bgColor)
uni.setNavigationBarColor({
frontColor: '#000000',
backgroundColor: bgColor,
success: () => {
console.log('【全局主题】导航栏颜色设置成功(黑色前景)')
},
fail: (err) => {
console.warn('【全局主题】导航栏颜色设置失败(黑色前景):', err)
console.log('【全局主题】尝试只设置背景色')
uni.setNavigationBarColor({
backgroundColor: bgColor,
success: () => {
console.log('【全局主题】导航栏背景色设置成功')
},
fail: (err2) => {
console.warn('【全局主题】导航栏背景色设置也失败:', err2)
}
})
}
})
} catch (e) {
console.error('【全局主题】setNavBarBlack 失败:', e)
}
}
/**
* 验证并修复颜色值
* @param {string} color 颜色值
* @returns {string} 验证后的颜色值
*/
const validateColor = (color) => {
if (!color) return '#ffffff'
if (!color.startsWith('#')) {
return '#' + color
}
if (color.length === 4) {
return '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3]
}
return color
}
/**
* 应用TabBar主题
* @param {Object} themeConfig 主题配置
*/
export const applyTabBarTheme = (themeConfig) => {
try {
uni.setTabBarStyle({
color: validateColor(themeConfig.textSecondary),
selectedColor: validateColor(themeConfig.primaryColor),
backgroundColor: validateColor(themeConfig.cardBgColor),
borderStyle: themeConfig.bgColor === '#1a202c' ? 'black' : 'white',
success: () => {
console.log('【全局主题】TabBar样式设置成功')
},
fail: (err) => {
console.warn('【全局主题】TabBar样式设置失败:', err)
}
})
} catch (e) {
console.error('【全局主题】TabBar主题设置失败:', e)
}
}
/**
* 根据背景色判断前景色(文字颜色)
* @param {string} bgColor 背景色
* @returns {string} '#000000' 或 '#ffffff'
*/
export const getFrontColor = (bgColor) => {
return isDarkColor(bgColor) ? '#ffffff' : '#000000'
}
/**
* 初始化全局主题(在App.vue或页面入口调用)
*/
export const initGlobalTheme = () => {
isInitializing = true
try {
const themeConfig = getThemeConfigFromStorage() || THEME_PRESETS['default']
currentThemeConfig = themeConfig
setTimeout(() => {
applyGlobalTheme(themeConfig)
isInitializing = false
}, 300)
} catch (e) {
console.error('【全局主题】初始化失败:', e)
isInitializing = false
}
}
/**
* 注册全局主题变更监听
*/
export const registerGlobalThemeListener = () => {
uni.$on(THEME_CHANGE_EVENT, (themeData) => {
try {
let newThemeConfig = null
if (themeData?.config) {
newThemeConfig = themeData.config
} else if (themeData?.key && THEME_PRESETS[themeData.key]) {
newThemeConfig = THEME_PRESETS[themeData.key]
}
if (newThemeConfig) {
applyGlobalTheme(newThemeConfig)
}
} catch (e) {
console.error('【全局主题】监听处理失败:', e)
}
})
console.log('【全局主题】已注册主题变更监听')
}
/**
* 移除全局主题监听
*/
export const unregisterGlobalThemeListener = () => {
uni.$off(THEME_CHANGE_EVENT)
console.log('【全局主题】已移除主题变更监听')
}
/**
* 在页面onShow中调用的混入(用于页面自动应用全局主题)
*/
export const globalThemeMixin = {
onShow() {
const themeConfig = getThemeConfigFromStorage() || THEME_PRESETS['default']
applyNavigationBarTheme(themeConfig)
}
}
/**
* 页面级导航栏刷新方法(简化调用)
*/
export const onPageShow = () => {
refreshNavBarTheme()
}
export default {
applyGlobalTheme,
applyNavigationBarTheme,
applyTabBarTheme,
forceApplyTabBarTheme,
initGlobalTheme,
registerGlobalThemeListener,
unregisterGlobalThemeListener,
getFrontColor,
refreshNavBarTheme,
onPageShow,
globalThemeMixin
}
+136
View File
@@ -0,0 +1,136 @@
/**
* 本地工具类(无需接口的功能)
*/
// ===================== 本地缓存清理(核心实现) =====================
/**
* 清理小程序缓存
* @returns {Promise<{size: string, success: boolean}>}
*/
export function clearCache() {
return new Promise((resolve) => {
try {
// 1. 清理本地存储(除了token,避免清理后直接退出)
const token = uni.getStorageSync('user_token') // 保留登录态
uni.clearStorageSync()
if (token) uni.setStorageSync('user_token', token) // 恢复token
// 2. 清理临时文件(如图片、视频缓存)
uni.getFileSystemManager().rmdir({
dirPath: `${wx.env.USER_DATA_PATH}/tmp`, // 微信小程序临时目录
recursive: true,
fail: () => {} // 目录不存在时忽略
})
// 3. 计算清理的缓存大小(模拟,真实场景可通过getStorageInfo获取)
uni.getStorageInfo({
success: (res) => {
const clearSize = (res.currentSize || 0).toFixed(2)
uni.showToast({ title: `清理完成,释放${clearSize}KB`, icon: 'success' })
resolve({
success: true,
size: `${clearSize}KB`
})
},
fail: () => {
uni.showToast({ title: '缓存清理成功', icon: 'success' })
resolve({ success: true, size: '未知' })
}
})
} catch (err) {
console.error('清理缓存失败:', err)
uni.showToast({ title: '缓存清理失败', icon: 'none' })
resolve({ success: false, size: '0KB' })
}
})
}
// ===================== 偏好设置管理(适配后端接口) =====================
// 偏好设置本地存储键名
const PREFERENCE_KEY = 'user_preferences'
/**
* 默认偏好设置(后端易适配的核心配置)
*/
const DEFAULT_PREFERENCES = {
theme: 'light', // 主题:light(浅色)/dark(深色)/auto(跟随系统)
listStyle: 'card', // 列表样式:card(卡片)/list(列表)
budgetWarn: true, // 预算超限提醒:true/false
autoSync: true, // 自动同步(后端接口适配:true-后台自动同步/false-手动同步)
remindTime: '08:00', // 待办提醒时间:HH:mm
monthBudget: 5000, // 月预算(数字,后端用于超限提醒)
currency: 'CNY' // 货币单位(后端统一适配)
}
/**
* 获取偏好设置(本地优先,无则返回默认值)
*/
export function getPreferences() {
try {
const localPrefs = uni.getStorageSync(PREFERENCE_KEY)
if (localPrefs && typeof localPrefs === 'object') {
// 合并默认值(避免新增配置项缺失)
return { ...DEFAULT_PREFERENCES, ...localPrefs }
}
return { ...DEFAULT_PREFERENCES }
} catch (err) {
console.error('获取偏好设置失败:', err)
return { ...DEFAULT_PREFERENCES }
}
}
/**
* 保存偏好设置(本地+可选同步到后端)
* @param {Object} prefs - 要修改的偏好设置
* @param {boolean} [syncToServer=true] - 是否同步到后端
* @returns {Promise<Object>} 最新的偏好设置
*/
export async function savePreferences(prefs, syncToServer = true) {
try {
// 1. 合并并保存到本地
const currentPrefs = getPreferences()
const newPrefs = { ...currentPrefs, ...prefs }
uni.setStorageSync(PREFERENCE_KEY, newPrefs)
// 2. 同步到后端(可选)
if (syncToServer) {
try {
// 导入request(避免循环依赖)
const request = (await import('./request.js')).default
await request.post('/settings/save', { preferences: newPrefs })
} catch (err) {
console.warn('偏好设置同步后端失败:', err)
// 同步失败不影响本地保存
}
}
uni.showToast({ title: '设置保存成功', icon: 'success' })
return newPrefs
} catch (err) {
console.error('保存偏好设置失败:', err)
uni.showToast({ title: '设置保存失败', icon: 'none' })
return getPreferences()
}
}
/**
* 重置偏好设置为默认值
*/
export function resetPreferences() {
uni.setStorageSync(PREFERENCE_KEY, DEFAULT_PREFERENCES)
uni.showToast({ title: '已恢复默认设置', icon: 'success' })
return { ...DEFAULT_PREFERENCES }
}
// ===================== 其他本地实用功能 =====================
/**
* 获取小程序版本信息
*/
export function getAppVersion() {
const systemInfo = uni.getSystemInfoSync()
return {
version: systemInfo.version || '未知',
platform: systemInfo.platform || '未知',
appName: '备忘录' // 替换为你的小程序名称
}
}