From e19873850c8d2983f3ad3091c434dabc245f2913 Mon Sep 17 00:00:00 2001 From: z66 <1415243231@qq.com> Date: Fri, 26 Dec 2025 17:29:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC-=E5=89=8D?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/__init__.py | 2 - backend/app/api/api_v1/endpoints/auth.py | 77 ++ backend/app/core/config.py | 44 ++ backend/app/db/base.py | 14 + backend/app/models/chat.py | 22 + backend/models.py | 144 ---- frontend/ATTRIBUTIONS.md | 3 + frontend/README.md | 11 + frontend/guidelines/Guidelines.md | 61 ++ frontend/index.html | 15 + frontend/package.json | 89 +++ frontend/postcss.config.mjs | 15 + frontend/src/app/App.tsx | 24 + .../src/app/components/AccountingPage.tsx | 204 +++++ frontend/src/app/components/ChatPage.tsx | 113 +++ .../src/app/components/FileSharingPage.tsx | 115 +++ frontend/src/app/components/HomePage.tsx | 115 +++ frontend/src/app/components/MediaPage.tsx | 271 +++++++ frontend/src/app/components/Navigation.tsx | 61 ++ frontend/src/app/components/TodoPage.tsx | 283 +++++++ frontend/src/app/components/WeatherPage.tsx | 89 +++ .../components/figma/ImageWithFallback.tsx | 27 + frontend/src/app/components/ui/accordion.tsx | 66 ++ .../src/app/components/ui/alert-dialog.tsx | 157 ++++ frontend/src/app/components/ui/alert.tsx | 66 ++ .../src/app/components/ui/aspect-ratio.tsx | 11 + frontend/src/app/components/ui/avatar.tsx | 53 ++ frontend/src/app/components/ui/badge.tsx | 46 ++ frontend/src/app/components/ui/breadcrumb.tsx | 109 +++ frontend/src/app/components/ui/button.tsx | 58 ++ frontend/src/app/components/ui/calendar.tsx | 75 ++ frontend/src/app/components/ui/card.tsx | 92 +++ frontend/src/app/components/ui/carousel.tsx | 241 ++++++ frontend/src/app/components/ui/chart.tsx | 353 +++++++++ frontend/src/app/components/ui/checkbox.tsx | 32 + .../src/app/components/ui/collapsible.tsx | 33 + frontend/src/app/components/ui/command.tsx | 177 +++++ .../src/app/components/ui/context-menu.tsx | 252 ++++++ frontend/src/app/components/ui/dialog.tsx | 135 ++++ frontend/src/app/components/ui/drawer.tsx | 132 ++++ .../src/app/components/ui/dropdown-menu.tsx | 257 +++++++ frontend/src/app/components/ui/form.tsx | 168 ++++ frontend/src/app/components/ui/hover-card.tsx | 44 ++ frontend/src/app/components/ui/input-otp.tsx | 77 ++ frontend/src/app/components/ui/input.tsx | 21 + frontend/src/app/components/ui/label.tsx | 24 + frontend/src/app/components/ui/menubar.tsx | 276 +++++++ .../src/app/components/ui/navigation-menu.tsx | 168 ++++ frontend/src/app/components/ui/pagination.tsx | 127 +++ frontend/src/app/components/ui/popover.tsx | 48 ++ frontend/src/app/components/ui/progress.tsx | 31 + .../src/app/components/ui/radio-group.tsx | 45 ++ frontend/src/app/components/ui/resizable.tsx | 56 ++ .../src/app/components/ui/scroll-area.tsx | 58 ++ frontend/src/app/components/ui/select.tsx | 189 +++++ frontend/src/app/components/ui/separator.tsx | 28 + frontend/src/app/components/ui/sheet.tsx | 139 ++++ frontend/src/app/components/ui/sidebar.tsx | 726 ++++++++++++++++++ frontend/src/app/components/ui/skeleton.tsx | 13 + frontend/src/app/components/ui/slider.tsx | 63 ++ frontend/src/app/components/ui/sonner.tsx | 25 + frontend/src/app/components/ui/switch.tsx | 31 + frontend/src/app/components/ui/table.tsx | 116 +++ frontend/src/app/components/ui/tabs.tsx | 66 ++ frontend/src/app/components/ui/textarea.tsx | 18 + .../src/app/components/ui/toggle-group.tsx | 73 ++ frontend/src/app/components/ui/toggle.tsx | 47 ++ frontend/src/app/components/ui/tooltip.tsx | 61 ++ frontend/src/app/components/ui/use-mobile.ts | 21 + frontend/src/app/components/ui/utils.ts | 6 + frontend/src/main.tsx | 7 + frontend/src/styles/fonts.css | 0 frontend/src/styles/index.css | 3 + frontend/src/styles/tailwind.css | 4 + frontend/src/styles/theme.css | 181 +++++ frontend/src/views/Todos.vue | 148 ---- frontend/vite.config.ts | 19 + 77 files changed, 6977 insertions(+), 294 deletions(-) create mode 100644 backend/app/api/api_v1/endpoints/auth.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/db/base.py create mode 100644 backend/app/models/chat.py delete mode 100644 backend/models.py create mode 100644 frontend/ATTRIBUTIONS.md create mode 100644 frontend/README.md create mode 100644 frontend/guidelines/Guidelines.md create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/src/app/App.tsx create mode 100644 frontend/src/app/components/AccountingPage.tsx create mode 100644 frontend/src/app/components/ChatPage.tsx create mode 100644 frontend/src/app/components/FileSharingPage.tsx create mode 100644 frontend/src/app/components/HomePage.tsx create mode 100644 frontend/src/app/components/MediaPage.tsx create mode 100644 frontend/src/app/components/Navigation.tsx create mode 100644 frontend/src/app/components/TodoPage.tsx create mode 100644 frontend/src/app/components/WeatherPage.tsx create mode 100644 frontend/src/app/components/figma/ImageWithFallback.tsx create mode 100644 frontend/src/app/components/ui/accordion.tsx create mode 100644 frontend/src/app/components/ui/alert-dialog.tsx create mode 100644 frontend/src/app/components/ui/alert.tsx create mode 100644 frontend/src/app/components/ui/aspect-ratio.tsx create mode 100644 frontend/src/app/components/ui/avatar.tsx create mode 100644 frontend/src/app/components/ui/badge.tsx create mode 100644 frontend/src/app/components/ui/breadcrumb.tsx create mode 100644 frontend/src/app/components/ui/button.tsx create mode 100644 frontend/src/app/components/ui/calendar.tsx create mode 100644 frontend/src/app/components/ui/card.tsx create mode 100644 frontend/src/app/components/ui/carousel.tsx create mode 100644 frontend/src/app/components/ui/chart.tsx create mode 100644 frontend/src/app/components/ui/checkbox.tsx create mode 100644 frontend/src/app/components/ui/collapsible.tsx create mode 100644 frontend/src/app/components/ui/command.tsx create mode 100644 frontend/src/app/components/ui/context-menu.tsx create mode 100644 frontend/src/app/components/ui/dialog.tsx create mode 100644 frontend/src/app/components/ui/drawer.tsx create mode 100644 frontend/src/app/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/app/components/ui/form.tsx create mode 100644 frontend/src/app/components/ui/hover-card.tsx create mode 100644 frontend/src/app/components/ui/input-otp.tsx create mode 100644 frontend/src/app/components/ui/input.tsx create mode 100644 frontend/src/app/components/ui/label.tsx create mode 100644 frontend/src/app/components/ui/menubar.tsx create mode 100644 frontend/src/app/components/ui/navigation-menu.tsx create mode 100644 frontend/src/app/components/ui/pagination.tsx create mode 100644 frontend/src/app/components/ui/popover.tsx create mode 100644 frontend/src/app/components/ui/progress.tsx create mode 100644 frontend/src/app/components/ui/radio-group.tsx create mode 100644 frontend/src/app/components/ui/resizable.tsx create mode 100644 frontend/src/app/components/ui/scroll-area.tsx create mode 100644 frontend/src/app/components/ui/select.tsx create mode 100644 frontend/src/app/components/ui/separator.tsx create mode 100644 frontend/src/app/components/ui/sheet.tsx create mode 100644 frontend/src/app/components/ui/sidebar.tsx create mode 100644 frontend/src/app/components/ui/skeleton.tsx create mode 100644 frontend/src/app/components/ui/slider.tsx create mode 100644 frontend/src/app/components/ui/sonner.tsx create mode 100644 frontend/src/app/components/ui/switch.tsx create mode 100644 frontend/src/app/components/ui/table.tsx create mode 100644 frontend/src/app/components/ui/tabs.tsx create mode 100644 frontend/src/app/components/ui/textarea.tsx create mode 100644 frontend/src/app/components/ui/toggle-group.tsx create mode 100644 frontend/src/app/components/ui/toggle.tsx create mode 100644 frontend/src/app/components/ui/tooltip.tsx create mode 100644 frontend/src/app/components/ui/use-mobile.ts create mode 100644 frontend/src/app/components/ui/utils.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles/fonts.css create mode 100644 frontend/src/styles/index.css create mode 100644 frontend/src/styles/tailwind.css create mode 100644 frontend/src/styles/theme.css delete mode 100644 frontend/src/views/Todos.vue create mode 100644 frontend/vite.config.ts diff --git a/backend/__init__.py b/backend/__init__.py index 92fb946..64fe3ab 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -8,8 +8,6 @@ from .app.models import ( ChatMessage, Upload ) -# 为了向后兼容,保留 get_session 别名 -get_session = get_db __all__ = [ "engine", diff --git a/backend/app/api/api_v1/endpoints/auth.py b/backend/app/api/api_v1/endpoints/auth.py new file mode 100644 index 0000000..2c28776 --- /dev/null +++ b/backend/app/api/api_v1/endpoints/auth.py @@ -0,0 +1,77 @@ +""" +认证相关 API 端点 +""" +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlmodel import Session, select +from datetime import timedelta + +from app.core.config import settings +from app.core.security import verify_password, create_access_token, get_password_hash +from app.db.session import get_db +from app.models.user import User +from app.schemas.user import Token, User as UserSchema, UserCreate + +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login") + + +@router.post("/register", response_model=UserSchema, status_code=status.HTTP_201_CREATED) +def register(user_in: UserCreate, db: Session = Depends(get_db)): + """用户注册""" + # 检查用户名是否已存在 + statement = select(User).where(User.username == user_in.username) + existing_user = db.exec(statement).first() + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="用户名已存在" + ) + + # 检查邮箱是否已存在 + if user_in.email: + statement = select(User).where(User.email == user_in.email) + existing_email = db.exec(statement).first() + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="邮箱已被注册" + ) + + # 创建新用户 + hashed_password = get_password_hash(user_in.password) + db_user = User( + username=user_in.username, + email=user_in.email, + hashed_password=hashed_password + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + + return db_user + + +@router.post("/login", response_model=Token) +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + """用户登录""" + # 查找用户 + statement = select(User).where(User.username == form_data.username) + user = db.exec(statement).first() + + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户名或密码错误", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # 创建访问令牌 + access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..655d1f1 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,44 @@ +""" +应用配置 +""" +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + """应用配置类""" + + # 项目信息 + PROJECT_NAME: str = "个人博客网站" + VERSION: str = "0.1.0" + API_V1_STR: str = "/api/v1" + + # 数据库配置 + DATABASE_URL: str = "sqlite:///./blogweb.db" + + # 安全配置 + SECRET_KEY: str = "your-secret-key-change-in-production" # 生产环境请修改 + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7天 + + # CORS 配置 + BACKEND_CORS_ORIGINS: list = [ + "http://localhost:3000", + "http://localhost:5173", + "http://localhost:8080", + ] + + # 文件上传配置 + UPLOAD_DIR: str = "./uploads" + MAX_UPLOAD_SIZE: int = 10 * 1024 * 1024 # 10MB + + # 第三方 API 配置 + OPENWEATHER_API_KEY: Optional[str] = None # OpenWeatherMap API Key + + class Config: + env_file = ".env" + case_sensitive = True + + +settings = Settings() + diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 0000000..d3cca23 --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,14 @@ +""" +数据库基础配置 +""" +from sqlmodel import SQLModel + +# 导入所有模型以确保表被注册到 metadata +from app.models import ( + User, Todo, Post, Transaction, Media, Tag, MediaTag, + ChatMessage, Upload +) + +# SQLModel 的 Base 类 +Base = SQLModel + diff --git a/backend/app/models/chat.py b/backend/app/models/chat.py new file mode 100644 index 0000000..c08d94e --- /dev/null +++ b/backend/app/models/chat.py @@ -0,0 +1,22 @@ +""" +聊天消息模型 +""" +from datetime import datetime +from typing import Optional +from sqlmodel import SQLModel, Field, Relationship +from .user import User + + +class ChatMessage(SQLModel, table=True): + """聊天消息表""" + __tablename__ = "chat_messages" + + id: Optional[int] = Field(default=None, primary_key=True) + content: str + sent_at: datetime = Field(default_factory=datetime.now) + user_id: int = Field(foreign_key="users.id", index=True) + room: str = Field(max_length=50, default="main", index=True) + + # 关系 + user: Optional[User] = Relationship() + diff --git a/backend/models.py b/backend/models.py deleted file mode 100644 index af4dfbc..0000000 --- a/backend/models.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -数据库模型定义 -使用 SQLModel 定义所有表结构 -""" -from datetime import datetime, date -from typing import Optional -from sqlmodel import SQLModel, Field, Relationship, Column -from sqlalchemy import Numeric -from decimal import Decimal - - -# ==================== 用户表 ==================== -class User(SQLModel, table=True): - """用户表""" - __tablename__ = "users" - - id: Optional[int] = Field(default=None, primary_key=True) - username: str = Field(max_length=50, unique=True, index=True) - email: Optional[str] = Field(default=None, max_length=100, unique=True, index=True) - hashed_password: str - created_at: datetime = Field(default_factory=datetime.now) - - -# ==================== 待办事项表 ==================== -class Todo(SQLModel, table=True): - """待办事项表""" - __tablename__ = "todos" - - id: Optional[int] = Field(default=None, primary_key=True) - title: str = Field(max_length=200) - done: bool = Field(default=False) - created_at: datetime = Field(default_factory=datetime.now) - user_id: int = Field(foreign_key="users.id", index=True) - - # 关系(可选,用于ORM查询) - user: Optional[User] = Relationship() - - -# ==================== 博客文章表 ==================== -class Post(SQLModel, table=True): - """博客文章表""" - __tablename__ = "posts" - - id: Optional[int] = Field(default=None, primary_key=True) - title: str = Field(max_length=200) - slug: str = Field(max_length=200, index=True) - content: str # 支持 Markdown - created_at: datetime = Field(default_factory=datetime.now) - updated_at: Optional[datetime] = Field(default=None) - user_id: int = Field(foreign_key="users.id", index=True) - - # 关系 - user: Optional[User] = Relationship() - - -# ==================== 记账记录表 ==================== -class Transaction(SQLModel, table=True): - """记账记录表""" - __tablename__ = "transactions" - - id: Optional[int] = Field(default=None, primary_key=True) - amount: Decimal = Field(sa_column=Column(Numeric(10, 2))) # 正数为收入,负数为支出 - category: str = Field(max_length=50) - description: Optional[str] = Field(default=None, max_length=200) - date: date - user_id: int = Field(foreign_key="users.id", index=True) - - # 关系 - user: Optional[User] = Relationship() - - -# ==================== 媒体-标签关联表(多对多)==================== -class MediaTag(SQLModel, table=True): - """媒体-标签关联表""" - __tablename__ = "media_tags" - - media_id: int = Field(foreign_key="media.id", primary_key=True) - tag_id: int = Field(foreign_key="tags.id", primary_key=True) - - -# ==================== 书影音收藏表 ==================== -class Media(SQLModel, table=True): - """书影音收藏表""" - __tablename__ = "media" - - id: Optional[int] = Field(default=None, primary_key=True) - title: str = Field(max_length=200) - media_type: str = Field(max_length=20) # book / movie / music - rating: Optional[float] = Field(default=None, ge=0.0, le=5.0) - comment: Optional[str] = Field(default=None) - external_id: Optional[str] = Field(default=None, max_length=100) # ISBN、IMDb ID等 - cover_url: Optional[str] = Field(default=None, max_length=300) - created_at: datetime = Field(default_factory=datetime.now) - user_id: int = Field(foreign_key="users.id", index=True) - - # 关系 - user: Optional[User] = Relationship() - tags: list["Tag"] = Relationship(back_populates="media", link_model=MediaTag) - - -# ==================== 媒体标签表 ==================== -class Tag(SQLModel, table=True): - """媒体标签表""" - __tablename__ = "tags" - - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(max_length=50, unique=True, index=True) - - # 关系 - media: list[Media] = Relationship(back_populates="tags", link_model=MediaTag) - - -# ==================== 聊天消息表 ==================== -class ChatMessage(SQLModel, table=True): - """聊天消息表""" - __tablename__ = "chat_messages" - - id: Optional[int] = Field(default=None, primary_key=True) - content: str - sent_at: datetime = Field(default_factory=datetime.now) - user_id: int = Field(foreign_key="users.id", index=True) - room: str = Field(max_length=50, default="main", index=True) - - # 关系 - user: Optional[User] = Relationship() - - -# ==================== 文件上传记录表 ==================== -class Upload(SQLModel, table=True): - """文件上传记录表""" - __tablename__ = "uploads" - - id: Optional[int] = Field(default=None, primary_key=True) - filename: str = Field(max_length=200) - stored_path: str = Field(max_length=300) - file_size: int # 字节数 - mime_type: str = Field(max_length=100) - uploaded_at: datetime = Field(default_factory=datetime.now) - expires_at: Optional[datetime] = Field(default=None) - user_id: int = Field(foreign_key="users.id", index=True) - - # 关系 - user: Optional[User] = Relationship() - diff --git a/frontend/ATTRIBUTIONS.md b/frontend/ATTRIBUTIONS.md new file mode 100644 index 0000000..9b7cd4e --- /dev/null +++ b/frontend/ATTRIBUTIONS.md @@ -0,0 +1,3 @@ +This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md). + +This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license). \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7cce22b --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,11 @@ + + # 个人博客网站 + + This is a code bundle for 个人博客网站. The original project is available at https://www.figma.com/design/BZRfx0JUvEQKYcNoSwKxtL/%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E7%BD%91%E7%AB%99. + + ## Running the code + + Run `npm i` to install the dependencies. + + Run `npm run dev` to start the development server. + \ No newline at end of file diff --git a/frontend/guidelines/Guidelines.md b/frontend/guidelines/Guidelines.md new file mode 100644 index 0000000..110f117 --- /dev/null +++ b/frontend/guidelines/Guidelines.md @@ -0,0 +1,61 @@ +**Add your own guidelines here** + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..bd29cba --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + 个人博客网站 + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1a47a88 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,89 @@ +{ + "name": "@figma/my-make-file", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "7.3.5", + "@mui/material": "7.3.5", + "@popperjs/core": "2.11.8", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "0.487.0", + "motion": "12.23.24", + "next-themes": "0.4.6", + "react-day-picker": "8.10.1", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", + "react-hook-form": "7.55.0", + "react-popper": "2.3.0", + "react-resizable-panels": "2.1.7", + "react-responsive-masonry": "2.7.1", + "react-router-dom": "^7.11.0", + "react-slick": "0.31.0", + "recharts": "2.15.2", + "sonner": "2.0.3", + "tailwind-merge": "3.2.0", + "tw-animate-css": "1.3.8", + "vaul": "1.1.2" + }, + "devDependencies": { + "@tailwindcss/vite": "4.1.12", + "@vitejs/plugin-react": "4.7.0", + "tailwindcss": "4.1.12", + "vite": "6.3.5" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "pnpm": { + "overrides": { + "vite": "6.3.5" + } + } +} \ No newline at end of file diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..531dbec --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,15 @@ +/** + * PostCSS Configuration + * + * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required + * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here. + * + * This file only exists for adding additional PostCSS plugins, if needed. + * For example: + * + * import postcssNested from 'postcss-nested' + * export default { plugins: [postcssNested()] } + * + * Otherwise, you can leave this file empty. + */ +export default {} diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx new file mode 100644 index 0000000..c01c1b1 --- /dev/null +++ b/frontend/src/app/App.tsx @@ -0,0 +1,24 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import HomePage from './components/HomePage'; +import TodoPage from './components/TodoPage'; +import WeatherPage from './components/WeatherPage'; +import MediaPage from './components/MediaPage'; +import AccountingPage from './components/AccountingPage'; +import ChatPage from './components/ChatPage'; +import FileSharingPage from './components/FileSharingPage'; + +export default function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/frontend/src/app/components/AccountingPage.tsx b/frontend/src/app/components/AccountingPage.tsx new file mode 100644 index 0000000..279fbed --- /dev/null +++ b/frontend/src/app/components/AccountingPage.tsx @@ -0,0 +1,204 @@ +import { useState } from 'react'; +import Navigation from './Navigation'; +import { Plus, TrendingUp, TrendingDown } from 'lucide-react'; + +interface Transaction { + id: number; + type: '收入' | '支出'; + amount: number; + category: string; + note: string; + date: string; +} + +export default function AccountingPage() { + const [transactions, setTransactions] = useState([]); + const [type, setType] = useState<'收入' | '支出'>('支出'); + const [amount, setAmount] = useState(''); + const [category, setCategory] = useState(''); + const [note, setNote] = useState(''); + + const addTransaction = () => { + if (amount && category) { + setTransactions([ + { + id: Date.now(), + type, + amount: parseFloat(amount), + category, + note, + date: new Date().toLocaleDateString('zh-CN'), + }, + ...transactions, + ]); + setAmount(''); + setCategory(''); + setNote(''); + } + }; + + const totalIncome = transactions + .filter(t => t.type === '收入') + .reduce((sum, t) => sum + t.amount, 0); + + const totalExpense = transactions + .filter(t => t.type === '支出') + .reduce((sum, t) => sum + t.amount, 0); + + const balance = totalIncome - totalExpense; + + return ( +
+ + +
+
+ {/* Header */} +
+

