提交修改
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
import NavBar from '@/components/NavBar'
|
import NavBar from '@/components/NavBar'
|
||||||
import { getAllAchievements, getUserAchievements, RARITY_CONFIG, type IAchievement } from '@/services/achievements'
|
import { getAllAchievements, getUserAchievements, RARITY_CONFIG, type IAchievement } from '@/services/achievements'
|
||||||
|
|
||||||
function AchievementsPage() {
|
function AchievementsPage() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [allAchievements, setAllAchievements] = useState<IAchievement[]>([])
|
const [allAchievements, setAllAchievements] = useState<IAchievement[]>([])
|
||||||
@@ -30,7 +30,17 @@
|
|||||||
setPageLoading(false)
|
setPageLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || pageLoading) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (loading || pageLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="成就徽章" showBack />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">加载中</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
const grouped = {
|
const grouped = {
|
||||||
@@ -43,21 +53,55 @@
|
|||||||
<>
|
<>
|
||||||
<NavBar title="成就徽章" showBack />
|
<NavBar title="成就徽章" showBack />
|
||||||
|
|
||||||
{/* 统计 */}
|
<div className="page-container">
|
||||||
<div className="card" style={{ marginTop: 16, display: 'flex', justifyContent: 'space-around', padding: '20px 16px' }}>
|
{/* Stats */}
|
||||||
|
<div className="card" style={{
|
||||||
|
marginTop: 14,
|
||||||
|
display: 'flex', justifyContent: 'space-around',
|
||||||
|
padding: '20px 16px',
|
||||||
|
background: 'linear-gradient(135deg, rgba(255, 179, 71, 0.03), rgba(255, 60, 126, 0.02))',
|
||||||
|
}}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>{unlockedIds.size}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>已获得</p>
|
fontSize: 26, fontWeight: 700, color: 'var(--color-gold)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{unlockedIds.size}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
已获得
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>{allAchievements.length}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>总成就</p>
|
fontSize: 26, fontWeight: 700, color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{allAchievements.length}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
总成就
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>
|
<p style={{
|
||||||
|
fontSize: 26, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
{allAchievements.length > 0 ? Math.round((unlockedIds.size / allAchievements.length) * 100) : 0}%
|
{allAchievements.length > 0 ? Math.round((unlockedIds.size / allAchievements.length) * 100) : 0}%
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>完成度</p>
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
完成度
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,30 +111,69 @@
|
|||||||
if (items.length === 0) return null
|
if (items.length === 0) return null
|
||||||
return (
|
return (
|
||||||
<div key={rarity}>
|
<div key={rarity}>
|
||||||
<h3 style={{ fontSize: 15, color: '#86868B', padding: '20px 16px 8px' }}>
|
<div className="section-title-bar" style={{ marginTop: 8 }}>
|
||||||
{config.label}质徽章 ({items.length})
|
<span className="section-title-bar-text">
|
||||||
</h3>
|
{config.label.toUpperCase()} ({items.length})
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10, margin: '0 16px' }}>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
|
gap: 10, margin: '0 16px',
|
||||||
|
}}>
|
||||||
{items.map(a => {
|
{items.map(a => {
|
||||||
const unlocked = unlockedIds.has(a.id)
|
const unlocked = unlockedIds.has(a.id)
|
||||||
return (
|
return (
|
||||||
<div key={a.id} style={{
|
<div key={a.id} style={{
|
||||||
borderRadius: 12, padding: 16, textAlign: 'center',
|
borderRadius: 14, padding: 18, textAlign: 'center',
|
||||||
background: unlocked ? config.bg : '#F5F5F7',
|
background: unlocked
|
||||||
border: `1.5px solid ${unlocked ? config.color + '44' : '#E5E5EA'}`,
|
? `linear-gradient(135deg, ${config.color}11, ${config.color}05)`
|
||||||
|
: 'var(--color-card)',
|
||||||
|
border: unlocked
|
||||||
|
? `1px solid ${config.color}44`
|
||||||
|
: '1px solid var(--color-border)',
|
||||||
opacity: unlocked ? 1 : 0.5,
|
opacity: unlocked ? 1 : 0.5,
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
|
{unlocked && (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 44, height: 44, borderRadius: 22, margin: '0 auto 8px',
|
position: 'absolute', top: 0, right: 0,
|
||||||
background: unlocked ? `linear-gradient(135deg, ${config.color}, ${config.color}88)` : '#E5E5EA',
|
padding: '2px 8px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
fontSize: 8, fontWeight: 700,
|
||||||
fontSize: 20,
|
background: config.color + '33',
|
||||||
|
color: config.color,
|
||||||
|
borderRadius: '0 14px 0 8px',
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
letterSpacing: 1,
|
||||||
}}>
|
}}>
|
||||||
{unlocked ? '🏅' : '🔒'}
|
{config.label}
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: 13, fontWeight: 600, marginBottom: 2 }}>{a.name}</p>
|
)}
|
||||||
<p style={{ fontSize: 11, color: '#86868B' }}>{unlocked ? a.condition : '???'}</p>
|
<div style={{
|
||||||
|
width: 48, height: 48, borderRadius: 24,
|
||||||
|
margin: '0 auto 10px',
|
||||||
|
background: unlocked
|
||||||
|
? `linear-gradient(135deg, ${config.color}, ${config.color}88)`
|
||||||
|
: 'var(--color-progress-bg)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 22,
|
||||||
|
boxShadow: unlocked ? `0 0 24px ${config.color}44` : 'none',
|
||||||
|
}}>
|
||||||
|
{unlocked ? '◈' : '◆'}
|
||||||
|
</div>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 13, fontWeight: 600, marginBottom: 3,
|
||||||
|
color: unlocked ? 'var(--color-text)' : 'var(--color-text-muted)',
|
||||||
|
}}>
|
||||||
|
{a.name}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{unlocked ? a.condition : '???'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -98,12 +181,11 @@
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
<div style={{ height: 80 }} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AchievementsRoute() {
|
export default function AchievementsRoute() {
|
||||||
return <AuthProvider><AchievementsPage /></AuthProvider>
|
return <AuthProvider><AchievementsPage /></AuthProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function DetailPage() {
|
|||||||
const [itemData, streakData, recordsData] = await Promise.all([
|
const [itemData, streakData, recordsData] = await Promise.all([
|
||||||
getItem(id),
|
getItem(id),
|
||||||
getStreak(id),
|
getStreak(id),
|
||||||
getRecords({ itemId: id, start: year + '-' + String(month).padStart(2,'0') + '-01', end: year + '-' + String(month).padStart(2,'0') + '-31' }),
|
getRecords({ itemId: id, start: year + '-' + String(month).padStart(2, '0') + '-01', end: year + '-' + String(month).padStart(2, '0') + '-31' }),
|
||||||
])
|
])
|
||||||
setItem(itemData)
|
setItem(itemData)
|
||||||
setStreak(streakData.streak)
|
setStreak(streakData.streak)
|
||||||
@@ -83,83 +83,193 @@ function DetailPage() {
|
|||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageLoading || !item) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (pageLoading || !item) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="任务详情" showBack />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">加载中</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
const cfg = CATEGORY_CONFIG[item.category] || CATEGORY_CONFIG.habit
|
const cfg = CATEGORY_CONFIG[item.category] || CATEGORY_CONFIG.habit
|
||||||
const daysInMonth = getDaysInMonth(year, month)
|
const daysInMonth = getDaysInMonth(year, month)
|
||||||
const firstDay = getFirstDayOfMonth(year, month)
|
const firstDay = getFirstDayOfMonth(year, month)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title={item.name} showBack />
|
<NavBar title="任务详情" showBack />
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
|
<div className="page-container">
|
||||||
|
{/* Action menu */}
|
||||||
|
<div style={{ padding: '0 16px', display: 'flex', justifyContent: 'flex-end', position: 'relative' }}>
|
||||||
<button onClick={() => setShowActions(!showActions)}
|
<button onClick={() => setShowActions(!showActions)}
|
||||||
style={{ position: 'absolute', right: 16, top: 12, background: 'none', border: 'none', fontSize: 20, cursor: 'pointer' }}>
|
className="btn-cyber" style={{ padding: '6px 12px', fontSize: 14 }}>
|
||||||
...
|
●●●
|
||||||
</button>
|
</button>
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<div className="card" style={{ position: 'absolute', right: 16, top: 44, zIndex: 10, minWidth: 140, padding: 0 }}>
|
<div className="card" style={{
|
||||||
{item.status === 'active' ? (
|
position: 'absolute', right: 16, top: 44, zIndex: 10,
|
||||||
<div className="ios-list-item" onClick={handleTogglePause}><span>暂停打卡</span></div>
|
minWidth: 150, padding: 0, margin: 0, overflow: 'hidden',
|
||||||
) : (
|
}}>
|
||||||
<div className="ios-list-item" onClick={handleTogglePause}><span>恢复打卡</span></div>
|
<button onClick={() => { handleTogglePause(); setShowActions(false) }}
|
||||||
)}
|
style={{
|
||||||
<div className="ios-list-item" onClick={handleDelete} style={{ color: '#FF3B30' }}><span>删除</span></div>
|
display: 'block', width: '100%', padding: '14px 18px',
|
||||||
|
border: 'none', borderBottom: '1px solid var(--color-border)',
|
||||||
|
cursor: 'pointer', fontSize: 13, textAlign: 'left',
|
||||||
|
background: 'transparent', color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => e.currentTarget.style.background = 'var(--color-card-hover)'}
|
||||||
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
||||||
|
>
|
||||||
|
{item.status === 'active' ? '■ 暂停' : '► 恢复'}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => { handleDelete(); setShowActions(false) }}
|
||||||
|
style={{
|
||||||
|
display: 'block', width: '100%', padding: '14px 18px',
|
||||||
|
border: 'none', cursor: 'pointer', fontSize: 13, textAlign: 'left',
|
||||||
|
background: 'transparent', color: 'var(--color-error)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => e.currentTarget.style.background = 'var(--color-card-hover)'}
|
||||||
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
||||||
|
>
|
||||||
|
✕ 删除
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ marginTop: 8 }}>
|
{/* Item info */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div className="card" style={{
|
||||||
|
marginTop: 8,
|
||||||
|
background: 'linear-gradient(135deg, rgba(0, 229, 255, 0.03), transparent)',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 52, height: 52, borderRadius: 14,
|
width: 52, height: 52, borderRadius: 14,
|
||||||
background: cfg.color + '20', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 26,
|
background: `linear-gradient(135deg, ${cfg.color}22, ${cfg.color}11)`,
|
||||||
}}>{item.icon || cfg.icon}</div>
|
border: `1px solid ${cfg.color}33`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 26,
|
||||||
|
}}>
|
||||||
|
{item.icon || cfg.icon}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ fontSize: 18, fontWeight: 700 }}>{item.name}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B', marginTop: 2 }}>
|
fontSize: 17, fontWeight: 700, color: 'var(--color-text)',
|
||||||
{cfg.label} · {CYCLE_LABEL[item.cycleType]} · {item.status === 'active' ? '进行中' : item.status === 'paused' ? '已暂停' : item.status}
|
}}>
|
||||||
|
{item.name}
|
||||||
</p>
|
</p>
|
||||||
{item.dailyGoal && <p style={{ fontSize: 13, color: '#86868B', marginTop: 2 }}>目标: {item.dailyGoal}</p>}
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: cfg.color }}>▸</span> {cfg.label} / {CYCLE_LABEL[item.cycleType]}
|
||||||
|
{' · '}
|
||||||
|
{item.status === 'active' ? '进行中' : item.status === 'paused' ? '已暂停' : item.status}
|
||||||
|
</p>
|
||||||
|
{item.dailyGoal && (
|
||||||
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-secondary)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
目标: {item.dailyGoal}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
{/* Stats */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-around', padding: '8px 0' }}>
|
<div className="card" style={{ padding: '16px 20px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-around', padding: '4px 0' }}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>{streak}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>连续天数</p>
|
fontSize: 26, fontWeight: 700, color: 'var(--color-gold)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{streak}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
连续
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>{records.length}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>本月打卡</p>
|
fontSize: 26, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{records.length}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
本月打卡
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 28, fontWeight: 700, color: '#FF6B35' }}>
|
<p style={{
|
||||||
|
fontSize: 26, fontWeight: 700, color: 'var(--color-accent)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
{daysInMonth > 0 ? Math.round((records.length / daysInMonth) * 100) : 0}%
|
{daysInMonth > 0 ? Math.round((records.length / daysInMonth) * 100) : 0}%
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>完成率</p>
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
完成率
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
{/* Calendar */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<div className="card" style={{ padding: '16px 18px' }}>
|
||||||
<button onClick={() => { if (month === 1) { setYear(y => y-1); setMonth(12) } else setMonth(m => m-1) }}
|
<div style={{
|
||||||
style={{ background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 4 }}>←</button>
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16,
|
||||||
<span style={{ fontSize: 16, fontWeight: 600 }}>{year} 年 {month} 月</span>
|
}}>
|
||||||
<button onClick={() => { if (month === 12) { setYear(y => y+1); setMonth(1) } else setMonth(m => m+1) }}
|
<button onClick={() => { if (month === 1) { setYear(y => y - 1); setMonth(12) } else setMonth(m => m - 1) }}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 4 }}>→</button>
|
className="btn-cyber" style={{ padding: '6px 10px', fontSize: 12 }}>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 14, fontWeight: 600, color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
{year}.{String(month).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => { if (month === 12) { setYear(y => y + 1); setMonth(1) } else setMonth(m => m + 1) }}
|
||||||
|
className="btn-cyber" style={{ padding: '6px 10px', fontSize: 12 }}>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4, textAlign: 'center' }}>
|
<div style={{
|
||||||
{['日','一','二','三','四','五','六'].map(d => (
|
display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
|
||||||
<div key={d} style={{ fontSize: 12, color: '#86868B', padding: '6px 0' }}>{d}</div>
|
gap: 3, textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(d => (
|
||||||
|
<div key={d} style={{
|
||||||
|
fontSize: 10, color: 'var(--color-text-muted)',
|
||||||
|
padding: '6px 0', fontFamily: 'var(--font-display)',
|
||||||
|
}}>
|
||||||
|
{d}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{Array.from({ length: firstDay }, (_, i) => <div key={'empty-' + i} />)}
|
{Array.from({ length: firstDay }, (_, i) => <div key={'empty-' + i} />)}
|
||||||
{Array.from({ length: daysInMonth }, (_, i) => {
|
{Array.from({ length: daysInMonth }, (_, i) => {
|
||||||
const day = i + 1
|
const day = i + 1
|
||||||
const dateStr = year + '-' + String(month).padStart(2,'0') + '-' + String(day).padStart(2,'0')
|
const dateStr = year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0')
|
||||||
const checked = recordDates.has(dateStr)
|
const checked = recordDates.has(dateStr)
|
||||||
const isMakeup = makeupDates.has(dateStr)
|
const isMakeup = makeupDates.has(dateStr)
|
||||||
const isToday = dateStr === todayStr
|
const isToday = dateStr === todayStr
|
||||||
@@ -167,26 +277,44 @@ function DetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div key={day} onClick={() => canMakeup && handleMakeup(dateStr)}
|
<div key={day} onClick={() => canMakeup && handleMakeup(dateStr)}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 0', fontSize: 14, borderRadius: 8, cursor: canMakeup ? 'pointer' : 'default',
|
padding: '7px 0', fontSize: 13, borderRadius: 8,
|
||||||
background: isToday ? '#FFF0EB' : 'transparent',
|
cursor: canMakeup ? 'pointer' : 'default',
|
||||||
|
background: isToday ? 'var(--color-primary-dim)' : 'transparent',
|
||||||
fontWeight: isToday ? 600 : 400,
|
fontWeight: isToday ? 600 : 400,
|
||||||
}}>
|
color: isToday ? 'var(--color-primary)' : 'var(--color-text)',
|
||||||
|
border: isToday ? '1px solid rgba(0, 229, 255, 0.2)' : '1px solid transparent',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (canMakeup) e.currentTarget.style.background = 'rgba(255, 179, 71, 0.1)' }}
|
||||||
|
onMouseLeave={e => { if (canMakeup) e.currentTarget.style.background = isToday ? 'var(--color-primary-dim)' : 'transparent' }}
|
||||||
|
>
|
||||||
{day}
|
{day}
|
||||||
{checked && <div style={{
|
{checked && (
|
||||||
width: 6, height: 6, borderRadius: 3, margin: '2px auto 0',
|
<div style={{
|
||||||
background: isMakeup ? '#FF9500' : '#34C759',
|
position: 'absolute', bottom: 3, left: '50%',
|
||||||
}} />}
|
transform: 'translateX(-50%)',
|
||||||
{!checked && isToday && <div style={{
|
width: 5, height: 5, borderRadius: '50%',
|
||||||
width: 6, height: 6, borderRadius: 3, margin: '2px auto 0',
|
background: isMakeup ? 'var(--color-warning)' : 'var(--color-accent)',
|
||||||
background: '#E5E5EA',
|
boxShadow: isMakeup ? '0 0 6px rgba(255, 179, 71, 0.4)' : '0 0 6px rgba(0, 230, 118, 0.4)',
|
||||||
}} />}
|
}} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ height: 80 }} />
|
<div style={{ padding: '0 16px', marginTop: 8 }}>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', textAlign: 'center',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
点击过去未打卡的日期可补卡
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
import NavBar from '@/components/NavBar'
|
import NavBar from '@/components/NavBar'
|
||||||
import { createItem, CATEGORY_CONFIG, type CheckInCategory, type CycleType } from '@/services/checkin'
|
import { createItem, CATEGORY_CONFIG, type CheckInCategory, type CycleType } from '@/services/checkin'
|
||||||
@@ -11,7 +11,7 @@ const CYCLE_OPTIONS: { value: CycleType; label: string }[] = [
|
|||||||
{ value: 'daily', label: '每日' },
|
{ value: 'daily', label: '每日' },
|
||||||
{ value: 'weekly', label: '每周' },
|
{ value: 'weekly', label: '每周' },
|
||||||
{ value: 'every_other_day', label: '隔日' },
|
{ value: 'every_other_day', label: '隔日' },
|
||||||
{ value: 'custom', label: '自定义间隔' },
|
{ value: 'custom', label: '自定义' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function CreateCheckInPage() {
|
function CreateCheckInPage() {
|
||||||
@@ -51,43 +51,67 @@ function CreateCheckInPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="创建打卡项" showBack />
|
<NavBar title="创建任务" showBack />
|
||||||
|
<div className="page-container">
|
||||||
<form onSubmit={handleSubmit} style={{ padding: 16 }}>
|
<form onSubmit={handleSubmit} style={{ padding: 16 }}>
|
||||||
{/* 名称 */}
|
{/* Name */}
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>名称 *</label>
|
<label style={{
|
||||||
<input className="input-field" value={name} onChange={e => setName(e.target.value)}
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
任务名称 *
|
||||||
|
</label>
|
||||||
|
<input className="input-cyber" value={name} onChange={e => setName(e.target.value)}
|
||||||
placeholder="如「每天背单词」" style={{ marginBottom: 20 }} maxLength={50} />
|
placeholder="如「每天背单词」" style={{ marginBottom: 20 }} maxLength={50} />
|
||||||
|
|
||||||
{/* 分类 */}
|
{/* Category */}
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 8, display: 'block' }}>分类</label>
|
<label style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 8, display: 'block',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
分类
|
||||||
|
</label>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, marginBottom: 20 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, marginBottom: 20 }}>
|
||||||
{CATEGORIES.map(([key, cfg]) => (
|
{CATEGORIES.map(([key, cfg]) => (
|
||||||
<button type="button" key={key}
|
<button type="button" key={key}
|
||||||
onClick={() => setCategory(key)}
|
onClick={() => setCategory(key)}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 8px', borderRadius: 10, border: '2px solid',
|
padding: '14px 8px', borderRadius: 12, border: '1px solid',
|
||||||
borderColor: category === key ? cfg.color : '#E5E5EA',
|
borderColor: category === key ? cfg.color + '66' : 'var(--color-border)',
|
||||||
background: category === key ? cfg.color + '15' : '#f8f8fa',
|
background: category === key ? `${cfg.color}15` : 'transparent',
|
||||||
cursor: 'pointer', fontSize: 13, textAlign: 'center', transition: 'all 0.2s',
|
cursor: 'pointer', fontSize: 12, textAlign: 'center',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
color: category === key ? cfg.color : 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 20 }}>{cfg.icon}</span>
|
<span style={{ fontSize: 22 }}>{cfg.icon}</span>
|
||||||
<p style={{ marginTop: 4, fontWeight: category === key ? 600 : 400 }}>{cfg.label}</p>
|
<p style={{ marginTop: 4, fontWeight: category === key ? 600 : 400 }}>
|
||||||
|
{cfg.label}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 打卡周期 */}
|
{/* Cycle */}
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>打卡周期</label>
|
<label style={{
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
周期
|
||||||
|
</label>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap',
|
||||||
|
}}>
|
||||||
{CYCLE_OPTIONS.map(opt => (
|
{CYCLE_OPTIONS.map(opt => (
|
||||||
<button type="button" key={opt.value}
|
<button type="button" key={opt.value}
|
||||||
onClick={() => setCycleType(opt.value)}
|
onClick={() => setCycleType(opt.value)}
|
||||||
|
className="btn-cyber"
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 16px', borderRadius: 8, border: '1.5px solid',
|
padding: '8px 16px', fontSize: 11, letterSpacing: 1,
|
||||||
borderColor: cycleType === opt.value ? '#FF6B35' : '#E5E5EA',
|
borderColor: cycleType === opt.value ? 'var(--color-primary)' : 'var(--color-border)',
|
||||||
background: cycleType === opt.value ? '#FFF0EB' : '#f8f8fa',
|
color: cycleType === opt.value ? 'var(--color-primary)' : 'var(--color-text-muted)',
|
||||||
cursor: 'pointer', fontSize: 14, transition: 'all 0.2s',
|
background: cycleType === opt.value ? 'rgba(0, 229, 255, 0.1)' : 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -95,26 +119,45 @@ function CreateCheckInPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 每日目标 */}
|
{/* Daily Goal */}
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>每日目标(选填)</label>
|
<label style={{
|
||||||
<input className="input-field" value={dailyGoal} onChange={e => setDailyGoal(e.target.value)}
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
每日目标(选填)
|
||||||
|
</label>
|
||||||
|
<input className="input-cyber" value={dailyGoal} onChange={e => setDailyGoal(e.target.value)}
|
||||||
placeholder="如「背 50 个单词」" style={{ marginBottom: 20 }} maxLength={200} />
|
placeholder="如「背 50 个单词」" style={{ marginBottom: 20 }} maxLength={200} />
|
||||||
|
|
||||||
{/* 隐私 */}
|
{/* Privacy */}
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>隐私设置</label>
|
<label style={{
|
||||||
<select className="input-field" value={privacy} onChange={e => setPrivacy(Number(e.target.value))}
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
style={{ marginBottom: 24 }}>
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
隐私设置
|
||||||
|
</label>
|
||||||
|
<select className="input-cyber" value={privacy} onChange={e => setPrivacy(Number(e.target.value))}
|
||||||
|
style={{ marginBottom: 24, cursor: 'pointer', appearance: 'auto' }}>
|
||||||
<option value={1}>公开</option>
|
<option value={1}>公开</option>
|
||||||
<option value={2}>仅好友可见</option>
|
<option value={2}>好友可见</option>
|
||||||
<option value={3}>仅自己可见</option>
|
<option value={3}>私密</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{error && <p style={{ fontSize: 13, color: '#FF3B30', marginBottom: 12, textAlign: 'center' }}>{error}</p>}
|
{error && (
|
||||||
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-error)', marginBottom: 12, textAlign: 'center',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
ERROR: {error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<button className="btn-primary" type="submit" disabled={submitting}>
|
<button className="btn-cyber btn-cyber--primary" type="submit" disabled={submitting}
|
||||||
{submitting ? '创建中...' : '创建打卡项'}
|
style={{ width: '100%', padding: 14, fontSize: 14, letterSpacing: 2 }}>
|
||||||
|
{submitting ? '创建中...' : '创建任务'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+564
-106
@@ -1,128 +1,586 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-primary: #FF6B35;
|
--color-bg: #080C18;
|
||||||
--color-primary-light: #FF8C5A;
|
--color-bg-alt: #0C1124;
|
||||||
--color-primary-gradient: linear-gradient(135deg, #FF6B35, #FF4D6D);
|
--color-card: #0F1529;
|
||||||
--color-accent: #7209B7;
|
--color-card-hover: #161E3A;
|
||||||
--color-bg: #F5F5F7;
|
--color-border: #1A2245;
|
||||||
|
--color-border-glow: rgba(0, 229, 255, 0.12);
|
||||||
|
--color-primary: #00E5FF;
|
||||||
|
--color-primary-dim: rgba(0, 229, 255, 0.08);
|
||||||
|
--color-primary-glow: rgba(0, 229, 255, 0.3);
|
||||||
|
--color-secondary: #3B82F6;
|
||||||
|
--color-accent: #00E676;
|
||||||
|
--color-gold: #FFB347;
|
||||||
|
--color-social: #FF3C7E;
|
||||||
|
--color-text: #E0E8FF;
|
||||||
|
--color-text-secondary: #6B7BA3;
|
||||||
|
--color-text-muted: #3D4F7A;
|
||||||
|
--color-error: #FF1744;
|
||||||
|
--color-success: #00E676;
|
||||||
|
--color-warning: #FFB347;
|
||||||
|
--color-progress-bg: rgba(26, 34, 69, 0.6);
|
||||||
|
|
||||||
|
--font-display: 'Orbitron', monospace;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-xl: 20px;
|
||||||
|
|
||||||
|
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-glow-cyan: 0 0 20px rgba(0, 229, 255, 0.15);
|
||||||
|
--shadow-glow-green: 0 0 20px rgba(0, 230, 118, 0.2);
|
||||||
|
--shadow-glow-gold: 0 0 20px rgba(255, 179, 71, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--color-bg: #E8ECF4;
|
||||||
|
--color-bg-alt: #DEE2EC;
|
||||||
--color-card: #FFFFFF;
|
--color-card: #FFFFFF;
|
||||||
--color-text: #1D1D1F;
|
--color-card-hover: #F0F4FF;
|
||||||
--color-text-secondary: #86868B;
|
--color-border: #D0D7E6;
|
||||||
--color-border: #E5E5EA;
|
--color-border-glow: rgba(59, 130, 246, 0.12);
|
||||||
--color-nav-bg: rgba(245,245,247,0.92);
|
--color-primary: #2563EB;
|
||||||
--color-success: #34C759;
|
--color-primary-dim: rgba(37, 99, 235, 0.06);
|
||||||
--color-warning: #FF9500;
|
--color-primary-glow: rgba(37, 99, 235, 0.15);
|
||||||
--color-error: #FF3B30;
|
--color-secondary: #3B82F6;
|
||||||
--radius-card: 12px;
|
--color-accent: #059669;
|
||||||
--radius-button: 10px;
|
--color-gold: #D97706;
|
||||||
--font-base: -apple-system, BlinkMacSystemFont, 'SF Pro', 'PingFang SC', sans-serif;
|
--color-social: #DB2777;
|
||||||
}
|
--color-text: #1A1F36;
|
||||||
|
--color-text-secondary: #5B6B8F;
|
||||||
|
--color-text-muted: #95A5C9;
|
||||||
|
--color-error: #DC2626;
|
||||||
|
--color-success: #059669;
|
||||||
|
--color-warning: #D97706;
|
||||||
|
--color-progress-bg: #E5E9F0;
|
||||||
|
--shadow-card: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-glow-cyan: 0 0 12px rgba(37, 99, 235, 0.1);
|
||||||
|
--shadow-glow-green: 0 0 12px rgba(5, 150, 103, 0.1);
|
||||||
|
--shadow-glow-gold: 0 0 12px rgba(217, 119, 6, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
html {
|
||||||
--color-bg: #1C1C1E;
|
scroll-behavior: smooth;
|
||||||
--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-body);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
transition: background 0.3s, color 0.3s;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
/* iOS 风格列表 */
|
body::before {
|
||||||
.ios-list {
|
content: '';
|
||||||
background: var(--color-card);
|
position: fixed;
|
||||||
border-radius: var(--radius-card);
|
inset: 0;
|
||||||
overflow: hidden;
|
pointer-events: none;
|
||||||
margin: 16px;
|
z-index: 9999;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
background: repeating-linear-gradient(
|
||||||
}
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0, 229, 255, 0.015) 2px,
|
||||||
|
rgba(0, 229, 255, 0.015) 4px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.ios-list-item {
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 229, 255, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 229, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 60px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__next, main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(0, 229, 255, 0.25);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] ::selection {
|
||||||
|
background: rgba(37, 99, 235, 0.2);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-text-muted);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glowPulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 8px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.4), 0 0 40px rgba(0, 229, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glowPulseGreen {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 8px rgba(0, 230, 118, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px rgba(0, 230, 118, 0.4), 0 0 40px rgba(0, 230, 118, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanLine {
|
||||||
|
0% {
|
||||||
|
top: -10%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 110%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderRotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dataFlicker {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes orbitPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Page Layout ===== */
|
||||||
|
.page-container {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
animation: fadeUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 10px;
|
||||||
padding: 14px 16px;
|
margin-bottom: 16px;
|
||||||
border-bottom: 0.5px solid var(--color-border);
|
padding: 0 16px;
|
||||||
cursor: pointer;
|
}
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.ios-list-item:last-child { border-bottom: none; }
|
|
||||||
.ios-list-item:hover { background: #f8f8fa; }
|
|
||||||
|
|
||||||
.ios-list-item-label {
|
.section-title-bar::before {
|
||||||
font-size: 15px;
|
content: '';
|
||||||
color: var(--color-text);
|
width: 3px;
|
||||||
}
|
height: 14px;
|
||||||
.ios-list-item-value {
|
background: var(--color-primary);
|
||||||
font-size: 15px;
|
border-radius: 2px;
|
||||||
color: var(--color-text-secondary);
|
box-shadow: 0 0 8px var(--color-primary-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮 */
|
.section-title-bar-text {
|
||||||
.btn-primary {
|
font-family: var(--font-display);
|
||||||
display: block;
|
font-size: 13px;
|
||||||
width: 100%;
|
letter-spacing: 1px;
|
||||||
padding: 14px;
|
color: var(--color-primary);
|
||||||
background: var(--color-primary-gradient);
|
}
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-button);
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
.btn-primary:active { opacity: 0.8; }
|
|
||||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* 输入框 */
|
/* ===== Cards ===== */
|
||||||
.input-field {
|
.card {
|
||||||
width: 100%;
|
|
||||||
padding: 14px 16px;
|
|
||||||
background: #f8f8fa;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-button);
|
|
||||||
font-size: 17px;
|
|
||||||
font-family: var(--font-base);
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
.input-field:focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 页面标题 */
|
|
||||||
.page-title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 20px 16px 8px;
|
|
||||||
}
|
|
||||||
.page-subtitle {
|
|
||||||
font-size: 15px;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
padding: 0 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片 */
|
|
||||||
.card {
|
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border-radius: var(--radius-card);
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
margin: 8px 16px;
|
margin: 8px 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
box-shadow: var(--shadow-card);
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s, background 0.3s, transform 0.2s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-border-glow),
|
||||||
|
transparent 40%,
|
||||||
|
transparent 60%,
|
||||||
|
var(--color-border-glow)
|
||||||
|
);
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--color-primary-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-glow {
|
||||||
|
box-shadow: var(--shadow-card), var(--shadow-glow-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Ambient Orbs ===== */
|
||||||
|
.ambient-orb {
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ambient-orb--cyan {
|
||||||
|
background: #00E5FF;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
animation: float 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ambient-orb--blue {
|
||||||
|
background: #3B82F6;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
bottom: 10%;
|
||||||
|
left: -80px;
|
||||||
|
animation: float 10s ease-in-out infinite 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ambient-orb--pink {
|
||||||
|
background: #FF3C7E;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
bottom: 20%;
|
||||||
|
right: -50px;
|
||||||
|
animation: float 12s ease-in-out infinite 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .ambient-orb--cyan {
|
||||||
|
opacity: 0.06;
|
||||||
|
background: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .ambient-orb--blue {
|
||||||
|
opacity: 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .ambient-orb--pink {
|
||||||
|
opacity: 0.04;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Buttons ===== */
|
||||||
|
.btn-cyber {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 229, 255, 0.1), rgba(59, 130, 246, 0.1));
|
||||||
|
border: 1px solid rgba(0, 229, 255, 0.25);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(0, 229, 255, 0.2), rgba(59, 130, 246, 0.2));
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber--primary {
|
||||||
|
background: linear-gradient(135deg, #00E5FF, #3B82F6);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #080C18;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber--primary:hover {
|
||||||
|
background: linear-gradient(135deg, #33EAFF, #5B9BF7);
|
||||||
|
box-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber--primary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber--danger {
|
||||||
|
border-color: rgba(255, 23, 68, 0.3);
|
||||||
|
color: var(--color-error);
|
||||||
|
background: rgba(255, 23, 68, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber--danger:hover {
|
||||||
|
background: rgba(255, 23, 68, 0.15);
|
||||||
|
border-color: var(--color-error);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 23, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyber:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Inputs ===== */
|
||||||
|
.input-cyber {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(15, 21, 41, 0.6);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
color: var(--color-text);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-cyber:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-cyber::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .input-cyber {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Typography ===== */
|
||||||
|
.text-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, #00E5FF, #3B82F6);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-gold {
|
||||||
|
background: linear-gradient(135deg, #FFB347, #FF8C00);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-green {
|
||||||
|
background: linear-gradient(135deg, #00E676, #00C853);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient-pink {
|
||||||
|
background: linear-gradient(135deg, #FF3C7E, #FF1744);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Loading ===== */
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-top-color: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
animation: dataFlicker 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Badge / Tag ===== */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--cyan {
|
||||||
|
background: rgba(0, 229, 255, 0.1);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 1px solid rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--green {
|
||||||
|
background: rgba(0, 230, 118, 0.1);
|
||||||
|
color: var(--color-accent);
|
||||||
|
border: 1px solid rgba(0, 230, 118, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--gold {
|
||||||
|
background: rgba(255, 179, 71, 0.1);
|
||||||
|
color: var(--color-gold);
|
||||||
|
border: 1px solid rgba(255, 179, 71, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== iOS compatibility ===== */
|
||||||
|
.ios-safe-bottom {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page title (legacy compat) */
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 20px 16px 4px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
padding: 0 16px 20px;
|
||||||
|
}
|
||||||
|
|||||||
+18
-1
@@ -1,7 +1,20 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { Orbitron, JetBrains_Mono } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
|
|
||||||
|
const orbitron = Orbitron({
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-display',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-mono',
|
||||||
|
display: 'swap',
|
||||||
|
})
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: '寸进 — 日日皆有成长',
|
title: '寸进 — 日日皆有成长',
|
||||||
description: '寸进(InchStep)综合成长打卡平台',
|
description: '寸进(InchStep)综合成长打卡平台',
|
||||||
@@ -9,8 +22,12 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="zh-CN" suppressHydrationWarning>
|
<html lang="zh-CN" suppressHydrationWarning
|
||||||
|
className={`${orbitron.variable} ${jetbrainsMono.variable}`}>
|
||||||
<body>
|
<body>
|
||||||
|
<div className="ambient-orb ambient-orb--cyan" />
|
||||||
|
<div className="ambient-orb ambient-orb--blue" />
|
||||||
|
<div className="ambient-orb ambient-orb--pink" />
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+66
-18
@@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const [phone, setPhone] = useState('')
|
const [phone, setPhone] = useState('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -28,24 +28,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 32 }}>
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 32,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
}}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div style={{ textAlign: 'center', marginBottom: 48 }}>
|
<div style={{ textAlign: 'center', marginBottom: 48 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 80, height: 80, borderRadius: 20,
|
width: 80, height: 80, borderRadius: 20,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'linear-gradient(135deg, #00E5FF, #3B82F6)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
margin: '0 auto 16px', fontSize: 36
|
margin: '0 auto 16px', fontSize: 32,
|
||||||
|
boxShadow: '0 0 40px rgba(0, 229, 255, 0.2)',
|
||||||
|
position: 'relative',
|
||||||
}}>
|
}}>
|
||||||
📈
|
<div style={{
|
||||||
|
position: 'absolute', inset: -3,
|
||||||
|
borderRadius: 23,
|
||||||
|
background: 'linear-gradient(135deg, rgba(0, 229, 255, 0.3), rgba(59, 130, 246, 0.1))',
|
||||||
|
zIndex: -1,
|
||||||
|
}} />
|
||||||
|
◈
|
||||||
</div>
|
</div>
|
||||||
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 4 }}>寸进</h1>
|
<h1 style={{
|
||||||
<p style={{ fontSize: 15, color: '#86868B' }}>日日皆有成长</p>
|
fontSize: 28, fontWeight: 700, marginBottom: 4,
|
||||||
|
background: 'linear-gradient(135deg, #00E5FF, #3B82F6)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
letterSpacing: 4,
|
||||||
|
}}>
|
||||||
|
寸进
|
||||||
|
</h1>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 13, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
letterSpacing: 2,
|
||||||
|
}}>
|
||||||
|
寸进 · 日日皆有成长
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
<input
|
<input
|
||||||
className="input-field"
|
className="input-cyber"
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder="手机号"
|
placeholder="手机号"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
@@ -53,27 +85,43 @@
|
|||||||
onChange={e => { setPhone(e.target.value.replace(/\D/g, '')); setError('') }}
|
onChange={e => { setPhone(e.target.value.replace(/\D/g, '')); setError('') }}
|
||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ fontSize: 13, color: '#FF3B30', marginBottom: 12, textAlign: 'center' }}>{error}</p>
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-error)',
|
||||||
|
marginBottom: 12, textAlign: 'center',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
ERROR: {error}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button className="btn-primary" type="submit" disabled={isLoading}>
|
<button
|
||||||
{isLoading ? '登录中...' : '登录 / 注册'}
|
className="btn-cyber btn-cyber--primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
style={{ width: '100%', padding: 14, fontSize: 15, letterSpacing: 2 }}
|
||||||
|
>
|
||||||
|
{isLoading ? '连接中...' : '登录 / 注册'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p style={{ fontSize: 13, color: '#86868B', textAlign: 'center', marginTop: 16 }}>
|
<p style={{
|
||||||
未注册的手机号将自动创建账号
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
textAlign: 'center', marginTop: 16,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
// 未注册的手机号将自动创建账号
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-28
@@ -43,70 +43,155 @@ function Dashboard() {
|
|||||||
|
|
||||||
const activeItems = items.filter(i => i.status === 'active')
|
const activeItems = items.filter(i => i.status === 'active')
|
||||||
const pausedItems = items.filter(i => i.status === 'paused')
|
const pausedItems = items.filter(i => i.status === 'paused')
|
||||||
|
const doneCount = activeItems.filter(i => todayStatus[i.id]).length
|
||||||
|
|
||||||
if (loading || pageLoading) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (loading || pageLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="寸进" />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">连接中</div>
|
||||||
|
</div>
|
||||||
|
<TabBar />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="寸进" />
|
<NavBar title="寸进" />
|
||||||
|
|
||||||
<div className="card" style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
<div className="page-container">
|
||||||
|
{/* Agent Status Card */}
|
||||||
|
<div className="card" style={{
|
||||||
|
marginTop: 14,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 14,
|
||||||
|
padding: '18px 20px',
|
||||||
|
background: 'linear-gradient(135deg, rgba(0, 229, 255, 0.04), rgba(59, 130, 246, 0.02))',
|
||||||
|
border: '1px solid rgba(0, 229, 255, 0.12)',
|
||||||
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 44, height: 44, borderRadius: 22,
|
width: 48, height: 48, borderRadius: 14,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'linear-gradient(135deg, #00E5FF22, #3B82F611)',
|
||||||
|
border: '1px solid rgba(0, 229, 255, 0.2)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 18, color: 'white', fontWeight: 600, flexShrink: 0,
|
fontSize: 20, color: 'var(--color-primary)', fontWeight: 700, flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{user.nickname?.[0] || user.phone?.slice(-4) || '?'}
|
{user.nickname?.[0] || user.phone?.slice(-4) || '?'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<p style={{ fontSize: 16, fontWeight: 600 }}>{user.nickname || '用户'}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>积分 {user.points} · {activeItems.length} 个活跃项</p>
|
fontSize: 16, fontWeight: 600, color: 'var(--color-text)',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
}}>
|
||||||
|
{user.nickname || '用户'}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-secondary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
}}>
|
||||||
|
<span style={{ color: 'var(--color-gold)' }}>◆</span>
|
||||||
|
<span>{user.points} 积分</span>
|
||||||
|
<span style={{ color: 'var(--color-text-muted)' }}>|</span>
|
||||||
|
<span style={{ color: 'var(--color-primary)' }}>◈ {activeItems.length}</span>
|
||||||
|
<span>活跃</span>
|
||||||
|
{doneCount > 0 && (
|
||||||
|
<>
|
||||||
|
<span style={{ color: 'var(--color-text-muted)' }}>|</span>
|
||||||
|
<span style={{ color: 'var(--color-accent)' }}>✓ {doneCount}/{activeItems.length}</span>
|
||||||
|
<span>今日</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => router.push('/profile')}
|
<button
|
||||||
style={{ background: 'none', border: 'none', fontSize: 14, color: '#FF6B35', cursor: 'pointer' }}>
|
onClick={() => router.push('/profile')}
|
||||||
我的
|
className="btn-cyber"
|
||||||
|
style={{ padding: '8px 14px', fontSize: 12, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
个人
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ padding: '20px 16px 8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
{/* Today's Missions */}
|
||||||
<h2 style={{ fontSize: 20, fontWeight: 700 }}>今日打卡</h2>
|
<div className="section-title-bar" style={{ marginTop: 8 }}>
|
||||||
<button onClick={() => router.push('/checkin/create')}
|
<span className="section-title-bar-text">今日任务</span>
|
||||||
style={{
|
|
||||||
padding: '6px 14px', borderRadius: 8, border: '1.5px solid #FF6B35',
|
|
||||||
background: 'white', color: '#FF6B35', fontSize: 14, fontWeight: 600, cursor: 'pointer',
|
|
||||||
}}>
|
|
||||||
+ 创建
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeItems.length === 0 ? (
|
{activeItems.length === 0 ? (
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '40px 16px' }}>
|
<div className="card" style={{
|
||||||
<p style={{ fontSize: 40, marginBottom: 12 }}>📋</p>
|
textAlign: 'center',
|
||||||
<p style={{ fontSize: 15, color: '#86868B' }}>
|
padding: '48px 20px',
|
||||||
还没有打卡项<br />
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12,
|
||||||
<span style={{ fontSize: 13 }}>点击右上角"创建"开始你的成长之旅</span>
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 56, height: 56, borderRadius: 16,
|
||||||
|
background: 'rgba(0, 229, 255, 0.08)',
|
||||||
|
border: '1px solid rgba(0, 229, 255, 0.15)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 24,
|
||||||
|
}}>
|
||||||
|
◈
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
|
||||||
|
还没有活跃的打卡项
|
||||||
</p>
|
</p>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||||
|
点击下方按钮创建你的第一个任务
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/checkin/create')}
|
||||||
|
className="btn-cyber btn-cyber--primary"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
+ 创建任务
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
activeItems.map(item => (
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 16px 4px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/checkin/create')}
|
||||||
|
className="btn-cyber"
|
||||||
|
style={{ padding: '6px 14px', fontSize: 11, letterSpacing: 1 }}
|
||||||
|
>
|
||||||
|
+ 新建
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{activeItems.map((item, i) => (
|
||||||
|
<div key={item.id} style={{
|
||||||
|
animation: 'fadeUp 0.5s ease-out',
|
||||||
|
animationDelay: `${i * 0.08}s`,
|
||||||
|
animationFillMode: 'both',
|
||||||
|
}}>
|
||||||
<CheckInCard key={item.id} item={item} streak={streaks[item.id]}
|
<CheckInCard key={item.id} item={item} streak={streaks[item.id]}
|
||||||
isDone={todayStatus[item.id]}
|
isDone={todayStatus[item.id]}
|
||||||
onCheckIn={(id) => setTodayStatus(s => ({ ...s, [id]: true }))} />
|
onCheckIn={(id) => setTodayStatus(s => ({ ...s, [id]: true }))} />
|
||||||
))
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Paused Items */}
|
||||||
{pausedItems.length > 0 && (
|
{pausedItems.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h3 style={{ fontSize: 15, color: '#86868B', padding: '20px 16px 4px' }}>已暂停 ({pausedItems.length})</h3>
|
<div className="section-title-bar" style={{ marginTop: 16 }}>
|
||||||
|
<span className="section-title-bar-text" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
|
已暂停 ({pausedItems.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{pausedItems.map(item => (
|
{pausedItems.map(item => (
|
||||||
<CheckInCard key={item.id} item={item} />
|
<CheckInCard key={item.id} item={item} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ height: 100 }} />
|
|
||||||
<TabBar />
|
<TabBar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+148
-46
@@ -42,88 +42,190 @@ function ProfilePage() {
|
|||||||
setEditing(true)
|
setEditing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || !profile) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (loading || !profile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="个人主页" />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">加载中</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="个人主页" showBack />
|
<NavBar title="个人主页" />
|
||||||
|
|
||||||
{/* 头像与基本信息 */}
|
<div className="page-container">
|
||||||
<div className="card" style={{ marginTop: 16, textAlign: 'center', padding: '32px 16px' }}>
|
{/* Avatar & Bio */}
|
||||||
|
<div className="card" style={{
|
||||||
|
marginTop: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '36px 20px 28px',
|
||||||
|
background: 'linear-gradient(135deg, rgba(0, 229, 255, 0.03), rgba(59, 130, 246, 0.02))',
|
||||||
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 72, height: 72, borderRadius: 36,
|
width: 72, height: 72, borderRadius: 18,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'linear-gradient(135deg, #00E5FF22, #3B82F611)',
|
||||||
|
border: '2px solid rgba(0, 229, 255, 0.15)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
margin: '0 auto 12px', fontSize: 28, color: 'white', fontWeight: 600
|
margin: '0 auto 14px', fontSize: 28, color: 'var(--color-primary)', fontWeight: 600,
|
||||||
|
boxShadow: '0 0 30px rgba(0, 229, 255, 0.1)',
|
||||||
}}>
|
}}>
|
||||||
{profile.nickname?.[0] || profile.phone?.slice(-4) || '?'}
|
{profile.nickname?.[0] || profile.phone?.slice(-4) || '?'}
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: 22, fontWeight: 700 }}>{profile.nickname || '未设置昵称'}</h2>
|
<h2 style={{
|
||||||
<p style={{ fontSize: 14, color: '#86868B', marginTop: 4 }}>{profile.bio || '还没有个性签名'}</p>
|
fontSize: 20, fontWeight: 700, color: 'var(--color-text)',
|
||||||
|
letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
{profile.nickname || '未设置昵称'}
|
||||||
|
</h2>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)', marginTop: 6,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{profile.bio || '没有个性签名'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计数据 */}
|
{/* Stats */}
|
||||||
<div className="card">
|
<div className="card" style={{ padding: '16px 20px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-around', padding: '8px 0' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-around', padding: '4px 0' }}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>{profile.points}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>总积分</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-gold)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{profile.points}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
积分
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>--</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>连续天数</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
--
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
连续
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>--</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>徽章</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-social)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
--
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
徽章
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作列表 */}
|
{/* Actions */}
|
||||||
<div className="ios-list">
|
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||||
{!editing ? (
|
{!editing ? (
|
||||||
<>
|
<>
|
||||||
<div className="ios-list-item" onClick={startEdit}>
|
<button onClick={startEdit} className="ios-list-item" style={{
|
||||||
<span className="ios-list-item-label">编辑资料</span>
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
<span className="ios-list-item-value">→</span>
|
padding: '16px 20px', width: '100%', border: 'none', cursor: 'pointer',
|
||||||
</div>
|
background: 'transparent', color: 'var(--color-text)', fontSize: 14,
|
||||||
<div className="ios-list-item">
|
borderBottom: '1px solid var(--color-border)',
|
||||||
<span className="ios-list-item-label">隐私设置</span>
|
transition: 'background 0.2s',
|
||||||
<span className="ios-list-item-value">
|
}}
|
||||||
{profile.privacy === 1 ? '公开' : profile.privacy === 2 ? '仅好友可见' : '仅自己可见'}
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--color-card-hover)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
<span>编辑资料</span>
|
||||||
|
<span style={{ color: 'var(--color-text-muted)' }}>→</span>
|
||||||
|
</button>
|
||||||
|
<div className="ios-list-item" style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '16px 20px', width: '100%',
|
||||||
|
borderBottom: '1px solid var(--color-border)',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 14, color: 'var(--color-text)' }}>隐私设置</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{profile.privacy === 1 ? '公开' : profile.privacy === 2 ? '好友可见' : '私密'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ios-list-item" onClick={() => router.push('/achievements')}>
|
<button onClick={() => router.push('/achievements')} className="ios-list-item" style={{
|
||||||
<span className="ios-list-item-label">成就徽章</span>
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
<span className="ios-list-item-value">→</span>
|
padding: '16px 20px', width: '100%', border: 'none', cursor: 'pointer',
|
||||||
</div>
|
background: 'transparent', color: 'var(--color-text)', fontSize: 14,
|
||||||
<div className="ios-list-item" onClick={logout}>
|
borderBottom: '1px solid var(--color-border)',
|
||||||
<span className="ios-list-item-label" style={{ color: '#FF3B30' }}>退出登录</span>
|
transition: 'background 0.2s',
|
||||||
</div>
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--color-card-hover)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
<span>成就徽章</span>
|
||||||
|
<span style={{ color: 'var(--color-text-muted)' }}>→</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={logout} className="ios-list-item" style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '16px 20px', width: '100%', border: 'none', cursor: 'pointer',
|
||||||
|
background: 'transparent', fontSize: 14,
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--color-card-hover)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--color-error)' }}>退出登录</span>
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: 16 }}>
|
<div style={{ padding: 20 }}>
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>昵称</label>
|
<label style={{
|
||||||
<input className="input-field" value={nickname} onChange={e => setNickname(e.target.value)} style={{ marginBottom: 12 }} placeholder="输入昵称" />
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
<label style={{ fontSize: 13, color: '#86868B', marginBottom: 6, display: 'block' }}>个性签名</label>
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
<input className="input-field" value={bio} onChange={e => setBio(e.target.value)} style={{ marginBottom: 16 }} placeholder="最多 50 字" maxLength={50} />
|
}}>
|
||||||
|
昵称
|
||||||
|
</label>
|
||||||
|
<input className="input-cyber" value={nickname} onChange={e => setNickname(e.target.value)}
|
||||||
|
style={{ marginBottom: 14 }} placeholder="输入昵称" />
|
||||||
|
<label style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 6, display: 'block',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
签名
|
||||||
|
</label>
|
||||||
|
<input className="input-cyber" value={bio} onChange={e => setBio(e.target.value)}
|
||||||
|
style={{ marginBottom: 18 }} placeholder="最多 50 字" maxLength={50} />
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
<button className="btn-primary" onClick={handleSave} disabled={saving} style={{ flex: 1 }}>
|
<button className="btn-cyber btn-cyber--primary" onClick={handleSave} disabled={saving}
|
||||||
|
style={{ flex: 1, padding: 12, fontSize: 13 }}>
|
||||||
{saving ? '保存中...' : '保存'}
|
{saving ? '保存中...' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setEditing(false)} style={{
|
<button onClick={() => setEditing(false)} className="btn-cyber"
|
||||||
flex: 1, padding: 14, background: '#f8f8fa', border: '1px solid #E5E5EA',
|
style={{ flex: 1, padding: 12, fontSize: 13, opacity: 0.6 }}>
|
||||||
borderRadius: 10, fontSize: 17, cursor: 'pointer', color: '#86868B'
|
|
||||||
}}>
|
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ height: 100 }} />
|
|
||||||
<TabBar />
|
<TabBar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
import NavBar from '@/components/NavBar'
|
import NavBar from '@/components/NavBar'
|
||||||
import { listFriends, sendFriendRequest, removeFriend, type IUserBrief } from '@/services/social'
|
import { listFriends, sendFriendRequest, removeFriend, type IUserBrief } from '@/services/social'
|
||||||
|
|
||||||
function FriendsPage() {
|
function FriendsPage() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [friends, setFriends] = useState<IUserBrief[]>([])
|
const [friends, setFriends] = useState<IUserBrief[]>([])
|
||||||
@@ -53,72 +53,118 @@
|
|||||||
? friends.filter(f => f.nickname?.includes(search))
|
? friends.filter(f => f.nickname?.includes(search))
|
||||||
: friends
|
: friends
|
||||||
|
|
||||||
if (loading || pageLoading) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (loading || pageLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="好友" showBack />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">加载中</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="好友" showBack />
|
<NavBar title="好友" showBack />
|
||||||
|
|
||||||
{/* 添加好友 */}
|
<div className="page-container">
|
||||||
<div className="card" style={{ marginTop: 16 }}>
|
{/* Add friend */}
|
||||||
<p style={{ fontSize: 15, fontWeight: 600, marginBottom: 8 }}>添加好友</p>
|
<div className="card" style={{ marginTop: 14 }}>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 10,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
添加好友
|
||||||
|
</p>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<input className="input-field" value={addPhone} onChange={e => setAddPhone(e.target.value)}
|
<input className="input-cyber" value={addPhone} onChange={e => setAddPhone(e.target.value)}
|
||||||
placeholder="输入手机号" style={{ flex: 1 }} />
|
placeholder="输入手机号" style={{ flex: 1, fontSize: 14 }} />
|
||||||
<button onClick={handleAdd} disabled={adding || !addPhone.trim()}
|
<button onClick={handleAdd} disabled={adding || !addPhone.trim()}
|
||||||
|
className="btn-cyber btn-cyber--primary"
|
||||||
style={{
|
style={{
|
||||||
padding: '0 16px', borderRadius: 10, border: 'none',
|
padding: '0 18px', fontSize: 12, letterSpacing: 1,
|
||||||
background: addPhone.trim() ? '#FF6B35' : '#E5E5EA',
|
opacity: addPhone.trim() ? 1 : 0.4,
|
||||||
color: 'white', fontSize: 14, cursor: addPhone.trim() ? 'pointer' : 'default',
|
|
||||||
}}>
|
}}>
|
||||||
添加
|
添加
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 好友列表 */}
|
{/* Friends list */}
|
||||||
<div style={{ padding: '20px 16px 8px', display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{
|
||||||
<h3 style={{ fontSize: 15, fontWeight: 600 }}>好友列表 ({friends.length})</h3>
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
|
padding: '20px 16px 12px',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
好友 ({friends.length})
|
||||||
|
</span>
|
||||||
{friends.length > 0 && (
|
{friends.length > 0 && (
|
||||||
<input className="input-field" value={search} onChange={e => setSearch(e.target.value)}
|
<input className="input-cyber" value={search} onChange={e => setSearch(e.target.value)}
|
||||||
placeholder="搜索..." style={{ width: 140, fontSize: 13, padding: '6px 10px' }} />
|
placeholder="搜索" style={{
|
||||||
|
width: 130, fontSize: 11, padding: '6px 10px',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '40px 16px' }}>
|
<div className="card" style={{
|
||||||
<p style={{ fontSize: 15, color: '#86868B' }}>
|
textAlign: 'center', padding: '40px 16px',
|
||||||
{search ? '未找到匹配的好友' : '还没有好友,输入手机号添加吧'}
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--color-text-secondary)' }}>
|
||||||
|
{search ? '未找到匹配的好友' : '还没有好友'}
|
||||||
</p>
|
</p>
|
||||||
|
{!search && (
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
输入手机号添加好友
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filtered.map(f => (
|
filtered.map(f => (
|
||||||
<div key={f.id} className="ios-list-item" style={{ margin: '0 16px', borderRadius: 12, background: 'white', padding: '12px 16px' }}>
|
<div key={f.id} className="card" style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '12px 18px', margin: '4px 16px',
|
||||||
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 36, height: 36, borderRadius: 18,
|
width: 38, height: 38, borderRadius: 10,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'linear-gradient(135deg, rgba(255, 60, 126, 0.15), rgba(255, 23, 68, 0.08))',
|
||||||
|
border: '1px solid rgba(255, 60, 126, 0.2)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 14, color: 'white', fontWeight: 600,
|
fontSize: 14, color: 'var(--color-social)', fontWeight: 600,
|
||||||
}}>
|
}}>
|
||||||
{f.nickname?.[0] || '?'}
|
{f.nickname?.[0] || '?'}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: 15 }}>{f.nickname || '用户'}</span>
|
<span style={{
|
||||||
|
fontSize: 14, color: 'var(--color-text)',
|
||||||
|
}}>
|
||||||
|
{f.nickname || '用户'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => handleRemove(f.id)}
|
<button onClick={() => handleRemove(f.id)}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 13, color: '#FF3B30', cursor: 'pointer' }}>
|
className="btn-cyber btn-cyber--danger"
|
||||||
|
style={{ padding: '6px 14px', fontSize: 11 }}>
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div style={{ height: 80 }} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FriendsRoute() {
|
export default function FriendsRoute() {
|
||||||
return <AuthProvider><FriendsPage /></AuthProvider>
|
return <AuthProvider><FriendsPage /></AuthProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-39
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
import NavBar from '@/components/NavBar'
|
import NavBar from '@/components/NavBar'
|
||||||
import TabBar from '@/components/TabBar'
|
import TabBar from '@/components/TabBar'
|
||||||
import FeedCard from '@/components/FeedCard'
|
import FeedCard from '@/components/FeedCard'
|
||||||
import { getFeed, createPost, type IPost } from '@/services/social'
|
import { getFeed, createPost, type IPost } from '@/services/social'
|
||||||
|
|
||||||
function FeedPage() {
|
function FeedPage() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [posts, setPosts] = useState<IPost[]>([])
|
const [posts, setPosts] = useState<IPost[]>([])
|
||||||
@@ -55,41 +55,79 @@
|
|||||||
<>
|
<>
|
||||||
<NavBar title="广场" />
|
<NavBar title="广场" />
|
||||||
|
|
||||||
{/* 发布动态入口 */}
|
<div className="page-container">
|
||||||
<div className="card" style={{ marginTop: 16, cursor: 'pointer' }} onClick={() => setShowCreate(true)}>
|
{/* Create post entry */}
|
||||||
|
<div className="card" style={{
|
||||||
|
marginTop: 14, cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
border: '1px solid rgba(255, 60, 126, 0.08)',
|
||||||
|
}}
|
||||||
|
onClick={() => setShowCreate(true)}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(255, 60, 126, 0.2)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(255, 60, 126, 0.08)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 36, height: 36, borderRadius: 18,
|
width: 38, height: 38, borderRadius: 10,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'rgba(255, 60, 126, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 60, 126, 0.2)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 14, color: 'white', fontWeight: 600,
|
fontSize: 15, color: 'var(--color-social)', fontWeight: 600,
|
||||||
}}>
|
}}>
|
||||||
{user.nickname?.[0] || user.phone?.slice(-4) || '?'}
|
{user.nickname?.[0] || user.phone?.slice(-4) || '?'}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: 15, color: '#86868B' }}>分享你的打卡动态...</span>
|
<span style={{
|
||||||
|
fontSize: 14, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
分享你的打卡动态...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 创建动态弹层 */}
|
{/* Create post modal */}
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.4)',
|
position: 'fixed', inset: 0, zIndex: 200,
|
||||||
|
background: 'rgba(8, 12, 24, 0.7)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
|
display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
|
||||||
}} onClick={() => setShowCreate(false)}>
|
}} onClick={() => setShowCreate(false)}>
|
||||||
<div className="card" style={{ width: '100%', maxWidth: 500, margin: 0, borderRadius: '16px 16px 0 0' }}
|
<div className="card" style={{
|
||||||
onClick={e => e.stopPropagation()}>
|
width: '100%', maxWidth: 500, margin: 0,
|
||||||
<p style={{ fontSize: 17, fontWeight: 600, textAlign: 'center', marginBottom: 12 }}>发布动态</p>
|
borderRadius: '20px 20px 0 0',
|
||||||
|
padding: '24px 20px 32px',
|
||||||
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 16, fontWeight: 600, textAlign: 'center', marginBottom: 16,
|
||||||
|
color: 'var(--color-social)',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
发布动态
|
||||||
|
</p>
|
||||||
<textarea ref={inputRef} value={newContent} onChange={e => setNewContent(e.target.value)}
|
<textarea ref={inputRef} value={newContent} onChange={e => setNewContent(e.target.value)}
|
||||||
placeholder="分享你的打卡故事..." rows={4}
|
placeholder="分享你的打卡故事..."
|
||||||
style={{ width: '100%', border: '1px solid #E5E5EA', borderRadius: 10, padding: 12, fontSize: 15, resize: 'none', outline: 'none', fontFamily: 'inherit' }}
|
rows={4}
|
||||||
|
className="input-cyber"
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: 14, fontSize: 14, resize: 'none',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', gap: 12, marginTop: 12 }}>
|
<div style={{ display: 'flex', gap: 12, marginTop: 14 }}>
|
||||||
<button onClick={() => { setShowCreate(false); setNewContent('') }}
|
<button onClick={() => { setShowCreate(false); setNewContent('') }}
|
||||||
style={{ flex: 1, padding: 12, background: '#f8f8fa', border: '1px solid #E5E5EA', borderRadius: 10, fontSize: 15, cursor: 'pointer' }}>
|
className="btn-cyber" style={{ flex: 1, padding: 12, fontSize: 13 }}>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleCreate} disabled={creating || !newContent.trim()}
|
<button onClick={handleCreate} disabled={creating || !newContent.trim()}
|
||||||
className="btn-primary" style={{ flex: 1, padding: 12 }}>
|
className="btn-cyber btn-cyber--primary" style={{
|
||||||
|
flex: 1, padding: 12, fontSize: 13,
|
||||||
|
opacity: creating || !newContent.trim() ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
{creating ? '发布中...' : '发布'}
|
{creating ? '发布中...' : '发布'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,37 +135,62 @@
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Feed 列表 */}
|
{/* Feed list */}
|
||||||
{feedLoading ? (
|
{feedLoading ? (
|
||||||
<div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">加载中</div>
|
||||||
|
</div>
|
||||||
) : posts.length === 0 ? (
|
) : posts.length === 0 ? (
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '40px 16px' }}>
|
<div className="card" style={{
|
||||||
<p style={{ fontSize: 40, marginBottom: 12 }}>🌊</p>
|
textAlign: 'center',
|
||||||
<p style={{ fontSize: 15, color: '#86868B' }}>还没有动态,快来发布第一条吧</p>
|
padding: '48px 20px',
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 56, height: 56, borderRadius: 16,
|
||||||
|
background: 'rgba(255, 60, 126, 0.08)',
|
||||||
|
border: '1px solid rgba(255, 60, 126, 0.15)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 24,
|
||||||
|
}}>
|
||||||
|
◎
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--color-text-secondary)' }}>
|
||||||
|
还没有动态
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
||||||
|
快来发布第一条吧
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
posts.map(post => (
|
posts.map((post, i) => (
|
||||||
|
<div key={post.id} style={{
|
||||||
|
animation: 'fadeUp 0.4s ease-out',
|
||||||
|
animationDelay: `${i * 0.04}s`,
|
||||||
|
animationFillMode: 'both',
|
||||||
|
}}>
|
||||||
<FeedCard key={post.id} post={post} onUpdate={loadFeed} />
|
<FeedCard key={post.id} post={post} onUpdate={loadFeed} />
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 加载更多 */}
|
{/* Load more */}
|
||||||
{posts.length < total && (
|
{posts.length < total && (
|
||||||
<button onClick={() => setPage(p => p + 1)} style={{
|
<button onClick={() => setPage(p => p + 1)} className="btn-cyber" style={{
|
||||||
display: 'block', margin: '16px auto', padding: '10px 24px',
|
display: 'block', margin: '16px auto', padding: '10px 28px',
|
||||||
background: 'white', border: '1px solid #E5E5EA', borderRadius: 8,
|
fontSize: 12, letterSpacing: 1,
|
||||||
fontSize: 14, color: '#86868B', cursor: 'pointer',
|
|
||||||
}}>
|
}}>
|
||||||
加载更多
|
加载更多
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ height: 100 }} />
|
|
||||||
<TabBar />
|
<TabBar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SocialRoute() {
|
export default function SocialRoute() {
|
||||||
return <AuthProvider><FeedPage /></AuthProvider>
|
return <AuthProvider><FeedPage /></AuthProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-52
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
import { AuthProvider, useAuth } from '@/components/AuthProvider'
|
||||||
import NavBar from '@/components/NavBar'
|
import NavBar from '@/components/NavBar'
|
||||||
import TabBar from '@/components/TabBar'
|
import TabBar from '@/components/TabBar'
|
||||||
import ProgressRing from '@/components/ProgressRing'
|
import ProgressRing from '@/components/ProgressRing'
|
||||||
import { getDashboardStats, type DashboardStats } from '@/services/analytics'
|
import { getDashboardStats, type DashboardStats } from '@/services/analytics'
|
||||||
|
|
||||||
function StatsPage() {
|
function StatsPage() {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
@@ -30,99 +30,233 @@ function StatsPage() {
|
|||||||
setPageLoading(false)
|
setPageLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || pageLoading) return <div style={{ padding: 40, textAlign: 'center', color: '#86868B' }}>加载中...</div>
|
if (loading || pageLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavBar title="统计" />
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<div className="loading-text">分析中</div>
|
||||||
|
</div>
|
||||||
|
<TabBar />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar title="统计" />
|
<NavBar title="统计" />
|
||||||
|
|
||||||
{/* 月份切换 */}
|
<div className="page-container">
|
||||||
<div className="card" style={{ marginTop: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
{/* Month selector */}
|
||||||
<button onClick={() => { if (month === 1) { setYear(y => y-1); setMonth(12) } else setMonth(m => m-1) }}
|
<div className="card" style={{
|
||||||
style={{ background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 8 }}>←</button>
|
marginTop: 14, display: 'flex', justifyContent: 'space-between',
|
||||||
<span style={{ fontSize: 17, fontWeight: 600 }}>{year} 年 {month} 月</span>
|
alignItems: 'center', padding: '14px 18px',
|
||||||
<button onClick={() => { if (month === 12) { setYear(y => y+1); setMonth(1) } else setMonth(m => m+1) }}
|
}}>
|
||||||
style={{ background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 8 }}>→</button>
|
<button onClick={() => { if (month === 1) { setYear(y => y - 1); setMonth(12) } else setMonth(m => m - 1) }}
|
||||||
|
className="btn-cyber" style={{ padding: '6px 12px', fontSize: 14, border: '1px solid rgba(0, 229, 255, 0.15)' }}>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 15, fontWeight: 600, color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 2,
|
||||||
|
}}>
|
||||||
|
{year}.{String(month).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => { if (month === 12) { setYear(y => y + 1); setMonth(1) } else setMonth(m => m + 1) }}
|
||||||
|
className="btn-cyber" style={{ padding: '6px 12px', fontSize: 14, border: '1px solid rgba(0, 229, 255, 0.15)' }}>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
{/* 概览卡片 */}
|
{/* Overview */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, margin: '8px 16px' }}>
|
<div style={{
|
||||||
<div className="card" style={{ margin: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, margin: '8px 16px',
|
||||||
<ProgressRing percent={data.summary.completionRate} size={90} label="完成率" />
|
}}>
|
||||||
|
<div className="card card-glow" style={{ margin: 0, padding: '20px 12px', display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<ProgressRing percent={data.summary.completionRate} size={96} label="完成率" />
|
||||||
</div>
|
</div>
|
||||||
<div className="card" style={{ margin: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 12 }}>
|
<div className="card" style={{
|
||||||
|
margin: 0, display: 'flex', flexDirection: 'column',
|
||||||
|
justifyContent: 'center', gap: 14, padding: '16px 18px',
|
||||||
|
}}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>{data.summary.totalRecords}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 12, color: '#86868B' }}>本月打卡</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{data.summary.totalRecords}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
打卡
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>{data.summary.totalItems}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 12, color: '#86868B' }}>活跃项</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{data.summary.totalItems}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2,
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
活跃项
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 最高连续天数 */}
|
{/* Max streak */}
|
||||||
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '16px 20px' }}>
|
<div className="card" style={{
|
||||||
<span style={{ fontSize: 32 }}>🔥</span>
|
display: 'flex', alignItems: 'center', gap: 14, padding: '16px 20px',
|
||||||
|
background: 'linear-gradient(135deg, rgba(255, 179, 71, 0.04), transparent)',
|
||||||
|
border: '1px solid rgba(255, 179, 71, 0.12)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 44, height: 44, borderRadius: 12,
|
||||||
|
background: 'rgba(255, 179, 71, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 179, 71, 0.2)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 20,
|
||||||
|
}}>
|
||||||
|
◆
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ fontSize: 24, fontWeight: 700, color: '#FF6B35' }}>{data.summary.maxStreak} 天</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B' }}>最高连续打卡</p>
|
fontSize: 22, fontWeight: 700, color: 'var(--color-gold)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{data.summary.maxStreak}
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'var(--font-body)', fontSize: 14, fontWeight: 400,
|
||||||
|
color: 'var(--color-text-secondary)', marginLeft: 4,
|
||||||
|
}}>天</span>
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-display)', letterSpacing: 1, marginTop: 2,
|
||||||
|
}}>
|
||||||
|
最高连续
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分类分布 */}
|
{/* Category distribution */}
|
||||||
<h3 style={{ fontSize: 15, color: '#86868B', padding: '20px 16px 8px' }}>分类分布</h3>
|
<div className="section-title-bar" style={{ marginTop: 8 }}>
|
||||||
<div className="card" style={{ padding: '12px 16px' }}>
|
<span className="section-title-bar-text">分类分布</span>
|
||||||
{data.byCategory.map(c => {
|
</div>
|
||||||
|
<div className="card" style={{ padding: '14px 18px' }}>
|
||||||
|
{data.byCategory.map((c) => {
|
||||||
const pct = data.summary.totalItems > 0 ? Math.round((c.count / data.summary.totalItems) * 100) : 0
|
const pct = data.summary.totalItems > 0 ? Math.round((c.count / data.summary.totalItems) * 100) : 0
|
||||||
return (
|
return (
|
||||||
<div key={c.category} style={{ marginBottom: 10 }}>
|
<div key={c.category} style={{ marginBottom: 12 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
<div style={{
|
||||||
<span style={{ fontSize: 14 }}>{c.label}</span>
|
display: 'flex', justifyContent: 'space-between', marginBottom: 5,
|
||||||
<span style={{ fontSize: 13, color: '#86868B' }}>{c.count} 项 · {pct}%</span>
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 13, color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: c.color }}>▸</span> {c.label}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{c.count} 项 · {pct}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 6, borderRadius: 3, background: '#F0F0F0', overflow: 'hidden' }}>
|
<div style={{
|
||||||
<div style={{ height: '100%', borderRadius: 3, background: c.color, width: pct + '%', transition: 'width 0.5s' }} />
|
height: 5, borderRadius: 3,
|
||||||
|
background: 'var(--color-progress-bg)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%', borderRadius: 3,
|
||||||
|
background: c.color,
|
||||||
|
width: pct + '%',
|
||||||
|
transition: 'width 0.6s ease',
|
||||||
|
boxShadow: `0 0 8px ${c.color}66`,
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 单项详情 */}
|
{/* Per-item stats */}
|
||||||
<h3 style={{ fontSize: 15, color: '#86868B', padding: '20px 16px 8px' }}>单项统计</h3>
|
<div className="section-title-bar" style={{ marginTop: 8 }}>
|
||||||
|
<span className="section-title-bar-text">单项统计</span>
|
||||||
|
</div>
|
||||||
{data.items.map((item: any) => {
|
{data.items.map((item: any) => {
|
||||||
const rate = data.daysInMonth > 0 ? Math.round((item.monthlyCount / data.daysInMonth) * 100) : 0
|
const rate = data.daysInMonth > 0 ? Math.round((item.monthlyCount / data.daysInMonth) * 100) : 0
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="card" onClick={() => router.push(`/checkin/${item.id}`)}
|
<div key={item.id} className="card"
|
||||||
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12 }}>
|
onClick={() => router.push(`/checkin/${item.id}`)}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(0, 229, 255, 0.2)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.borderColor = ''
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 40, height: 40, borderRadius: 10,
|
width: 40, height: 40, borderRadius: 10,
|
||||||
background: '#FFF0EB', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18,
|
background: 'var(--color-primary-dim)',
|
||||||
|
border: '1px solid rgba(0, 229, 255, 0.1)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18,
|
||||||
}}>
|
}}>
|
||||||
{item.icon || '📋'}
|
{item.icon || '◈'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<p style={{ fontSize: 15, fontWeight: 600 }}>{item.name}</p>
|
<p style={{
|
||||||
<p style={{ fontSize: 12, color: '#86868B' }}>
|
fontSize: 14, fontWeight: 600, color: 'var(--color-text)',
|
||||||
打卡 {item.monthlyCount}/{data.daysInMonth} 天 · 连续 {item.streak} 天
|
}}>
|
||||||
|
{item.name}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ height: 4, borderRadius: 2, background: '#F0F0F0', marginTop: 4, overflow: 'hidden' }}>
|
<p style={{
|
||||||
<div style={{ height: '100%', borderRadius: 2, background: '#FF6B35', width: rate + '%', transition: 'width 0.5s' }} />
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{item.monthlyCount}/{data.daysInMonth} 天 · 连续 {item.streak} 天
|
||||||
|
</p>
|
||||||
|
<div style={{
|
||||||
|
height: 3, borderRadius: 2,
|
||||||
|
background: 'var(--color-progress-bg)', marginTop: 5, overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%', borderRadius: 2,
|
||||||
|
background: 'var(--color-primary)',
|
||||||
|
width: rate + '%',
|
||||||
|
transition: 'width 0.6s ease',
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#FF6B35' }}>{rate}%</span>
|
<span style={{
|
||||||
|
fontSize: 14, fontWeight: 700, color: 'var(--color-primary)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{rate}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ height: 100 }} />
|
|
||||||
<TabBar />
|
<TabBar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,53 +31,118 @@ export default function CheckInCard({ item, onCheckIn, streak, isDone }: CheckIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isActive = item.status === 'active'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card"
|
className="card"
|
||||||
onClick={() => router.push(`/checkin/${item.id}`)}
|
onClick={() => router.push(`/checkin/${item.id}`)}
|
||||||
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px' }}
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 14,
|
||||||
|
padding: '16px 18px',
|
||||||
|
margin: '6px 16px',
|
||||||
|
animation: 'fadeUp 0.5s ease-out',
|
||||||
|
opacity: isActive ? 1 : 0.5,
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
if (isActive) e.currentTarget.style.borderColor = 'rgba(0, 229, 255, 0.2)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.borderColor = ''
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 分类图标 */}
|
{/* Category icon */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 44, height: 44, borderRadius: 12,
|
width: 44, height: 44, borderRadius: 12,
|
||||||
background: cfg.color + '20',
|
background: `linear-gradient(135deg, ${cfg.color}22, ${cfg.color}11)`,
|
||||||
|
border: `1px solid ${cfg.color}33`,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 22, flexShrink: 0,
|
fontSize: 20, flexShrink: 0,
|
||||||
|
position: 'relative',
|
||||||
}}>
|
}}>
|
||||||
{item.icon || cfg.icon}
|
{item.icon || cfg.icon}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 名称与信息 */}
|
{/* Info */}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<p style={{ fontSize: 16, fontWeight: 600, marginBottom: 2 }}>{item.name}</p>
|
<div style={{
|
||||||
<p style={{ fontSize: 13, color: '#86868B', display: 'flex', gap: 8, alignItems: 'center' }}>
|
display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3,
|
||||||
|
}}>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 15, fontWeight: 600, color: 'var(--color-text)',
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{item.name}
|
||||||
|
</p>
|
||||||
|
{done && (
|
||||||
|
<span className="badge badge--green" style={{ flexShrink: 0 }}>
|
||||||
|
✓ 已打卡
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)',
|
||||||
|
display: 'flex', gap: 8, alignItems: 'center',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: cfg.color }}>▸</span>
|
||||||
<span>{cfg.label}</span>
|
<span>{cfg.label}</span>
|
||||||
<span>·</span>
|
<span style={{ color: 'var(--color-text-muted)' }}>/</span>
|
||||||
<span>{CYCLE_LABEL[item.cycleType] || item.cycleType}</span>
|
<span>{CYCLE_LABEL[item.cycleType] || item.cycleType}</span>
|
||||||
{streak !== undefined && (
|
{streak !== undefined && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span style={{ color: 'var(--color-text-muted)' }}>/</span>
|
||||||
<span style={{ color: '#FF6B35' }}>🔥 {streak} 天</span>
|
<span style={{ color: 'var(--color-gold)' }}>
|
||||||
|
◆ {streak}
|
||||||
|
<span style={{ fontFamily: 'var(--font-body)', marginLeft: 2 }}>天</span>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 打卡按钮 */}
|
{/* Check-in button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleCheckIn}
|
onClick={handleCheckIn}
|
||||||
disabled={checking || done}
|
disabled={checking || done || !isActive}
|
||||||
style={{
|
style={{
|
||||||
width: 44, height: 44, borderRadius: 22, border: 'none', cursor: done ? 'default' : 'pointer',
|
width: 46, height: 46, borderRadius: 14,
|
||||||
|
border: done
|
||||||
|
? '1px solid rgba(0, 230, 118, 0.3)'
|
||||||
|
: '1px solid rgba(0, 229, 255, 0.2)',
|
||||||
|
cursor: done || !isActive ? 'default' : 'pointer',
|
||||||
background: done
|
background: done
|
||||||
? 'linear-gradient(135deg, #34C759, #28A745)'
|
? 'linear-gradient(135deg, #00E676, #00C853)'
|
||||||
: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
: 'linear-gradient(135deg, rgba(0, 229, 255, 0.15), rgba(59, 130, 246, 0.15))',
|
||||||
color: 'white', fontSize: 18, fontWeight: 600, flexShrink: 0,
|
color: done ? '#080C18' : 'var(--color-primary)',
|
||||||
transition: 'transform 0.2s, opacity 0.2s',
|
fontSize: 20, fontWeight: 700, flexShrink: 0,
|
||||||
|
transition: 'all 0.3s',
|
||||||
opacity: checking ? 0.6 : 1,
|
opacity: checking ? 0.6 : 1,
|
||||||
|
boxShadow: done ? '0 0 20px rgba(0, 230, 118, 0.25)' : 'none',
|
||||||
|
animation: done ? 'glowPulseGreen 2s ease-in-out infinite' : 'none',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
if (!done && isActive) {
|
||||||
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(0, 229, 255, 0.25), rgba(59, 130, 246, 0.25))'
|
||||||
|
e.currentTarget.style.boxShadow = '0 0 20px rgba(0, 229, 255, 0.15)'
|
||||||
|
e.currentTarget.style.transform = 'scale(1.05)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
if (!done) {
|
||||||
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(0, 229, 255, 0.15), rgba(59, 130, 246, 0.15))'
|
||||||
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
|
e.currentTarget.style.transform = 'scale(1)'
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{done ? '✓' : checking ? '...' : '+'}
|
{done ? '✓' : checking ? '⋯' : '+'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { likePost, commentPost, type IPost } from '@/services/social'
|
import { likePost, commentPost, type IPost } from '@/services/social'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
interface FeedCardProps {
|
interface FeedCardProps {
|
||||||
post: IPost
|
post: IPost
|
||||||
onUpdate?: () => void
|
onUpdate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeedCard({ post, onUpdate }: FeedCardProps) {
|
export default function FeedCard({ post, onUpdate }: FeedCardProps) {
|
||||||
const [liking, setLiking] = useState(false)
|
const [liking, setLiking] = useState(false)
|
||||||
const [likes, setLikes] = useState(post.likes)
|
const [likes, setLikes] = useState(post.likes)
|
||||||
const [comments, setComments] = useState(post.comments)
|
const [comments, setComments] = useState(post.comments)
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
const [commentText, setCommentText] = useState('')
|
const [commentText, setCommentText] = useState('')
|
||||||
|
|
||||||
const createdAt = new Date(post.createdAt)
|
const createdAt = new Date(post.createdAt)
|
||||||
const timeStr = `${createdAt.getMonth()+1}月${createdAt.getDate()}日`
|
const timeStr = `${createdAt.getMonth() + 1}月${createdAt.getDate()}日`
|
||||||
|
|
||||||
async function handleLike() {
|
async function handleLike() {
|
||||||
if (liking) return
|
if (liking) return
|
||||||
@@ -38,43 +38,59 @@
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析图片数组
|
|
||||||
let images: string[] = []
|
let images: string[] = []
|
||||||
try { if (post.images) images = JSON.parse(post.images) } catch { images = [] }
|
try { if (post.images) images = JSON.parse(post.images) } catch { images = [] }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card" style={{ animation: 'fadeUp 0.5s ease-out', padding: '18px 18px' }}>
|
||||||
{/* 用户信息 */}
|
{/* User info */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 36, height: 36, borderRadius: 18,
|
width: 38, height: 38, borderRadius: 10,
|
||||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
background: 'linear-gradient(135deg, rgba(255, 60, 126, 0.2), rgba(255, 23, 68, 0.1))',
|
||||||
|
border: '1px solid rgba(255, 60, 126, 0.25)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 14, color: 'white', fontWeight: 600, flexShrink: 0,
|
fontSize: 15, color: 'var(--color-social)', fontWeight: 600, flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{post.user?.nickname?.[0] || '?'}
|
{post.user?.nickname?.[0] || '?'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<p style={{ fontSize: 14, fontWeight: 600 }}>{post.user?.nickname || '用户'}</p>
|
<p style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||||
<p style={{ fontSize: 12, color: '#86868B' }}>{timeStr}</p>
|
{post.user?.nickname || '用户'}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 11, color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
{timeStr}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容 */}
|
{/* Content */}
|
||||||
{post.content && (
|
{post.content && (
|
||||||
<p style={{ fontSize: 15, lineHeight: 1.5, marginBottom: images.length > 0 ? 10 : 0, whiteSpace: 'pre-wrap' }}>
|
<p style={{
|
||||||
|
fontSize: 14, lineHeight: 1.7, marginBottom: images.length > 0 ? 12 : 0,
|
||||||
|
whiteSpace: 'pre-wrap', color: 'var(--color-text)',
|
||||||
|
}}>
|
||||||
{post.content}
|
{post.content}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 图片 */}
|
{/* Images */}
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: images.length === 1 ? '1fr' : `repeat(${Math.min(images.length, 3)}, 1fr)`, gap: 4, marginBottom: 10 }}>
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: images.length === 1 ? '1fr' : `repeat(${Math.min(images.length, 3)}, 1fr)`,
|
||||||
|
gap: 4, marginBottom: 12, borderRadius: 8, overflow: 'hidden',
|
||||||
|
}}>
|
||||||
{images.slice(0, 3).map((url, i) => (
|
{images.slice(0, 3).map((url, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
aspectRatio: '1', borderRadius: 8, background: '#F0F0F0',
|
aspectRatio: '1', borderRadius: 6,
|
||||||
|
background: 'var(--color-bg)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
fontSize: 12, color: '#86868B', overflow: 'hidden',
|
fontSize: 12, color: 'var(--color-text-muted)', overflow: 'hidden',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
}}>
|
}}>
|
||||||
<img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
<img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -82,30 +98,56 @@
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 互动按钮 */}
|
{/* Actions */}
|
||||||
<div style={{ display: 'flex', gap: 20, paddingTop: 8, borderTop: '0.5px solid #F0F0F0' }}>
|
<div style={{
|
||||||
|
display: 'flex', gap: 20, paddingTop: 10,
|
||||||
|
borderTop: '1px solid var(--color-border)',
|
||||||
|
}}>
|
||||||
<button onClick={handleLike} disabled={liking}
|
<button onClick={handleLike} disabled={liking}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: '#86868B', display: 'flex', alignItems: 'center', gap: 4 }}>
|
style={{
|
||||||
{likes > 0 ? '❤️' : '🤍'} {likes}
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
|
fontSize: 12, color: likes > 0 ? 'var(--color-social)' : 'var(--color-text-muted)',
|
||||||
|
display: 'flex', alignItems: 'center', gap: 5,
|
||||||
|
transition: 'color 0.2s',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 14 }}>{likes > 0 ? '♥' : '♡'}</span>
|
||||||
|
<span>{likes}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setShowComment(!showComment)}
|
<button onClick={() => setShowComment(!showComment)}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: '#86868B', display: 'flex', alignItems: 'center', gap: 4 }}>
|
style={{
|
||||||
💬 {comments}
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
|
fontSize: 12, color: 'var(--color-text-muted)',
|
||||||
|
display: 'flex', alignItems: 'center', gap: 5,
|
||||||
|
transition: 'color 0.2s',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 14 }}>✎</span>
|
||||||
|
<span>{comments}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 评论输入 */}
|
{/* Comment input */}
|
||||||
{showComment && (
|
{showComment && (
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||||
<input className="input-field" value={commentText} onChange={e => setCommentText(e.target.value)}
|
<input
|
||||||
placeholder="说点什么..." style={{ flex: 1, fontSize: 14, padding: '8px 12px' }}
|
className="input-cyber"
|
||||||
onKeyDown={e => e.key === 'Enter' && handleComment()} />
|
value={commentText}
|
||||||
<button onClick={handleComment} disabled={!commentText.trim()}
|
onChange={e => setCommentText(e.target.value)}
|
||||||
|
placeholder="说点什么..."
|
||||||
|
style={{ flex: 1, fontSize: 13, padding: '10px 14px' }}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleComment()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleComment}
|
||||||
|
disabled={!commentText.trim()}
|
||||||
|
className="btn-cyber"
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 16px', borderRadius: 8, border: 'none',
|
padding: '10px 18px',
|
||||||
background: commentText.trim() ? '#FF6B35' : '#E5E5EA',
|
fontSize: 13,
|
||||||
color: 'white', fontSize: 14, cursor: commentText.trim() ? 'pointer' : 'default',
|
opacity: commentText.trim() ? 1 : 0.4,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
发送
|
发送
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,18 +10,17 @@ interface NavBarProps {
|
|||||||
showLogout?: boolean
|
showLogout?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBar({ title, showBack = false, showLogout = false }: NavBarProps) {
|
export default function NavBar({ title, showBack = false }: NavBarProps) {
|
||||||
const { logout, user } = useAuth()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { mode, toggle } = useTheme()
|
const { mode, toggle } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav style={{
|
<nav style={{
|
||||||
position: 'sticky', top: 0, zIndex: 100,
|
position: 'sticky', top: 0, zIndex: 100,
|
||||||
background: 'var(--color-nav-bg)',
|
background: 'rgba(8, 12, 24, 0.85)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(24px) saturate(1.4)',
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
WebkitBackdropFilter: 'blur(24px) saturate(1.4)',
|
||||||
borderBottom: '0.5px solid var(--color-border)',
|
borderBottom: '1px solid rgba(26, 34, 69, 0.6)',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
}}>
|
}}>
|
||||||
@@ -29,42 +28,74 @@ export default function NavBar({ title, showBack = false, showLogout = false }:
|
|||||||
{showBack && (
|
{showBack && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
style={{ background: 'none', border: 'none', fontSize: 20, cursor: 'pointer', padding: 4, color: 'var(--color-text)' }}
|
style={{
|
||||||
|
background: 'rgba(0, 229, 255, 0.08)',
|
||||||
|
border: '1px solid rgba(0, 229, 255, 0.15)',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '6px 10px',
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(0, 229, 255, 0.15)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 0 12px rgba(0, 229, 255, 0.1)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(0, 229, 255, 0.08)'
|
||||||
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{showLogout && user && (
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/profile')}
|
|
||||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-primary)', padding: 4 }}
|
|
||||||
>
|
|
||||||
我的
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ fontSize: 17, fontWeight: 600, flex: 1, textAlign: 'center', color: 'var(--color-text)' }}>{title}</span>
|
<div style={{
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
flex: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
background: 'linear-gradient(135deg, #00E5FF, #3B82F6)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, minWidth: 60 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6, minWidth: 60 }}>
|
||||||
<button
|
<button
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
title={mode === 'light' ? '切换暗黑模式' : '切换明亮模式'}
|
title={mode === 'light' ? '切换暗黑模式' : '切换明亮模式'}
|
||||||
style={{
|
style={{
|
||||||
background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 4,
|
background: 'rgba(0, 229, 255, 0.08)',
|
||||||
color: 'var(--color-text-secondary)', lineHeight: 1,
|
border: '1px solid rgba(0, 229, 255, 0.15)',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '6px 10px',
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
lineHeight: 1,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(0, 229, 255, 0.15)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 0 12px rgba(0, 229, 255, 0.1)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = 'rgba(0, 229, 255, 0.08)'
|
||||||
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mode === 'light' ? '🌙' : '☀️'}
|
{mode === 'light' ? '🌙' : '☀️'}
|
||||||
</button>
|
</button>
|
||||||
{showLogout && (
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-text-secondary)', padding: 4 }}
|
|
||||||
>
|
|
||||||
退出
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
interface ProgressRingProps {
|
interface ProgressRingProps {
|
||||||
percent: number
|
percent: number
|
||||||
size?: number
|
size?: number
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
@@ -8,33 +8,57 @@
|
|||||||
label?: string
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProgressRing({ percent, size = 80, strokeWidth = 6, color = '#FF6B35', label }: ProgressRingProps) {
|
export default function ProgressRing({ percent, size = 80, strokeWidth = 6, color, label }: ProgressRingProps) {
|
||||||
const radius = (size - strokeWidth) / 2
|
const radius = (size - strokeWidth) / 2
|
||||||
const circumference = 2 * Math.PI * radius
|
const circumference = 2 * Math.PI * radius
|
||||||
const offset = circumference - (Math.min(percent, 100) / 100) * circumference
|
const offset = circumference - (Math.min(percent, 100) / 100) * circumference
|
||||||
const center = size / 2
|
const center = size / 2
|
||||||
|
const ringColor = color || 'var(--color-primary)'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
||||||
<svg width={size} height={size}>
|
<svg width={size} height={size} style={{ filter: `drop-shadow(0 0 6px ${ringColor}44)` }}>
|
||||||
{/* 背景圆环 */}
|
{/* Background ring */}
|
||||||
<circle cx={center} cy={center} r={radius}
|
<circle cx={center} cy={center} r={radius}
|
||||||
fill="none" stroke="#F0F0F0" strokeWidth={strokeWidth} />
|
fill="none" stroke="var(--color-progress-bg)" strokeWidth={strokeWidth} />
|
||||||
{/* 进度圆环 */}
|
{/* Progress ring */}
|
||||||
<circle cx={center} cy={center} r={radius}
|
<circle cx={center} cy={center} r={radius}
|
||||||
fill="none" stroke={color} strokeWidth={strokeWidth}
|
fill="none" stroke={ringColor} strokeWidth={strokeWidth}
|
||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
strokeDashoffset={offset}
|
strokeDashoffset={offset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
transform={`rotate(-90 ${center} ${center})`}
|
transform={`rotate(-90 ${center} ${center})`}
|
||||||
style={{ transition: 'stroke-dashoffset 0.6s ease-out' }}
|
style={{ transition: 'stroke-dashoffset 0.8s ease-out' }}
|
||||||
/>
|
/>
|
||||||
|
{/* Glow dot at the end */}
|
||||||
|
{percent > 0 && percent < 100 && (
|
||||||
|
<circle cx={center} cy={center} r={radius}
|
||||||
|
fill="none" stroke={ringColor} strokeWidth={strokeWidth + 4}
|
||||||
|
strokeDasharray={`${(percent / 100) * circumference - 2} ${circumference}`}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
opacity={0.2}
|
||||||
|
transform={`rotate(-90 ${center} ${center})`}
|
||||||
|
style={{ transition: 'stroke-dashoffset 0.8s ease-out' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<text x={center} y={center} textAnchor="middle" dominantBaseline="central"
|
<text x={center} y={center} textAnchor="middle" dominantBaseline="central"
|
||||||
fontSize={size * 0.28} fontWeight={700} fill="#1D1D1F">
|
fontSize={size * 0.26} fontWeight={700}
|
||||||
|
fill="var(--color-text)"
|
||||||
|
fontFamily="var(--font-mono)">
|
||||||
{percent}%
|
{percent}%
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
{label && <span style={{ fontSize: 12, color: '#86868B' }}>{label}</span>}
|
{label && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import { usePathname, useRouter } from 'next/navigation'
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ path: '/', label: '打卡', icon: '📋' },
|
{ path: '/', label: '打卡', icon: '◈' },
|
||||||
{ path: '/social', label: '广场', icon: '🌊' },
|
{ path: '/social', label: '广场', icon: '◎' },
|
||||||
{ path: '/stats', label: '统计', icon: '📊' },
|
{ path: '/stats', label: '统计', icon: '◆' },
|
||||||
{ path: '/profile', label: '我的', icon: '👤' },
|
{ path: '/profile', label: '我的', icon: '◇' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function TabBar() {
|
export default function TabBar() {
|
||||||
@@ -23,12 +23,12 @@ 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: 'var(--color-nav-bg)',
|
background: 'rgba(8, 12, 24, 0.9)',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(24px) saturate(1.4)',
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
WebkitBackdropFilter: 'blur(24px) saturate(1.4)',
|
||||||
borderTop: '0.5px solid var(--color-border)',
|
borderTop: '1px solid rgba(26, 34, 69, 0.6)',
|
||||||
display: 'flex', justifyContent: 'space-around',
|
display: 'flex', justifyContent: 'space-around',
|
||||||
padding: '6px 0 20px',
|
padding: '6px 0 24px',
|
||||||
}}>
|
}}>
|
||||||
{TABS.map(tab => {
|
{TABS.map(tab => {
|
||||||
const active = isActive(tab.path)
|
const active = isActive(tab.path)
|
||||||
@@ -36,12 +36,42 @@ export default function TabBar() {
|
|||||||
<button key={tab.path} onClick={() => router.push(tab.path)}
|
<button key={tab.path} onClick={() => router.push(tab.path)}
|
||||||
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: 3,
|
||||||
fontSize: 10, color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)', fontWeight: active ? 600 : 400,
|
fontSize: 12,
|
||||||
transition: 'color 0.2s',
|
color: active ? 'var(--color-primary)' : 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
letterSpacing: 1,
|
||||||
|
transition: 'color 0.3s',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '4px 12px',
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontSize: 22 }}>{tab.icon}</span>
|
{active && (
|
||||||
<span>{tab.label}</span>
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -7,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: 20,
|
||||||
|
height: 2,
|
||||||
|
background: 'var(--color-primary)',
|
||||||
|
borderRadius: 1,
|
||||||
|
boxShadow: '0 0 8px rgba(0, 229, 255, 0.5)',
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
<span style={{
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
filter: active ? 'none' : 'none',
|
||||||
|
textShadow: active ? '0 0 10px rgba(0, 229, 255, 0.3)' : 'none',
|
||||||
|
}}>
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: active ? 600 : 400,
|
||||||
|
}}>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface IThemeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<IThemeContext>({
|
const ThemeContext = createContext<IThemeContext>({
|
||||||
mode: 'light',
|
mode: 'dark',
|
||||||
toggle: () => {},
|
toggle: () => {},
|
||||||
setMode: () => {},
|
setMode: () => {},
|
||||||
})
|
})
|
||||||
@@ -20,7 +20,7 @@ export function useTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [mode, setModeState] = useState<ThemeMode>('light')
|
const [mode, setModeState] = useState<ThemeMode>('dark')
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,6 +29,8 @@ export default function ThemeProvider({ children }: { children: React.ReactNode
|
|||||||
setModeState(stored)
|
setModeState(stored)
|
||||||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
setModeState('dark')
|
setModeState('dark')
|
||||||
|
} else {
|
||||||
|
setModeState('dark')
|
||||||
}
|
}
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
+28
-28
@@ -17,34 +17,34 @@ export interface IThemeColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const THEME: Record<ThemeMode, IThemeColors> = {
|
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: {
|
dark: {
|
||||||
primary: '#FF6B35',
|
primary: '#00E5FF',
|
||||||
primaryLight: '#FF8C5A',
|
primaryLight: '#33EAFF',
|
||||||
primaryGradient: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
primaryGradient: 'linear-gradient(135deg, #00E5FF, #3B82F6)',
|
||||||
accent: '#BB86FC',
|
accent: '#3B82F6',
|
||||||
bg: '#1C1C1E',
|
bg: '#080C18',
|
||||||
card: '#2C2C2E',
|
card: '#0F1529',
|
||||||
text: '#F5F5F7',
|
text: '#E0E8FF',
|
||||||
textSecondary: '#98989D',
|
textSecondary: '#6B7BA3',
|
||||||
border: '#38383A',
|
border: '#1A2245',
|
||||||
navBg: 'rgba(28,28,30,0.92)',
|
navBg: 'rgba(8, 12, 24, 0.85)',
|
||||||
success: '#34C759',
|
success: '#00E676',
|
||||||
warning: '#FF9500',
|
warning: '#FFB347',
|
||||||
error: '#FF453A',
|
error: '#FF1744',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
primary: '#2563EB',
|
||||||
|
primaryLight: '#3B82F6',
|
||||||
|
primaryGradient: 'linear-gradient(135deg, #2563EB, #3B82F6)',
|
||||||
|
accent: '#3B82F6',
|
||||||
|
bg: '#E8ECF4',
|
||||||
|
card: '#FFFFFF',
|
||||||
|
text: '#1A1F36',
|
||||||
|
textSecondary: '#5B6B8F',
|
||||||
|
border: '#D0D7E6',
|
||||||
|
navBg: 'rgba(232, 236, 244, 0.9)',
|
||||||
|
success: '#059669',
|
||||||
|
warning: '#D97706',
|
||||||
|
error: '#DC2626',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user