feat: 修复 H5 底部导航覆盖 + 更新项目进度文档

## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
This commit is contained in:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+201
View File
@@ -0,0 +1,201 @@
from typing import Dict, Any, Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, or_
from app.models.team import Team, TeamMember
from app.models.user import User
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class TeamService:
def __init__(self, db: AsyncSession):
self.db = db
async def create_team(self, owner_id: str, name: str, description: str = None) -> Dict[str, Any]:
existing = await self.db.execute(
select(Team).where(and_(Team.owner_id == owner_id, Team.is_active == True))
)
if existing.scalar_one_or_none():
raise ValueError("You already own an active team")
user_result = await self.db.execute(select(User).where(User.id == owner_id))
user = user_result.scalar_one_or_none()
if not user:
raise ValueError("User not found")
max_members = 20 if user.tier == "enterprise" else (10 if user.tier == "pro" else 5)
team = Team(
name=name,
owner_id=owner_id,
description=description,
max_members=max_members,
tier=user.tier,
)
self.db.add(team)
await self.db.flush()
member = TeamMember(
team_id=team.id,
user_id=owner_id,
role="owner",
status="active",
)
self.db.add(member)
await self.db.flush()
return await self._to_dict(team, include_members=True)
async def get_team(self, team_id: str, user_id: str) -> Optional[Dict[str, Any]]:
result = await self.db.execute(
select(Team).where(Team.id == team_id)
)
team = result.scalar_one_or_none()
if not team:
return None
is_member = await self.db.execute(
select(TeamMember).where(
and_(TeamMember.team_id == team_id, TeamMember.user_id == user_id)
)
)
if not is_member.scalar_one_or_none():
return None
return await self._to_dict(team, include_members=True)
async def list_user_teams(self, user_id: str) -> List[Dict[str, Any]]:
member_result = await self.db.execute(
select(TeamMember.team_id).where(TeamMember.user_id == user_id)
)
team_ids = [r[0] for r in member_result.all()]
if not team_ids:
return []
result = await self.db.execute(
select(Team).where(Team.id.in_(team_ids), Team.is_active == True)
)
teams = result.scalars().all()
return [await self._to_dict(t) for t in teams]
async def invite_member(self, team_id: str, owner_id: str, user_id: str) -> Dict[str, Any]:
team_result = await self.db.execute(
select(Team).where(and_(Team.id == team_id, Team.owner_id == owner_id))
)
team = team_result.scalar_one_or_none()
if not team:
raise ValueError("Team not found or not authorized")
member_count = await self.db.execute(
select(func.count(TeamMember.id)).where(
and_(TeamMember.team_id == team_id, TeamMember.status == "active")
)
)
if (member_count.scalar() or 0) >= team.max_members:
raise ValueError("Team member limit reached")
existing = await self.db.execute(
select(TeamMember).where(
and_(TeamMember.team_id == team_id, TeamMember.user_id == user_id)
)
)
if existing.scalar_one_or_none():
raise ValueError("User is already a member")
member = TeamMember(
team_id=team_id,
user_id=user_id,
role="member",
invited_by=owner_id,
status="active",
)
self.db.add(member)
await self.db.flush()
return {"user_id": user_id, "role": "member", "status": "active"}
async def remove_member(self, team_id: str, owner_id: str, user_id: str) -> bool:
team_result = await self.db.execute(
select(Team).where(and_(Team.id == team_id, Team.owner_id == owner_id))
)
team = team_result.scalar_one_or_none()
if not team:
return False
result = await self.db.execute(
select(TeamMember).where(
and_(TeamMember.team_id == team_id, TeamMember.user_id == user_id)
)
)
member = result.scalar_one_or_none()
if not member or member.role == "owner":
return False
await self.db.delete(member)
return True
async def leave_team(self, team_id: str, user_id: str) -> bool:
result = await self.db.execute(
select(TeamMember).where(
and_(TeamMember.team_id == team_id, TeamMember.user_id == user_id)
)
)
member = result.scalar_one_or_none()
if not member or member.role == "owner":
return False
await self.db.delete(member)
return True
async def update_role(self, team_id: str, owner_id: str, user_id: str, role: str) -> bool:
team_result = await self.db.execute(
select(Team).where(and_(Team.id == team_id, Team.owner_id == owner_id))
)
if not team_result.scalar_one_or_none():
return False
result = await self.db.execute(
select(TeamMember).where(
and_(TeamMember.team_id == team_id, TeamMember.user_id == user_id)
)
)
member = result.scalar_one_or_none()
if not member or member.role == "owner":
return False
member.role = role
await self.db.flush()
return True
async def _to_dict(self, team: Team, include_members: bool = False) -> Dict[str, Any]:
result = {
"id": str(team.id),
"name": team.name,
"owner_id": str(team.owner_id),
"description": team.description,
"tier": team.tier,
"is_active": team.is_active,
"created_at": team.created_at.isoformat() if team.created_at else None,
}
if include_members:
members_result = await self.db.execute(
select(TeamMember).where(TeamMember.team_id == team.id)
)
members = members_result.scalars().all()
result["members"] = [
{
"user_id": str(m.user_id),
"role": m.role,
"status": m.status,
"joined_at": m.joined_at.isoformat() if m.joined_at else None,
}
for m in members
]
result["member_count"] = len([m for m in members if m.status == "active"])
return result