diff --git a/miniapp/app.js b/miniapp/app.js index fb65c9f..24fb1f2 100644 --- a/miniapp/app.js +++ b/miniapp/app.js @@ -2,7 +2,8 @@ App({ globalData: { token: '', user: null, - apiUrl: 'http://localhost:8080' + apiUrl: 'http://localhost:8080', + themeMode: 'light', }, onLaunch() { const token = wx.getStorageSync('token') @@ -13,5 +14,47 @@ App({ if (user) this.globalData.user = user } catch(e) {} } - } + this.initTheme() + }, + initTheme() { + const stored = wx.getStorageSync('inchstep-theme') + if (stored === 'light' || stored === 'dark') { + this.globalData.themeMode = stored + } else { + try { + const sysInfo = wx.getSystemInfoSync() + this.globalData.themeMode = sysInfo.theme === 'dark' ? 'dark' : 'light' + } catch(e) {} + } + this.applyTheme(this.globalData.themeMode) + }, + applyTheme(mode) { + this.globalData.themeMode = mode + wx.setStorageSync('inchstep-theme', mode) + try { + wx.setNavigationBarColor({ + frontColor: mode === 'dark' ? '#ffffff' : '#000000', + backgroundColor: mode === 'dark' ? '#1C1C1E' : '#F5F5F7', + }) + wx.setTabBarStyle({ + backgroundColor: mode === 'dark' ? '#1C1C1E' : '#F5F5F7', + borderStyle: mode === 'dark' ? 'black' : 'white', + }) + } catch(e) {} + this.notifyPages(mode) + }, + toggleTheme() { + const newMode = this.globalData.themeMode === 'light' ? 'dark' : 'light' + this.applyTheme(newMode) + }, + notifyPages(mode) { + const pages = getCurrentPages() + pages.forEach(page => { + if (page.setData && typeof page.setThemeData === 'function') { + page.setThemeData(mode) + } else if (page.setData) { + page.setData({ themeMode: mode }) + } + }) + }, }) diff --git a/miniapp/app.wxss b/miniapp/app.wxss index 96cf8a0..6e69207 100644 --- a/miniapp/app.wxss +++ b/miniapp/app.wxss @@ -1,6 +1,40 @@ -page { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #F5F5F7; color: #1D1D1F; } -.card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.btn-primary { width: 100%; padding: 14px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); color: white; border: none; border-radius: 10px; font-size: 17px; font-weight: 600; text-align: center; } -.input-field { width: 100%; padding: 14px 16px; background: #f8f8fa; border: 1px solid #E5E5EA; border-radius: 10px; font-size: 17px; } +page { + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; + --color-primary: #FF6B35; + --color-primary-light: #FF8C5A; + --color-primary-gradient: linear-gradient(135deg, #FF6B35, #FF4D6D); + --color-accent: #7209B7; + --color-bg: #F5F5F7; + --color-card: #FFFFFF; + --color-text: #1D1D1F; + --color-text-secondary: #86868B; + --color-border: #E5E5EA; + --color-nav-bg: rgba(245,245,247,0.95); + --color-success: #34C759; + --color-warning: #FF9500; + --color-error: #FF3B30; + --color-input-bg: #f8f8fa; + --color-active-bg: #FFF0EB; + --color-divider: #F0F0F0; + background: var(--color-bg); + color: var(--color-text); +} +page[data-theme="dark"] { + --color-bg: #1C1C1E; + --color-card: #2C2C2E; + --color-text: #F5F5F7; + --color-text-secondary: #98989D; + --color-border: #38383A; + --color-nav-bg: rgba(28,28,30,0.95); + --color-accent: #BB86FC; + --color-error: #FF453A; + --color-input-bg: #3A3A3C; + --color-active-bg: #3A3A3C; + --color-divider: #38383A; +} + +.card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.btn-primary { width: 100%; padding: 14px; background: var(--color-primary-gradient); color: white; border: none; border-radius: 10px; font-size: 17px; font-weight: 600; text-align: center; } +.input-field { width: 100%; padding: 14px 16px; background: var(--color-input-bg); border: 1px solid var(--color-border); border-radius: 10px; font-size: 17px; } .page-title { font-size: 28px; font-weight: 600; padding: 20px 16px 8px; } -.page-subtitle { font-size: 15px; color: #86868B; padding: 0 16px 20px; } +.page-subtitle { font-size: 15px; color: var(--color-text-secondary); padding: 0 16px 20px; } \ No newline at end of file diff --git a/miniapp/components/checkin-card/index.wxss b/miniapp/components/checkin-card/index.wxss index a75a73a..00ad5ab 100644 --- a/miniapp/components/checkin-card/index.wxss +++ b/miniapp/components/checkin-card/index.wxss @@ -1,7 +1,7 @@ .card { display: flex; align-items: center; - background: white; + background: var(--color-card); border-radius: 12px; margin: 0 12px 8px; padding: 14px; @@ -12,7 +12,7 @@ .card-icon { width: 44px; height: 44px; border-radius: 12px; - background: #FFF0EB; + background: var(--color-active-bg); display: flex; align-items: center; justify-content: center; @@ -20,7 +20,7 @@ margin-right: 14px; } .card-body { flex: 1; display: flex; flex-direction: column; gap: 2px; } -.card-name { font-size: 16px; font-weight: 600; } -.card-meta { font-size: 13px; color: #86868B; } -.done-badge { font-size: 12px; color: #34C759; font-weight: 600; margin-top: 2px; } -.card-streak { font-size: 14px; color: #FF6B35; font-weight: 600; } \ No newline at end of file +.card-name { font-size: 16px; font-weight: 600; color: var(--color-text); } +.card-meta { font-size: 13px; color: var(--color-text-secondary); } +.done-badge { font-size: 12px; color: var(--color-success); font-weight: 600; margin-top: 2px; } +.card-streak { font-size: 14px; color: var(--color-primary); font-weight: 600; } \ No newline at end of file diff --git a/miniapp/components/nav-bar/index.js b/miniapp/components/nav-bar/index.js index cb723a7..924cbbe 100644 --- a/miniapp/components/nav-bar/index.js +++ b/miniapp/components/nav-bar/index.js @@ -1,6 +1,9 @@ +const app = getApp() + Component({ properties: { active: { type: String, value: 'home' }, + themeMode: { type: String, value: 'light' }, tabs: { type: Array, value: [ @@ -10,6 +13,11 @@ Component({ ], }, }, + lifetimes: { + attached() { + this.setData({ themeMode: app.globalData.themeMode }) + }, + }, methods: { onSwitch(e) { this.triggerEvent('switch', { key: e.currentTarget.dataset.key }) diff --git a/miniapp/components/nav-bar/index.wxml b/miniapp/components/nav-bar/index.wxml index dd650cd..8e4fab2 100644 --- a/miniapp/components/nav-bar/index.wxml +++ b/miniapp/components/nav-bar/index.wxml @@ -1,4 +1,4 @@ - + {{item.icon}} {{item.label}} diff --git a/miniapp/components/nav-bar/index.wxss b/miniapp/components/nav-bar/index.wxss index fc7c0eb..1701ab5 100644 --- a/miniapp/components/nav-bar/index.wxss +++ b/miniapp/components/nav-bar/index.wxss @@ -3,8 +3,8 @@ align-items: center; justify-content: space-around; padding: 8px 0 24px; - border-top: 0.5px solid #E5E5EA; - background: rgba(245,245,247,0.95); + border-top: 0.5px solid var(--color-border); + background: var(--color-nav-bg); position: fixed; bottom: 0; left: 0; @@ -20,5 +20,5 @@ padding: 4px; } .nav-icon { font-size: 22px; } -.nav-label { font-size: 10px; color: #86868B; } -.nav-label.active { color: #FF6B35; font-weight: 600; } \ No newline at end of file +.nav-label { font-size: 10px; color: var(--color-text-secondary); } +.nav-label.active { color: var(--color-primary); font-weight: 600; } \ No newline at end of file diff --git a/miniapp/pages/checkin/create/index.js b/miniapp/pages/checkin/create/index.js index cb565be..4e280c8 100644 --- a/miniapp/pages/checkin/create/index.js +++ b/miniapp/pages/checkin/create/index.js @@ -1,6 +1,10 @@ const api = require('../../../services/api') +const app = getApp() Page({ - data: { name: '', category: 'study', cycleType: 'daily', dailyGoal: '', privacy: 2, submitting: false }, + data: { name: '', category: 'study', cycleType: 'daily', dailyGoal: '', privacy: 2, submitting: false, themeMode: 'light' }, + onShow() { + this.setData({ themeMode: app.globalData.themeMode }) + }, onNameInput(e) { this.setData({ name: e.detail.value }) }, onGoalInput(e) { this.setData({ dailyGoal: e.detail.value }) }, setCategory(e) { this.setData({ category: e.currentTarget.dataset.cat }) }, diff --git a/miniapp/pages/checkin/create/index.wxml b/miniapp/pages/checkin/create/index.wxml index eaab741..c3844f7 100644 --- a/miniapp/pages/checkin/create/index.wxml +++ b/miniapp/pages/checkin/create/index.wxml @@ -1,4 +1,4 @@ - + 名称 * @@ -29,4 +29,4 @@ - + diff --git a/miniapp/pages/checkin/create/index.wxss b/miniapp/pages/checkin/create/index.wxss index 48c87a5..07c1def 100644 --- a/miniapp/pages/checkin/create/index.wxss +++ b/miniapp/pages/checkin/create/index.wxss @@ -1,11 +1,11 @@ .form { padding: 16px; } -.label { font-size: 13px; color: #86868B; margin-bottom: 6px; display: block; } +.label { font-size: 13px; color: var(--color-text-secondary); margin-bottom: 6px; display: block; } .category-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; } -.cat-item { padding: 12px 8px; border-radius: 10px; border: 2px solid #E5E5EA; background: #f8f8fa; text-align: center; font-size: 14px; } -.cat-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; } +.cat-item { padding: 12px 8px; border-radius: 10px; border: 2px solid var(--color-border); background: var(--color-input-bg); text-align: center; font-size: 14px; color: var(--color-text); } +.cat-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; } .cycle-row { display: flex; gap: 8px; } -.cycle-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid #E5E5EA; background: #f8f8fa; font-size: 14px; } -.cycle-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; } +.cycle-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid var(--color-border); background: var(--color-input-bg); font-size: 14px; color: var(--color-text); } +.cycle-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; } .privacy-row { display: flex; gap: 8px; } -.privacy-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid #E5E5EA; background: #f8f8fa; font-size: 14px; } -.privacy-item.active { border-color: #FF6B35; background: #FFF0EB; font-weight: 600; } +.privacy-item { padding: 8px 16px; border-radius: 8px; border: 1.5px solid var(--color-border); background: var(--color-input-bg); font-size: 14px; color: var(--color-text); } +.privacy-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; } \ No newline at end of file diff --git a/miniapp/pages/checkin/detail/index.js b/miniapp/pages/checkin/detail/index.js index 25979da..39b2da3 100644 --- a/miniapp/pages/checkin/detail/index.js +++ b/miniapp/pages/checkin/detail/index.js @@ -1,6 +1,10 @@ const api = require('../../../services/api') +const app = getApp() Page({ - data: { item: null, streak: 0, records: [], year: 0, month: 0, days: [], loading: true, showActions: false }, + data: { item: null, streak: 0, records: [], year: 0, month: 0, days: [], loading: true, showActions: false, themeMode: 'light' }, + onShow() { + this.setData({ themeMode: app.globalData.themeMode }) + }, onLoad(options) { const now = new Date() this.setData({ id: options.id, year: now.getFullYear(), month: now.getMonth() + 1 }) diff --git a/miniapp/pages/checkin/detail/index.wxml b/miniapp/pages/checkin/detail/index.wxml index 0ce2f5b..5ae57a2 100644 --- a/miniapp/pages/checkin/detail/index.wxml +++ b/miniapp/pages/checkin/detail/index.wxml @@ -1,5 +1,5 @@ 加载中... - + @@ -41,4 +41,4 @@ - + diff --git a/miniapp/pages/checkin/detail/index.wxss b/miniapp/pages/checkin/detail/index.wxss index 262acc9..5334c10 100644 --- a/miniapp/pages/checkin/detail/index.wxss +++ b/miniapp/pages/checkin/detail/index.wxss @@ -1,24 +1,24 @@ -.loading { text-align: center; padding: 40px; color: #86868B; } -.card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.loading { text-align: center; padding: 40px; color: var(--color-text-secondary); } +.card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } .action-btn { position: absolute; right: 16px; top: 12px; font-size: 20px; padding: 4px 8px; z-index: 10; } -.action-menu { position: absolute; right: 16px; top: 44px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 10; min-width: 120px; } -.action-item { padding: 12px 16px; font-size: 14px; border-bottom: 0.5px solid #E5E5EA; } +.action-menu { position: absolute; right: 16px; top: 44px; background: var(--color-card); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 10; min-width: 120px; } +.action-item { padding: 12px 16px; font-size: 14px; border-bottom: 0.5px solid var(--color-border); } .action-item:last-child { border-bottom: none; } .item-header { display: flex; align-items: center; gap: 12px; } -.item-icon { width: 52px; height: 52px; border-radius: 14px; background: #FFF0EB; display: flex; align-items: center; justify-content: center; font-size: 26px; } -.item-name { font-size: 18px; font-weight: 700; display: block; } -.item-meta { font-size: 13px; color: #86868B; display: block; margin-top: 2px; } -.item-goal { font-size: 13px; color: #86868B; display: block; margin-top: 2px; } -.stats-row { display: flex; justify-content: space-around; background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.item-icon { width: 52px; height: 52px; border-radius: 14px; background: var(--color-active-bg); display: flex; align-items: center; justify-content: center; font-size: 26px; } +.item-name { font-size: 18px; font-weight: 700; display: block; color: var(--color-text); } +.item-meta { font-size: 13px; color: var(--color-text-secondary); display: block; margin-top: 2px; } +.item-goal { font-size: 13px; color: var(--color-text-secondary); display: block; margin-top: 2px; } +.stats-row { display: flex; justify-content: space-around; background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } .stat { text-align: center; } -.stat-num { font-size: 28px; font-weight: 700; color: #FF6B35; display: block; } -.stat-lbl { font-size: 13px; color: #86868B; } +.stat-num { font-size: 28px; font-weight: 700; color: var(--color-primary); display: block; } +.stat-lbl { font-size: 13px; color: var(--color-text-secondary); } .calendar { padding: 16px; } -.cal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 16px; font-weight: 600; } +.cal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 16px; font-weight: 600; color: var(--color-text); } .cal-nav { font-size: 18px; padding: 4px 8px; } .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; } -.cal-weekday { font-size: 12px; color: #86868B; padding: 6px 0; } -.cal-day { padding: 8px 0; font-size: 14px; border-radius: 8px; position: relative; } +.cal-weekday { font-size: 12px; color: var(--color-text-secondary); padding: 6px 0; } +.cal-day { padding: 8px 0; font-size: 14px; border-radius: 8px; position: relative; color: var(--color-text); } .cal-day.checked { font-weight: 600; } -.cal-day.today { background: #FFF0EB; } -.cal-dot { width: 6px; height: 6px; border-radius: 3px; background: #34C759; margin: 2px auto 0; } +.cal-day.today { background: var(--color-active-bg); } +.cal-dot { width: 6px; height: 6px; border-radius: 3px; background: var(--color-success); margin: 2px auto 0; } \ No newline at end of file diff --git a/miniapp/pages/index/index.js b/miniapp/pages/index/index.js index a4a8309..ea9b667 100644 --- a/miniapp/pages/index/index.js +++ b/miniapp/pages/index/index.js @@ -2,10 +2,10 @@ const api = require('../../services/api') const app = getApp() Page({ - data: { user: null, items: [], streaks: {}, loading: true }, + data: { user: null, items: [], streaks: {}, loading: true, themeMode: 'light' }, onShow() { if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return } - this.setData({ user: app.globalData.user }) + this.setData({ user: app.globalData.user, themeMode: app.globalData.themeMode }) this.loadItems() }, onPullDownRefresh() { this.loadItems().then(() => wx.stopPullDownRefresh()) }, diff --git a/miniapp/pages/index/index.wxml b/miniapp/pages/index/index.wxml index 0a5e95d..5bdbe00 100644 --- a/miniapp/pages/index/index.wxml +++ b/miniapp/pages/index/index.wxml @@ -1,31 +1,32 @@ - - {{user.nickname ? user.nickname[0] : (user.phone ? user.phone.slice(-4) : '?')}} - - {{user.nickname || '用户'}} - 积分 {{user.points}} · {{items.filter(i=>i.status==='active').length}} 个活跃项 + + + {{user.nickname ? user.nickname[0] : (user.phone ? user.phone.slice(-4) : '?')}} + + {{user.nickname || '用户'}} + 积分 {{user.points}} · {{items.filter(i=>i.status==='active').length}} 个活跃项 + - - - - 今日打卡 - - - - - {{item.icon || '📋'}} - - {{item.name}} - {{item.category}} · {{item.cycleType}} + + 今日打卡 + - 🔥 {{streaks[item.id]}}天 - - - 📋 - 还没有打卡项 - 点击上方"创建"开始 + + {{item.icon || '📋'}} + + {{item.name}} + {{item.category}} · {{item.cycleType}} + + 🔥 {{streaks[item.id]}}天 + + + + 📋 + 还没有打卡项 + 点击上方"创建"开始 + 加载中... diff --git a/miniapp/pages/index/index.wxss b/miniapp/pages/index/index.wxss index a59b037..d871233 100644 --- a/miniapp/pages/index/index.wxss +++ b/miniapp/pages/index/index.wxss @@ -1,20 +1,19 @@ -.header-card { background: white; border-radius: 12px; margin: 12px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.avatar { width: 44px; height: 44px; border-radius: 22px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; font-weight: 600; flex-shrink: 0; } +.header-card { background: var(--color-card); border-radius: 12px; margin: 12px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.avatar { width: 44px; height: 44px; border-radius: 22px; background: var(--color-primary-gradient); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; font-weight: 600; flex-shrink: 0; } .header-info { flex: 1; } -.header-name { font-size: 16px; font-weight: 600; display: block; } -.header-sub { font-size: 13px; color: #86868B; } -.profile-btn { background: none; border: 1px solid #FF6B35; color: #FF6B35; font-size: 14px; padding: 4px 12px; border-radius: 8px; } +.header-name { font-size: 16px; font-weight: 600; display: block; color: var(--color-text); } +.header-sub { font-size: 13px; color: var(--color-text-secondary); } .section-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 16px 8px; } -.section-title { font-size: 20px; font-weight: 700; } -.create-btn { background: #FF6B35; color: white; border: none; font-size: 14px; padding: 6px 14px; border-radius: 8px; } -.checkin-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.card-icon { width: 44px; height: 44px; border-radius: 12px; background: #FFF0EB; display: flex; align-items: center; justify-content: center; font-size: 22px; } +.section-title { font-size: 20px; font-weight: 700; color: var(--color-text); } +.create-btn { background: var(--color-primary); color: white; border: none; font-size: 14px; padding: 6px 14px; border-radius: 8px; } +.checkin-card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 14px 16px; display: flex; align-items: center; gap: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.card-icon { width: 44px; height: 44px; border-radius: 12px; background: var(--color-active-bg); display: flex; align-items: center; justify-content: center; font-size: 22px; } .card-body { flex: 1; } -.card-name { font-size: 16px; font-weight: 600; display: block; } -.card-meta { font-size: 13px; color: #86868B; } -.card-streak { font-size: 14px; color: #FF6B35; font-weight: 600; } +.card-name { font-size: 16px; font-weight: 600; display: block; color: var(--color-text); } +.card-meta { font-size: 13px; color: var(--color-text-secondary); } +.card-streak { font-size: 14px; color: var(--color-primary); font-weight: 600; } .empty { text-align: center; padding: 60px 16px; } .empty-icon { font-size: 40px; } -.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; } -.empty-sub { font-size: 13px; color: #86868B; } -.loading { text-align: center; padding: 40px; color: #86868B; font-size: 15px; } +.empty-text { font-size: 15px; color: var(--color-text-secondary); display: block; margin-top: 8px; } +.empty-sub { font-size: 13px; color: var(--color-text-secondary); } +.loading { text-align: center; padding: 40px; color: var(--color-text-secondary); font-size: 15px; } diff --git a/miniapp/pages/login/index.js b/miniapp/pages/login/index.js index f63cb47..1c3d9de 100644 --- a/miniapp/pages/login/index.js +++ b/miniapp/pages/login/index.js @@ -2,7 +2,10 @@ const api = require('../../services/api') const app = getApp() Page({ - data: { phone: '', loading: false, error: '' }, + data: { phone: '', loading: false, error: '', themeMode: 'light' }, + onShow() { + this.setData({ themeMode: app.globalData.themeMode }) + }, onLoad() { if (app.globalData.token) wx.reLaunch({ url: '/pages/index/index' }) }, diff --git a/miniapp/pages/login/index.wxss b/miniapp/pages/login/index.wxss index 718de2d..80cde65 100644 --- a/miniapp/pages/login/index.wxss +++ b/miniapp/pages/login/index.wxss @@ -1,9 +1,9 @@ page { background: white; } .container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 32px; box-sizing: border-box; } .logo-area { text-align: center; margin-bottom: 48px; } -.logo { width: 80px; height: 80px; line-height: 80px; border-radius: 20px; background: linear-gradient(135deg, #FF6B35, #FF4D6D); text-align: center; font-size: 36px; margin: 0 auto 16px; } -.slogan { font-size: 28px; font-weight: 700; } -.sub-slogan { font-size: 15px; color: #86868B; margin-top: 4px; } -.error { font-size: 13px; color: #FF3B30; margin-bottom: 12px; text-align: center; } +.logo { width: 80px; height: 80px; line-height: 80px; border-radius: 20px; background: var(--color-primary-gradient); text-align: center; font-size: 36px; margin: 0 auto 16px; } +.slogan { font-size: 28px; font-weight: 700; color: var(--color-text); } +.sub-slogan { font-size: 15px; color: var(--color-text-secondary); margin-top: 4px; } +.error { font-size: 13px; color: var(--color-error); margin-bottom: 12px; text-align: center; } .btn-primary { margin-top: 12px; } -.tip { font-size: 13px; color: #86868B; margin-top: 16px; text-align: center; } +.tip { font-size: 13px; color: var(--color-text-secondary); margin-top: 16px; text-align: center; } \ No newline at end of file diff --git a/miniapp/pages/profile/index/index.js b/miniapp/pages/profile/index/index.js index cd14de6..1b66088 100644 --- a/miniapp/pages/profile/index/index.js +++ b/miniapp/pages/profile/index/index.js @@ -2,9 +2,10 @@ const api = require('../../../services/api') const app = getApp() Page({ - data: { user: null }, + data: { user: null, themeMode: 'light' }, onShow() { if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return } + this.setData({ themeMode: app.globalData.themeMode }) this.loadProfile() }, async loadProfile() { @@ -16,6 +17,10 @@ Page({ }, goEdit() { wx.navigateTo({ url: '/pages/profile/edit/index' }) }, goAchievements() { wx.navigateTo({ url: '/pages/achievements/index' }) }, + onThemeToggle(e) { + app.toggleTheme() + this.setData({ themeMode: app.globalData.themeMode }) + }, onLogout() { wx.showModal({ title: '确认退出', content: '确定要退出登录吗?', success: (res) => { if (res.confirm) { diff --git a/miniapp/pages/profile/index/index.wxml b/miniapp/pages/profile/index/index.wxml index fc31610..b10601c 100644 --- a/miniapp/pages/profile/index/index.wxml +++ b/miniapp/pages/profile/index/index.wxml @@ -1,16 +1,19 @@ - - {{(user.nickname?.[0]) || (user.phone?.slice(-4)) || '?'}} - {{user.nickname || '未设置昵称'}} - {{user.bio || '还没有个性签名'}} - + + + {{(user.nickname?.[0]) || (user.phone?.slice(-4)) || '?'}} + {{user.nickname || '未设置昵称'}} + {{user.bio || '还没有个性签名'}} + - - {{user?.points || 0}}积分 - --连续 - --徽章 - + + {{user?.points || 0}}积分 + --连续 + --徽章 + - - 成就徽章 - 退出登录 + + 成就徽章 + 深色模式 + 退出登录 + diff --git a/miniapp/pages/profile/index/index.wxss b/miniapp/pages/profile/index/index.wxss index 628c4a0..c510347 100644 --- a/miniapp/pages/profile/index/index.wxss +++ b/miniapp/pages/profile/index/index.wxss @@ -1,12 +1,12 @@ -.profile-card { background: white; border-radius: 12px; margin: 12px 16px; padding: 32px 16px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.profile-avatar { width: 72px; height: 72px; border-radius: 36px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 28px; color: white; font-weight: 600; margin: 0 auto 12px; } -.profile-name { font-size: 22px; font-weight: 700; display: block; } -.profile-bio { font-size: 14px; color: #86868B; margin-top: 4px; display: block; } -.stats-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; display: flex; justify-content: space-around; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.profile-card { background: var(--color-card); border-radius: 12px; margin: 12px 16px; padding: 32px 16px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.profile-avatar { width: 72px; height: 72px; border-radius: 36px; background: var(--color-primary-gradient); display: flex; align-items: center; justify-content: center; font-size: 28px; color: white; font-weight: 600; margin: 0 auto 12px; } +.profile-name { font-size: 22px; font-weight: 700; display: block; color: var(--color-text); } +.profile-bio { font-size: 14px; color: var(--color-text-secondary); margin-top: 4px; display: block; } +.stats-card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 16px; display: flex; justify-content: space-around; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } .stat-item { text-align: center; } -.stat-value { font-size: 24px; font-weight: 700; color: #FF6B35; display: block; } -.stat-label { font-size: 13px; color: #86868B; } -.menu-card { background: white; border-radius: 12px; margin: 8px 16px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.menu-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 0.5px solid #E5E5EA; font-size: 15px; } +.stat-value { font-size: 24px; font-weight: 700; color: var(--color-primary); display: block; } +.stat-label { font-size: 13px; color: var(--color-text-secondary); } +.menu-card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.menu-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 0.5px solid var(--color-border); font-size: 15px; color: var(--color-text); } .menu-item:last-child { border-bottom: none; } -.menu-arrow { font-size: 15px; color: #86868B; } +.menu-arrow { font-size: 15px; color: var(--color-text-secondary); } diff --git a/miniapp/pages/social/feed/index.js b/miniapp/pages/social/feed/index.js index 91a4e6e..934fb91 100644 --- a/miniapp/pages/social/feed/index.js +++ b/miniapp/pages/social/feed/index.js @@ -1,7 +1,11 @@ const api = require('../../../services/api') +const app = getApp() Page({ - data: { posts: [], page: 1, loading: true, showCreate: false, content: '' }, - onShow() { this.loadFeed() }, + data: { posts: [], page: 1, loading: true, showCreate: false, content: '', themeMode: 'light' }, + onShow() { + this.setData({ themeMode: app.globalData.themeMode }) + this.loadFeed() + }, onPullDownRefresh() { this.setData({ page: 1 }); this.loadFeed().then(() => wx.stopPullDownRefresh()) }, async loadFeed() { try { diff --git a/miniapp/pages/social/feed/index.wxml b/miniapp/pages/social/feed/index.wxml index fc489b5..75bb07d 100644 --- a/miniapp/pages/social/feed/index.wxml +++ b/miniapp/pages/social/feed/index.wxml @@ -1,3 +1,4 @@ + @@ -24,6 +25,7 @@ - + + diff --git a/miniapp/pages/social/feed/index.wxss b/miniapp/pages/social/feed/index.wxss index c6fd44d..6ff6f49 100644 --- a/miniapp/pages/social/feed/index.wxss +++ b/miniapp/pages/social/feed/index.wxss @@ -1,22 +1,22 @@ -.create-post-btn { display: block; margin: 12px 16px; background: white; border-radius: 12px; padding: 16px; color: #86868B; font-size: 15px; text-align: left; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.post-card { background: white; border-radius: 12px; margin: 8px 16px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.create-post-btn { display: block; margin: 12px 16px; background: var(--color-card); border-radius: 12px; padding: 16px; color: var(--color-text-secondary); font-size: 15px; text-align: left; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.post-card { background: var(--color-card); border-radius: 12px; margin: 8px 16px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } .post-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } -.post-avatar { width: 36px; height: 36px; border-radius: 18px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; } -.post-user { font-size: 14px; font-weight: 600; display: block; } -.post-time { font-size: 12px; color: #86868B; } -.post-content { font-size: 15px; line-height: 1.5; display: block; } -.post-actions { display: flex; gap: 20px; padding-top: 8px; margin-top: 8px; border-top: 0.5px solid #F0F0F0; } -.action-btn { font-size: 13px; color: #86868B; } -.load-more { text-align: center; padding: 16px; color: #86868B; font-size: 14px; } +.post-avatar { width: 36px; height: 36px; border-radius: 18px; background: var(--color-primary-gradient); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; } +.post-user { font-size: 14px; font-weight: 600; display: block; color: var(--color-text); } +.post-time { font-size: 12px; color: var(--color-text-secondary); } +.post-content { font-size: 15px; line-height: 1.5; display: block; color: var(--color-text); } +.post-actions { display: flex; gap: 20px; padding-top: 8px; margin-top: 8px; border-top: 0.5px solid var(--color-divider); } +.action-btn { font-size: 13px; color: var(--color-text-secondary); } +.load-more { text-align: center; padding: 16px; color: var(--color-text-secondary); font-size: 14px; } .empty { text-align: center; padding: 60px 16px; font-size: 40px; } -.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; } -.loading { text-align: center; padding: 40px; color: #86868B; } +.empty-text { font-size: 15px; color: var(--color-text-secondary); display: block; margin-top: 8px; } +.loading { text-align: center; padding: 40px; color: var(--color-text-secondary); } .modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: flex-end; z-index: 100; } -.modal-content { background: white; border-radius: 16px 16px 0 0; width: 100%; padding: 20px 16px; } -.modal-title { font-size: 17px; font-weight: 600; text-align: center; display: block; margin-bottom: 12px; } -.modal-textarea { width: 100%; border: 1px solid #E5E5EA; border-radius: 10px; padding: 12px; font-size: 15px; min-height: 120px; box-sizing: border-box; } +.modal-content { background: var(--color-card); border-radius: 16px 16px 0 0; width: 100%; padding: 20px 16px; } +.modal-title { font-size: 17px; font-weight: 600; text-align: center; display: block; margin-bottom: 12px; color: var(--color-text); } +.modal-textarea { width: 100%; border: 1px solid var(--color-border); border-radius: 10px; padding: 12px; font-size: 15px; min-height: 120px; box-sizing: border-box; background: var(--color-input-bg); color: var(--color-text); } .modal-actions { display: flex; gap: 12px; margin-top: 12px; } -.cancel-btn { flex: 1; padding: 12px; background: #f8f8fa; border: 1px solid #E5E5EA; border-radius: 10px; font-size: 15px; text-align: center; } -.submit-btn { flex: 1; padding: 12px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; text-align: center; } -.submit-btn[disabled] { opacity: 0.5; } +.cancel-btn { flex: 1; padding: 12px; background: var(--color-input-bg); border: 1px solid var(--color-border); border-radius: 10px; font-size: 15px; text-align: center; color: var(--color-text); } +.submit-btn { flex: 1; padding: 12px; background: var(--color-primary-gradient); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; text-align: center; } +.submit-btn[disabled] { opacity: 0.5; } \ No newline at end of file diff --git a/miniapp/pages/social/friends/index.js b/miniapp/pages/social/friends/index.js index f92e91c..3fd1b0e 100644 --- a/miniapp/pages/social/friends/index.js +++ b/miniapp/pages/social/friends/index.js @@ -1,7 +1,11 @@ const api = require('../../../services/api') +const app = getApp() Page({ - data: { friends: [], addPhone: '', loading: true }, - onShow() { this.loadFriends() }, + data: { friends: [], addPhone: '', loading: true, themeMode: 'light' }, + onShow() { + this.setData({ themeMode: app.globalData.themeMode }) + this.loadFriends() + }, async loadFriends() { try { const f = await api.get('/api/social/friends'); this.setData({ friends: f || [] }) } catch(e) {} this.setData({ loading: false }) diff --git a/miniapp/pages/social/friends/index.wxml b/miniapp/pages/social/friends/index.wxml index 1f260f4..607a997 100644 --- a/miniapp/pages/social/friends/index.wxml +++ b/miniapp/pages/social/friends/index.wxml @@ -1,12 +1,14 @@ - - - - + + + + + -好友列表 ({{friends.length}}) - - {{item.nickname?.[0] || '?'}} - {{item.nickname || '用户'}} - + 好友列表 ({{friends.length}}) + + {{item.nickname?.[0] || '?'}} + {{item.nickname || '用户'}} + + + 还没有好友 -还没有好友 diff --git a/miniapp/pages/social/friends/index.wxss b/miniapp/pages/social/friends/index.wxss index 1e9d7bb..4560bfa 100644 --- a/miniapp/pages/social/friends/index.wxss +++ b/miniapp/pages/social/friends/index.wxss @@ -1,10 +1,10 @@ .add-section { display: flex; gap: 8px; padding: 12px 16px; align-items: center; } -.add-btn { padding: 10px 20px; background: #FF6B35; color: white; border: none; border-radius: 10px; font-size: 14px; } -.add-btn[disabled] { background: #E5E5EA; } -.section-title { font-size: 15px; color: #86868B; padding: 16px 16px 8px; display: block; } -.friend-item { display: flex; align-items: center; gap: 10px; background: white; margin: 4px 16px; padding: 12px 16px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } -.friend-avatar { width: 36px; height: 36px; border-radius: 18px; background: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; } -.friend-name { flex: 1; font-size: 15px; } -.remove-btn { background: none; border: none; color: #FF3B30; font-size: 13px; padding: 4px 8px; } +.add-btn { padding: 10px 20px; background: var(--color-primary); color: white; border: none; border-radius: 10px; font-size: 14px; } +.add-btn[disabled] { background: var(--color-border); } +.section-title { font-size: 15px; color: var(--color-text-secondary); padding: 16px 16px 8px; display: block; } +.friend-item { display: flex; align-items: center; gap: 10px; background: var(--color-card); margin: 4px 16px; padding: 12px 16px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); } +.friend-avatar { width: 36px; height: 36px; border-radius: 18px; background: var(--color-primary-gradient); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; } +.friend-name { flex: 1; font-size: 15px; color: var(--color-text); } +.remove-btn { background: none; border: none; color: var(--color-error); font-size: 13px; padding: 4px 8px; } .empty { text-align: center; padding: 40px; } -.empty-text { font-size: 15px; color: #86868B; } +.empty-text { font-size: 15px; color: var(--color-text-secondary); } \ No newline at end of file diff --git a/mobile/App.tsx b/mobile/App.tsx index 01831a9..1e369f9 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -4,6 +4,7 @@ import { NavigationContainer } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import { SafeAreaProvider } from 'react-native-safe-area-context' import { AuthProvider, useAuth } from './src/components/AuthProvider' +import ThemeProvider, { useTheme } from './src/components/ThemeProvider' import { getStoredToken } from './src/services/api' import LoginScreen from './src/screens/LoginScreen' import MainTabs from './src/navigation/MainTabs' @@ -11,9 +12,15 @@ import type { RootStackParamList } from './src/navigation/types' const RootStack = createNativeStackNavigator() +function AppStatusBar() { + const { isDark } = useTheme() + return +} + function AuthGate() { const { user, loading } = useAuth() const [initialRoute, setInitialRoute] = useState<'Login' | 'MainTabs' | null>(null) + const { colors } = useTheme() useEffect(() => { if (!loading) { @@ -25,8 +32,8 @@ function AuthGate() { if (!initialRoute) { return ( - - 加载中... + + 加载中... ) } @@ -43,10 +50,12 @@ export default function App() { return ( - - - - + + + + + + ) diff --git a/mobile/src/components/ThemeProvider.tsx b/mobile/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..853dedd --- /dev/null +++ b/mobile/src/components/ThemeProvider.tsx @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' +import { useColorScheme } from 'react-native' +import AsyncStorage from '@react-native-async-storage/async-storage' +import type { ThemeMode, IThemeColors } from '../constants/theme' +import { THEME } from '../constants/theme' + +const STORAGE_KEY = 'inchstep-theme' + +interface IThemeContext { + mode: ThemeMode + colors: IThemeColors + toggle: () => void + isDark: boolean +} + +const ThemeContext = createContext({ + mode: 'light', + colors: THEME.light, + toggle: () => {}, + isDark: false, +}) + +export function useTheme() { + return useContext(ThemeContext) +} + +export default function ThemeProvider({ children }: { children: React.ReactNode }) { + const systemScheme = useColorScheme() + const [mode, setModeState] = useState('light') + const [loaded, setLoaded] = useState(false) + + useEffect(() => { + AsyncStorage.getItem(STORAGE_KEY).then((stored) => { + if (stored === 'light' || stored === 'dark') { + setModeState(stored) + } else if (systemScheme === 'dark') { + setModeState('dark') + } + setLoaded(true) + }) + }, [systemScheme]) + + useEffect(() => { + if (loaded) { + AsyncStorage.setItem(STORAGE_KEY, mode) + } + }, [mode, loaded]) + + const toggle = useCallback(() => { + setModeState(prev => (prev === 'light' ? 'dark' : 'light')) + }, []) + + if (!loaded) { + return <>{children} + } + + return ( + + {children} + + ) +} diff --git a/mobile/src/constants/theme.ts b/mobile/src/constants/theme.ts new file mode 100644 index 0000000..58d5c92 --- /dev/null +++ b/mobile/src/constants/theme.ts @@ -0,0 +1,52 @@ +import { useColorScheme } from 'react-native' + +export type ThemeMode = 'light' | 'dark' + +export interface IThemeColors { + primary: string + primaryLight: string + primaryGradient: string + accent: string + bg: string + card: string + text: string + textSecondary: string + border: string + navBg: string + success: string + warning: string + error: string +} + +export const THEME: Record = { + light: { + primary: '#FF6B35', + primaryLight: '#FF8C5A', + primaryGradient: ['#FF6B35', '#FF4D6D'] as unknown as string, + accent: '#7209B7', + bg: '#F5F5F7', + card: '#FFFFFF', + text: '#1D1D1F', + textSecondary: '#86868B', + border: '#E5E5EA', + navBg: 'rgba(245,245,247,0.95)', + success: '#34C759', + warning: '#FF9500', + error: '#FF3B30', + }, + dark: { + primary: '#FF6B35', + primaryLight: '#FF8C5A', + primaryGradient: ['#FF6B35', '#FF4D6D'] as unknown as string, + accent: '#BB86FC', + bg: '#1C1C1E', + card: '#2C2C2E', + text: '#F5F5F7', + textSecondary: '#98989D', + border: '#38383A', + navBg: 'rgba(28,28,30,0.95)', + success: '#34C759', + warning: '#FF9500', + error: '#FF453A', + }, +} diff --git a/mobile/src/navigation/MainTabs.tsx b/mobile/src/navigation/MainTabs.tsx index f0449a7..5394d1c 100644 --- a/mobile/src/navigation/MainTabs.tsx +++ b/mobile/src/navigation/MainTabs.tsx @@ -1,6 +1,7 @@ import React from 'react' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import Ionicons from '@expo/vector-icons/Ionicons' +import { useTheme } from '../components/ThemeProvider' import type { MainTabParamList } from './types' import HomeStack from './HomeStack' import FeedStack from './FeedStack' @@ -9,15 +10,17 @@ import ProfileStack from './ProfileStack' const Tab = createBottomTabNavigator() export default function MainTabs() { + const { colors, isDark } = useTheme() + return ( >() const { user } = useAuth() + const { colors } = useTheme() const [items, setItems] = useState([]) const [streaks, setStreaks] = useState>({}) const [todayStatus, setTodayStatus] = useState>({}) @@ -34,37 +37,39 @@ export default function HomeScreen() { const activeItems = items.filter(i => i.status === 'active') + const styles = createStyles(colors) + return ( - { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}> - - {user?.nickname?.[0] || user?.phone?.slice(-4) || '?'} + { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}> + + {user?.nickname?.[0] || user?.phone?.slice(-4) || '?'} - {user?.nickname || '用户'} - 积分 {user?.points} · {activeItems.length} 个活跃项 + {user?.nickname || '用户'} + 积分 {user?.points} · {activeItems.length} 个活跃项 - - 今日打卡 - navigation.navigate('CreateCheckIn')}>+ 创建 + + 今日打卡 + navigation.navigate('CreateCheckIn')}>+ 创建 {activeItems.length === 0 ? ( - + 📋 - 还没有打卡项 - 点击上方"创建"开始 + 还没有打卡项 + 点击上方"创建"开始 ) : activeItems.map((item: any) => ( navigation.navigate('CheckInDetail', { itemId: item.id })}> - - {item.icon || '📋'} + + {item.icon || '📋'} - {item.name} - {item.category} - {todayStatus[item.id] && ✓ 已打卡} + {item.name} + {item.category} + {todayStatus[item.id] && ✓ 已打卡} - {streaks[item.id] !== undefined && 🔥 {streaks[item.id]}天} + {streaks[item.id] !== undefined && 🔥 {streaks[item.id]}天} ))} @@ -72,22 +77,24 @@ export default function HomeScreen() { ) } -const s = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#F5F5F7' }, - header: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, margin: 12, padding: 14, shadowOpacity: 0.04, shadowRadius: 3, elevation: 1 }, - avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center' }, - avatarText: { fontSize: 18, color: 'white', fontWeight: '600' }, - headerName: { fontSize: 16, fontWeight: '600' }, - headerSub: { fontSize: 13, color: '#86868B' }, - sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 }, - sectionTitle: { fontSize: 20, fontWeight: '700' }, - createBtn: { color: '#FF6B35', fontSize: 14, fontWeight: '600' }, - card: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, marginHorizontal: 12, marginBottom: 8, padding: 14, shadowOpacity: 0.04, elevation: 1 }, - cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: '#FFF0EB', justifyContent: 'center', alignItems: 'center', marginRight: 14 }, - cardName: { fontSize: 16, fontWeight: '600' }, - cardMeta: { fontSize: 13, color: '#86868B', marginTop: 2 }, - doneText: { fontSize: 12, color: '#34C759', fontWeight: '600', marginTop: 2 }, - streak: { fontSize: 14, color: '#FF6B35', fontWeight: '600' }, - empty: { alignItems: 'center', padding: 60, marginTop: 20 }, - emptyText: { fontSize: 15, color: '#86868B', marginTop: 8 }, -}) +function createStyles(c: IThemeColors) { + return StyleSheet.create({ + container: { flex: 1, backgroundColor: c.bg }, + header: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.card, borderRadius: 12, margin: 12, padding: 14, shadowOpacity: 0.04, shadowRadius: 3, elevation: 1 }, + avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: c.primary, justifyContent: 'center', alignItems: 'center' }, + avatarText: { fontSize: 18, color: 'white', fontWeight: '600' }, + headerName: { fontSize: 16, fontWeight: '600', color: c.text }, + headerSub: { fontSize: 13, color: c.textSecondary }, + sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 }, + sectionTitle: { fontSize: 20, fontWeight: '700', color: c.text }, + createBtn: { color: c.primary, fontSize: 14, fontWeight: '600' }, + card: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.card, borderRadius: 12, marginHorizontal: 12, marginBottom: 8, padding: 14, shadowOpacity: 0.04, elevation: 1 }, + cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: c.primaryLight + '20', justifyContent: 'center', alignItems: 'center', marginRight: 14 }, + cardName: { fontSize: 16, fontWeight: '600', color: c.text }, + cardMeta: { fontSize: 13, color: c.textSecondary, marginTop: 2 }, + doneText: { fontSize: 12, color: c.success, fontWeight: '600', marginTop: 2 }, + streak: { fontSize: 14, color: c.primary, fontWeight: '600' }, + empty: { alignItems: 'center', padding: 60, marginTop: 20 }, + emptyText: { fontSize: 15, color: c.textSecondary, marginTop: 8 }, + }) +} diff --git a/mobile/src/screens/ProfileScreen.tsx b/mobile/src/screens/ProfileScreen.tsx index 677e904..9626162 100644 --- a/mobile/src/screens/ProfileScreen.tsx +++ b/mobile/src/screens/ProfileScreen.tsx @@ -1,12 +1,14 @@ import React from 'react' -import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert } from 'react-native' +import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert, Switch } from 'react-native' import { useNavigation } from '@react-navigation/native' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useAuth } from '../components/AuthProvider' +import { useTheme } from '../components/ThemeProvider' import type { ProfileStackParamList } from '../navigation/types' export default function ProfileScreen() { const { user, logout } = useAuth() + const { colors, isDark, toggle } = useTheme() const navigation = useNavigation>() const handleLogout = () => { @@ -17,23 +19,29 @@ export default function ProfileScreen() { } return ( - - + + {user?.nickname?.[0] || user?.phone?.slice(-4) || '?'} - {user?.nickname || '未设置昵称'} - {user?.bio || '还没有个性签名'} + {user?.nickname || '未设置昵称'} + {user?.bio || '还没有个性签名'} - - {user?.points || 0}积分 - --连续 - --徽章 + + {user?.points || 0}积分 + --连续 + --徽章 - - navigation.navigate('Achievements')}>成就徽章 + + navigation.navigate('Achievements')}> + 成就徽章 + + + 深色模式 + + - 退出登录 + 退出登录 @@ -41,15 +49,15 @@ export default function ProfileScreen() { } const styles = StyleSheet.create({ - profileCard: { backgroundColor: 'white', borderRadius: 12, margin: 12, padding: 32, alignItems: 'center', shadowOpacity: 0.04, elevation: 1 }, + profileCard: { borderRadius: 12, margin: 12, padding: 32, alignItems: 'center', shadowOpacity: 0.04, elevation: 1 }, avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center', marginBottom: 12 }, name: { fontSize: 22, fontWeight: '700' }, - bio: { fontSize: 14, color: '#86868B', marginTop: 4 }, - statsRow: { flexDirection: 'row', justifyContent: 'space-around', backgroundColor: 'white', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 }, + bio: { fontSize: 14, marginTop: 4 }, + statsRow: { flexDirection: 'row', justifyContent: 'space-around', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 }, stat: { alignItems: 'center' }, - statVal: { fontSize: 24, fontWeight: '700', color: '#FF6B35' }, - statLbl: { fontSize: 13, color: '#86868B' }, - menu: { backgroundColor: 'white', borderRadius: 12, margin: 8, overflow: 'hidden', shadowOpacity: 0.04, elevation: 1 }, - menuItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 0.5, borderBottomColor: '#E5E5EA' }, - arrow: { fontSize: 15, color: '#86868B' }, + statVal: { fontSize: 24, fontWeight: '700' }, + statLbl: { fontSize: 13 }, + menu: { borderRadius: 12, margin: 8, overflow: 'hidden', shadowOpacity: 0.04, elevation: 1 }, + menuItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 0.5 }, + arrow: { fontSize: 15 }, }) diff --git a/server/.env.example b/server/.env.development similarity index 100% rename from server/.env.example rename to server/.env.development diff --git a/server/api.exe b/server/api.exe index db1dd9c..b685e8b 100644 Binary files a/server/api.exe and b/server/api.exe differ diff --git a/server/internal/config/config.go b/server/internal/config/config.go index c908568..4b12fb9 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -21,17 +21,25 @@ type Config struct { S3Bucket string } -// Load 加载配置(优先读取环境变量,支持 .env 文件) -// 查找顺序:可执行文件同目录 → 当前目录 → 环境变量 +// Load 加载配置(支持 .env 文件) +// 1. 先加载 .env(基础配置,本地开发友好) +// 2. 再加载 .env.(环境覆盖,由 ENV 环境变量指定) +// 搜索路径:可执行文件目录 → 当前目录 → server/ 子目录 +// 优先级(高 → 低):环境覆盖 → .env → 系统环境变量 → 默认值 func Load() *Config { - // 从可执行文件所在目录加载 .env(宝塔场景) - if exe, err := os.Executable(); err == nil { - exeDir := filepath.Dir(exe) - _ = godotenv.Load(filepath.Join(exeDir, ".env")) + loadEnv := func(name string) { + if exe, err := os.Executable(); err == nil { + _ = godotenv.Overload(filepath.Join(filepath.Dir(exe), name)) + } + _ = godotenv.Overload(name) + _ = godotenv.Overload(filepath.Join(".", "server", name)) } - // 从当前目录加载 .env(本地开发场景) - _ = godotenv.Load() + loadEnv(".env") + + if env := os.Getenv("ENV"); env != "" { + loadEnv(".env." + env) + } c := &Config{ Port: getEnv("PORT", "8080"), diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 964a00f..bfb3e42 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -4,31 +4,44 @@ padding: 0; } - :root { - --color-primary: #FF6B35; - --color-primary-light: #FF8C5A; - --color-primary-gradient: linear-gradient(135deg, #FF6B35, #FF4D6D); - --color-accent: #7209B7; - --color-bg: #F5F5F7; - --color-card: #FFFFFF; - --color-text: #1D1D1F; - --color-text-secondary: #86868B; - --color-border: #E5E5EA; - --color-success: #34C759; - --color-warning: #FF9500; - --color-error: #FF3B30; - --radius-card: 12px; - --radius-button: 10px; - --font-base: -apple-system, BlinkMacSystemFont, 'SF Pro', 'PingFang SC', sans-serif; - } - - body { - font-family: var(--font-base); - background: var(--color-bg); - color: var(--color-text); - line-height: 1.5; - -webkit-font-smoothing: antialiased; - } +:root { + --color-primary: #FF6B35; + --color-primary-light: #FF8C5A; + --color-primary-gradient: linear-gradient(135deg, #FF6B35, #FF4D6D); + --color-accent: #7209B7; + --color-bg: #F5F5F7; + --color-card: #FFFFFF; + --color-text: #1D1D1F; + --color-text-secondary: #86868B; + --color-border: #E5E5EA; + --color-nav-bg: rgba(245,245,247,0.92); + --color-success: #34C759; + --color-warning: #FF9500; + --color-error: #FF3B30; + --radius-card: 12px; + --radius-button: 10px; + --font-base: -apple-system, BlinkMacSystemFont, 'SF Pro', 'PingFang SC', sans-serif; + } + + [data-theme="dark"] { + --color-bg: #1C1C1E; + --color-card: #2C2C2E; + --color-text: #F5F5F7; + --color-text-secondary: #98989D; + --color-border: #38383A; + --color-nav-bg: rgba(28,28,30,0.92); + --color-accent: #BB86FC; + --color-error: #FF453A; + } + + body { + font-family: var(--font-base); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + transition: background 0.3s, color 0.3s; + } /* iOS 风格列表 */ .ios-list { diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index be08690..5a3ba41 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,15 +1,18 @@ - import type { Metadata } from 'next' - import './globals.css' - - export const metadata: Metadata = { - title: '寸进 — 日日皆有成长', - description: '寸进(InchStep)综合成长打卡平台', - } - - export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) - } +import type { Metadata } from 'next' +import './globals.css' +import ThemeProvider from '@/components/ThemeProvider' + +export const metadata: Metadata = { + title: '寸进 — 日日皆有成长', + description: '寸进(InchStep)综合成长打卡平台', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index bd5ff0e..9308591 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -49,7 +49,7 @@ function Dashboard() { return ( <> - +
-
- {showBack && ( - - )} - {showLogout && user && ( - - )} -
- - {title} - -
- {showLogout && ( - - )} -
- - ) - } + +import { useRouter } from 'next/navigation' +import { useTheme } from './ThemeProvider' +import { useAuth } from './AuthProvider' + +interface NavBarProps { + title: string + showBack?: boolean + showLogout?: boolean +} + +export default function NavBar({ title, showBack = false, showLogout = false }: NavBarProps) { + const { logout, user } = useAuth() + const router = useRouter() + const { mode, toggle } = useTheme() + + return ( + + ) +} diff --git a/web/src/components/TabBar.tsx b/web/src/components/TabBar.tsx index b3f341a..1df21ba 100644 --- a/web/src/components/TabBar.tsx +++ b/web/src/components/TabBar.tsx @@ -22,11 +22,11 @@ export default function TabBar() { return (