From 471013938d95a5831e8a8c0c86de8d4e341518df Mon Sep 17 00:00:00 2001 From: zeronline <122611088@qq.com> Date: Mon, 29 Jun 2026 17:38:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/app/achievements/page.tsx | 298 +++++++---- web/src/app/checkin/[id]/page.tsx | 314 ++++++++---- web/src/app/checkin/create/page.tsx | 167 ++++--- web/src/app/globals.css | 706 ++++++++++++++++++++++----- web/src/app/layout.tsx | 19 +- web/src/app/login/page.tsx | 204 +++++--- web/src/app/page.tsx | 191 ++++++-- web/src/app/profile/page.tsx | 242 ++++++--- web/src/app/social/friends/page.tsx | 190 ++++--- web/src/app/social/page.tsx | 231 +++++---- web/src/app/stats/page.tsx | 298 +++++++---- web/src/components/CheckInCard.tsx | 103 +++- web/src/components/FeedCard.tsx | 138 ++++-- web/src/components/NavBar.tsx | 85 +++- web/src/components/ProgressRing.tsx | 52 +- web/src/components/TabBar.tsx | 60 ++- web/src/components/ThemeProvider.tsx | 6 +- web/src/constants/theme.ts | 56 +-- 18 files changed, 2380 insertions(+), 980 deletions(-) diff --git a/web/src/app/achievements/page.tsx b/web/src/app/achievements/page.tsx index a8514dc..e3aa49a 100644 --- a/web/src/app/achievements/page.tsx +++ b/web/src/app/achievements/page.tsx @@ -1,109 +1,191 @@ 'use client' - - import { useEffect, useState } from 'react' - import { useRouter } from 'next/navigation' - import { AuthProvider, useAuth } from '@/components/AuthProvider' - import NavBar from '@/components/NavBar' - import { getAllAchievements, getUserAchievements, RARITY_CONFIG, type IAchievement } from '@/services/achievements' - - function AchievementsPage() { - const { user, loading } = useAuth() - const router = useRouter() - const [allAchievements, setAllAchievements] = useState([]) - const [unlockedIds, setUnlockedIds] = useState>(new Set()) - const [pageLoading, setPageLoading] = useState(true) - - useEffect(() => { - if (!loading && !user) { router.push('/login'); return } - if (user) loadData() - }, [user, loading, router]) - - async function loadData() { - try { - const [all, mine] = await Promise.all([ - getAllAchievements(), - getUserAchievements(), - ]) - setAllAchievements(all) - setUnlockedIds(new Set(mine.map(u => u.achievementID))) - } catch { /* ignore */ } - setPageLoading(false) - } - - if (loading || pageLoading) return
加载中...
- if (!user) return null - - const grouped = { - gold: allAchievements.filter(a => a.rarity === 'gold'), - silver: allAchievements.filter(a => a.rarity === 'silver'), - bronze: allAchievements.filter(a => a.rarity === 'bronze'), - } - - return ( - <> - - - {/* 统计 */} -
-
-

{unlockedIds.size}

-

已获得

-
-
-

{allAchievements.length}

-

总成就

-
-
-

- {allAchievements.length > 0 ? Math.round((unlockedIds.size / allAchievements.length) * 100) : 0}% -

-

完成度

-
-
- - {(['gold', 'silver', 'bronze'] as const).map(rarity => { - const config = RARITY_CONFIG[rarity] - const items = grouped[rarity] - if (items.length === 0) return null - return ( -
-

- {config.label}质徽章 ({items.length}) -

-
- {items.map(a => { - const unlocked = unlockedIds.has(a.id) - return ( -
-
- {unlocked ? '🏅' : '🔒'} -
-

{a.name}

-

{unlocked ? a.condition : '???'}

-
- ) - })} -
-
- ) - })} - -
- - ) - } - - export default function AchievementsRoute() { - return - } + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { AuthProvider, useAuth } from '@/components/AuthProvider' +import NavBar from '@/components/NavBar' +import { getAllAchievements, getUserAchievements, RARITY_CONFIG, type IAchievement } from '@/services/achievements' + +function AchievementsPage() { + const { user, loading } = useAuth() + const router = useRouter() + const [allAchievements, setAllAchievements] = useState([]) + const [unlockedIds, setUnlockedIds] = useState>(new Set()) + const [pageLoading, setPageLoading] = useState(true) + + useEffect(() => { + if (!loading && !user) { router.push('/login'); return } + if (user) loadData() + }, [user, loading, router]) + + async function loadData() { + try { + const [all, mine] = await Promise.all([ + getAllAchievements(), + getUserAchievements(), + ]) + setAllAchievements(all) + setUnlockedIds(new Set(mine.map(u => u.achievementID))) + } catch { /* ignore */ } + setPageLoading(false) + } + + if (loading || pageLoading) { + return ( + <> + +
+
+
加载中
+
+ + ) + } + if (!user) return null + + const grouped = { + gold: allAchievements.filter(a => a.rarity === 'gold'), + silver: allAchievements.filter(a => a.rarity === 'silver'), + bronze: allAchievements.filter(a => a.rarity === 'bronze'), + } + + return ( + <> + + +
+ {/* Stats */} +
+
+

+ {unlockedIds.size} +

+

+ 已获得 +

+
+
+

+ {allAchievements.length} +

+

+ 总成就 +

+
+
+

+ {allAchievements.length > 0 ? Math.round((unlockedIds.size / allAchievements.length) * 100) : 0}% +

+

+ 完成度 +

+
+
+ + {(['gold', 'silver', 'bronze'] as const).map(rarity => { + const config = RARITY_CONFIG[rarity] + const items = grouped[rarity] + if (items.length === 0) return null + return ( +
+
+ + {config.label.toUpperCase()} ({items.length}) + +
+
+ {items.map(a => { + const unlocked = unlockedIds.has(a.id) + return ( +
+ {unlocked && ( +
+ {config.label} +
+ )} +
+ {unlocked ? '◈' : '◆'} +
+

+ {a.name} +

+

+ {unlocked ? a.condition : '???'} +

+
+ ) + })} +
+
+ ) + })} +
+ + ) +} + +export default function AchievementsRoute() { + return +} diff --git a/web/src/app/checkin/[id]/page.tsx b/web/src/app/checkin/[id]/page.tsx index 809fabc..5e76dc2 100644 --- a/web/src/app/checkin/[id]/page.tsx +++ b/web/src/app/checkin/[id]/page.tsx @@ -25,7 +25,7 @@ function DetailPage() { const [itemData, streakData, recordsData] = await Promise.all([ getItem(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) setStreak(streakData.streak) @@ -83,110 +83,238 @@ function DetailPage() { loadData() } - if (pageLoading || !item) return
加载中...
+ if (pageLoading || !item) { + return ( + <> + +
+
+
加载中
+
+ + ) + } const cfg = CATEGORY_CONFIG[item.category] || CATEGORY_CONFIG.habit const daysInMonth = getDaysInMonth(year, month) const firstDay = getFirstDayOfMonth(year, month) return ( <> - -
- - {showActions && ( -
- {item.status === 'active' ? ( -
暂停打卡
- ) : ( -
恢复打卡
- )} -
删除
-
- )} -
+ -
-
-
{item.icon || cfg.icon}
-
-

{item.name}

-

- {cfg.label} · {CYCLE_LABEL[item.cycleType]} · {item.status === 'active' ? '进行中' : item.status === 'paused' ? '已暂停' : item.status} -

- {item.dailyGoal &&

目标: {item.dailyGoal}

} -
-
-
- -
-
-
-

{streak}

-

连续天数

-
-
-

{records.length}

-

本月打卡

-
-
-

- {daysInMonth > 0 ? Math.round((records.length / daysInMonth) * 100) : 0}% -

-

完成率

-
-
-
- -
-
- - {year} 年 {month} 月 - -
- -
- {['日','一','二','三','四','五','六'].map(d => ( -
{d}
- ))} - {Array.from({ length: firstDay }, (_, i) =>
)} - {Array.from({ length: daysInMonth }, (_, i) => { - const day = i + 1 - const dateStr = year + '-' + String(month).padStart(2,'0') + '-' + String(day).padStart(2,'0') - const checked = recordDates.has(dateStr) - const isMakeup = makeupDates.has(dateStr) - const isToday = dateStr === todayStr - const canMakeup = !checked && dateStr < todayStr - return ( -
canMakeup && handleMakeup(dateStr)} +
+ {/* Action menu */} +
+ + {showActions && ( +
+ + +
+ )} +
+ + {/* Item info */} +
+
+
+ {item.icon || cfg.icon} +
+
+

+ {item.name} +

+

+ {cfg.label} / {CYCLE_LABEL[item.cycleType]} + {' · '} + {item.status === 'active' ? '进行中' : item.status === 'paused' ? '已暂停' : item.status} +

+ {item.dailyGoal && ( +

- {day} - {checked &&

} - {!checked && isToday &&
} + 目标: {item.dailyGoal} +

+ )} +
+
+
+ + {/* Stats */} +
+
+
+

+ {streak} +

+

+ 连续 +

+
+
+

+ {records.length} +

+

+ 本月打卡 +

+
+
+

+ {daysInMonth > 0 ? Math.round((records.length / daysInMonth) * 100) : 0}% +

+

+ 完成率 +

+
+
+
+ + {/* Calendar */} +
+
+ + + {year}.{String(month).padStart(2, '0')} + + +
+ +
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(d => ( +
+ {d}
- ) - })} + ))} + {Array.from({ length: firstDay }, (_, i) =>
)} + {Array.from({ length: daysInMonth }, (_, i) => { + const day = i + 1 + const dateStr = year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0') + const checked = recordDates.has(dateStr) + const isMakeup = makeupDates.has(dateStr) + const isToday = dateStr === todayStr + const canMakeup = !checked && dateStr < todayStr + return ( +
canMakeup && handleMakeup(dateStr)} + style={{ + padding: '7px 0', fontSize: 13, borderRadius: 8, + cursor: canMakeup ? 'pointer' : 'default', + background: isToday ? 'var(--color-primary-dim)' : 'transparent', + 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} + {checked && ( +
+ )} +
+ ) + })} +
+
+ +
+

+ 点击过去未打卡的日期可补卡 +

- -
) } diff --git a/web/src/app/checkin/create/page.tsx b/web/src/app/checkin/create/page.tsx index 9402362..6a7f621 100644 --- a/web/src/app/checkin/create/page.tsx +++ b/web/src/app/checkin/create/page.tsx @@ -1,7 +1,7 @@ 'use client' - - import { useState, type FormEvent } from 'react' - import { useRouter } from 'next/navigation' + +import { useState, type FormEvent } from 'react' +import { useRouter } from 'next/navigation' import { AuthProvider, useAuth } from '@/components/AuthProvider' import NavBar from '@/components/NavBar' 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: 'weekly', label: '每周' }, { value: 'every_other_day', label: '隔日' }, - { value: 'custom', label: '自定义间隔' }, + { value: 'custom', label: '自定义' }, ] function CreateCheckInPage() { @@ -51,70 +51,113 @@ function CreateCheckInPage() { return ( <> - -
- {/* 名称 */} - - setName(e.target.value)} - placeholder="如「每天背单词」" style={{ marginBottom: 20 }} maxLength={50} /> + +
+ + {/* Name */} + + setName(e.target.value)} + placeholder="如「每天背单词」" style={{ marginBottom: 20 }} maxLength={50} /> - {/* 分类 */} - -
- {CATEGORIES.map(([key, cfg]) => ( - - ))} -
+ {/* Category */} + +
+ {CATEGORIES.map(([key, cfg]) => ( + + ))} +
- {/* 打卡周期 */} - -
- {CYCLE_OPTIONS.map(opt => ( - - ))} -
+ {/* Cycle */} + +
+ {CYCLE_OPTIONS.map(opt => ( + + ))} +
- {/* 每日目标 */} - - setDailyGoal(e.target.value)} - placeholder="如「背 50 个单词」" style={{ marginBottom: 20 }} maxLength={200} /> + {/* Daily Goal */} + + setDailyGoal(e.target.value)} + placeholder="如「背 50 个单词」" style={{ marginBottom: 20 }} maxLength={200} /> - {/* 隐私 */} - - + {/* Privacy */} + + - {error &&

{error}

} + {error && ( +

+ ERROR: {error} +

+ )} - - + + +
) } diff --git a/web/src/app/globals.css b/web/src/app/globals.css index bfb3e42..91da66d 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1,128 +1,586 @@ - * { - box-sizing: border-box; - margin: 0; - padding: 0; - } - +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + :root { - --color-primary: #FF6B35; - --color-primary-light: #FF8C5A; - --color-primary-gradient: linear-gradient(135deg, #FF6B35, #FF4D6D); - --color-accent: #7209B7; - --color-bg: #F5F5F7; - --color-card: #FFFFFF; - --color-text: #1D1D1F; - --color-text-secondary: #86868B; - --color-border: #E5E5EA; - --color-nav-bg: rgba(245,245,247,0.92); - --color-success: #34C759; - --color-warning: #FF9500; - --color-error: #FF3B30; - --radius-card: 12px; - --radius-button: 10px; - --font-base: -apple-system, BlinkMacSystemFont, 'SF Pro', 'PingFang SC', sans-serif; - } + --color-bg: #080C18; + --color-bg-alt: #0C1124; + --color-card: #0F1529; + --color-card-hover: #161E3A; + --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); - [data-theme="dark"] { - --color-bg: #1C1C1E; - --color-card: #2C2C2E; - --color-text: #F5F5F7; - --color-text-secondary: #98989D; - --color-border: #38383A; - --color-nav-bg: rgba(28,28,30,0.92); - --color-accent: #BB86FC; - --color-error: #FF453A; - } + --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; - body { - font-family: var(--font-base); - background: var(--color-bg); - color: var(--color-text); - line-height: 1.5; - -webkit-font-smoothing: antialiased; - transition: background 0.3s, color 0.3s; + --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-hover: #F0F4FF; + --color-border: #D0D7E6; + --color-border-glow: rgba(59, 130, 246, 0.12); + --color-primary: #2563EB; + --color-primary-dim: rgba(37, 99, 235, 0.06); + --color-primary-glow: rgba(37, 99, 235, 0.15); + --color-secondary: #3B82F6; + --color-accent: #059669; + --color-gold: #D97706; + --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); +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; + min-height: 100vh; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 229, 255, 0.015) 2px, + rgba(0, 229, 255, 0.015) 4px + ); +} + +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); } - - /* iOS 风格列表 */ - .ios-list { - background: var(--color-card); - border-radius: var(--radius-card); - overflow: hidden; - margin: 16px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); - } - - .ios-list-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px; - border-bottom: 0.5px solid var(--color-border); - cursor: pointer; - transition: background 0.2s; - } - .ios-list-item:last-child { border-bottom: none; } - .ios-list-item:hover { background: #f8f8fa; } - - .ios-list-item-label { - font-size: 15px; - color: var(--color-text); - } - .ios-list-item-value { - font-size: 15px; - color: var(--color-text-secondary); - } - - /* 按钮 */ - .btn-primary { - display: block; - width: 100%; - padding: 14px; - 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; } - - /* 输入框 */ - .input-field { - 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); - border-radius: var(--radius-card); - margin: 8px 16px; - padding: 16px; - box-shadow: 0 1px 3px rgba(0,0,0,0.04); - } + 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; + align-items: center; + gap: 10px; + margin-bottom: 16px; + padding: 0 16px; +} + +.section-title-bar::before { + content: ''; + width: 3px; + height: 14px; + background: var(--color-primary); + border-radius: 2px; + box-shadow: 0 0 8px var(--color-primary-glow); +} + +.section-title-bar-text { + font-family: var(--font-display); + font-size: 13px; + letter-spacing: 1px; + color: var(--color-primary); +} + +/* ===== Cards ===== */ +.card { + background: var(--color-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + margin: 8px 16px; + padding: 16px; + 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; +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 5a3ba41..9f84eda 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,7 +1,20 @@ import type { Metadata } from 'next' +import { Orbitron, JetBrains_Mono } from 'next/font/google' import './globals.css' 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 = { title: '寸进 — 日日皆有成长', description: '寸进(InchStep)综合成长打卡平台', @@ -9,8 +22,12 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + +
+
+
{children} diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index f0e9f9c..d462f19 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -1,79 +1,127 @@ 'use client' - - import { useState, type FormEvent } from 'react' - import { AuthProvider, useAuth } from '@/components/AuthProvider' - - function LoginForm() { - const [phone, setPhone] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState('') - const { login } = useAuth() - - async function handleSubmit(e: FormEvent) { - e.preventDefault() - if (phone.length !== 11) { - setError('请输入正确的 11 位手机号') - return - } - setIsLoading(true) - setError('') - try { - await login(phone) - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : '登录失败' - setError(msg) - } finally { - setIsLoading(false) - } - } - - return ( -
- {/* Logo */} -
-
- 📈 -
-

寸进

-

日日皆有成长

-
- -
- { setPhone(e.target.value.replace(/\D/g, '')); setError('') }} - style={{ marginBottom: 12 }} - /> - - {error && ( -

{error}

- )} - - - -

- 未注册的手机号将自动创建账号 -

-
-
- ) - } - - export default function LoginPage() { - return ( - - - - ) - } + +import { useState, type FormEvent } from 'react' +import { AuthProvider, useAuth } from '@/components/AuthProvider' + +function LoginForm() { + const [phone, setPhone] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + const { login } = useAuth() + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + if (phone.length !== 11) { + setError('请输入正确的 11 位手机号') + return + } + setIsLoading(true) + setError('') + try { + await login(phone) + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : '登录失败' + setError(msg) + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Logo */} +
+
+
+ ◈ +
+

+ 寸进 +

+

+ 寸进 · 日日皆有成长 +

+
+ +
+
+ { setPhone(e.target.value.replace(/\D/g, '')); setError('') }} + style={{ marginBottom: 12 }} + /> +
+ + {error && ( +

+ ERROR: {error} +

+ )} + + + +

