diff --git a/backend/app/main.py b/backend/app/main.py
index ab6b9aa..eef4fa7 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -64,7 +64,7 @@ app.add_middleware(
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
# 注册路由
-from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments
+from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments, leaves, trees, capsules
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证"])
app.include_router(users.router, prefix="/api/v1/users", tags=["用户"])
@@ -74,6 +74,9 @@ app.include_router(friends.router, prefix="/api/v1/friends", tags=["好友"])
app.include_router(admin.router, prefix="/api/v1/admin", tags=["管理"])
app.include_router(uploads.router, prefix="/api/v1/uploads", tags=["上传"])
app.include_router(moments.router, prefix="/api/v1/moments", tags=["朋友圈"])
+app.include_router(leaves.router, prefix="/api/v1/leaves", tags=["心情叶"])
+app.include_router(trees.router, prefix="/api/v1/trees", tags=["好友之树"])
+app.include_router(capsules.router, prefix="/api/v1/capsules", tags=["时光胶囊"])
# WebSocket
from app.websocket.router import websocket_router
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index 8c20269..8d4a1ca 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -8,6 +8,9 @@ from app.models.friend import Friend
from app.models.friend_request import FriendRequest
from app.models.system_config import SystemConfig
from app.models.moment import Moment, MomentLike, MomentComment
+from app.models.daily_leaf import DailyMoodLeaf
+from app.models.friendship_tree import FriendshipTree
+from app.models.time_capsule import TimeCapsule
__all__ = [
"User",
@@ -20,4 +23,7 @@ __all__ = [
"Moment",
"MomentLike",
"MomentComment",
+ "DailyMoodLeaf",
+ "FriendshipTree",
+ "TimeCapsule",
]
diff --git a/backend/app/models/daily_leaf.py b/backend/app/models/daily_leaf.py
new file mode 100644
index 0000000..2df7501
--- /dev/null
+++ b/backend/app/models/daily_leaf.py
@@ -0,0 +1,25 @@
+"""每日心情叶模型"""
+
+from datetime import datetime, date
+
+from sqlalchemy import String, Text, DateTime, Date, ForeignKey, UniqueConstraint
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.database import Base
+
+
+class DailyMoodLeaf(Base):
+ __tablename__ = "daily_mood_leaves"
+ __table_args__ = (
+ UniqueConstraint("user_id", "leaf_date", name="uq_user_daily_leaf"),
+ )
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
+ user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
+ leaf_date: Mapped[date] = mapped_column(Date, nullable=False)
+ mood: Mapped[str | None] = mapped_column(String(20), nullable=True) # 预设心情标签
+ note: Mapped[str | None] = mapped_column(Text, nullable=True) # 心情备注
+ leaf_seed: Mapped[str] = mapped_column(String(32), nullable=False) # 确定性生成种子
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
+
+ user = relationship("User", foreign_keys=[user_id])
diff --git a/backend/app/models/friendship_tree.py b/backend/app/models/friendship_tree.py
new file mode 100644
index 0000000..240c33d
--- /dev/null
+++ b/backend/app/models/friendship_tree.py
@@ -0,0 +1,25 @@
+"""好友之树模型"""
+
+from datetime import datetime
+
+from sqlalchemy import String, Integer, DateTime, ForeignKey, UniqueConstraint
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.database import Base
+
+
+class FriendshipTree(Base):
+ __tablename__ = "friendship_trees"
+ __table_args__ = (
+ UniqueConstraint("user_a_id", "user_b_id", name="uq_friendship_tree"),
+ )
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
+ user_a_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
+ user_b_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
+ water_count: Mapped[int] = mapped_column(Integer, default=0)
+ last_watered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
+
+ user_a = relationship("User", foreign_keys=[user_a_id])
+ user_b = relationship("User", foreign_keys=[user_b_id])
diff --git a/backend/app/models/time_capsule.py b/backend/app/models/time_capsule.py
new file mode 100644
index 0000000..6e5b8df
--- /dev/null
+++ b/backend/app/models/time_capsule.py
@@ -0,0 +1,25 @@
+"""时光胶囊模型"""
+
+from datetime import datetime
+
+from sqlalchemy import String, Text, DateTime, Boolean, ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from app.database import Base
+
+
+class TimeCapsule(Base):
+ __tablename__ = "time_capsules"
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
+ sender_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
+ recipient_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
+ title: Mapped[str] = mapped_column(String(100), nullable=False)
+ content: Mapped[str] = mapped_column(Text, nullable=False)
+ unlock_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
+ mood: Mapped[str | None] = mapped_column(String(20), nullable=True) # 种子心情色
+ is_opened: Mapped[bool] = mapped_column(Boolean, default=False)
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
+
+ sender = relationship("User", foreign_keys=[sender_id])
+ recipient = relationship("User", foreign_keys=[recipient_id])
diff --git a/backend/app/routers/capsules.py b/backend/app/routers/capsules.py
new file mode 100644
index 0000000..367c23f
--- /dev/null
+++ b/backend/app/routers/capsules.py
@@ -0,0 +1,51 @@
+"""时光胶囊路由"""
+
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.dependencies import get_db, get_current_user
+from app.models.user import User
+from app.schemas.capsule import CapsuleCreate
+from app.services.capsule_service import CapsuleService
+
+router = APIRouter()
+
+
+@router.get("/")
+async def list_capsules(
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取我的时光胶囊"""
+ service = CapsuleService(db)
+ return await service.get_capsules(user.id)
+
+
+@router.get("/{capsule_id}")
+async def get_capsule(
+ capsule_id: str,
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取单个胶囊"""
+ service = CapsuleService(db)
+ try:
+ return await service.get_capsule(user.id, capsule_id)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@router.post("/")
+async def create_capsule(
+ req: CapsuleCreate,
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """创建时光胶囊"""
+ service = CapsuleService(db)
+ try:
+ return await service.create_capsule(
+ user.id, req.recipient_id, req.title, req.content, req.unlock_at, req.mood
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
diff --git a/backend/app/routers/leaves.py b/backend/app/routers/leaves.py
new file mode 100644
index 0000000..711a767
--- /dev/null
+++ b/backend/app/routers/leaves.py
@@ -0,0 +1,46 @@
+"""心情叶路由"""
+
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.dependencies import get_db, get_current_user
+from app.models.user import User
+from app.schemas.leaf import LeafUpdate
+from app.services.leaf_service import LeafService
+
+router = APIRouter()
+
+
+@router.get("/today")
+async def get_today_leaf(
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取或创建今日心情叶"""
+ service = LeafService(db)
+ return await service.get_or_create_today(user.id)
+
+
+@router.put("/{leaf_id}")
+async def update_leaf(
+ leaf_id: str,
+ req: LeafUpdate,
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """更新今日叶子的心情和备注"""
+ service = LeafService(db)
+ try:
+ return await service.update_leaf(user.id, leaf_id, req.mood, req.note)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+
+@router.get("/collection")
+async def get_collection(
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取叶子收藏"""
+ service = LeafService(db)
+ return await service.get_collection(user.id)
diff --git a/backend/app/routers/trees.py b/backend/app/routers/trees.py
new file mode 100644
index 0000000..82e9a2e
--- /dev/null
+++ b/backend/app/routers/trees.py
@@ -0,0 +1,45 @@
+"""好友之树路由"""
+
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.dependencies import get_db, get_current_user
+from app.models.user import User
+from app.services.tree_service import TreeService
+
+router = APIRouter()
+
+
+@router.get("/")
+async def list_trees(
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取所有好友的树"""
+ service = TreeService(db)
+ return await service.get_all_trees(user.id)
+
+
+@router.get("/{friend_id}")
+async def get_tree(
+ friend_id: str,
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """获取与某好友的树"""
+ service = TreeService(db)
+ return await service.get_tree(user.id, friend_id)
+
+
+@router.post("/{friend_id}/water")
+async def water_tree(
+ friend_id: str,
+ user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """浇水"""
+ service = TreeService(db)
+ try:
+ return await service.water(user.id, friend_id)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
diff --git a/backend/app/schemas/capsule.py b/backend/app/schemas/capsule.py
new file mode 100644
index 0000000..9763b34
--- /dev/null
+++ b/backend/app/schemas/capsule.py
@@ -0,0 +1,13 @@
+"""时光胶囊 Schema"""
+
+from datetime import datetime
+
+from pydantic import BaseModel, Field
+
+
+class CapsuleCreate(BaseModel):
+ recipient_id: str
+ title: str = Field(..., min_length=1, max_length=100)
+ content: str = Field(..., min_length=1, max_length=5000)
+ unlock_at: datetime
+ mood: str | None = Field(None, max_length=20)
diff --git a/backend/app/schemas/leaf.py b/backend/app/schemas/leaf.py
new file mode 100644
index 0000000..6b61c68
--- /dev/null
+++ b/backend/app/schemas/leaf.py
@@ -0,0 +1,8 @@
+"""心情叶 Schema"""
+
+from pydantic import BaseModel, Field
+
+
+class LeafUpdate(BaseModel):
+ mood: str | None = Field(None, max_length=20)
+ note: str | None = Field(None, max_length=500)
diff --git a/backend/app/services/capsule_service.py b/backend/app/services/capsule_service.py
new file mode 100644
index 0000000..e778e2f
--- /dev/null
+++ b/backend/app/services/capsule_service.py
@@ -0,0 +1,105 @@
+"""时光胶囊服务(懒解锁)"""
+
+import uuid
+from datetime import datetime
+
+from sqlalchemy import select, or_
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.time_capsule import TimeCapsule
+from app.models.friend import Friend
+
+
+class CapsuleService:
+ def __init__(self, db: AsyncSession):
+ self.db = db
+
+ async def create_capsule(self, sender_id: str, recipient_id: str, title: str,
+ content: str, unlock_at: datetime,
+ mood: str | None = None) -> dict:
+ """创建时光胶囊"""
+ # 校验解锁时间是未来
+ if unlock_at <= datetime.utcnow():
+ raise ValueError("解锁时间必须是未来时间")
+
+ # 校验收件人:自己 或 好友
+ if recipient_id != sender_id:
+ friend_result = await self.db.execute(
+ select(Friend).where(
+ Friend.user_id == sender_id,
+ Friend.friend_user_id == recipient_id,
+ )
+ )
+ if not friend_result.scalars().first():
+ raise ValueError("只能给好友或自己寄胶囊")
+
+ capsule = TimeCapsule(
+ id=str(uuid.uuid4()),
+ sender_id=sender_id,
+ recipient_id=recipient_id,
+ title=title,
+ content=content,
+ unlock_at=unlock_at,
+ mood=mood,
+ )
+ self.db.add(capsule)
+ await self.db.flush()
+ return self._capsule_to_dict(capsule, sender_id)
+
+ async def get_capsules(self, user_id: str) -> list[dict]:
+ """获取我发的 + 我收的胶囊"""
+ result = await self.db.execute(
+ select(TimeCapsule).where(
+ or_(
+ TimeCapsule.sender_id == user_id,
+ TimeCapsule.recipient_id == user_id,
+ )
+ ).order_by(TimeCapsule.unlock_at.desc())
+ )
+ return [self._capsule_to_dict(c, user_id) for c in result.scalars().all()]
+
+ async def get_capsule(self, user_id: str, capsule_id: str) -> dict:
+ """获取单个胶囊"""
+ result = await self.db.execute(
+ select(TimeCapsule).where(TimeCapsule.id == capsule_id)
+ )
+ capsule = result.scalars().first()
+ if not capsule:
+ raise ValueError("胶囊不存在")
+ if capsule.sender_id != user_id and capsule.recipient_id != user_id:
+ raise ValueError("无权查看此胶囊")
+
+ # 若已解锁且收件人首次查看,标记 opened
+ now = datetime.utcnow()
+ if now >= capsule.unlock_at and capsule.recipient_id == user_id and not capsule.is_opened:
+ capsule.is_opened = True
+ await self.db.flush()
+ return self._capsule_to_dict(capsule, user_id)
+
+ def _capsule_to_dict(self, capsule: TimeCapsule, viewer_id: str) -> dict:
+ """转字典,应用懒解锁:未到期则隐藏内容"""
+ now = datetime.utcnow()
+ locked = now < capsule.unlock_at
+ seconds_left = int((capsule.unlock_at - now).total_seconds()) if locked else 0
+
+ # 内容可见性:发送人可看自己的(即使锁定);收件人锁定时不可看
+ can_see_content = (
+ capsule.sender_id == viewer_id # 发送人始终能看自己发的
+ or not locked # 已解锁
+ )
+
+ return {
+ "id": capsule.id,
+ "sender_id": capsule.sender_id,
+ "recipient_id": capsule.recipient_id,
+ "is_mine_sent": capsule.sender_id == viewer_id,
+ "is_mine_received": capsule.recipient_id == viewer_id,
+ "title": capsule.title,
+ "content": capsule.content if can_see_content else None,
+ "unlock_at": capsule.unlock_at.isoformat(),
+ "mood": capsule.mood,
+ "locked": locked,
+ "seconds_left": seconds_left,
+ "is_opened": capsule.is_opened,
+ "created_at": capsule.created_at.isoformat(),
+ }
diff --git a/backend/app/services/leaf_service.py b/backend/app/services/leaf_service.py
new file mode 100644
index 0000000..c2fe1e4
--- /dev/null
+++ b/backend/app/services/leaf_service.py
@@ -0,0 +1,76 @@
+"""每日心情叶服务"""
+
+import hashlib
+import uuid
+from datetime import date
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.daily_leaf import DailyMoodLeaf
+
+
+class LeafService:
+ def __init__(self, db: AsyncSession):
+ self.db = db
+
+ async def get_or_create_today(self, user_id: str) -> dict:
+ """获取或创建今日心情叶"""
+ today = date.today()
+ result = await self.db.execute(
+ select(DailyMoodLeaf).where(
+ DailyMoodLeaf.user_id == user_id,
+ DailyMoodLeaf.leaf_date == today,
+ )
+ )
+ leaf = result.scalars().first()
+ if not leaf:
+ seed = hashlib.md5(f"{user_id}:{today.isoformat()}".encode()).hexdigest()[:16]
+ leaf = DailyMoodLeaf(
+ id=str(uuid.uuid4()),
+ user_id=user_id,
+ leaf_date=today,
+ leaf_seed=seed,
+ )
+ self.db.add(leaf)
+ await self.db.flush()
+ return self._leaf_to_dict(leaf)
+
+ async def update_leaf(self, user_id: str, leaf_id: str, mood: str | None,
+ note: str | None) -> dict:
+ """更新今日叶子的心情和备注"""
+ result = await self.db.execute(
+ select(DailyMoodLeaf).where(DailyMoodLeaf.id == leaf_id)
+ )
+ leaf = result.scalars().first()
+ if not leaf:
+ raise ValueError("叶子不存在")
+ if leaf.user_id != user_id:
+ raise ValueError("无权操作此叶子")
+ if mood is not None:
+ leaf.mood = mood
+ if note is not None:
+ leaf.note = note
+ await self.db.flush()
+ return self._leaf_to_dict(leaf)
+
+ async def get_collection(self, user_id: str, limit: int = 60) -> list[dict]:
+ """获取叶子收藏(历史)"""
+ result = await self.db.execute(
+ select(DailyMoodLeaf)
+ .where(DailyMoodLeaf.user_id == user_id)
+ .order_by(DailyMoodLeaf.leaf_date.desc())
+ .limit(limit)
+ )
+ return [self._leaf_to_dict(l) for l in result.scalars().all()]
+
+ def _leaf_to_dict(self, leaf: DailyMoodLeaf) -> dict:
+ return {
+ "id": leaf.id,
+ "user_id": leaf.user_id,
+ "leaf_date": leaf.leaf_date.isoformat() if leaf.leaf_date else None,
+ "mood": leaf.mood,
+ "note": leaf.note,
+ "leaf_seed": leaf.leaf_seed,
+ "created_at": leaf.created_at,
+ }
diff --git a/backend/app/services/tree_service.py b/backend/app/services/tree_service.py
new file mode 100644
index 0000000..61e78ad
--- /dev/null
+++ b/backend/app/services/tree_service.py
@@ -0,0 +1,149 @@
+"""好友之树服务"""
+
+import uuid
+from datetime import datetime
+
+from sqlalchemy import select, func
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.friendship_tree import FriendshipTree
+from app.models.conversation import Conversation
+from app.models.conversation_member import ConversationMember
+from app.models.message import Message
+from app.models.friend import Friend
+from app.models.user import User
+
+
+# 阶段定义:分数 -> (阶段索引, 名称, emoji)
+STAGES = [
+ (0, "种子", "🌱"),
+ (11, "萌芽", "🌿"),
+ (41, "幼苗", "🪴"),
+ (151, "小树", "🌲"),
+ (401, "大树", "🌳"),
+ (1001, "古树", "🌲"),
+]
+
+
+def stage_for_score(score: int) -> tuple[int, str, str]:
+ """根据分数返回 (阶段索引, 名称, emoji)"""
+ idx = 0
+ for i, (threshold, name, emoji) in enumerate(STAGES):
+ if score >= threshold:
+ idx = i
+ return idx, STAGES[idx][1], STAGES[idx][2]
+
+
+class TreeService:
+ def __init__(self, db: AsyncSession):
+ self.db = db
+
+ async def _get_or_create_tree_row(self, user_id: str, friend_id: str) -> FriendshipTree:
+ """规范化(a < b)后查/建树行"""
+ a, b = (user_id, friend_id) if user_id < friend_id else (friend_id, user_id)
+ result = await self.db.execute(
+ select(FriendshipTree).where(
+ FriendshipTree.user_a_id == a,
+ FriendshipTree.user_b_id == b,
+ )
+ )
+ tree = result.scalars().first()
+ if not tree:
+ tree = FriendshipTree(
+ id=str(uuid.uuid4()),
+ user_a_id=a,
+ user_b_id=b,
+ )
+ self.db.add(tree)
+ await self.db.flush()
+ return tree
+
+ async def _count_messages_between(self, user_id: str, friend_id: str) -> int:
+ """统计两人私聊会话中的消息数(复用 get_or_create_private 的查找逻辑)"""
+ # 找到两人共有的私聊会话
+ result = await self.db.execute(
+ select(Conversation).join(ConversationMember)
+ .where(
+ Conversation.type == "private",
+ ConversationMember.user_id == user_id,
+ )
+ )
+ conv_id = None
+ for conv in result.scalars().all():
+ member_result = await self.db.execute(
+ select(ConversationMember).where(
+ ConversationMember.conversation_id == conv.id,
+ ConversationMember.user_id == friend_id,
+ )
+ )
+ if member_result.scalars().first():
+ conv_id = conv.id
+ break
+ if not conv_id:
+ return 0
+ count_result = await self.db.execute(
+ select(func.count(Message.id)).where(
+ Message.conversation_id == conv_id,
+ Message.is_deleted == False,
+ )
+ )
+ return count_result.scalar() or 0
+
+ async def get_tree(self, user_id: str, friend_id: str) -> dict:
+ """获取好友之树"""
+ tree = await self._get_or_create_tree_row(user_id, friend_id)
+ msg_count = await self._count_messages_between(user_id, friend_id)
+ total_score = msg_count + tree.water_count * 5
+ stage_idx, stage_name, stage_emoji = stage_for_score(total_score)
+
+ # 下个阶段的分数门槛
+ next_threshold = STAGES[stage_idx + 1][0] if stage_idx + 1 < len(STAGES) else None
+
+ # 好友信息
+ friend_result = await self.db.execute(select(User).where(User.id == friend_id))
+ friend = friend_result.scalars().first()
+
+ return {
+ "tree_id": tree.id,
+ "friend_id": friend_id,
+ "friend_name": friend.nickname or friend.username if friend else "未知",
+ "friend_avatar": friend.avatar_url if friend else None,
+ "message_count": msg_count,
+ "water_count": tree.water_count,
+ "total_score": total_score,
+ "stage_index": stage_idx,
+ "stage_name": stage_name,
+ "stage_emoji": stage_emoji,
+ "next_threshold": next_threshold,
+ "last_watered_at": tree.last_watered_at,
+ "seed": tree.id[:16], # 用于程序化树形态
+ }
+
+ async def water(self, user_id: str, friend_id: str) -> dict:
+ """浇水,返回带 leveled_up 标志"""
+ tree = await self._get_or_create_tree_row(user_id, friend_id)
+ old_score = (await self._count_messages_between(user_id, friend_id)) + tree.water_count * 5
+ old_stage_idx = stage_for_score(old_score)[0]
+
+ tree.water_count += 1
+ tree.last_watered_at = datetime.utcnow()
+ await self.db.flush()
+
+ result = await self.get_tree(user_id, friend_id)
+ result["leveled_up"] = result["stage_index"] > old_stage_idx
+ return result
+
+ async def get_all_trees(self, user_id: str) -> list[dict]:
+ """获取所有好友的树(花园概览用)"""
+ # 获取好友列表
+ friends_result = await self.db.execute(
+ select(Friend.friend_user_id).where(Friend.user_id == user_id)
+ )
+ friend_ids = [r[0] for r in friends_result.all()]
+
+ trees = []
+ for fid in friend_ids:
+ trees.append(await self.get_tree(user_id, fid))
+ # 按分数排序
+ trees.sort(key=lambda t: t["total_score"], reverse=True)
+ return trees
diff --git a/frontend/src/api/capsules.ts b/frontend/src/api/capsules.ts
new file mode 100644
index 0000000..bad12c3
--- /dev/null
+++ b/frontend/src/api/capsules.ts
@@ -0,0 +1,17 @@
+import api from './client'
+
+export interface CapsuleCreateData {
+ recipient_id: string
+ title: string
+ content: string
+ unlock_at: string
+ mood?: string
+}
+
+export const capsulesApi = {
+ getAll: () => api.get('/capsules/'),
+
+ getOne: (id: string) => api.get(`/capsules/${id}`),
+
+ create: (data: CapsuleCreateData) => api.post('/capsules/', data),
+}
diff --git a/frontend/src/api/leaves.ts b/frontend/src/api/leaves.ts
new file mode 100644
index 0000000..08674cc
--- /dev/null
+++ b/frontend/src/api/leaves.ts
@@ -0,0 +1,10 @@
+import api from './client'
+
+export const leavesApi = {
+ getToday: () => api.get('/leaves/today'),
+
+ updateLeaf: (leafId: string, data: { mood?: string; note?: string }) =>
+ api.put(`/leaves/${leafId}`, data),
+
+ getCollection: () => api.get('/leaves/collection'),
+}
diff --git a/frontend/src/api/trees.ts b/frontend/src/api/trees.ts
new file mode 100644
index 0000000..c273786
--- /dev/null
+++ b/frontend/src/api/trees.ts
@@ -0,0 +1,9 @@
+import api from './client'
+
+export const treesApi = {
+ getAll: () => api.get('/trees/'),
+
+ getTree: (friendId: string) => api.get(`/trees/${friendId}`),
+
+ water: (friendId: string) => api.post(`/trees/${friendId}/water`),
+}
diff --git a/frontend/src/layouts/UnifiedLayout.vue b/frontend/src/layouts/UnifiedLayout.vue
index 10998a4..b5ec6a5 100644
--- a/frontend/src/layouts/UnifiedLayout.vue
+++ b/frontend/src/layouts/UnifiedLayout.vue
@@ -20,6 +20,9 @@
寄一颗种子给未来,时间到了,它会发芽
+还没有胶囊,种下第一颗吧
+在这里,关系会生长 · 每一片叶子都是独一无二的回忆
+每天收获一片独一无二的叶子,记录今日心情
+你们的友谊会长成一棵树,越聊越茂盛
+给未来的自己或好友,寄一颗会发芽的种子
+「过去 · 当下 · 未来,都在这片花园里生长」
+{{ today }}
+你们的友谊,会长成一棵树
+还没有好友,加个好友一起种树吧
+