Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions backend/alembic/versions/006_add_member_contact_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Add contact fields to committee members

Revision ID: 006
Revises: 005
Create Date: 2026-02-10

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '006'
down_revision = '005'
branch_labels = None
depends_on = None


def upgrade() -> None:
# Make email and organization NOT NULL
op.alter_column('committee_members', 'email',
existing_type=sa.String(200),
nullable=False)
op.alter_column('committee_members', 'organization',
existing_type=sa.String(200),
nullable=False)

# Add new optional contact fields
op.add_column('committee_members', sa.Column('gitcode_id', sa.String(100), nullable=True))
op.add_column('committee_members', sa.Column('github_id', sa.String(100), nullable=True))


def downgrade() -> None:
# Remove new fields
op.drop_column('committee_members', 'github_id')
op.drop_column('committee_members', 'gitcode_id')

# Revert email and organization to nullable
op.alter_column('committee_members', 'organization',
existing_type=sa.String(200),
nullable=True)
op.alter_column('committee_members', 'email',
existing_type=sa.String(200),
nullable=True)
11 changes: 9 additions & 2 deletions backend/app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import secrets
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user, get_current_active_superuser
from app.core.security import create_access_token, verify_password, get_password_hash
from app.config import settings
from app.database import get_db
from app.models import User
from app.models.password_reset import PasswordResetToken
from app.schemas import (
LoginRequest, Token, UserCreate, UserOut, UserInfoResponse,
InitialSetupRequest, PasswordResetRequest,
PasswordResetConfirm, SystemStatusResponse,
)

Check failure on line 17 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:1:1: I001 Import block is un-sorted or un-formatted

router = APIRouter()

Expand Down Expand Up @@ -200,12 +200,19 @@
"""
Get current user information and their accessible communities with roles.
"""
from app.core.dependencies import get_user_community_role
from app.schemas.community import CommunityWithRole

from app.models.community import Community

Check failure on line 205 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:203:5: I001 Import block is un-sorted or un-formatted

# Superusers can see all communities
if current_user.is_superuser:
communities = db.query(Community).all()
else:
communities = current_user.communities

# Build communities with roles
communities_with_roles = []
for community in current_user.communities:
for community in communities:
role = get_user_community_role(current_user, community.id, db)
communities_with_roles.append(
CommunityWithRole(
Expand All @@ -218,7 +225,7 @@
role=role or "user",
)
)

Check failure on line 228 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

app/api/auth.py:228:1: W293 Blank line contains whitespace
return UserInfoResponse(
user=current_user,
communities=communities_with_roles
Expand Down Expand Up @@ -334,9 +341,9 @@

def _send_password_reset_email(to_email: str, token: str) -> None:
"""Send a password reset email via SMTP."""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

Check failure on line 346 in backend/app/api/auth.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

app/api/auth.py:344:5: I001 Import block is un-sorted or un-formatted

reset_url = f"{settings.FRONTEND_URL}/reset-password?token={token}"

Expand Down
6 changes: 4 additions & 2 deletions backend/app/models/committee.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ class CommitteeMember(Base):
)

name = Column(String(200), nullable=False)
email = Column(String(200), nullable=True, index=True)
email = Column(String(200), nullable=False, index=True)
phone = Column(String(50), nullable=True)
wechat = Column(String(100), nullable=True)
organization = Column(String(200), nullable=True)
organization = Column(String(200), nullable=False)
gitcode_id = Column(String(100), nullable=True)
github_id = Column(String(100), nullable=True)

# 角色标签(中文,可多选,JSON数组)
# 可选值: ["主席", "副主席", "委员", "常务委员"]
Expand Down
18 changes: 12 additions & 6 deletions backend/app/schemas/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ class CommitteeBrief(BaseModel):

class CommitteeMemberCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
email: Optional[str] = None
email: str = Field(..., min_length=1, max_length=200)
phone: Optional[str] = None
wechat: Optional[str] = None
organization: Optional[str] = None
organization: str = Field(..., min_length=1, max_length=200)
gitcode_id: Optional[str] = None
github_id: Optional[str] = None
roles: list[str] = []
term_start: Optional[date] = None
term_end: Optional[date] = None
Expand All @@ -70,10 +72,12 @@ class CommitteeMemberCreate(BaseModel):

class CommitteeMemberUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
email: Optional[str] = None
email: Optional[str] = Field(None, min_length=1, max_length=200)
phone: Optional[str] = None
wechat: Optional[str] = None
organization: Optional[str] = None
organization: Optional[str] = Field(None, min_length=1, max_length=200)
gitcode_id: Optional[str] = None
github_id: Optional[str] = None
roles: Optional[list[str]] = None
term_start: Optional[date] = None
term_end: Optional[date] = None
Expand All @@ -86,10 +90,12 @@ class CommitteeMemberOut(BaseModel):
id: int
committee_id: int
name: str
email: Optional[str] = None
email: str
phone: Optional[str] = None
wechat: Optional[str] = None
organization: Optional[str] = None
organization: str
gitcode_id: Optional[str] = None
github_id: Optional[str] = None
roles: list[str] = []
term_start: Optional[date] = None
term_end: Optional[date] = None
Expand Down
51 changes: 35 additions & 16 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
text-color="#bbb"
active-text-color="#409eff"
>
<el-menu-item index="/">
<el-icon><DataAnalysis /></el-icon>
<span>仪表板</span>
</el-menu-item>
<el-menu-item index="/community-overview">
<el-icon><OfficeBuilding /></el-icon>
<span>社区总览</span>
Expand Down Expand Up @@ -66,10 +62,6 @@
<span>批量管理</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item index="/settings">
<el-icon><Setting /></el-icon>
<span>渠道设置</span>
</el-menu-item>
<el-menu-item v-if="isSuperuser" index="/communities">
<el-icon><OfficeBuilding /></el-icon>
<span>社区管理</span>
Expand All @@ -78,11 +70,15 @@
<el-icon><UserFilled /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/">
<el-icon><DataAnalysis /></el-icon>
<span>仪表板</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="app-header">
<community-switcher />
<community-switcher v-if="showCommunitySwitcher" />
<div class="header-right">
<el-dropdown @command="handleCommand">
<span class="user-info">
Expand Down Expand Up @@ -132,14 +128,37 @@ const showLayout = computed(() => {
return !noLayoutRoutes.includes(route.name as string)
})