+ // 未注册的手机号将自动创建账号 +

+
+
+ ) +} + +export default function LoginPage() { + return ( + + + + ) +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 9308591..63bad3c 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -43,70 +43,155 @@ function Dashboard() { const activeItems = items.filter(i => i.status === 'active') const pausedItems = items.filter(i => i.status === 'paused') + const doneCount = activeItems.filter(i => todayStatus[i.id]).length - if (loading || pageLoading) return
加载中...
+ if (loading || pageLoading) { + return ( + <> + +
+
+
连接中
+
+ + + ) + } if (!user) return null return ( <> -
-
+ {/* Agent Status Card */} +
- {user.nickname?.[0] || user.phone?.slice(-4) || '?'} -
-
-

{user.nickname || '用户'}

-

积分 {user.points} · {activeItems.length} 个活跃项

-
- -
- -
-

今日打卡

- + {user.nickname?.[0] || user.phone?.slice(-4) || '?'} +
+
+

+ {user.nickname || '用户'} +

+

+ + {user.points} 积分 + | + ◈ {activeItems.length} + 活跃 + {doneCount > 0 && ( + <> + | + ✓ {doneCount}/{activeItems.length} + 今日 + + )} +

+
+ +
+ + {/* Today's Missions */} +
+ 今日任务 +
+ + {activeItems.length === 0 ? ( +
+
+ ◈ +
+

+ 还没有活跃的打卡项 +

+

+ 点击下方按钮创建你的第一个任务 +

+ +
+ ) : ( + <> +
+ +
+ {activeItems.map((item, i) => ( +
+ setTodayStatus(s => ({ ...s, [id]: true }))} /> +
+ ))} + + )} + + {/* Paused Items */} + {pausedItems.length > 0 && ( + <> +
+ + 已暂停 ({pausedItems.length}) + +
+ {pausedItems.map(item => ( + + ))} + + )}
- {activeItems.length === 0 ? ( -
-

📋

-

- 还没有打卡项
- 点击右上角"创建"开始你的成长之旅 -

-
- ) : ( - activeItems.map(item => ( - setTodayStatus(s => ({ ...s, [id]: true }))} /> - )) - )} - - {pausedItems.length > 0 && ( - <> -

已暂停 ({pausedItems.length})

- {pausedItems.map(item => ( - - ))} - - )} - -
) diff --git a/web/src/app/profile/page.tsx b/web/src/app/profile/page.tsx index b5cd00d..6fd7a3f 100644 --- a/web/src/app/profile/page.tsx +++ b/web/src/app/profile/page.tsx @@ -42,88 +42,190 @@ function ProfilePage() { setEditing(true) } - if (loading || !profile) return
加载中...
+ if (loading || !profile) { + return ( + <> + +
+
+
加载中
+
+ + ) + } return ( <> - + - {/* 头像与基本信息 */} -
-
+ {/* Avatar & Bio */} +
- {profile.nickname?.[0] || profile.phone?.slice(-4) || '?'} +
+ {profile.nickname?.[0] || profile.phone?.slice(-4) || '?'} +
+

+ {profile.nickname || '未设置昵称'} +

+

+ {profile.bio || '没有个性签名'} +

-

{profile.nickname || '未设置昵称'}

-

{profile.bio || '还没有个性签名'}

-
- {/* 统计数据 */} -
-
-
-

{profile.points}

-

总积分

-
-
-

--

-

连续天数

-
-
-

--

-

徽章

-
-
-
- - {/* 操作列表 */} -
- {!editing ? ( - <> -
- 编辑资料 - -
-
- 隐私设置 - - {profile.privacy === 1 ? '公开' : profile.privacy === 2 ? '仅好友可见' : '仅自己可见'} - -
-
router.push('/achievements')}> - 成就徽章 - -
-
- 退出登录 -
- - ) : ( -
- - setNickname(e.target.value)} style={{ marginBottom: 12 }} placeholder="输入昵称" /> - - setBio(e.target.value)} style={{ marginBottom: 16 }} placeholder="最多 50 字" maxLength={50} /> -
- - + {profile.points} +

