首次提交:初始化项目代码
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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%)'
|
||||
}
|
||||
}
|
||||
Vendored
+25
File diff suppressed because one or more lines are too long
+109
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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]))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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: '备忘录' // 替换为你的小程序名称
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user