提交修改
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import { CATEGORY_CONFIG, CYCLE_LABEL, type ICheckInItem } from '@/services/checkin'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
@@ -31,53 +31,118 @@ export default function CheckInCard({ item, onCheckIn, streak, isDone }: CheckIn
|
||||
}
|
||||
}
|
||||
|
||||
const isActive = item.status === 'active'
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card"
|
||||
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={{
|
||||
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',
|
||||
fontSize: 22, flexShrink: 0,
|
||||
fontSize: 20, flexShrink: 0,
|
||||
position: 'relative',
|
||||
}}>
|
||||
{item.icon || cfg.icon}
|
||||
</div>
|
||||
|
||||
{/* 名称与信息 */}
|
||||
{/* Info */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: 16, fontWeight: 600, marginBottom: 2 }}>{item.name}</p>
|
||||
<p style={{ fontSize: 13, color: '#86868B', display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<div style={{
|
||||
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>·</span>
|
||||
<span style={{ color: 'var(--color-text-muted)' }}>/</span>
|
||||
<span>{CYCLE_LABEL[item.cycleType] || item.cycleType}</span>
|
||||
{streak !== undefined && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span style={{ color: '#FF6B35' }}>🔥 {streak} 天</span>
|
||||
<span style={{ color: 'var(--color-text-muted)' }}>/</span>
|
||||
<span style={{ color: 'var(--color-gold)' }}>
|
||||
◆ {streak}
|
||||
<span style={{ fontFamily: 'var(--font-body)', marginLeft: 2 }}>天</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 打卡按钮 */}
|
||||
{/* Check-in button */}
|
||||
<button
|
||||
onClick={handleCheckIn}
|
||||
disabled={checking || done}
|
||||
disabled={checking || done || !isActive}
|
||||
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
|
||||
? 'linear-gradient(135deg, #34C759, #28A745)'
|
||||
: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
||||
color: 'white', fontSize: 18, fontWeight: 600, flexShrink: 0,
|
||||
transition: 'transform 0.2s, opacity 0.2s',
|
||||
? 'linear-gradient(135deg, #00E676, #00C853)'
|
||||
: 'linear-gradient(135deg, rgba(0, 229, 255, 0.15), rgba(59, 130, 246, 0.15))',
|
||||
color: done ? '#080C18' : 'var(--color-primary)',
|
||||
fontSize: 20, fontWeight: 700, flexShrink: 0,
|
||||
transition: 'all 0.3s',
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { likePost, commentPost, type IPost } from '@/services/social'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface FeedCardProps {
|
||||
|
||||
import { likePost, commentPost, type IPost } from '@/services/social'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface FeedCardProps {
|
||||
post: IPost
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
export default function FeedCard({ post, onUpdate }: FeedCardProps) {
|
||||
|
||||
export default function FeedCard({ post, onUpdate }: FeedCardProps) {
|
||||
const [liking, setLiking] = useState(false)
|
||||
const [likes, setLikes] = useState(post.likes)
|
||||
const [comments, setComments] = useState(post.comments)
|
||||
const [showComment, setShowComment] = useState(false)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
|
||||
|
||||
const createdAt = new Date(post.createdAt)
|
||||
const timeStr = `${createdAt.getMonth()+1}月${createdAt.getDate()}日`
|
||||
|
||||
const timeStr = `${createdAt.getMonth() + 1}月${createdAt.getDate()}日`
|
||||
|
||||
async function handleLike() {
|
||||
if (liking) return
|
||||
setLiking(true)
|
||||
@@ -27,7 +27,7 @@
|
||||
} catch { /* ignore */ }
|
||||
setLiking(false)
|
||||
}
|
||||
|
||||
|
||||
async function handleComment() {
|
||||
if (!commentText.trim()) return
|
||||
try {
|
||||
@@ -37,75 +37,117 @@
|
||||
setShowComment(false)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 解析图片数组
|
||||
|
||||
let images: string[] = []
|
||||
try { if (post.images) images = JSON.parse(post.images) } catch { images = [] }
|
||||
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
{/* 用户信息 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<div className="card" style={{ animation: 'fadeUp 0.5s ease-out', padding: '18px 18px' }}>
|
||||
{/* User info */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 18,
|
||||
background: 'linear-gradient(135deg, #FF6B35, #FF4D6D)',
|
||||
width: 38, height: 38, borderRadius: 10,
|
||||
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',
|
||||
fontSize: 14, color: 'white', fontWeight: 600, flexShrink: 0,
|
||||
fontSize: 15, color: 'var(--color-social)', fontWeight: 600, flexShrink: 0,
|
||||
}}>
|
||||
{post.user?.nickname?.[0] || '?'}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<p style={{ fontSize: 14, fontWeight: 600 }}>{post.user?.nickname || '用户'}</p>
|
||||
<p style={{ fontSize: 12, color: '#86868B' }}>{timeStr}</p>
|
||||
<p style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
{post.user?.nickname || '用户'}
|
||||
</p>
|
||||
<p style={{
|
||||
fontSize: 11, color: 'var(--color-text-muted)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}}>
|
||||
{timeStr}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
|
||||
{/* 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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 图片 */}
|
||||
|
||||
{/* Images */}
|
||||
{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) => (
|
||||
<div key={i} style={{
|
||||
aspectRatio: '1', borderRadius: 8, background: '#F0F0F0',
|
||||
aspectRatio: '1', borderRadius: 6,
|
||||
background: 'var(--color-bg)',
|
||||
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' }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 互动按钮 */}
|
||||
<div style={{ display: 'flex', gap: 20, paddingTop: 8, borderTop: '0.5px solid #F0F0F0' }}>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 20, paddingTop: 10,
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
}}>
|
||||
<button onClick={handleLike} disabled={liking}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: '#86868B', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{likes > 0 ? '❤️' : '🤍'} {likes}
|
||||
style={{
|
||||
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 onClick={() => setShowComment(!showComment)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: '#86868B', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
💬 {comments}
|
||||
style={{
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* 评论输入 */}
|
||||
|
||||
{/* Comment input */}
|
||||
{showComment && (
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<input className="input-field" value={commentText} onChange={e => setCommentText(e.target.value)}
|
||||
placeholder="说点什么..." style={{ flex: 1, fontSize: 14, padding: '8px 12px' }}
|
||||
onKeyDown={e => e.key === 'Enter' && handleComment()} />
|
||||
<button onClick={handleComment} disabled={!commentText.trim()}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
<input
|
||||
className="input-cyber"
|
||||
value={commentText}
|
||||
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={{
|
||||
padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||
background: commentText.trim() ? '#FF6B35' : '#E5E5EA',
|
||||
color: 'white', fontSize: 14, cursor: commentText.trim() ? 'pointer' : 'default',
|
||||
}}>
|
||||
padding: '10px 18px',
|
||||
fontSize: 13,
|
||||
opacity: commentText.trim() ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -10,18 +10,17 @@ interface NavBarProps {
|
||||
showLogout?: boolean
|
||||
}
|
||||
|
||||
export default function NavBar({ title, showBack = false, showLogout = false }: NavBarProps) {
|
||||
const { logout, user } = useAuth()
|
||||
export default function NavBar({ title, showBack = false }: NavBarProps) {
|
||||
const router = useRouter()
|
||||
const { mode, toggle } = useTheme()
|
||||
|
||||
return (
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 100,
|
||||
background: 'var(--color-nav-bg)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
borderBottom: '0.5px solid var(--color-border)',
|
||||
background: 'rgba(8, 12, 24, 0.85)',
|
||||
backdropFilter: 'blur(24px) saturate(1.4)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(1.4)',
|
||||
borderBottom: '1px solid rgba(26, 34, 69, 0.6)',
|
||||
padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
@@ -29,42 +28,74 @@ export default function NavBar({ title, showBack = false, showLogout = false }:
|
||||
{showBack && (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
{showLogout && user && (
|
||||
<button
|
||||
onClick={() => router.push('/profile')}
|
||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-primary)', padding: 4 }}
|
||||
>
|
||||
我的
|
||||
</button>
|
||||
)}
|
||||
</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
|
||||
onClick={toggle}
|
||||
title={mode === 'light' ? '切换暗黑模式' : '切换明亮模式'}
|
||||
style={{
|
||||
background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', padding: 4,
|
||||
color: 'var(--color-text-secondary)', lineHeight: 1,
|
||||
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)',
|
||||
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' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
{showLogout && (
|
||||
<button
|
||||
onClick={logout}
|
||||
style={{ background: 'none', border: 'none', fontSize: 15, cursor: 'pointer', color: 'var(--color-text-secondary)', padding: 4 }}
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -1,40 +1,64 @@
|
||||
'use client'
|
||||
|
||||
interface ProgressRingProps {
|
||||
|
||||
interface ProgressRingProps {
|
||||
percent: number
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: 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 circumference = 2 * Math.PI * radius
|
||||
const offset = circumference - (Math.min(percent, 100) / 100) * circumference
|
||||
const center = size / 2
|
||||
|
||||
const ringColor = color || 'var(--color-primary)'
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
||||
<svg width={size} height={size}>
|
||||
{/* 背景圆环 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
|
||||
<svg width={size} height={size} style={{ filter: `drop-shadow(0 0 6px ${ringColor}44)` }}>
|
||||
{/* Background ring */}
|
||||
<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}
|
||||
fill="none" stroke={color} strokeWidth={strokeWidth}
|
||||
fill="none" stroke={ringColor} strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
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"
|
||||
fontSize={size * 0.28} fontWeight={700} fill="#1D1D1F">
|
||||
fontSize={size * 0.26} fontWeight={700}
|
||||
fill="var(--color-text)"
|
||||
fontFamily="var(--font-mono)">
|
||||
{percent}%
|
||||
</text>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
|
||||
const TABS = [
|
||||
{ path: '/', label: '打卡', icon: '📋' },
|
||||
{ path: '/social', label: '广场', icon: '🌊' },
|
||||
{ path: '/stats', label: '统计', icon: '📊' },
|
||||
{ path: '/profile', label: '我的', icon: '👤' },
|
||||
{ path: '/', label: '打卡', icon: '◈' },
|
||||
{ path: '/social', label: '广场', icon: '◎' },
|
||||
{ path: '/stats', label: '统计', icon: '◆' },
|
||||
{ path: '/profile', label: '我的', icon: '◇' },
|
||||
]
|
||||
|
||||
export default function TabBar() {
|
||||
@@ -22,13 +22,13 @@ export default function TabBar() {
|
||||
|
||||
return (
|
||||
<nav style={{
|
||||
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 100,
|
||||
background: 'var(--color-nav-bg)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
borderTop: '0.5px solid var(--color-border)',
|
||||
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 100,
|
||||
background: 'rgba(8, 12, 24, 0.9)',
|
||||
backdropFilter: 'blur(24px) saturate(1.4)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(1.4)',
|
||||
borderTop: '1px solid rgba(26, 34, 69, 0.6)',
|
||||
display: 'flex', justifyContent: 'space-around',
|
||||
padding: '6px 0 20px',
|
||||
padding: '6px 0 24px',
|
||||
}}>
|
||||
{TABS.map(tab => {
|
||||
const active = isActive(tab.path)
|
||||
@@ -36,12 +36,42 @@ export default function TabBar() {
|
||||
<button key={tab.path} onClick={() => router.push(tab.path)}
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
|
||||
fontSize: 10, color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)', fontWeight: active ? 600 : 400,
|
||||
transition: 'color 0.2s',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
|
||||
fontSize: 12,
|
||||
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>
|
||||
<span>{tab.label}</span>
|
||||
{active && (
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface IThemeContext {
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<IThemeContext>({
|
||||
mode: 'light',
|
||||
mode: 'dark',
|
||||
toggle: () => {},
|
||||
setMode: () => {},
|
||||
})
|
||||
@@ -20,7 +20,7 @@ export function useTheme() {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,6 +29,8 @@ export default function ThemeProvider({ children }: { children: React.ReactNode
|
||||
setModeState(stored)
|
||||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
setModeState('dark')
|
||||
} else {
|
||||
setModeState('dark')
|
||||
}
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
Reference in New Issue
Block a user