记账本

+

追踪你的收支情况

+
+ + {/* Summary Cards */} +
+
+
+ 总收入 + +
+

¥{totalIncome.toFixed(2)}

+
+
+
+ 总支出 + +
+

¥{totalExpense.toFixed(2)}

+
+
+
+ 结余 + + + +
+

¥{balance.toFixed(2)}

+
+
+ + {/* Add Transaction Card */} +
+
+
+ + +
+ setAmount(e.target.value)} + placeholder="金额" + className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400" + /> +
+
+ setCategory(e.target.value)} + placeholder="类别 (如:餐饮、交通、工资等)" + className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400" + /> + setNote(e.target.value)} + placeholder="备注 (可选)" + className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400" + /> +
+ +
+ + {/* Transactions List */} +
+

交易记录

+
+ {transactions.length === 0 ? ( +
+ 暂无交易记录 +
+ ) : ( + transactions.map((transaction) => ( +
+
+
+ {transaction.type === '收入' ? ( + + ) : ( + + )} +
+
+

{transaction.category}

+ {transaction.note && ( +

{transaction.note}

+ )} +

{transaction.date}

+
+
+

+ {transaction.type === '收入' ? '+' : '-'}¥{transaction.amount.toFixed(2)} +

+
+ )) + )} +
+
+
+
+ + {/* Floating Action Button */} + +
+ ); +} diff --git a/frontend/src/app/components/ChatPage.tsx b/frontend/src/app/components/ChatPage.tsx new file mode 100644 index 0000000..8037547 --- /dev/null +++ b/frontend/src/app/components/ChatPage.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import Navigation from './Navigation'; +import { Send } from 'lucide-react'; + +interface Message { + id: number; + text: string; + sender: 'me' | 'other'; + time: string; +} + +export default function ChatPage() { + const [messages, setMessages] = useState([ + { + id: 1, + text: '欢迎来到聊天室!', + sender: 'other', + time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + }, + ]); + const [inputValue, setInputValue] = useState(''); + + const sendMessage = () => { + if (inputValue.trim()) { + const newMessage: Message = { + id: Date.now(), + text: inputValue, + sender: 'me', + time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + }; + setMessages([...messages, newMessage]); + setInputValue(''); + + // Simulate a reply + setTimeout(() => { + const reply: Message = { + id: Date.now() + 1, + text: '这是一个自动回复消息', + sender: 'other', + time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + }; + setMessages(prev => [...prev, reply]); + }, 1000); + } + }; + + return ( +
+ + +
+
+ {/* Header */} +
+

聊天室

+

与朋友愉快交流

+
+ + {/* Chat Container */} +
+ {/* Messages Area */} +
+ {messages.map((message) => ( +
+
+

{message.text}

+

+ {message.time} +

+
+
+ ))} +
+ + {/* Input Area */} +
+
+ setInputValue(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && sendMessage()} + placeholder="输入消息..." + className="flex-1 px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-400" + /> + +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/components/FileSharingPage.tsx b/frontend/src/app/components/FileSharingPage.tsx new file mode 100644 index 0000000..699b895 --- /dev/null +++ b/frontend/src/app/components/FileSharingPage.tsx @@ -0,0 +1,115 @@ +import { useState } from 'react'; +import Navigation from './Navigation'; +import { Upload, File, Download, Trash2 } from 'lucide-react'; + +interface FileItem { + id: number; + name: string; + size: string; + date: string; +} + +export default function FileSharingPage() { + const [files, setFiles] = useState([]); + + const handleFileUpload = (e: React.ChangeEvent) => { + const fileList = e.target.files; + if (fileList && fileList.length > 0) { + const newFiles = Array.from(fileList).map((file) => ({ + id: Date.now() + Math.random(), + name: file.name, + size: (file.size / 1024).toFixed(2) + ' KB', + date: new Date().toLocaleDateString('zh-CN'), + })); + setFiles([...newFiles, ...files]); + } + }; + + const deleteFile = (id: number) => { + setFiles(files.filter(file => file.id !== id)); + }; + + return ( +
+ + +
+
+ {/* Header */} +
+

文件分享

+

上传和分享文件

+
+ + {/* Upload Area */} +
+ +
+ + {/* Files List */} +
+

文件列表

+
+ {files.length === 0 ? ( +
+ 暂无文件,开始上传吧! +
+ ) : ( + files.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

+ {file.size} · {file.date} +

+
+
+
+ + +
+
+ )) + )} +
+
+
+
+ + {/* Floating Action Button */} + +
+ ); +} diff --git a/frontend/src/app/components/HomePage.tsx b/frontend/src/app/components/HomePage.tsx new file mode 100644 index 0000000..0efd5d4 --- /dev/null +++ b/frontend/src/app/components/HomePage.tsx @@ -0,0 +1,115 @@ +import { Link } from 'react-router-dom'; +import Navigation from './Navigation'; +import { CheckSquare, Cloud, Film, Wallet, MessageCircle, FolderOpen } from 'lucide-react'; + +const features = [ + { + title: 'Todo清单', + description: '管理你的待办事项', + path: '/todo', + icon: CheckSquare, + bgColor: 'bg-gradient-to-br from-purple-300 to-purple-400', + pattern: 'opacity-20', + }, + { + title: '天气查询', + description: '查看实时天气信息', + path: '/weather', + icon: Cloud, + bgColor: 'bg-gradient-to-br from-blue-100 to-blue-200', + pattern: 'opacity-20', + }, + { + title: '书影音清单', + description: '记录你的观影阅读', + path: '/media', + icon: Film, + bgColor: 'bg-gradient-to-br from-orange-300 to-orange-400', + pattern: 'opacity-20', + }, + { + title: '记账本', + description: '追踪你的收支情况', + path: '/accounting', + icon: Wallet, + bgColor: 'bg-gradient-to-br from-purple-300 to-purple-400', + pattern: 'opacity-20', + }, + { + title: '聊天室', + description: '与朋友愉快交流', + path: '/chat', + icon: MessageCircle, + bgColor: 'bg-gradient-to-br from-orange-300 to-orange-400', + pattern: 'opacity-20', + }, + { + title: '文件分享', + description: '上传和分享文件', + path: '/files', + icon: FolderOpen, + bgColor: 'bg-gradient-to-br from-blue-100 to-blue-200', + pattern: 'opacity-20', + }, +]; + +export default function HomePage() { + return ( +
+ + +
+
+ {/* Hero Section */} +
+

博客网站

+

一个多功能的个人管理平台

+
+ + {/* Features Grid */} +
+ {features.map((feature, index) => { + const Icon = feature.icon; + return ( + +
+ {/* Decorative Pattern */} +
+ + + + + +
+ + {/* Icon */} +
+ +
+ + {/* Title */} +

{feature.title}

+ + {/* Description */} +

{feature.description}

+
+ + ); + })} +
+
+
+ + {/* Floating Action Button */} + +
+ ); +} diff --git a/frontend/src/app/components/MediaPage.tsx b/frontend/src/app/components/MediaPage.tsx new file mode 100644 index 0000000..03cdc6a --- /dev/null +++ b/frontend/src/app/components/MediaPage.tsx @@ -0,0 +1,271 @@ +import { useState } from 'react'; +import Navigation from './Navigation'; +import { Plus, X, Book, Film as FilmIcon, Music } from 'lucide-react'; + +interface MediaItem { + id: number; + title: string; + description: string; + type: '书籍' | '电影' | '音乐'; + rating: number; +} + +export default function MediaPage() { + const [items, setItems] = useState([]); + const [showDialog, setShowDialog] = useState(false); + const [activeFilter, setActiveFilter] = useState<'全部' | '书籍' | '电影' | '音乐'>('全部'); + + // Form state + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [type, setType] = useState<'书籍' | '电影' | '音乐'>('电影'); + const [rating, setRating] = useState(0); + + const addItem = () => { + if (title.trim()) { + setItems([ + { + id: Date.now(), + title, + description, + type, + rating, + }, + ...items, + ]); + // Reset form + setTitle(''); + setDescription(''); + setType('电影'); + setRating(0); + setShowDialog(false); + } + }; + + const filteredItems = activeFilter === '全部' + ? items + : items.filter(item => item.type === activeFilter); + + return ( +
+ + +
+
+ {/* Header */} +
+

书影音清单

+

记录你的阅读、观影和音乐体验

+
+ + {/* Add Button */} +
+ +
+ + {/* Main Card */} +
+ {/* Filter Tabs */} +
+ {(['全部', '书籍', '电影', '音乐'] as const).map((filter) => ( + + ))} +
+ + {/* Items Grid */} +
+ {filteredItems.length === 0 ? ( +
+ 暂无记录 +
+ ) : ( +
+ {filteredItems.map((item) => ( +
+ {/* Icon based on type */} +
+
+ {item.type === '书籍' ? : + item.type === '电影' ? : + } +
+ + {item.type} + +
+ +

{item.title}

+ + {item.description && ( +

{item.description}

+ )} + + {/* Rating */} +
+ {[1, 2, 3, 4, 5].map((star) => ( + + + + ))} +
+
+ ))} +
+ )} +
+
+
+
+ + {/* Add Dialog */} + {showDialog && ( +
+
+ + +

添加新记录

+ + {/* Title Input */} +
+ + setTitle(e.target.value)} + placeholder="输入标题" + className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-400" + /> +
+ + {/* Description Input */} +
+ +