From b6e32eff3a60102afe8944e9b22f36a32ebc5fe0 Mon Sep 17 00:00:00 2001 From: Zhenyu Zheng Date: Wed, 25 Feb 2026 19:02:42 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=B4=BB=E5=8A=A8=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=A4=9A=E9=A1=B9=E4=BC=98=E5=8C=96=E2=80=94=E2=80=94?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E7=B1=BB=E5=9E=8B=E3=80=81=E6=97=B6=E9=95=BF?= =?UTF-8?q?=E5=8D=95=E4=BD=8D=E3=80=81=E7=8A=B6=E6=80=81=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E3=80=81=E7=99=BB=E5=BD=95=20Logo=20=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增活动类型选择(线上 / 线下 / 混合),支持创建和编辑 - 活动时长从"分钟"改为"小时"(保留 1 位小数),Alembic 迁移完成数据转换 - 移除"草稿"状态,默认状态改为"策划中";移除"已取消"状态 - 同步修复社区工作台因字段改名(duration_minutes→duration_hours)引发的 500 错误 - 修正登录页 Logo 拉伸变形(改用方形版本 openGecko-vertical.png) - 更新相关前端视图、API 类型定义、测试用例 --- ...346\224\271\344\270\272duration_hours_.py" | 50 +++++++++++++++++++ backend/app/api/community_dashboard.py | 12 ++--- backend/app/api/events.py | 2 +- backend/app/models/event.py | 8 +-- backend/app/schemas/event.py | 8 +-- backend/tests/test_events_api.py | 8 +-- frontend/src/api/event.ts | 4 +- frontend/src/views/EventDetail.vue | 38 ++++++++------ frontend/src/views/Events.vue | 6 --- frontend/src/views/Login.vue | 2 +- 10 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 "backend/alembic/versions/ffbd6edaf13b_\346\264\273\345\212\250\346\250\241\345\235\227_duration_minutes\346\224\271\344\270\272duration_hours_.py" diff --git "a/backend/alembic/versions/ffbd6edaf13b_\346\264\273\345\212\250\346\250\241\345\235\227_duration_minutes\346\224\271\344\270\272duration_hours_.py" "b/backend/alembic/versions/ffbd6edaf13b_\346\264\273\345\212\250\346\250\241\345\235\227_duration_minutes\346\224\271\344\270\272duration_hours_.py" new file mode 100644 index 0000000..79112e9 --- /dev/null +++ "b/backend/alembic/versions/ffbd6edaf13b_\346\264\273\345\212\250\346\250\241\345\235\227_duration_minutes\346\224\271\344\270\272duration_hours_.py" @@ -0,0 +1,50 @@ +"""活动模块:duration_minutes改为duration_hours,移除draft和cancelled状态 + +Revision ID: ffbd6edaf13b +Revises: 986ddbdad1d7 +Create Date: 2026-02-25 18:03:52.645280 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ffbd6edaf13b' +down_revision: Union[str, None] = '986ddbdad1d7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # 1. 数据迁移:将 draft/cancelled 状态更新为 planning + op.execute(sa.text("UPDATE events SET status = 'planning' WHERE status IN ('draft', 'cancelled')")) + + # 2. 先添加 duration_hours 列 + with op.batch_alter_table('events', schema=None) as batch_op: + batch_op.add_column(sa.Column('duration_hours', sa.Float(), nullable=True)) + + # 3. 将 duration_minutes 换算为 duration_hours(保留 1 位小数) + op.execute(sa.text( + "UPDATE events SET duration_hours = ROUND(CAST(duration_minutes AS FLOAT) / 60.0, 1) " + "WHERE duration_minutes IS NOT NULL" + )) + + # 4. 移除 duration_minutes 列 + with op.batch_alter_table('events', schema=None) as batch_op: + batch_op.drop_column('duration_minutes') + + +def downgrade() -> None: + with op.batch_alter_table('events', schema=None) as batch_op: + batch_op.add_column(sa.Column('duration_minutes', sa.INTEGER(), nullable=True)) + + op.execute(sa.text( + "UPDATE events SET duration_minutes = CAST(duration_hours * 60 AS INTEGER) " + "WHERE duration_hours IS NOT NULL" + )) + + with op.batch_alter_table('events', schema=None) as batch_op: + batch_op.drop_column('duration_hours') diff --git a/backend/app/api/community_dashboard.py b/backend/app/api/community_dashboard.py index 28f79b3..85ff56d 100644 --- a/backend/app/api/community_dashboard.py +++ b/backend/app/api/community_dashboard.py @@ -288,11 +288,9 @@ def get_community_dashboard( # 活动事件:按状态区分颜色 # planning → 紫色 #8b5cf6;ongoing → 蓝色 #0095ff;completed → 绿色 #10b981;cancelled → 灰色 #94a3b8 EVENT_COLORS = { - "draft": "#94a3b8", "planning": "#8b5cf6", "ongoing": "#0095ff", "completed": "#10b981", - "cancelled": "#94a3b8", } event_events = ( db.query(Event) @@ -359,15 +357,15 @@ def get_community_dashboard( # 活动事件(跨天支持):如果活动时长超过1天,则创建多天事件 for e in event_events: - event_color = EVENT_COLORS.get(e.status or "draft", "#94a3b8") - if e.planned_at and e.duration_minutes and e.duration_minutes > 1440: - days_count = (e.duration_minutes + 1439) // 1440 # 1440分钟 = 24小时 = 1天 + event_color = EVENT_COLORS.get(e.status or "planning", "#8b5cf6") + if e.planned_at and e.duration_hours and e.duration_hours > 24: + days_count = int(e.duration_hours / 24) + (1 if e.duration_hours % 24 else 0) for day_offset in range(days_count): day_date = e.planned_at + timedelta(days=day_offset) calendar_events.append( CalendarEvent( id=e.id, - type=f"event_{e.status or 'draft'}", + type=f"event_{e.status or 'planning'}", title=f"{e.title}" + (f" (第{day_offset + 1}天)" if day_offset > 0 else ""), date=day_date, color=event_color, @@ -379,7 +377,7 @@ def get_community_dashboard( calendar_events.append( CalendarEvent( id=e.id, - type=f"event_{e.status or 'draft'}", + type=f"event_{e.status or 'planning'}", title=e.title, date=e.planned_at, color=event_color, diff --git a/backend/app/api/events.py b/backend/app/api/events.py index 0953468..20faf0b 100644 --- a/backend/app/api/events.py +++ b/backend/app/api/events.py @@ -39,7 +39,7 @@ router = APIRouter() -VALID_EVENT_STATUSES = {"draft", "planning", "ongoing", "completed", "cancelled"} +VALID_EVENT_STATUSES = {"planning", "ongoing", "completed"} VALID_EVENT_TYPES = {"online", "offline", "hybrid"} diff --git a/backend/app/models/event.py b/backend/app/models/event.py index ba16100..1d2be65 100644 --- a/backend/app/models/event.py +++ b/backend/app/models/event.py @@ -1,4 +1,4 @@ -from sqlalchemy import JSON, Boolean, Column, Date, DateTime, ForeignKey, Integer, String, Table, Text +from sqlalchemy import JSON, Boolean, Column, Date, DateTime, Float, ForeignKey, Integer, String, Table, Text from sqlalchemy import Enum as SAEnum from sqlalchemy.orm import relationship @@ -74,11 +74,11 @@ class Event(Base): Integer, ForeignKey("event_templates.id", ondelete="SET NULL"), nullable=True ) status = Column( - SAEnum("draft", "planning", "ongoing", "completed", "cancelled", name="event_status_enum"), - default="draft", + SAEnum("planning", "ongoing", "completed", name="event_status_enum"), + default="planning", ) planned_at = Column(DateTime(timezone=True), nullable=True) - duration_minutes = Column(Integer, nullable=True) + duration_hours = Column(Float, nullable=True) location = Column(String(300), nullable=True) online_url = Column(String(500), nullable=True) description = Column(Text, nullable=True) diff --git a/backend/app/schemas/event.py b/backend/app/schemas/event.py index fc683fc..6520290 100644 --- a/backend/app/schemas/event.py +++ b/backend/app/schemas/event.py @@ -69,7 +69,7 @@ class EventCreate(BaseModel): community_ids: list[int] = [] template_id: int | None = None planned_at: datetime | None = None - duration_minutes: int | None = None + duration_hours: float | None = None location: str | None = None online_url: str | None = None description: str | None = None @@ -81,7 +81,7 @@ class EventUpdate(BaseModel): event_type: str | None = None community_ids: list[int] | None = None planned_at: datetime | None = None - duration_minutes: int | None = None + duration_hours: float | None = None location: str | None = None online_url: str | None = None description: str | None = None @@ -96,7 +96,7 @@ class EventUpdate(BaseModel): class EventStatusUpdate(BaseModel): - status: str # draft / planning / ongoing / completed / cancelled + status: str # planning / ongoing / completed class CommunitySimple(BaseModel): @@ -116,7 +116,7 @@ class EventOut(BaseModel): template_id: int | None status: str planned_at: datetime | None - duration_minutes: int | None + duration_hours: float | None location: str | None online_url: str | None description: str | None diff --git a/backend/tests/test_events_api.py b/backend/tests/test_events_api.py index 7486c9d..fffffdf 100644 --- a/backend/tests/test_events_api.py +++ b/backend/tests/test_events_api.py @@ -11,7 +11,7 @@ def _create_event( db_session: Session, community_id: int, title: str = "测试活动", - status: str = "draft", + status: str = "planning", event_type: str = "offline", ) -> Event: event = Event( @@ -64,16 +64,16 @@ def test_delete_event_no_auth( resp = client.delete(f"/api/events/{event.id}") assert resp.status_code == 401 - def test_delete_cancelled_event( + def test_delete_completed_event( self, client: TestClient, auth_headers: dict, db_session: Session, test_community: Community, ): - """已取消状态的活动也可以被删除""" + """已完成状态的活动也可以被删除""" event = _create_event( - db_session, test_community.id, title="已取消活动", status="cancelled" + db_session, test_community.id, title="已完成活动", status="completed" ) resp = client.delete(f"/api/events/{event.id}", headers=auth_headers) assert resp.status_code == 204 diff --git a/frontend/src/api/event.ts b/frontend/src/api/event.ts index d73d772..3ed204a 100644 --- a/frontend/src/api/event.ts +++ b/frontend/src/api/event.ts @@ -26,7 +26,7 @@ export interface EventDetail { template_id: number | null status: string planned_at: string | null - duration_minutes: number | null + duration_hours: number | null location: string | null online_url: string | null description: string | null @@ -49,7 +49,7 @@ export interface EventCreate { community_ids?: number[] template_id?: number | null planned_at?: string | null - duration_minutes?: number | null + duration_hours?: number | null location?: string | null online_url?: string | null description?: string | null diff --git a/frontend/src/views/EventDetail.vue b/frontend/src/views/EventDetail.vue index 30eab88..dcc3191 100644 --- a/frontend/src/views/EventDetail.vue +++ b/frontend/src/views/EventDetail.vue @@ -48,7 +48,7 @@
{{ formatDateTime(event.planned_at) }} - ({{ event.duration_minutes }} 分钟) + ({{ event.duration_hours }} 小时)
@@ -74,20 +74,25 @@ + + + + + + + - - - - + + @@ -423,12 +428,13 @@ const isEditing = ref(false) const saving = ref(false) const editForm = ref({ title: '', + event_type: 'offline' as string, planned_at: null as string | null, - duration_minutes: null as number | null, + duration_hours: null as number | null, location: '', online_url: '', description: '', - status: 'draft' as string, + status: 'planning' as string, community_ids: [] as number[], }) @@ -439,8 +445,8 @@ const personnelForm = ref({ role: '', role_label: '', assignee_type: 'internal', const feedbackForm = ref({ category: 'question', raised_by: '', content: '' }) // ─── Labels & Maps ──────────────────────────────────────────────────────────── -const statusLabel: Record = { draft: '草稿', planning: '策划中', ongoing: '进行中', completed: '已完成', cancelled: '已取消' } -const statusTagMap: Record = { draft: 'info', planning: 'warning', ongoing: 'primary', completed: 'success', cancelled: 'danger' } +const statusLabel: Record = { planning: '策划中', ongoing: '进行中', completed: '已完成' } +const statusTagMap: Record = { planning: 'warning', ongoing: 'primary', completed: 'success' } const typeLabel: Record = { offline: '线下', online: '线上', hybrid: '混合' } const typeTagMap: Record = { offline: '', online: 'success', hybrid: 'warning' } const phaseLabel: Record = { pre: '会前', during: '会中', post: '会后' } @@ -501,12 +507,13 @@ async function loadEvent() { isEditing.value = true editForm.value = { title: '', + event_type: 'offline', planned_at: null, - duration_minutes: null, + duration_hours: null, location: '', online_url: '', description: '', - status: 'draft', + status: 'planning', community_ids: communityStore.currentCommunityId ? [communityStore.currentCommunityId] : [], } } else { @@ -548,8 +555,9 @@ function startEdit() { if (!event.value) return editForm.value = { title: event.value.title, + event_type: event.value.event_type, planned_at: event.value.planned_at, - duration_minutes: event.value.duration_minutes, + duration_hours: event.value.duration_hours, location: event.value.location || '', online_url: event.value.online_url || '', description: event.value.description || '', @@ -580,8 +588,9 @@ async function saveEdit() { const communityIds = editForm.value.community_ids const newEvent = await createEvent({ title: editForm.value.title, + event_type: editForm.value.event_type, planned_at: editForm.value.planned_at || null, - duration_minutes: editForm.value.duration_minutes || null, + duration_hours: editForm.value.duration_hours || null, location: editForm.value.location || null, online_url: editForm.value.online_url || null, description: editForm.value.description || null, @@ -593,8 +602,9 @@ async function saveEdit() { } else { await updateEvent(eventId.value!, { title: editForm.value.title, + event_type: editForm.value.event_type, planned_at: editForm.value.planned_at, - duration_minutes: editForm.value.duration_minutes, + duration_hours: editForm.value.duration_hours, location: editForm.value.location || null, online_url: editForm.value.online_url || null, description: editForm.value.description || null, diff --git a/frontend/src/views/Events.vue b/frontend/src/views/Events.vue index dddcd6a..0426fa8 100644 --- a/frontend/src/views/Events.vue +++ b/frontend/src/views/Events.vue @@ -35,11 +35,9 @@ /> - - @@ -216,19 +214,15 @@ const createForm = ref({ }) const statusLabel: Record = { - draft: '草稿', planning: '策划中', ongoing: '进行中', completed: '已完成', - cancelled: '已取消', } const statusTagMap: Record = { - draft: 'info', planning: 'warning', ongoing: 'primary', completed: 'success', - cancelled: 'danger', } const typeLabel: Record = { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index c639e9d..5eabf5d 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -3,7 +3,7 @@