// 判断是否显示社区选择下拉框
// 社区总览、社区管理、用户管理页面不显示
const showCommunitySwitcher = computed(() => {
const hideSwitcherRoutes = [
'CommunityOverview',
'CommunityManage',
'UserManage',
'Dashboard'
]
return !hideSwitcherRoutes.includes(route.name as string)
})

onMounted(async () => {
if (authStore.isAuthenticated && !authStore.user) {
try {
const userInfo = await getUserInfo()
authStore.setUser(userInfo.user)
authStore.setCommunities(userInfo.communities)
} catch {
// If failed to get user info, clear auth
if (authStore.isAuthenticated) {
// Always fetch user info and communities to ensure they're up to date
if (!authStore.user || authStore.communities.length === 0) {
try {
const userInfo = await getUserInfo()
authStore.setUser(userInfo.user)
authStore.setCommunities(userInfo.communities)

// Set the first community as default if not already set
if (userInfo.communities.length > 0) {
const currentCommunityId = localStorage.getItem('current_community_id')
if (!currentCommunityId) {
localStorage.setItem('current_community_id', String(userInfo.communities[0].id))
}
}
} catch {
// If failed to get user info, clear auth
}
}
}
})
Expand Down
14 changes: 10 additions & 4 deletions frontend/src/api/governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export interface CommitteeMember {
id: number
committee_id: number
name: string
email?: string
email: string
phone?: string
wechat?: string
organization?: string
organization: string
gitcode_id?: string
github_id?: string
roles: string[]
term_start?: string
term_end?: string
Expand Down Expand Up @@ -58,10 +60,12 @@ export interface CommitteeUpdate {

export interface CommitteeMemberCreate {
name: string
email?: string
email: string
phone?: string
wechat?: string
organization?: string
organization: string
gitcode_id?: string
github_id?: string
roles?: string[]
term_start?: string
term_end?: string
Expand All @@ -74,6 +78,8 @@ export interface CommitteeMemberUpdate {
phone?: string
wechat?: string
organization?: string
gitcode_id?: string
github_id?: string
roles?: string[]
term_start?: string
term_end?: string
Expand Down
33 changes: 24 additions & 9 deletions frontend/src/components/CommunitySwitcher.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template>
<div class="community-switcher">
<el-select
v-model="selectedCommunityId"
:model-value="selectedCommunityId"
:placeholder="communities.length === 0 ? '暂无社区' : '选择社区'"
:disabled="communities.length === 0"
size="default"
filterable
@change="handleCommunityChange"
>
<el-option
Expand All @@ -23,22 +24,34 @@
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores/auth'
import { useCommunityStore } from '../stores/community'

const router = useRouter()
const authStore = useAuthStore()
const communityStore = useCommunityStore()

const communities = computed(() => authStore.communities)
const selectedCommunityId = ref<number | null>(communityStore.currentCommunityId)
const selectedCommunityId = computed(() => communityStore.currentCommunityId)

// Watch for external changes to current community
// Auto-select first community if no community is selected and communities are available
onMounted(() => {
if (!selectedCommunityId.value && communities.value.length > 0) {
communityStore.setCommunity(communities.value[0].id)
}
})

// Watch for changes in communities list
watch(
() => communityStore.currentCommunityId,
(newId) => {
selectedCommunityId.value = newId
() => communities.value.length,
(newLength) => {
// If we have communities but no selected community, select the first one
if (newLength > 0 && !selectedCommunityId.value) {
communityStore.setCommunity(communities.value[0].id)
}
}
)

Expand All @@ -47,8 +60,10 @@ const handleCommunityChange = (communityId: number) => {
if (community) {
communityStore.setCommunity(communityId)
ElMessage.success(`已切换到社区: ${community.name}`)
// Reload page to refresh data for new community
window.location.reload()

// Navigate to community overview instead of reloading
// This allows the app to handle data refresh internally
router.push('/community-overview')
}
}
</script>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/MemberCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@
<el-icon><ChatDotRound /></el-icon>
<span>{{ member.wechat }}</span>
</div>
<div v-if="member.gitcode_id" class="contact-item">
<el-icon><Link /></el-icon>
<span>Gitcode: {{ member.gitcode_id }}</span>
</div>
<div v-if="member.github_id" class="contact-item">
<el-icon><Link /></el-icon>
<span>GitHub: {{ member.github_id }}</span>
</div>
</div>

<div v-if="member.term_start || member.term_end" class="member-term">
Expand Down Expand Up @@ -75,7 +83,7 @@
</template>

<script setup lang="ts">
import { Message, Phone, ChatDotRound, Calendar, Edit, Delete } from '@element-plus/icons-vue'
import { Message, Phone, ChatDotRound, Calendar, Edit, Delete, Link } from '@element-plus/icons-vue'
import RoleBadge from './RoleBadge.vue'
import type { CommitteeMember } from '@/api/governance'

Expand Down
Loading
Loading