提交修改
This commit is contained in:
+44
-1
@@ -2,7 +2,8 @@ App({
|
|||||||
globalData: {
|
globalData: {
|
||||||
token: '',
|
token: '',
|
||||||
user: null,
|
user: null,
|
||||||
apiUrl: 'http://localhost:8080'
|
apiUrl: 'http://localhost:8080',
|
||||||
|
themeMode: 'light',
|
||||||
},
|
},
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
const token = wx.getStorageSync('token')
|
const token = wx.getStorageSync('token')
|
||||||
@@ -13,5 +14,47 @@ App({
|
|||||||
if (user) this.globalData.user = user
|
if (user) this.globalData.user = user
|
||||||
} catch(e) {}
|
} 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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
+39
-5
@@ -1,6 +1,40 @@
|
|||||||
page { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #F5F5F7; color: #1D1D1F; }
|
page {
|
||||||
.card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||||
.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; }
|
--color-primary: #FF6B35;
|
||||||
.input-field { width: 100%; padding: 14px 16px; background: #f8f8fa; border: 1px solid #E5E5EA; border-radius: 10px; font-size: 17px; }
|
--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-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; }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: var(--color-card);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin: 0 12px 8px;
|
margin: 0 12px 8px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
.card-icon {
|
.card-icon {
|
||||||
width: 44px; height: 44px;
|
width: 44px; height: 44px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #FFF0EB;
|
background: var(--color-active-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
margin-right: 14px;
|
margin-right: 14px;
|
||||||
}
|
}
|
||||||
.card-body { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
.card-body { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||||
.card-name { font-size: 16px; font-weight: 600; }
|
.card-name { font-size: 16px; font-weight: 600; color: var(--color-text); }
|
||||||
.card-meta { font-size: 13px; color: #86868B; }
|
.card-meta { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.done-badge { font-size: 12px; color: #34C759; font-weight: 600; margin-top: 2px; }
|
.done-badge { font-size: 12px; color: var(--color-success); font-weight: 600; margin-top: 2px; }
|
||||||
.card-streak { font-size: 14px; color: #FF6B35; font-weight: 600; }
|
.card-streak { font-size: 14px; color: var(--color-primary); font-weight: 600; }
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
const app = getApp()
|
||||||
|
|
||||||
Component({
|
Component({
|
||||||
properties: {
|
properties: {
|
||||||
active: { type: String, value: 'home' },
|
active: { type: String, value: 'home' },
|
||||||
|
themeMode: { type: String, value: 'light' },
|
||||||
tabs: {
|
tabs: {
|
||||||
type: Array,
|
type: Array,
|
||||||
value: [
|
value: [
|
||||||
@@ -10,6 +13,11 @@ Component({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
lifetimes: {
|
||||||
|
attached() {
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSwitch(e) {
|
onSwitch(e) {
|
||||||
this.triggerEvent('switch', { key: e.currentTarget.dataset.key })
|
this.triggerEvent('switch', { key: e.currentTarget.dataset.key })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view class="nav-bar">
|
<view data-theme="{{themeMode}}" class="nav-bar">
|
||||||
<view wx:for="{{tabs}}" wx:key="key" class="nav-item" data-key="{{item.key}}" bindtap="onSwitch">
|
<view wx:for="{{tabs}}" wx:key="key" class="nav-item" data-key="{{item.key}}" bindtap="onSwitch">
|
||||||
<text class="nav-icon">{{item.icon}}</text>
|
<text class="nav-icon">{{item.icon}}</text>
|
||||||
<text class="nav-label {{active===item.key?'active':''}}">{{item.label}}</text>
|
<text class="nav-label {{active===item.key?'active':''}}">{{item.label}}</text>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 8px 0 24px;
|
padding: 8px 0 24px;
|
||||||
border-top: 0.5px solid #E5E5EA;
|
border-top: 0.5px solid var(--color-border);
|
||||||
background: rgba(245,245,247,0.95);
|
background: var(--color-nav-bg);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
.nav-icon { font-size: 22px; }
|
.nav-icon { font-size: 22px; }
|
||||||
.nav-label { font-size: 10px; color: #86868B; }
|
.nav-label { font-size: 10px; color: var(--color-text-secondary); }
|
||||||
.nav-label.active { color: #FF6B35; font-weight: 600; }
|
.nav-label.active { color: var(--color-primary); font-weight: 600; }
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
const api = require('../../../services/api')
|
const api = require('../../../services/api')
|
||||||
|
const app = getApp()
|
||||||
Page({
|
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 }) },
|
onNameInput(e) { this.setData({ name: e.detail.value }) },
|
||||||
onGoalInput(e) { this.setData({ dailyGoal: e.detail.value }) },
|
onGoalInput(e) { this.setData({ dailyGoal: e.detail.value }) },
|
||||||
setCategory(e) { this.setData({ category: e.currentTarget.dataset.cat }) },
|
setCategory(e) { this.setData({ category: e.currentTarget.dataset.cat }) },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view class="form">
|
<view data-theme="{{themeMode}}"><view class="form">
|
||||||
<text class="label">名称 *</text>
|
<text class="label">名称 *</text>
|
||||||
<input class="input-field" value="{{name}}" bindinput="onNameInput" placeholder="如「每天背单词」" style="margin-bottom:20px" />
|
<input class="input-field" value="{{name}}" bindinput="onNameInput" placeholder="如「每天背单词」" style="margin-bottom:20px" />
|
||||||
|
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<button class="btn-primary" style="margin-top:32px" loading="{{submitting}}" disabled="{{submitting}}" bindtap="onSubmit">创建打卡项</button>
|
<button class="btn-primary" style="margin-top:32px" loading="{{submitting}}" disabled="{{submitting}}" bindtap="onSubmit">创建打卡项</button>
|
||||||
</view>
|
</view></view>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
.form { padding: 16px; }
|
.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; }
|
.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 { 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: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
.cat-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; }
|
||||||
.cycle-row { display: flex; gap: 8px; }
|
.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 { 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: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
.cycle-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; }
|
||||||
.privacy-row { display: flex; gap: 8px; }
|
.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 { 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: #FF6B35; background: #FFF0EB; font-weight: 600; }
|
.privacy-item.active { border-color: var(--color-primary); background: var(--color-active-bg); font-weight: 600; }
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
const api = require('../../../services/api')
|
const api = require('../../../services/api')
|
||||||
|
const app = getApp()
|
||||||
Page({
|
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) {
|
onLoad(options) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
this.setData({ id: options.id, year: now.getFullYear(), month: now.getMonth() + 1 })
|
this.setData({ id: options.id, year: now.getFullYear(), month: now.getMonth() + 1 })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<view wx:if="{{loading}}" class="loading">加载中...</view>
|
<view wx:if="{{loading}}" class="loading">加载中...</view>
|
||||||
<view wx:else>
|
<view wx:else><view data-theme="{{themeMode}}">
|
||||||
<!-- Actions button -->
|
<!-- Actions button -->
|
||||||
<view class="action-btn" bindtap="toggleActions">⋯</view>
|
<view class="action-btn" bindtap="toggleActions">⋯</view>
|
||||||
<view class="action-menu" wx:if="{{showActions}}">
|
<view class="action-menu" wx:if="{{showActions}}">
|
||||||
@@ -41,4 +41,4 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view></view>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
.loading { text-align: center; padding: 40px; color: #86868B; }
|
.loading { text-align: center; padding: 40px; color: var(--color-text-secondary); }
|
||||||
.card { background: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
.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-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-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 #E5E5EA; }
|
.action-item { padding: 12px 16px; font-size: 14px; border-bottom: 0.5px solid var(--color-border); }
|
||||||
.action-item:last-child { border-bottom: none; }
|
.action-item:last-child { border-bottom: none; }
|
||||||
.item-header { display: flex; align-items: center; gap: 12px; }
|
.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-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; }
|
.item-name { font-size: 18px; font-weight: 700; display: block; color: var(--color-text); }
|
||||||
.item-meta { font-size: 13px; color: #86868B; display: block; margin-top: 2px; }
|
.item-meta { font-size: 13px; color: var(--color-text-secondary); display: block; margin-top: 2px; }
|
||||||
.item-goal { font-size: 13px; color: #86868B; 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: white; border-radius: 12px; margin: 8px 16px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
.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 { text-align: center; }
|
||||||
.stat-num { font-size: 28px; font-weight: 700; color: #FF6B35; display: block; }
|
.stat-num { font-size: 28px; font-weight: 700; color: var(--color-primary); display: block; }
|
||||||
.stat-lbl { font-size: 13px; color: #86868B; }
|
.stat-lbl { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.calendar { padding: 16px; }
|
.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-nav { font-size: 18px; padding: 4px 8px; }
|
||||||
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; }
|
.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-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; }
|
.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.checked { font-weight: 600; }
|
||||||
.cal-day.today { background: #FFF0EB; }
|
.cal-day.today { background: var(--color-active-bg); }
|
||||||
.cal-dot { width: 6px; height: 6px; border-radius: 3px; background: #34C759; margin: 2px auto 0; }
|
.cal-dot { width: 6px; height: 6px; border-radius: 3px; background: var(--color-success); margin: 2px auto 0; }
|
||||||
@@ -2,10 +2,10 @@ const api = require('../../services/api')
|
|||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: { user: null, items: [], streaks: {}, loading: true },
|
data: { user: null, items: [], streaks: {}, loading: true, themeMode: 'light' },
|
||||||
onShow() {
|
onShow() {
|
||||||
if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return }
|
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()
|
this.loadItems()
|
||||||
},
|
},
|
||||||
onPullDownRefresh() { this.loadItems().then(() => wx.stopPullDownRefresh()) },
|
onPullDownRefresh() { this.loadItems().then(() => wx.stopPullDownRefresh()) },
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<view wx:if="{{!loading}}">
|
<view wx:if="{{!loading}}">
|
||||||
|
<view data-theme="{{themeMode}}">
|
||||||
<view class="header-card">
|
<view class="header-card">
|
||||||
<view class="avatar">{{user.nickname ? user.nickname[0] : (user.phone ? user.phone.slice(-4) : '?')}}</view>
|
<view class="avatar">{{user.nickname ? user.nickname[0] : (user.phone ? user.phone.slice(-4) : '?')}}</view>
|
||||||
<view class="header-info">
|
<view class="header-info">
|
||||||
<text class="header-name">{{user.nickname || '用户'}}</text>
|
<text class="header-name">{{user.nickname || '用户'}}</text>
|
||||||
<text class="header-sub">积分 {{user.points}} · {{items.filter(i=>i.status==='active').length}} 个活跃项</text>
|
<text class="header-sub">积分 {{user.points}} · {{items.filter(i=>i.status==='active').length}} 个活跃项</text>
|
||||||
</view>
|
</view>
|
||||||
<button class="profile-btn" bindtap="goProfile">我的</button>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
@@ -28,4 +28,5 @@
|
|||||||
<text class="empty-sub">点击上方"创建"开始</text>
|
<text class="empty-sub">点击上方"创建"开始</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
<view wx:if="{{loading}}" class="loading">加载中...</view>
|
<view wx:if="{{loading}}" class="loading">加载中...</view>
|
||||||
|
|||||||
@@ -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); }
|
.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: linear-gradient(135deg, #FF6B35, #FF4D6D); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; font-weight: 600; flex-shrink: 0; }
|
.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-info { flex: 1; }
|
||||||
.header-name { font-size: 16px; font-weight: 600; display: block; }
|
.header-name { font-size: 16px; font-weight: 600; display: block; color: var(--color-text); }
|
||||||
.header-sub { font-size: 13px; color: #86868B; }
|
.header-sub { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.profile-btn { background: none; border: 1px solid #FF6B35; color: #FF6B35; font-size: 14px; padding: 4px 12px; border-radius: 8px; }
|
|
||||||
.section-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 16px 8px; }
|
.section-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 16px 8px; }
|
||||||
.section-title { font-size: 20px; font-weight: 700; }
|
.section-title { font-size: 20px; font-weight: 700; color: var(--color-text); }
|
||||||
.create-btn { background: #FF6B35; color: white; border: none; font-size: 14px; padding: 6px 14px; border-radius: 8px; }
|
.create-btn { background: var(--color-primary); 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); }
|
.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: #FFF0EB; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
.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-body { flex: 1; }
|
||||||
.card-name { font-size: 16px; font-weight: 600; display: block; }
|
.card-name { font-size: 16px; font-weight: 600; display: block; color: var(--color-text); }
|
||||||
.card-meta { font-size: 13px; color: #86868B; }
|
.card-meta { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.card-streak { font-size: 14px; color: #FF6B35; font-weight: 600; }
|
.card-streak { font-size: 14px; color: var(--color-primary); font-weight: 600; }
|
||||||
.empty { text-align: center; padding: 60px 16px; }
|
.empty { text-align: center; padding: 60px 16px; }
|
||||||
.empty-icon { font-size: 40px; }
|
.empty-icon { font-size: 40px; }
|
||||||
.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; }
|
.empty-text { font-size: 15px; color: var(--color-text-secondary); display: block; margin-top: 8px; }
|
||||||
.empty-sub { font-size: 13px; color: #86868B; }
|
.empty-sub { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.loading { text-align: center; padding: 40px; color: #86868B; font-size: 15px; }
|
.loading { text-align: center; padding: 40px; color: var(--color-text-secondary); font-size: 15px; }
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ const api = require('../../services/api')
|
|||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: { phone: '', loading: false, error: '' },
|
data: { phone: '', loading: false, error: '', themeMode: 'light' },
|
||||||
|
onShow() {
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
|
},
|
||||||
onLoad() {
|
onLoad() {
|
||||||
if (app.globalData.token) wx.reLaunch({ url: '/pages/index/index' })
|
if (app.globalData.token) wx.reLaunch({ url: '/pages/index/index' })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
page { background: white; }
|
page { background: white; }
|
||||||
.container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 32px; box-sizing: border-box; }
|
.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-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; }
|
.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; }
|
.slogan { font-size: 28px; font-weight: 700; color: var(--color-text); }
|
||||||
.sub-slogan { font-size: 15px; color: #86868B; margin-top: 4px; }
|
.sub-slogan { font-size: 15px; color: var(--color-text-secondary); margin-top: 4px; }
|
||||||
.error { font-size: 13px; color: #FF3B30; margin-bottom: 12px; text-align: center; }
|
.error { font-size: 13px; color: var(--color-error); margin-bottom: 12px; text-align: center; }
|
||||||
.btn-primary { margin-top: 12px; }
|
.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; }
|
||||||
@@ -2,9 +2,10 @@ const api = require('../../../services/api')
|
|||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: { user: null },
|
data: { user: null, themeMode: 'light' },
|
||||||
onShow() {
|
onShow() {
|
||||||
if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return }
|
if (!app.globalData.token) { wx.reLaunch({ url: '/pages/login/index' }); return }
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
this.loadProfile()
|
this.loadProfile()
|
||||||
},
|
},
|
||||||
async loadProfile() {
|
async loadProfile() {
|
||||||
@@ -16,6 +17,10 @@ Page({
|
|||||||
},
|
},
|
||||||
goEdit() { wx.navigateTo({ url: '/pages/profile/edit/index' }) },
|
goEdit() { wx.navigateTo({ url: '/pages/profile/edit/index' }) },
|
||||||
goAchievements() { wx.navigateTo({ url: '/pages/achievements/index' }) },
|
goAchievements() { wx.navigateTo({ url: '/pages/achievements/index' }) },
|
||||||
|
onThemeToggle(e) {
|
||||||
|
app.toggleTheme()
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
|
},
|
||||||
onLogout() {
|
onLogout() {
|
||||||
wx.showModal({ title: '确认退出', content: '确定要退出登录吗?', success: (res) => {
|
wx.showModal({ title: '确认退出', content: '确定要退出登录吗?', success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<view data-theme="{{themeMode}}">
|
||||||
<view class="profile-card" wx:if="{{user}}">
|
<view class="profile-card" wx:if="{{user}}">
|
||||||
<view class="profile-avatar">{{(user.nickname?.[0]) || (user.phone?.slice(-4)) || '?'}}</view>
|
<view class="profile-avatar">{{(user.nickname?.[0]) || (user.phone?.slice(-4)) || '?'}}</view>
|
||||||
<text class="profile-name">{{user.nickname || '未设置昵称'}}</text>
|
<text class="profile-name">{{user.nickname || '未设置昵称'}}</text>
|
||||||
@@ -12,5 +13,7 @@
|
|||||||
|
|
||||||
<view class="menu-card">
|
<view class="menu-card">
|
||||||
<view class="menu-item" bindtap="goAchievements"><text>成就徽章</text><text class="menu-arrow">→</text></view>
|
<view class="menu-item" bindtap="goAchievements"><text>成就徽章</text><text class="menu-arrow">→</text></view>
|
||||||
<view class="menu-item" bindtap="onLogout" style="color:#FF3B30"><text>退出登录</text><text class="menu-arrow">→</text></view>
|
<view class="menu-item"><text>深色模式</text><switch checked="{{themeMode === 'dark'}}" bindchange="onThemeToggle" color="#FF6B35" /></view>
|
||||||
|
<view class="menu-item" bindtap="onLogout"><text style="color:var(--color-error)">退出登录</text><text class="menu-arrow">→</text></view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -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-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: 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-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; }
|
.profile-name { font-size: 22px; font-weight: 700; display: block; color: var(--color-text); }
|
||||||
.profile-bio { font-size: 14px; color: #86868B; margin-top: 4px; display: block; }
|
.profile-bio { font-size: 14px; color: var(--color-text-secondary); 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); }
|
.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-item { text-align: center; }
|
||||||
.stat-value { font-size: 24px; font-weight: 700; color: #FF6B35; display: block; }
|
.stat-value { font-size: 24px; font-weight: 700; color: var(--color-primary); display: block; }
|
||||||
.stat-label { font-size: 13px; color: #86868B; }
|
.stat-label { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.menu-card { background: white; border-radius: 12px; margin: 8px 16px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
.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 #E5E5EA; font-size: 15px; }
|
.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-item:last-child { border-bottom: none; }
|
||||||
.menu-arrow { font-size: 15px; color: #86868B; }
|
.menu-arrow { font-size: 15px; color: var(--color-text-secondary); }
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
const api = require('../../../services/api')
|
const api = require('../../../services/api')
|
||||||
|
const app = getApp()
|
||||||
Page({
|
Page({
|
||||||
data: { posts: [], page: 1, loading: true, showCreate: false, content: '' },
|
data: { posts: [], page: 1, loading: true, showCreate: false, content: '', themeMode: 'light' },
|
||||||
onShow() { this.loadFeed() },
|
onShow() {
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
|
this.loadFeed()
|
||||||
|
},
|
||||||
onPullDownRefresh() { this.setData({ page: 1 }); this.loadFeed().then(() => wx.stopPullDownRefresh()) },
|
onPullDownRefresh() { this.setData({ page: 1 }); this.loadFeed().then(() => wx.stopPullDownRefresh()) },
|
||||||
async loadFeed() {
|
async loadFeed() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<view data-theme="{{themeMode}}">
|
||||||
<button class="create-post-btn" bindtap="openCreate">+ 分享动态</button>
|
<button class="create-post-btn" bindtap="openCreate">+ 分享动态</button>
|
||||||
|
|
||||||
<view wx:for="{{posts}}" wx:key="id" class="post-card" wx:for-item="p">
|
<view wx:for="{{posts}}" wx:key="id" class="post-card" wx:for-item="p">
|
||||||
@@ -27,3 +28,4 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|||||||
@@ -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); }
|
.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: white; border-radius: 12px; margin: 8px 16px; padding: 14px 16px; 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-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-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; }
|
.post-user { font-size: 14px; font-weight: 600; display: block; color: var(--color-text); }
|
||||||
.post-time { font-size: 12px; color: #86868B; }
|
.post-time { font-size: 12px; color: var(--color-text-secondary); }
|
||||||
.post-content { font-size: 15px; line-height: 1.5; display: block; }
|
.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 #F0F0F0; }
|
.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: #86868B; }
|
.action-btn { font-size: 13px; color: var(--color-text-secondary); }
|
||||||
.load-more { text-align: center; padding: 16px; color: #86868B; font-size: 14px; }
|
.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-align: center; padding: 60px 16px; font-size: 40px; }
|
||||||
.empty-text { font-size: 15px; color: #86868B; display: block; margin-top: 8px; }
|
.empty-text { font-size: 15px; color: var(--color-text-secondary); display: block; margin-top: 8px; }
|
||||||
.loading { text-align: center; padding: 40px; color: #86868B; }
|
.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-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-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; }
|
.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 #E5E5EA; border-radius: 10px; padding: 12px; font-size: 15px; min-height: 120px; box-sizing: border-box; }
|
.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; }
|
.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; }
|
.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: linear-gradient(135deg,#FF6B35,#FF4D6D); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; text-align: center; }
|
.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; }
|
.submit-btn[disabled] { opacity: 0.5; }
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
const api = require('../../../services/api')
|
const api = require('../../../services/api')
|
||||||
|
const app = getApp()
|
||||||
Page({
|
Page({
|
||||||
data: { friends: [], addPhone: '', loading: true },
|
data: { friends: [], addPhone: '', loading: true, themeMode: 'light' },
|
||||||
onShow() { this.loadFriends() },
|
onShow() {
|
||||||
|
this.setData({ themeMode: app.globalData.themeMode })
|
||||||
|
this.loadFriends()
|
||||||
|
},
|
||||||
async loadFriends() {
|
async loadFriends() {
|
||||||
try { const f = await api.get('/api/social/friends'); this.setData({ friends: f || [] }) } catch(e) {}
|
try { const f = await api.get('/api/social/friends'); this.setData({ friends: f || [] }) } catch(e) {}
|
||||||
this.setData({ loading: false })
|
this.setData({ loading: false })
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<view data-theme="{{themeMode}}">
|
||||||
<view class="add-section">
|
<view class="add-section">
|
||||||
<input class="input-field" value="{{addPhone}}" bindinput="onPhoneInput" placeholder="输入手机号添加" style="flex:1;margin-bottom:0" />
|
<input class="input-field" value="{{addPhone}}" bindinput="onPhoneInput" placeholder="输入手机号添加" style="flex:1;margin-bottom:0" />
|
||||||
<button class="add-btn" bindtap="onAdd" disabled="{{!addPhone.trim()}}">添加</button>
|
<button class="add-btn" bindtap="onAdd" disabled="{{!addPhone.trim()}}">添加</button>
|
||||||
@@ -10,3 +11,4 @@
|
|||||||
<button class="remove-btn" data-id="{{item.id}}" bindtap="onRemove">删除</button>
|
<button class="remove-btn" data-id="{{item.id}}" bindtap="onRemove">删除</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="empty" wx:if="{{!loading && friends.length===0}}"><text class="empty-text">还没有好友</text></view>
|
<view class="empty" wx:if="{{!loading && friends.length===0}}"><text class="empty-text">还没有好友</text></view>
|
||||||
|
</view>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.add-section { display: flex; gap: 8px; padding: 12px 16px; align-items: center; }
|
.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 { padding: 10px 20px; background: var(--color-primary); color: white; border: none; border-radius: 10px; font-size: 14px; }
|
||||||
.add-btn[disabled] { background: #E5E5EA; }
|
.add-btn[disabled] { background: var(--color-border); }
|
||||||
.section-title { font-size: 15px; color: #86868B; padding: 16px 16px 8px; display: block; }
|
.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: white; margin: 4px 16px; padding: 12px 16px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
.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: linear-gradient(135deg,#FF6B35,#FF4D6D); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; }
|
.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; }
|
.friend-name { flex: 1; font-size: 15px; color: var(--color-text); }
|
||||||
.remove-btn { background: none; border: none; color: #FF3B30; font-size: 13px; padding: 4px 8px; }
|
.remove-btn { background: none; border: none; color: var(--color-error); font-size: 13px; padding: 4px 8px; }
|
||||||
.empty { text-align: center; padding: 40px; }
|
.empty { text-align: center; padding: 40px; }
|
||||||
.empty-text { font-size: 15px; color: #86868B; }
|
.empty-text { font-size: 15px; color: var(--color-text-secondary); }
|
||||||
+12
-3
@@ -4,6 +4,7 @@ import { NavigationContainer } from '@react-navigation/native'
|
|||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
import { AuthProvider, useAuth } from './src/components/AuthProvider'
|
import { AuthProvider, useAuth } from './src/components/AuthProvider'
|
||||||
|
import ThemeProvider, { useTheme } from './src/components/ThemeProvider'
|
||||||
import { getStoredToken } from './src/services/api'
|
import { getStoredToken } from './src/services/api'
|
||||||
import LoginScreen from './src/screens/LoginScreen'
|
import LoginScreen from './src/screens/LoginScreen'
|
||||||
import MainTabs from './src/navigation/MainTabs'
|
import MainTabs from './src/navigation/MainTabs'
|
||||||
@@ -11,9 +12,15 @@ import type { RootStackParamList } from './src/navigation/types'
|
|||||||
|
|
||||||
const RootStack = createNativeStackNavigator<RootStackParamList>()
|
const RootStack = createNativeStackNavigator<RootStackParamList>()
|
||||||
|
|
||||||
|
function AppStatusBar() {
|
||||||
|
const { isDark } = useTheme()
|
||||||
|
return <StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} background={isDark ? '#1C1C1E' : '#F5F5F7'} />
|
||||||
|
}
|
||||||
|
|
||||||
function AuthGate() {
|
function AuthGate() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const [initialRoute, setInitialRoute] = useState<'Login' | 'MainTabs' | null>(null)
|
const [initialRoute, setInitialRoute] = useState<'Login' | 'MainTabs' | null>(null)
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
@@ -25,8 +32,8 @@ function AuthGate() {
|
|||||||
|
|
||||||
if (!initialRoute) {
|
if (!initialRoute) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5F5F7' }}>
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.bg }}>
|
||||||
<Text style={{ color: '#86868B' }}>加载中...</Text>
|
<Text style={{ color: colors.textSecondary }}>加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -43,10 +50,12 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<ThemeProvider>
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<StatusBar barStyle="dark-content" background="#F5F5F7" />
|
<AppStatusBar />
|
||||||
<AuthGate />
|
<AuthGate />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
</ThemeProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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<IThemeContext>({
|
||||||
|
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<ThemeMode>('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 (
|
||||||
|
<ThemeContext.Provider value={{ mode, colors: THEME[mode], toggle, isDark: mode === 'dark' }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<ThemeMode, IThemeColors> = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
import Ionicons from '@expo/vector-icons/Ionicons'
|
import Ionicons from '@expo/vector-icons/Ionicons'
|
||||||
|
import { useTheme } from '../components/ThemeProvider'
|
||||||
import type { MainTabParamList } from './types'
|
import type { MainTabParamList } from './types'
|
||||||
import HomeStack from './HomeStack'
|
import HomeStack from './HomeStack'
|
||||||
import FeedStack from './FeedStack'
|
import FeedStack from './FeedStack'
|
||||||
@@ -9,15 +10,17 @@ import ProfileStack from './ProfileStack'
|
|||||||
const Tab = createBottomTabNavigator<MainTabParamList>()
|
const Tab = createBottomTabNavigator<MainTabParamList>()
|
||||||
|
|
||||||
export default function MainTabs() {
|
export default function MainTabs() {
|
||||||
|
const { colors, isDark } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarActiveTintColor: '#FF6B35',
|
tabBarActiveTintColor: colors.primary,
|
||||||
tabBarInactiveTintColor: '#86868B',
|
tabBarInactiveTintColor: colors.textSecondary,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: 'rgba(245,245,247,0.95)',
|
backgroundColor: colors.navBg,
|
||||||
borderTopColor: '#E5E5EA',
|
borderTopColor: colors.border,
|
||||||
borderTopWidth: 0.5,
|
borderTopWidth: 0.5,
|
||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import { api } from '../services/api'
|
import { api } from '../services/api'
|
||||||
import { useAuth } from '../components/AuthProvider'
|
import { useAuth } from '../components/AuthProvider'
|
||||||
|
import { useTheme } from '../components/ThemeProvider'
|
||||||
import type { HomeStackParamList } from '../navigation/types'
|
import type { HomeStackParamList } from '../navigation/types'
|
||||||
|
import type { IThemeColors } from '../constants/theme'
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList, 'HomeMain'>>()
|
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList, 'HomeMain'>>()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const { colors } = useTheme()
|
||||||
const [items, setItems] = useState<any[]>([])
|
const [items, setItems] = useState<any[]>([])
|
||||||
const [streaks, setStreaks] = useState<Record<string, number>>({})
|
const [streaks, setStreaks] = useState<Record<string, number>>({})
|
||||||
const [todayStatus, setTodayStatus] = useState<Record<string, boolean>>({})
|
const [todayStatus, setTodayStatus] = useState<Record<string, boolean>>({})
|
||||||
@@ -34,37 +37,39 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
const activeItems = items.filter(i => i.status === 'active')
|
const activeItems = items.filter(i => i.status === 'active')
|
||||||
|
|
||||||
|
const styles = createStyles(colors)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={s.container} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}>
|
<ScrollView style={styles.container} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}>
|
||||||
<View style={s.header}>
|
<View style={styles.header}>
|
||||||
<View style={s.avatar}><Text style={s.avatarText}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
<View style={styles.avatar}><Text style={styles.avatarText}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
||||||
<View style={{ flex: 1, marginLeft: 12 }}>
|
<View style={{ flex: 1, marginLeft: 12 }}>
|
||||||
<Text style={s.headerName}>{user?.nickname || '用户'}</Text>
|
<Text style={styles.headerName}>{user?.nickname || '用户'}</Text>
|
||||||
<Text style={s.headerSub}>积分 {user?.points} · {activeItems.length} 个活跃项</Text>
|
<Text style={styles.headerSub}>积分 {user?.points} · {activeItems.length} 个活跃项</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={s.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
<Text style={s.sectionTitle}>今日打卡</Text>
|
<Text style={styles.sectionTitle}>今日打卡</Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('CreateCheckIn')}><Text style={s.createBtn}>+ 创建</Text></TouchableOpacity>
|
<TouchableOpacity onPress={() => navigation.navigate('CreateCheckIn')}><Text style={styles.createBtn}>+ 创建</Text></TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{activeItems.length === 0 ? (
|
{activeItems.length === 0 ? (
|
||||||
<View style={s.empty}>
|
<View style={styles.empty}>
|
||||||
<Text style={{ fontSize: 40 }}>📋</Text>
|
<Text style={{ fontSize: 40 }}>📋</Text>
|
||||||
<Text style={s.emptyText}>还没有打卡项</Text>
|
<Text style={styles.emptyText}>还没有打卡项</Text>
|
||||||
<Text style={{ fontSize: 13, color: '#86868B', marginTop: 4 }}>点击上方"创建"开始</Text>
|
<Text style={{ fontSize: 13, color: colors.textSecondary, marginTop: 4 }}>点击上方"创建"开始</Text>
|
||||||
</View>
|
</View>
|
||||||
) : activeItems.map((item: any) => (
|
) : activeItems.map((item: any) => (
|
||||||
<TouchableOpacity key={item.id} onPress={() => navigation.navigate('CheckInDetail', { itemId: item.id })}>
|
<TouchableOpacity key={item.id} onPress={() => navigation.navigate('CheckInDetail', { itemId: item.id })}>
|
||||||
<View style={s.card}>
|
<View style={styles.card}>
|
||||||
<View style={s.cardIcon}><Text style={{ fontSize: 22 }}>{item.icon || '📋'}</Text></View>
|
<View style={styles.cardIcon}><Text style={{ fontSize: 22 }}>{item.icon || '📋'}</Text></View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={s.cardName}>{item.name}</Text>
|
<Text style={styles.cardName}>{item.name}</Text>
|
||||||
<Text style={s.cardMeta}>{item.category}</Text>
|
<Text style={styles.cardMeta}>{item.category}</Text>
|
||||||
{todayStatus[item.id] && <Text style={s.doneText}>✓ 已打卡</Text>}
|
{todayStatus[item.id] && <Text style={styles.doneText}>✓ 已打卡</Text>}
|
||||||
</View>
|
</View>
|
||||||
{streaks[item.id] !== undefined && <Text style={s.streak}>🔥 {streaks[item.id]}天</Text>}
|
{streaks[item.id] !== undefined && <Text style={styles.streak}>🔥 {streaks[item.id]}天</Text>}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -72,22 +77,24 @@ export default function HomeScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const s = StyleSheet.create({
|
function createStyles(c: IThemeColors) {
|
||||||
container: { flex: 1, backgroundColor: '#F5F5F7' },
|
return StyleSheet.create({
|
||||||
header: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, margin: 12, padding: 14, shadowOpacity: 0.04, shadowRadius: 3, elevation: 1 },
|
container: { flex: 1, backgroundColor: c.bg },
|
||||||
avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center' },
|
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' },
|
avatarText: { fontSize: 18, color: 'white', fontWeight: '600' },
|
||||||
headerName: { fontSize: 16, fontWeight: '600' },
|
headerName: { fontSize: 16, fontWeight: '600', color: c.text },
|
||||||
headerSub: { fontSize: 13, color: '#86868B' },
|
headerSub: { fontSize: 13, color: c.textSecondary },
|
||||||
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 },
|
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 },
|
||||||
sectionTitle: { fontSize: 20, fontWeight: '700' },
|
sectionTitle: { fontSize: 20, fontWeight: '700', color: c.text },
|
||||||
createBtn: { color: '#FF6B35', fontSize: 14, fontWeight: '600' },
|
createBtn: { color: c.primary, fontSize: 14, fontWeight: '600' },
|
||||||
card: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, marginHorizontal: 12, marginBottom: 8, padding: 14, shadowOpacity: 0.04, elevation: 1 },
|
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: '#FFF0EB', justifyContent: 'center', alignItems: 'center', marginRight: 14 },
|
cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: c.primaryLight + '20', justifyContent: 'center', alignItems: 'center', marginRight: 14 },
|
||||||
cardName: { fontSize: 16, fontWeight: '600' },
|
cardName: { fontSize: 16, fontWeight: '600', color: c.text },
|
||||||
cardMeta: { fontSize: 13, color: '#86868B', marginTop: 2 },
|
cardMeta: { fontSize: 13, color: c.textSecondary, marginTop: 2 },
|
||||||
doneText: { fontSize: 12, color: '#34C759', fontWeight: '600', marginTop: 2 },
|
doneText: { fontSize: 12, color: c.success, fontWeight: '600', marginTop: 2 },
|
||||||
streak: { fontSize: 14, color: '#FF6B35', fontWeight: '600' },
|
streak: { fontSize: 14, color: c.primary, fontWeight: '600' },
|
||||||
empty: { alignItems: 'center', padding: 60, marginTop: 20 },
|
empty: { alignItems: 'center', padding: 60, marginTop: 20 },
|
||||||
emptyText: { fontSize: 15, color: '#86868B', marginTop: 8 },
|
emptyText: { fontSize: 15, color: c.textSecondary, marginTop: 8 },
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React from 'react'
|
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 { useNavigation } from '@react-navigation/native'
|
||||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import { useAuth } from '../components/AuthProvider'
|
import { useAuth } from '../components/AuthProvider'
|
||||||
|
import { useTheme } from '../components/ThemeProvider'
|
||||||
import type { ProfileStackParamList } from '../navigation/types'
|
import type { ProfileStackParamList } from '../navigation/types'
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
|
const { colors, isDark, toggle } = useTheme()
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<ProfileStackParamList, 'ProfileMain'>>()
|
const navigation = useNavigation<NativeStackNavigationProp<ProfileStackParamList, 'ProfileMain'>>()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
@@ -17,23 +19,29 @@ export default function ProfileScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{ flex: 1, backgroundColor: '#F5F5F7' }}>
|
<ScrollView style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||||
<View style={styles.profileCard}>
|
<View style={[styles.profileCard, { backgroundColor: colors.card }]}>
|
||||||
<View style={styles.avatar}><Text style={{ fontSize: 28, color: 'white', fontWeight: '700' }}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
<View style={styles.avatar}><Text style={{ fontSize: 28, color: 'white', fontWeight: '700' }}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
||||||
<Text style={styles.name}>{user?.nickname || '未设置昵称'}</Text>
|
<Text style={[styles.name, { color: colors.text }]}>{user?.nickname || '未设置昵称'}</Text>
|
||||||
<Text style={styles.bio}>{user?.bio || '还没有个性签名'}</Text>
|
<Text style={[styles.bio, { color: colors.textSecondary }]}>{user?.bio || '还没有个性签名'}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statsRow}>
|
<View style={[styles.statsRow, { backgroundColor: colors.card }]}>
|
||||||
<View style={styles.stat}><Text style={styles.statVal}>{user?.points || 0}</Text><Text style={styles.statLbl}>积分</Text></View>
|
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>{user?.points || 0}</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>积分</Text></View>
|
||||||
<View style={styles.stat}><Text style={styles.statVal}>--</Text><Text style={styles.statLbl}>连续</Text></View>
|
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>--</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>连续</Text></View>
|
||||||
<View style={styles.stat}><Text style={styles.statVal}>--</Text><Text style={styles.statLbl}>徽章</Text></View>
|
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>--</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>徽章</Text></View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.menu}>
|
<View style={[styles.menu, { backgroundColor: colors.card }]}>
|
||||||
<TouchableOpacity style={styles.menuItem} onPress={() => navigation.navigate('Achievements')}><Text>成就徽章</Text><Text style={styles.arrow}>→</Text></TouchableOpacity>
|
<TouchableOpacity style={[styles.menuItem, { borderBottomColor: colors.border }]} onPress={() => navigation.navigate('Achievements')}>
|
||||||
|
<Text style={{ color: colors.text }}>成就徽章</Text><Text style={[styles.arrow, { color: colors.textSecondary }]}>→</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={[styles.menuItem, { borderBottomColor: colors.border }]}>
|
||||||
|
<Text style={{ color: colors.text }}>深色模式</Text>
|
||||||
|
<Switch value={isDark} onValueChange={toggle} trackColor={{ false: colors.border, true: colors.primary }} thumbColor="white" />
|
||||||
|
</View>
|
||||||
<TouchableOpacity style={[styles.menuItem, { borderBottomWidth: 0 }]} onPress={handleLogout}>
|
<TouchableOpacity style={[styles.menuItem, { borderBottomWidth: 0 }]} onPress={handleLogout}>
|
||||||
<Text style={{ color: '#FF3B30' }}>退出登录</Text><Text style={styles.arrow}>→</Text>
|
<Text style={{ color: colors.error }}>退出登录</Text><Text style={[styles.arrow, { color: colors.textSecondary }]}>→</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -41,15 +49,15 @@ export default function ProfileScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
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 },
|
avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||||
name: { fontSize: 22, fontWeight: '700' },
|
name: { fontSize: 22, fontWeight: '700' },
|
||||||
bio: { fontSize: 14, color: '#86868B', marginTop: 4 },
|
bio: { fontSize: 14, marginTop: 4 },
|
||||||
statsRow: { flexDirection: 'row', justifyContent: 'space-around', backgroundColor: 'white', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 },
|
statsRow: { flexDirection: 'row', justifyContent: 'space-around', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 },
|
||||||
stat: { alignItems: 'center' },
|
stat: { alignItems: 'center' },
|
||||||
statVal: { fontSize: 24, fontWeight: '700', color: '#FF6B35' },
|
statVal: { fontSize: 24, fontWeight: '700' },
|
||||||
statLbl: { fontSize: 13, color: '#86868B' },
|
statLbl: { fontSize: 13 },
|
||||||
menu: { backgroundColor: 'white', borderRadius: 12, margin: 8, overflow: 'hidden', shadowOpacity: 0.04, elevation: 1 },
|
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, borderBottomColor: '#E5E5EA' },
|
menuItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 0.5 },
|
||||||
arrow: { fontSize: 15, color: '#86868B' },
|
arrow: { fontSize: 15 },
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
@@ -21,17 +21,25 @@ type Config struct {
|
|||||||
S3Bucket string
|
S3Bucket string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 加载配置(优先读取环境变量,支持 .env 文件)
|
// Load 加载配置(支持 .env 文件)
|
||||||
// 查找顺序:可执行文件同目录 → 当前目录 → 环境变量
|
// 1. 先加载 .env(基础配置,本地开发友好)
|
||||||
|
// 2. 再加载 .env.<ENV>(环境覆盖,由 ENV 环境变量指定)
|
||||||
|
// 搜索路径:可执行文件目录 → 当前目录 → server/ 子目录
|
||||||
|
// 优先级(高 → 低):环境覆盖 → .env → 系统环境变量 → 默认值
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
// 从可执行文件所在目录加载 .env(宝塔场景)
|
loadEnv := func(name string) {
|
||||||
if exe, err := os.Executable(); err == nil {
|
if exe, err := os.Executable(); err == nil {
|
||||||
exeDir := filepath.Dir(exe)
|
_ = godotenv.Overload(filepath.Join(filepath.Dir(exe), name))
|
||||||
_ = godotenv.Load(filepath.Join(exeDir, ".env"))
|
}
|
||||||
|
_ = godotenv.Overload(name)
|
||||||
|
_ = godotenv.Overload(filepath.Join(".", "server", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从当前目录加载 .env(本地开发场景)
|
loadEnv(".env")
|
||||||
_ = godotenv.Load()
|
|
||||||
|
if env := os.Getenv("ENV"); env != "" {
|
||||||
|
loadEnv(".env." + env)
|
||||||
|
}
|
||||||
|
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Port: getEnv("PORT", "8080"),
|
Port: getEnv("PORT", "8080"),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
--color-text: #1D1D1F;
|
--color-text: #1D1D1F;
|
||||||
--color-text-secondary: #86868B;
|
--color-text-secondary: #86868B;
|
||||||
--color-border: #E5E5EA;
|
--color-border: #E5E5EA;
|
||||||
|
--color-nav-bg: rgba(245,245,247,0.92);
|
||||||
--color-success: #34C759;
|
--color-success: #34C759;
|
||||||
--color-warning: #FF9500;
|
--color-warning: #FF9500;
|
||||||
--color-error: #FF3B30;
|
--color-error: #FF3B30;
|
||||||
@@ -22,12 +23,24 @@
|
|||||||
--font-base: -apple-system, BlinkMacSystemFont, 'SF Pro', 'PingFang SC', sans-serif;
|
--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 {
|
body {
|
||||||
font-family: var(--font-base);
|
font-family: var(--font-base);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS 风格列表 */
|
/* iOS 风格列表 */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: '寸进 — 日日皆有成长',
|
title: '寸进 — 日日皆有成长',
|
||||||
@@ -8,8 +9,10 @@
|
|||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" suppressHydrationWarning>
|
||||||
<body>{children}</body>
|
<body>
|
||||||
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="寸进" showLogout />
|
<NavBar title="寸进" />
|
||||||
|
|
||||||
<div className="card" style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div className="card" style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useAuth } from './AuthProvider'
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useTheme } from './ThemeProvider'
|
||||||
|
import { useAuth } from './AuthProvider'
|
||||||
|
|
||||||
interface NavBarProps {
|
interface NavBarProps {
|
||||||
title: string
|
title: string
|
||||||
@@ -12,14 +13,15 @@
|
|||||||
export default function NavBar({ title, showBack = false, showLogout = false }: NavBarProps) {
|
export default function NavBar({ title, showBack = false, showLogout = false }: NavBarProps) {
|
||||||
const { logout, user } = useAuth()
|
const { logout, user } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { mode, toggle } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav style={{
|
<nav style={{
|
||||||
position: 'sticky', top: 0, zIndex: 100,
|
position: 'sticky', top: 0, zIndex: 100,
|
||||||
background: 'rgba(245,245,247,0.92)',
|
background: 'var(--color-nav-bg)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
WebkitBackdropFilter: 'blur(20px)',
|
||||||
borderBottom: '0.5px solid #E5E5EA',
|
borderBottom: '0.5px solid var(--color-border)',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
}}>
|
}}>
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
{showBack && (
|
{showBack && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 20, cursor: 'pointer', padding: 4 }}
|
style={{ background: 'none', border: 'none', fontSize: 20, cursor: 'pointer', padding: 4, color: 'var(--color-text)' }}
|
||||||
>
|
>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
@@ -35,20 +37,30 @@
|
|||||||
{showLogout && user && (
|
{showLogout && user && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/profile')}
|
onClick={() => router.push('/profile')}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: '#FF6B35', padding: 4 }}
|
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-primary)', padding: 4 }}
|
||||||
>
|
>
|
||||||
我的
|
我的
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ fontSize: 17, fontWeight: 600, flex: 1, textAlign: 'center' }}>{title}</span>
|
<span style={{ fontSize: 17, fontWeight: 600, flex: 1, textAlign: 'center', color: 'var(--color-text)' }}>{title}</span>
|
||||||
|
|
||||||
<div style={{ minWidth: 60, textAlign: 'right' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, minWidth: 60 }}>
|
||||||
|
<button
|
||||||
|
onClick={toggle}
|
||||||
|
title={mode === 'light' ? '切换暗黑模式' : '切换明亮模式'}
|
||||||
|
style={{
|
||||||
|
background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 4,
|
||||||
|
color: 'var(--color-text-secondary)', lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === 'light' ? '🌙' : '☀️'}
|
||||||
|
</button>
|
||||||
{showLogout && (
|
{showLogout && (
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: '#86868B', padding: 4 }}
|
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-text-secondary)', padding: 4 }}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ export default function TabBar() {
|
|||||||
return (
|
return (
|
||||||
<nav style={{
|
<nav style={{
|
||||||
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 100,
|
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 100,
|
||||||
background: 'rgba(245,245,247,0.95)',
|
background: 'var(--color-nav-bg)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
WebkitBackdropFilter: 'blur(20px)',
|
||||||
borderTop: '0.5px solid #E5E5EA',
|
borderTop: '0.5px solid var(--color-border)',
|
||||||
display: 'flex', justifyContent: 'space-around',
|
display: 'flex', justifyContent: 'space-around',
|
||||||
padding: '6px 0 20px',
|
padding: '6px 0 20px',
|
||||||
}}>
|
}}>
|
||||||
@@ -37,7 +37,7 @@ export default function TabBar() {
|
|||||||
style={{
|
style={{
|
||||||
background: 'none', border: 'none', cursor: 'pointer',
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
|
||||||
fontSize: 10, color: active ? '#FF6B35' : '#86868B', fontWeight: active ? 600 : 400,
|
fontSize: 10, color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)', fontWeight: active ? 600 : 400,
|
||||||
transition: 'color 0.2s',
|
transition: 'color 0.2s',
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 22 }}>{tab.icon}</span>
|
<span style={{ fontSize: 22 }}>{tab.icon}</span>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
||||||
|
import type { ThemeMode } from '@/constants/theme'
|
||||||
|
|
||||||
|
interface IThemeContext {
|
||||||
|
mode: ThemeMode
|
||||||
|
toggle: () => void
|
||||||
|
setMode: (mode: ThemeMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<IThemeContext>({
|
||||||
|
mode: 'light',
|
||||||
|
toggle: () => {},
|
||||||
|
setMode: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
return useContext(ThemeContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [mode, setModeState] = useState<ThemeMode>('light')
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem('inchstep-theme') as ThemeMode | null
|
||||||
|
if (stored === 'light' || stored === 'dark') {
|
||||||
|
setModeState(stored)
|
||||||
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
setModeState('dark')
|
||||||
|
}
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted) return
|
||||||
|
document.documentElement.setAttribute('data-theme', mode)
|
||||||
|
localStorage.setItem('inchstep-theme', mode)
|
||||||
|
}, [mode, mounted])
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setModeState(prev => (prev === 'light' ? 'dark' : 'light'))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setMode = useCallback((m: ThemeMode) => {
|
||||||
|
setModeState(m)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return <div style={{ visibility: 'hidden' }}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ mode, toggle, setMode }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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<ThemeMode, IThemeColors> = {
|
||||||
|
light: {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryGradient: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
||||||
|
accent: '#7209B7',
|
||||||
|
bg: '#F5F5F7',
|
||||||
|
card: '#FFFFFF',
|
||||||
|
text: '#1D1D1F',
|
||||||
|
textSecondary: '#86868B',
|
||||||
|
border: '#E5E5EA',
|
||||||
|
navBg: 'rgba(245,245,247,0.92)',
|
||||||
|
success: '#34C759',
|
||||||
|
warning: '#FF9500',
|
||||||
|
error: '#FF3B30',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: '#FF6B35',
|
||||||
|
primaryLight: '#FF8C5A',
|
||||||
|
primaryGradient: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
||||||
|
accent: '#BB86FC',
|
||||||
|
bg: '#1C1C1E',
|
||||||
|
card: '#2C2C2E',
|
||||||
|
text: '#F5F5F7',
|
||||||
|
textSecondary: '#98989D',
|
||||||
|
border: '#38383A',
|
||||||
|
navBg: 'rgba(28,28,30,0.92)',
|
||||||
|
success: '#34C759',
|
||||||
|
warning: '#FF9500',
|
||||||
|
error: '#FF453A',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -10,5 +10,5 @@ export {
|
|||||||
truncate,
|
truncate,
|
||||||
shortId,
|
shortId,
|
||||||
CYCLE_LABEL,
|
CYCLE_LABEL,
|
||||||
} from '../../shared/utils'
|
} from '@shared/utils'
|
||||||
export type { CheckinCategory } from '../../shared/utils'
|
export type { CheckinCategory } from '@shared/utils'
|
||||||
Reference in New Issue
Block a user