+

+ 积分 +

+
+
+

+ -- +

+

+ 连续 +

+
+
+

+ -- +

+

+ 徽章 +

- )} +
+ + {/* Actions */} +
+ {!editing ? ( + <> + +
+ 隐私设置 + + {profile.privacy === 1 ? '公开' : profile.privacy === 2 ? '好友可见' : '私密'} + +
+ + + + ) : ( +
+ + setNickname(e.target.value)} + style={{ marginBottom: 14 }} placeholder="输入昵称" /> + + setBio(e.target.value)} + style={{ marginBottom: 18 }} placeholder="最多 50 字" maxLength={50} /> +
+ + +
+
+ )} +
-
) diff --git a/web/src/app/social/friends/page.tsx b/web/src/app/social/friends/page.tsx index 7f54c62..29ddb4e 100644 --- a/web/src/app/social/friends/page.tsx +++ b/web/src/app/social/friends/page.tsx @@ -1,12 +1,12 @@ 'use client' - - import { useEffect, useState } from 'react' - import { useRouter } from 'next/navigation' - import { AuthProvider, useAuth } from '@/components/AuthProvider' - import NavBar from '@/components/NavBar' - import { listFriends, sendFriendRequest, removeFriend, type IUserBrief } from '@/services/social' - - function FriendsPage() { + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { AuthProvider, useAuth } from '@/components/AuthProvider' +import NavBar from '@/components/NavBar' +import { listFriends, sendFriendRequest, removeFriend, type IUserBrief } from '@/services/social' + +function FriendsPage() { const { user, loading } = useAuth() const router = useRouter() const [friends, setFriends] = useState([]) @@ -14,12 +14,12 @@ const [search, setSearch] = useState('') const [addPhone, setAddPhone] = useState('') const [adding, setAdding] = useState(false) - + useEffect(() => { if (!loading && !user) { router.push('/login'); return } if (user) loadFriends() }, [user, loading, router]) - + async function loadFriends() { try { const list = await listFriends() @@ -27,7 +27,7 @@ } catch { /* ignore */ } setPageLoading(false) } - + async function handleAdd() { if (!addPhone.trim()) return setAdding(true) @@ -40,7 +40,7 @@ } setAdding(false) } - + async function handleRemove(friendId: string) { if (!confirm('确定删除这位好友?')) return try { @@ -48,77 +48,123 @@ setFriends(f => f.filter(ff => ff.id !== friendId)) } catch { /* ignore */ } } - + const filtered = search ? friends.filter(f => f.nickname?.includes(search)) : friends - - if (loading || pageLoading) return
加载中...
+ + if (loading || pageLoading) { + return ( + <> + +
+
+
加载中
+
+ + ) + } if (!user) return null - + return ( <> - - {/* 添加好友 */} -
-

添加好友

-
- setAddPhone(e.target.value)} - placeholder="输入手机号" style={{ flex: 1 }} /> - -
-
- - {/* 好友列表 */} -
-

好友列表 ({friends.length})

- {friends.length > 0 && ( - setSearch(e.target.value)} - placeholder="搜索..." style={{ width: 140, fontSize: 13, padding: '6px 10px' }} /> - )} -
- - {filtered.length === 0 ? ( -
-

- {search ? '未找到匹配的好友' : '还没有好友,输入手机号添加吧'} + +

+ {/* Add friend */} +
+

+ 添加好友

-
- ) : ( - filtered.map(f => ( -
-
-
+ setAddPhone(e.target.value)} + placeholder="输入手机号" style={{ flex: 1, fontSize: 14 }} /> +
- {f.nickname || '用户'} -
-
- )) - )} - -
+
+ + {/* Friends list */} +
+ + 好友 ({friends.length}) + + {friends.length > 0 && ( + setSearch(e.target.value)} + placeholder="搜索" style={{ + width: 130, fontSize: 11, padding: '6px 10px', + fontFamily: 'var(--font-mono)', + }} /> + )} +
+ + {filtered.length === 0 ? ( +
+

+ {search ? '未找到匹配的好友' : '还没有好友'} +

+ {!search && ( +

+ 输入手机号添加好友 +

+ )} +
+ ) : ( + filtered.map(f => ( +
+
+
+ {f.nickname?.[0] || '?'} +
+ + {f.nickname || '用户'} + +
+ +
+ )) + )} +
) - } - - export default function FriendsRoute() { +} + +export default function FriendsRoute() { return - } +} diff --git a/web/src/app/social/page.tsx b/web/src/app/social/page.tsx index b39bcca..dad69e4 100644 --- a/web/src/app/social/page.tsx +++ b/web/src/app/social/page.tsx @@ -1,14 +1,14 @@ 'use client' - - import { useEffect, useState, useRef } from 'react' - import { useRouter } from 'next/navigation' - import { AuthProvider, useAuth } from '@/components/AuthProvider' - import NavBar from '@/components/NavBar' - import TabBar from '@/components/TabBar' - import FeedCard from '@/components/FeedCard' - import { getFeed, createPost, type IPost } from '@/services/social' - - function FeedPage() { + +import { useEffect, useState, useRef } from 'react' +import { useRouter } from 'next/navigation' +import { AuthProvider, useAuth } from '@/components/AuthProvider' +import NavBar from '@/components/NavBar' +import TabBar from '@/components/TabBar' +import FeedCard from '@/components/FeedCard' +import { getFeed, createPost, type IPost } from '@/services/social' + +function FeedPage() { const { user, loading } = useAuth() const router = useRouter() const [posts, setPosts] = useState([]) @@ -19,12 +19,12 @@ const [newContent, setNewContent] = useState('') const [creating, setCreating] = useState(false) const inputRef = useRef(null) - + useEffect(() => { if (!loading && !user) { router.push('/login'); return } if (user) loadFeed() }, [user, loading, page, router]) - + async function loadFeed() { try { const data = await getFeed(page, 20) @@ -34,7 +34,7 @@ } catch { /* ignore */ } setFeedLoading(false) } - + async function handleCreate() { if (!newContent.trim()) return setCreating(true) @@ -47,87 +47,150 @@ } catch { /* ignore */ } setCreating(false) } - + if (loading) return null if (!user) return null - + return ( <> - - {/* 发布动态入口 */} -
setShowCreate(true)}> -
-
- {user.nickname?.[0] || user.phone?.slice(-4) || '?'} + +
+ {/* Create post entry */} +
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)' + }} + > +
+
+ {user.nickname?.[0] || user.phone?.slice(-4) || '?'} +
+ + 分享你的打卡动态... +
- 分享你的打卡动态...
-
- - {/* 创建动态弹层 */} - {showCreate && ( -
setShowCreate(false)}> -
e.stopPropagation()}> -

发布动态

-