Procházet zdrojové kódy

Initial commit of tongquU project (backend and static files)

0225015 před 1 měsícem
revize
f8823699eb

+ 37 - 0
.gitignore

@@ -0,0 +1,37 @@
+# Python/Django specific for tongqu_backend_v2
+tongqu_backend_v2/venv/
+tongqu_backend_v2/__pycache__/
+tongqu_backend_v2/*.py[cod]
+tongqu_backend_v2/*$py.class
+tongqu_backend_v2/*.log
+tongqu_backend_v2/db.sqlite3
+tongqu_backend_v2/db.sqlite3-journal
+tongqu_backend_v2/staticfiles_collected/ # 我们之前配置的STATIC_ROOT
+tongqu_backend_v2/media/ # 如果media内容是动态生成的,通常也忽略
+
+# General Python
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# IDE / Editor specific
+.vscode/
+.idea/
+*.swp
+*~
+.DS_Store

+ 168 - 0
README.md

@@ -0,0 +1,168 @@
+# 同趣U (InterestU) - 大学生兴趣社交平台 [Backend]
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+<!-- 可根据实际情况添加CI/CD等徽章 -->
+
+**同趣U (InterestU)** 是一款专为在校大学生设计的移动社交应用,旨在帮助他们基于共同的兴趣爱好轻松发现伙伴、组织活动、拓展社交圈,让大学生活更加丰富多彩。本项目是“同趣U”应用的后端服务部分。
+
+## 项目背景
+
+在当前的大学校园中,学生们拥有多元的兴趣和强烈的社交需求,但常常面临以下痛点:
+
+*   **信息不对称:** 难以高效找到与自己有相同小众或特定兴趣的伙伴。
+*   **平台错位:** 现有通用社交平台难以满足纯粹基于兴趣的连接需求。
+*   **连接障碍:** 主动发起线下或线上连接存在一定的社交压力。
+*   **活动分散:** 校园内外的兴趣活动信息零散,不易发现和组织。
+
+“同趣U”后端服务旨在解决这些问题,为前端App提供稳定、高效、安全的接口支持。
+
+## 核心目标 (后端)
+
+*   **用户管理:** 提供安全可靠的用户注册、登录、资料管理等功能。
+*   **兴趣匹配:** 支持基于兴趣标签的数据存储与检索,为前端实现精准匹配提供基础。
+*   **互动支持:** 为私信、小组、活动等社交场景提供API接口。
+*   **数据持久化:** 使用PostgreSQL数据库安全存储用户信息、兴趣数据、社交关系等。
+*   **API接口:** 提供清晰、规范的RESTful API供前端App调用。
+
+## 主要功能 (当前进度及规划)
+
+### 已实现/进行中 (示例,请根据你的实际进度修改)
+
+*   **用户系统 (v0.1):**
+    *   基于Django自定义User模型,包含手机号、头像、简介、学校等字段。
+    *   使用Django REST framework 和 djangorestframework-simplejwt 实现用户注册、登录(JWT认证)、获取用户信息的API接口。
+    *   配置PostgreSQL数据库连接。
+
+### 规划中
+
+*   **兴趣标签系统:**
+    *   用户兴趣标签的创建、选择、展示。
+    *   基于兴趣标签的推荐算法初步设计。
+*   **匹配与发现:**
+    *   实现基于共同兴趣的用户推荐API。
+*   **即时通讯 (私信):**
+    *   WebSocket基础搭建或选择第三方服务。
+    *   私信消息存储与API接口。
+*   **兴趣圈子/小组:**
+    *   小组创建、加入、管理API。
+    *   小组内动态发布与查看API。
+*   **活动发布与匹配:**
+    *   活动创建、报名、管理API。
+*   **校园认证流程。**
+*   **更完善的错误处理、日志记录和单元测试。**
+
+## 技术栈
+
+*   **后端框架:** Python 3.x, Django, Django REST framework
+*   **数据库:** PostgreSQL
+*   **认证:** JWT (JSON Web Tokens) via djangorestframework-simplejwt
+*   **Web服务器 (部署时):** Gunicorn (或 uWSGI)
+*   **反向代理 (部署时):** Nginx
+*   **版本控制:** Git
+
+## 环境搭建与运行
+
+### 前提条件
+
+*   Python 3.8+
+*   PostgreSQL 12+
+*   Git
+
+### 本地开发步骤
+
+1.  **克隆仓库 (如果已推送到远程):**
+    ```bash
+    git clone http://git.fmode.cn:3000/0225015/tongquU.git
+    cd tongquU
+    ```
+    或者如果你是在本地新初始化的项目,直接进入项目目录。
+
+2.  **创建并激活Python虚拟环境:**
+    ```bash
+    python -m venv venv
+    # Windows CMD:
+    # venv\Scripts\activate
+    # Windows PowerShell:
+    # .\venv\Scripts\Activate.ps1
+    # Linux/macOS:
+    # source venv/bin/activate
+    ```
+
+3.  **安装依赖:**
+    *(建议创建一个 `requirements.txt` 文件)*
+    ```bash
+    pip install django djangorestframework djangorestframework-simplejwt psycopg2-binary
+    # 如果有 requirements.txt:
+    # pip install -r requirements.txt
+    ```
+
+4.  **配置数据库:**
+    *   确保PostgreSQL服务正在运行。
+    *   在PostgreSQL中创建一个名为 `tongquu_db` (或其他你指定的名字) 的数据库。
+        ```sql
+        -- (在psql中)
+        CREATE DATABASE tongquu_db;
+        -- CREATE USER tongquu_user WITH PASSWORD 'your_password'; -- 可选
+        -- GRANT ALL PRIVILEGES ON DATABASE tongquu_db TO tongquu_user; -- 可选
+        ```
+    *   修改项目中的 `tongqu_project/settings.py` 文件,配置 `DATABASES` 部分:
+        ```python
+        DATABASES = {
+            'default': {
+                'ENGINE': 'django.db.backends.postgresql',
+                'NAME': 'tongquu_db',
+                'USER': 'postgres', # 或者你创建的专用用户
+                'PASSWORD': 'your_postgres_password', # 你的PostgreSQL密码
+                'HOST': 'localhost',
+                'PORT': '5432',
+            }
+        }
+        ```
+
+5.  **配置自定义用户模型 (如果尚未完成):**
+    *   在 `accounts/models.py` 中定义 `CustomUser`。
+    *   在 `tongqu_project/settings.py` 中设置 `AUTH_USER_MODEL = 'accounts.CustomUser'`。
+
+6.  **进行数据库迁移:**
+    ```bash
+    python manage.py makemigrations accounts
+    python manage.py migrate
+    ```
+
+7.  **创建超级用户 (用于访问Django Admin):**
+    ```bash
+    python manage.py createsuperuser
+    ```
+
+8.  **运行开发服务器:**
+    ```bash
+    python manage.py runserver
+    ```
+    服务将在 `http://127.0.0.1:8000/` 启动。
+
+## API 端点 (示例)
+
+*   **注册:** `POST /api/v1/accounts/register/`
+*   **登录 (获取Token):** `POST /api/v1/accounts/login/` (或 `POST /api/v1/accounts/token/`)
+*   **刷新Token:** `POST /api/v1/accounts/token/refresh/`
+*   **获取/更新用户资料:** `GET/PUT /api/v1/accounts/profile/` (需认证)
+
+*(更多端点将随功能开发添加)*
+
+## 如何贡献 (示例)
+
+我们欢迎各种形式的贡献!如果您对本项目感兴趣,可以通过以下方式参与:
+
+1.  **Fork** 本仓库 (如果项目公开)
+2.  创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
+3.  提交您的更改 (`git commit -m 'Add some AmazingFeature'`)
+4.  将您的更改推送到分支 (`git push origin feature/AmazingFeature`)
+5.  开启一个 **Pull Request**
+
+请确保您的代码遵循项目的编码规范,并添加必要的测试。
+
+## 许可证
+
+本项目采用 [MIT许可证](LICENSE.md)。
+
+---

+ 20 - 0
tongqu_backend_v2/.gitignore

@@ -0,0 +1,20 @@
+# Virtualenv
+venv/
+/venv/
+
+# Python bytecode and cache
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Django specific
+*.log
+db.sqlite3
+db.sqlite3-journal
+local_settings.py # 如果你打算用一个本地独有的设置文件
+
+# IDE / Editor specific (optional, but good practice)
+.vscode/
+.idea/
+*.swp
+*~

+ 0 - 0
tongqu_backend_v2/accounts/__init__.py


+ 67 - 0
tongqu_backend_v2/accounts/admin.py

@@ -0,0 +1,67 @@
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin # 重命名以避免与可能的自定义UserAdmin冲突
+from .models import CustomUser # 导入你的CustomUser模型
+
+# 如果你想为Admin中的用户创建和修改表单提供更精细的控制,可以创建自定义表单
+# from django.contrib.auth.forms import UserCreationForm, UserChangeForm
+# class CustomUserCreationForm(UserCreationForm):
+#     class Meta(UserCreationForm.Meta):
+#         model = CustomUser
+#         fields = ('email', 'phone_number', 'nickname') # 添加你想在创建时显示的字段
+
+# class CustomUserChangeForm(UserChangeForm):
+#     class Meta(UserChangeForm.Meta):
+#         model = CustomUser
+#         fields = '__all__' # 或者列出所有你想在编辑时显示的字段
+
+class CustomUserAdmin(BaseUserAdmin):
+    # 使用你 CustomUser 模型的字段来配置 Admin 界面
+
+    # 定义在列表页显示的字段 (list_display)
+    # 确保这里的字段名与你的 CustomUser 模型中的字段名一致
+    list_display = ('email', 'phone_number', 'nickname', 'is_staff', 'is_active', 'date_joined')
+
+    # 定义右侧的过滤器 (list_filter)
+    list_filter = ('is_staff', 'is_active', 'date_joined')
+
+    # 定义在编辑页的字段集 (fieldsets)
+    # 这是编辑现有用户时显示的表单布局
+    # 注意:BaseUserAdmin 的 fieldsets 是为包含 username 的 User 模型设计的
+    # 我们需要根据我们的 CustomUser 模型调整
+    fieldsets = (
+        (None, {'fields': ('email', 'password')}), # Django Admin 会处理密码的显示和哈希
+        ('个人信息', {'fields': ('phone_number', 'nickname', 'avatar', 'bio', 'school')}),
+        ('权限', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
+        ('重要日期', {'fields': ('last_login', 'date_joined')}),
+    )
+
+    # 定义在添加用户页的字段集 (add_fieldsets)
+    # 这是点击 "Add user" 时显示的表单布局
+    add_fieldsets = (
+        (None, {
+            'classes': ('wide',),
+            # USERNAME_FIELD 是 'email'
+            # REQUIRED_FIELDS 是 ['phone_number', 'nickname']
+            # UserCreationForm 通常处理 password 和 password2 (密码确认)
+            'fields': ('email', 'phone_number', 'nickname', 'password', 'password2'),
+        }),
+        # 你也可以在这里添加其他可选字段的分组,例如:
+        # ('可选个人信息', {'fields': ('avatar', 'bio', 'school')}),
+        # ('可选权限', {'fields': ('is_active', 'is_staff', 'is_superuser')}),
+    )
+
+    # 定义可搜索的字段 (search_fields)
+    search_fields = ('email', 'phone_number', 'nickname')
+
+    # 定义默认排序 (ordering)
+    ordering = ('email',)
+
+    # 对于多对多字段 (如 groups, user_permissions),使用 filter_horizontal 可以在Admin中提供更好的选择界面
+    filter_horizontal = ('groups', 'user_permissions',)
+
+    # 如果你创建了自定义的表单 (CustomUserCreationForm, CustomUserChangeForm),可以在这里指定
+    # add_form = CustomUserCreationForm
+    # form = CustomUserChangeForm
+
+# 注册你的 CustomUser 模型和自定义的 Admin 类
+admin.site.register(CustomUser, CustomUserAdmin)

+ 6 - 0
tongqu_backend_v2/accounts/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "accounts"

+ 40 - 0
tongqu_backend_v2/accounts/migrations/0001_initial.py

@@ -0,0 +1,40 @@
+# Generated by Django 5.2.1 on 2025-06-05 18:29
+
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('auth', '0012_alter_user_first_name_max_length'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CustomUser',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('password', models.CharField(max_length=128, verbose_name='password')),
+                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+                ('email', models.EmailField(max_length=254, unique=True, verbose_name='邮箱地址')),
+                ('phone_number', models.CharField(max_length=20, unique=True, verbose_name='手机号')),
+                ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
+                ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/', verbose_name='头像')),
+                ('bio', models.TextField(blank=True, verbose_name='简介')),
+                ('school', models.CharField(blank=True, max_length=100, verbose_name='学校')),
+                ('is_staff', models.BooleanField(default=False, help_text='指明用户是否可以登录到管理站点。', verbose_name='职员状态')),
+                ('is_active', models.BooleanField(default=True, help_text='指明用户是否被认为是活跃的。取消选择此项而不是删除账户。', verbose_name='激活状态')),
+                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='注册日期')),
+                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+            ],
+            options={
+                'verbose_name': '用户',
+                'verbose_name_plural': '用户们',
+            },
+        ),
+    ]

+ 0 - 0
tongqu_backend_v2/accounts/migrations/__init__.py


+ 90 - 0
tongqu_backend_v2/accounts/models.py

@@ -0,0 +1,90 @@
+from django.db import models
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+
+class CustomUserManager(BaseUserManager):
+    """
+    自定义用户模型的管理器。
+    """
+    def create_user(self, email, phone_number, password=None, **extra_fields):
+        """
+        创建并保存一个具有给定邮箱、手机号和密码的用户。
+        """
+        if not email:
+            raise ValueError(_('用户必须有一个邮箱地址'))
+        if not phone_number:
+            raise ValueError(_('用户必须有一个手机号'))
+
+        email = self.normalize_email(email)
+        # 确保普通用户创建时这些字段为False或默认值
+        extra_fields.setdefault('is_staff', False)
+        extra_fields.setdefault('is_superuser', False)
+        extra_fields.setdefault('is_active', True) # 普通用户默认激活
+
+        user = self.model(email=email, phone_number=phone_number, **extra_fields)
+        user.set_password(password) # 处理密码哈希
+        user.save(using=self._db)
+        return user
+
+    def create_superuser(self, email, phone_number, password=None, **extra_fields):
+        """
+        创建并保存一个具有给定邮箱、手机号和密码的超级用户。
+        """
+        extra_fields.setdefault('is_staff', True)
+        extra_fields.setdefault('is_superuser', True)
+        extra_fields.setdefault('is_active', True)
+
+        if extra_fields.get('is_staff') is not True:
+            raise ValueError(_('超级用户必须将 is_staff 设置为 True.'))
+        if extra_fields.get('is_superuser') is not True:
+            raise ValueError(_('超级用户必须将 is_superuser 设置为 True.'))
+        
+        # 对于 create_superuser,我们直接调用模型的构造函数并设置密码,
+        # 因为我们已经明确设置了 is_staff 和 is_superuser。
+        # 调用 self.create_user 可能会覆盖这些值,除非 create_user 逻辑特别处理。
+        if not email: # 再次检查,虽然 create_user 也会检查
+            raise ValueError(_('超级用户必须有一个邮箱地址'))
+        if not phone_number:
+            raise ValueError(_('超级用户必须有一个手机号'))
+
+        email = self.normalize_email(email)
+        user = self.model(email=email, phone_number=phone_number, **extra_fields)
+        user.set_password(password)
+        user.save(using=self._db)
+        return user
+
+class CustomUser(AbstractBaseUser, PermissionsMixin):
+    email = models.EmailField(_('邮箱地址'), unique=True)
+    phone_number = models.CharField(_('手机号'), max_length=20, unique=True)
+    nickname = models.CharField(_('昵称'), max_length=100, blank=True)
+    avatar = models.ImageField(_('头像'), upload_to='avatars/', null=True, blank=True) # 需要 Pillow 包
+    bio = models.TextField(_('简介'), blank=True)
+    school = models.CharField(_('学校'), max_length=100, blank=True)
+
+    is_staff = models.BooleanField(
+        _('职员状态'),
+        default=False,
+        help_text=_('指明用户是否可以登录到管理站点。'),
+    )
+    is_active = models.BooleanField(
+        _('激活状态'),
+        default=True,
+        help_text=_(
+            '指明用户是否被认为是活跃的。'
+            '取消选择此项而不是删除账户。'
+        ),
+    )
+    date_joined = models.DateTimeField(_('注册日期'), default=timezone.now)
+
+    USERNAME_FIELD = 'email'
+    REQUIRED_FIELDS = ['phone_number', 'nickname'] # 创建超级用户时需要的额外字段
+
+    objects = CustomUserManager() # 关联自定义的 Manager
+
+    def __str__(self):
+        return self.email
+
+    class Meta:
+        verbose_name = _('用户')
+        verbose_name_plural = _('用户们')

+ 79 - 0
tongqu_backend_v2/accounts/serializers.py

@@ -0,0 +1,79 @@
+from rest_framework import serializers
+from .models import CustomUser # 导入你的 CustomUser 模型
+from django.contrib.auth.password_validation import validate_password # Django 自带密码验证
+# from django.core.exceptions import ValidationError # 如果需要在 validate 方法中手动抛出
+
+class UserRegistrationSerializer(serializers.ModelSerializer):
+    password = serializers.CharField(
+        write_only=True,
+        required=True,
+        validators=[validate_password],
+        style={'input_type': 'password'} # 在DRF的可浏览API中显示为密码输入框
+    )
+    password2 = serializers.CharField(
+        write_only=True,
+        required=True,
+        label="确认密码", # 在DRF的可浏览API中的标签
+        style={'input_type': 'password'}
+    )
+    # 如果avatar是注册时可选上传的,确保在Meta中处理
+    avatar = serializers.ImageField(required=False, allow_null=True, max_length=None, use_url=True)
+
+
+    class Meta:
+        model = CustomUser
+        # 列出注册时需要用户提供的字段
+        # 确保这些字段与 CustomUser 模型中定义的一致,特别是 USERNAME_FIELD 和 REQUIRED_FIELDS
+        fields = ('email', 'phone_number', 'nickname', 'password', 'password2', 'school', 'bio', 'avatar')
+        extra_kwargs = {
+            # password 和 password2 已经在上面定义了 write_only=True
+            'nickname': {'required': True}, # 根据你的 CustomUser.REQUIRED_FIELDS,nickname是必填
+            'school': {'required': False, 'allow_blank': True},
+            'bio': {'required': False, 'allow_blank': True},
+            # 'avatar': {'required': False} # 已经在上面字段定义中处理
+        }
+
+    def validate(self, attrs):
+        if attrs['password'] != attrs['password2']:
+            raise serializers.ValidationError({"password2": "两次输入的密码不匹配。"}) # 将错误关联到password2字段
+        return attrs
+
+    def create(self, validated_data):
+        validated_data.pop('password2') # 移除 password2,它不需要保存到模型
+        
+        # 使用 CustomUser 模型的 create_user 方法创建用户
+        # 它会处理密码哈希
+        # validated_data 现在包含了 'email', 'phone_number', 'password', 'nickname' 以及可选的 'school', 'bio', 'avatar'
+        user = CustomUser.objects.create_user(**validated_data)
+        return user
+
+class UserProfileSerializer(serializers.ModelSerializer):
+    """
+    用于获取和更新用户个人资料的序列化器。
+    """
+    avatar_url = serializers.SerializerMethodField() # 用于获取完整的头像URL
+
+    class Meta:
+        model = CustomUser
+        # 列出用户可以查看和修改的字段
+        # 不应包含敏感信息如 password (除非是 write_only 用于修改密码的特殊场景)
+        fields = (
+            'id', 'email', 'phone_number', 'nickname', 
+            'avatar', 'avatar_url', 'bio', 'school', 
+            'date_joined', 'last_login', 'is_active' # is_active 可以考虑是否允许用户自己修改
+        )
+        # 定义只读字段 (在更新时不能被修改,或者需要特殊流程)
+        read_only_fields = ('id', 'email', 'phone_number', 'date_joined', 'last_login', 'avatar_url')
+        # avatar 字段本身可以用于上传,所以不是read_only,但 avatar_url 是只读的。
+        # email 和 phone_number 通常不应通过这个接口随意更改,如果允许更改,需要额外的验证流程(比如发送验证码)。
+
+    def get_avatar_url(self, obj):
+        request = self.context.get('request')
+        if obj.avatar and hasattr(obj.avatar, 'url'):
+            if request is not None:
+                return request.build_absolute_uri(obj.avatar.url)
+            return obj.avatar.url # Fallback if request is not available
+        return None # 如果没有头像或无法获取URL
+
+    # 如果允许通过这个Serializer更新头像,不需要特别做什么,DRF的ModelSerializer会自动处理ImageField
+    # 确保在视图中传递 request 到 serializer context,以便生成完整的URL

+ 3 - 0
tongqu_backend_v2/accounts/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 25 - 0
tongqu_backend_v2/accounts/urls.py

@@ -0,0 +1,25 @@
+from django.urls import path
+from .views import UserRegistrationView, UserProfileView # 导入我们刚创建的视图
+from rest_framework_simplejwt.views import (
+    TokenObtainPairView,    # SimpleJWT 自带的视图,用于登录并获取token对
+    TokenRefreshView,       # SimpleJWT 自带的视图,用于刷新access token
+)
+
+app_name = 'accounts' # 为这个应用的URL模式设置一个命名空间 (可选,但推荐)
+
+urlpatterns = [
+    # 用户注册
+    path('register/', UserRegistrationView.as_view(), name='user_register'),
+    
+    # 用户登录 (使用SimpleJWT提供的视图)
+    # 访问此URL并POST email和password,会返回access和refresh tokens
+    path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+    
+    # 刷新Access Token (使用SimpleJWT提供的视图)
+    # 访问此URL并POST refresh token,会返回新的access token
+    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
+    
+    # 用户个人资料 (获取和更新)
+    # 需要认证 (在视图中已设置 IsAuthenticated)
+    path('profile/', UserProfileView.as_view(), name='user_profile'),
+]

+ 67 - 0
tongqu_backend_v2/accounts/views.py

@@ -0,0 +1,67 @@
+from rest_framework import generics, status, permissions
+from rest_framework.response import Response
+from rest_framework_simplejwt.tokens import RefreshToken # 用于登录时或注册后手动生成token
+
+# 从当前应用的 models.py 和 serializers.py 导入
+from .models import CustomUser
+from .serializers import UserRegistrationSerializer, UserProfileSerializer
+
+class UserRegistrationView(generics.CreateAPIView):
+    """
+    用户注册视图。
+    允许任何人 (未认证用户) 访问此端点进行注册。
+    """
+    queryset = CustomUser.objects.all() # CreateAPIView 需要 queryset,即使我们不直接用它来过滤
+    serializer_class = UserRegistrationSerializer
+    permission_classes = [permissions.AllowAny] # 明确允许任何人访问此视图
+
+    def create(self, request, *args, **kwargs):
+        serializer = self.get_serializer(data=request.data)
+        serializer.is_valid(raise_exception=True) # 如果数据无效,DRF会自动返回400错误和详细信息
+        user = serializer.save() # 调用 serializer 的 create 方法,创建用户
+
+        # 注册成功后,为用户生成JWT令牌 (实现注册即登录)
+        refresh = RefreshToken.for_user(user)
+
+        # 构建响应数据
+        # 我们使用 UserProfileSerializer 来序列化新创建的 user 对象,以便返回更完整的用户信息
+        # 需要传递 request 到 context 以便 UserProfileSerializer 中的 get_avatar_url 能构建完整URL
+        user_data = UserProfileSerializer(user, context={'request': request}).data
+        
+        response_data = {
+            'refresh': str(refresh),
+            'access': str(refresh.access_token),
+            'user': user_data
+        }
+        return Response(response_data, status=status.HTTP_201_CREATED)
+
+
+class UserProfileView(generics.RetrieveUpdateAPIView):
+    """
+    获取和更新当前已认证用户的个人资料视图。
+    只允许已认证的用户访问他们自己的个人资料。
+    """
+    queryset = CustomUser.objects.all() # RetrieveUpdateAPIView 也需要 queryset
+    serializer_class = UserProfileSerializer
+    permission_classes = [permissions.IsAuthenticated] # 明确只有认证用户才能访问
+
+    def get_object(self):
+        # 此方法返回视图将要操作的对象实例
+        # 在这里,我们返回当前发出请求的认证用户
+        return self.request.user
+
+    # 当收到 PUT 或 PATCH 请求时,DRF的 RetrieveUpdateAPIView 会自动调用
+    # serializer_class 的 update() 或 partial_update() 方法。
+    # 我们在 UserProfileSerializer 中没有定义 update 方法,所以它会使用
+    # ModelSerializer 的默认 update 行为,这对于 ImageField (如 avatar) 通常是有效的。
+    # 如果需要更复杂的更新逻辑(例如,修改密码、验证邮箱/手机更改),则需要重写 update 方法。
+
+    # 为了让 UserProfileSerializer 中的 get_avatar_url 能正确工作,
+    # 我们需要确保 serializer 在实例化时能获取到 request 对象。
+    # RetrieveUpdateAPIView 会自动将 request 传递给 serializer context。
+    # def get_serializer_context(self):
+    #     context = super().get_serializer_context()
+    #     context.update({"request": self.request})
+    #     return context
+    # 上面的 get_serializer_context 是DRF 3.9版本之后默认会做的,所以通常不需要显式重写。
+    # 但如果头像URL不正确,可以尝试取消注释上面这段代码。

+ 0 - 0
tongqu_backend_v2/config/__init__.py


+ 16 - 0
tongqu_backend_v2/config/asgi.py

@@ -0,0 +1,16 @@
+"""
+ASGI config for config project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+
+application = get_asgi_application()

+ 200 - 0
tongqu_backend_v2/config/settings.py

@@ -0,0 +1,200 @@
+"""
+Django settings for config project.
+
+Generated by 'django-admin startproject' using Django 5.2.1.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+import os
+from pathlib import Path # Path 通常也在顶部
+from pathlib import Path
+from datetime import timedelta # 确保这行在 settings.py 文件顶部
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-12o%i9mzoyn8ua+j#l!8fed0-4d&y$x3%vtk00y82@97f9702+"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+    'rest_framework_simplejwt',
+    'accounts',  # <--- 确保是这个,并且没有拼写错误
+]
+
+MIDDLEWARE = [
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "config.urls"
+
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = "config.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'NAME': 'tongqu_v2_db',         # 你创建的数据库名
+        'USER': 'postgres',             # 你的PostgreSQL用户名 (如果是默认超级用户)
+                                        # 或者 'tongqu_v2_user' (如果你创建了专用用户)
+        'PASSWORD': '20030316', # 你的PostgreSQL密码
+        'HOST': 'localhost',            # 或者 '127.0.0.1'
+        'PORT': '5432',                 # PostgreSQL默认端口
+    }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
+
+LANGUAGE_CODE = 'zh-hans'
+
+TIME_ZONE = 'Asia/Shanghai'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Django 在开发时查找静态文件的目录列表
+STATICFILES_DIRS = [
+    os.path.join(BASE_DIR, 'static'), # 我们将在项目根目录创建这个 'static' 文件夹
+]
+
+# `collectstatic` 命令收集静态文件到这个目录,主要用于生产环境
+STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles_collected')
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles_collected') # 运行 collectstatic 时文件的存放位置
+STATICFILES_DIRS = [
+    os.path.join(BASE_DIR, 'static'), # 项目级static文件夹
+]
+MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+AUTH_USER_MODEL = 'accounts.CustomUser'
+from datetime import timedelta # 确保在文件顶部导入
+
+REST_FRAMEWORK = {
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework_simplejwt.authentication.JWTAuthentication', # 使用JWT进行API认证
+        # 如果你希望在浏览器中测试API时也能使用Django的session认证(比如DRF的可浏览API),可以取消下面这行的注释
+        # 'rest_framework.authentication.SessionAuthentication',
+    ),
+    'DEFAULT_PERMISSION_CLASSES': (
+        'rest_framework.permissions.IsAuthenticated', # 默认情况下,所有API端点都需要用户进行身份验证
+                                                      # 除非在视图中明确设置了 permission_classes = [permissions.AllowAny] (例如注册和登录接口)
+    ),
+    # 你可以根据需要添加其他全局DRF设置,例如:
+    # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+    # 'PAGE_SIZE': 10, # 每页返回10条数据
+    # 'DEFAULT_RENDERER_CLASSES': ( # API的默认输出格式
+    #     'rest_framework.renderers.JSONRenderer',
+    #     # 'rest_framework.renderers.BrowsableAPIRenderer', # 如果你想在浏览器中看到可浏览的API界面 (通常DEBUG=True时自动启用)
+    # ),
+    # 'DEFAULT_PARSER_CLASSES': ( # API接受的输入格式
+    #     'rest_framework.parsers.JSONParser',
+    #     'rest_framework.parsers.FormParser',
+    #     'rest_framework.parsers.MultiPartParser', # 支持文件上传
+    # )
+}
+SIMPLE_JWT = {
+    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),     # Access Token 的有效期,例如 60 分钟
+    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),        # Refresh Token 的有效期,例如 1 天
+    'ROTATE_REFRESH_TOKENS': False,                     # 如果为True, 每次刷新token时,旧的refresh token会失效,返回一个新的
+    'BLACKLIST_AFTER_ROTATION': True,                   # 和上面配合使用
+    'UPDATE_LAST_LOGIN': True,                          # 成功获取token后是否更新用户的 last_login 字段
+
+    'ALGORITHM': 'HS256',
+    'SIGNING_KEY': SECRET_KEY, # 使用Django的 SECRET_KEY 来签名JWT
+    'VERIFYING_KEY': None,
+    'AUDIENCE': None,
+    'ISSUER': None,
+    'JWK_URL': None,
+    'LEEWAY': 0,
+
+    'AUTH_HEADER_TYPES': ('Bearer',), # 期望在Authorization头中看到的类型,例如 "Bearer <token>"
+    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
+    'USER_ID_FIELD': 'id',                  # 你的 CustomUser 模型中用作主键的字段名
+    'USER_ID_CLAIM': 'user_id',             # JWT payload 中代表用户ID的claim名称
+
+    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+    'TOKEN_TYPE_CLAIM': 'token_type',
+    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
+
+    'JTI_CLAIM': 'jti',
+
+    # 如果你想自定义token payload中包含的内容,可以使用下面这个(需要写自定义的Token序列化器)
+    # 'TOKEN_OBTAIN_SERIALIZER': 'your_app.serializers.MyTokenObtainPairSerializer',
+}

+ 16 - 0
tongqu_backend_v2/config/urls.py

@@ -0,0 +1,16 @@
+from django.contrib import admin
+from django.urls import path, include # 确保 include 已被导入
+from django.conf import settings      # 导入settings
+from django.conf.urls.static import static # 导入static
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    # 新增下面这行,将所有以 'api/v1/accounts/' 开头的URL路由到 accounts 应用的 urls.py 文件
+    path('api/v1/accounts/', include('accounts.urls')),
+    # 你未来可以添加其他应用的API路由,例如:
+    # path('api/v1/posts/', include('posts.urls')),
+]
+
+# 在开发环境下,添加处理媒体文件URL的路由
+if settings.DEBUG:
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 16 - 0
tongqu_backend_v2/config/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for config project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+
+application = get_wsgi_application()

+ 22 - 0
tongqu_backend_v2/manage.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+    main()

binární
tongqu_backend_v2/media/avatars/QQ图片20221017114231.jpg


binární
tongqu_backend_v2/media/avatars/微信图片_20250606023911.jpg


binární
tongqu_backend_v2/media/avatars/微信图片_20250606023911_ZuYnlRl.jpg


binární
tongqu_backend_v2/requirements.txt


+ 1007 - 0
同趣U.html

@@ -0,0 +1,1007 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>大学生兴趣伙伴 App - 页面预览</title>
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+            margin: 0;
+            padding: 0;
+            background-color: #f0f2f5;
+            display: flex;
+            justify-content: center;
+            align-items: flex-start; /* Align items to the top */
+            padding-top: 20px; /* Add some top padding */
+            padding-bottom: 100px; /* Add padding to see all content */
+        }
+
+        .phone-mockup {
+            width: 375px; /* Typical phone width */
+            height: 750px; /* Typical phone height */
+            border: 10px solid #333;
+            border-radius: 40px;
+            background-color: #fff;
+            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+            position: relative; /* Needed for absolute positioning of header/footer */
+            overflow: hidden; /* Hide content that overflows */
+            display: flex;
+            flex-direction: column;
+        }
+
+        .app-header {
+            background-color: #007bff;
+            color: white;
+            padding: 15px;
+            text-align: center;
+            font-weight: bold;
+            font-size: 1.1em;
+            flex-shrink: 0; /* Prevent header from shrinking */
+            position: sticky; /* Keep header visible when scrolling content */
+            top: 0;
+            z-index: 10;
+            display: flex;
+            align-items: center;
+            justify-content: space-between; /* Align items */
+        }
+        .app-header .icon, .app-header .back-button {
+            color: white;
+            text-decoration: none;
+            font-size: 1.2em;
+            padding: 0 10px;
+        }
+         .app-header .back-button {
+            font-size: 1.5em; /* Make back arrow slightly larger */
+         }
+
+
+        .app-content {
+            flex-grow: 1; /* Allow content to fill available space */
+            overflow-y: auto; /* Enable scrolling for content */
+            padding: 15px;
+        }
+
+        .app-footer {
+            display: flex;
+            justify-content: space-around;
+            padding: 10px 0;
+            border-top: 1px solid #eee;
+            background-color: #fff;
+            flex-shrink: 0; /* Prevent footer from shrinking */
+            position: sticky; /* Keep footer visible */
+            bottom: 0;
+            z-index: 10;
+        }
+
+        .tab-item {
+            text-align: center;
+            color: #888;
+            text-decoration: none;
+            font-size: 0.8em;
+        }
+        .tab-item.active {
+            color: #007bff;
+        }
+        .tab-item span { /* Icon placeholder */
+            display: block;
+            font-size: 1.5em;
+            margin-bottom: 3px;
+        }
+
+        /* Basic Element Styles */
+        h2, h3 { margin-top: 10px; margin-bottom: 10px; color: #333; }
+        p { color: #555; line-height: 1.5; margin-bottom: 10px; }
+        button, .button-link {
+            display: inline-block;
+            padding: 10px 15px;
+            background-color: #007bff;
+            color: white;
+            border: none;
+            border-radius: 20px;
+            cursor: pointer;
+            text-align: center;
+            font-size: 0.9em;
+            margin: 5px 0;
+            text-decoration: none;
+        }
+        button:hover, .button-link:hover { background-color: #0056b3; }
+        button.secondary, .button-link.secondary { background-color: #6c757d; }
+        button.secondary:hover, .button-link.secondary:hover { background-color: #5a6268; }
+        button.outline, .button-link.outline { background-color: transparent; border: 1px solid #007bff; color: #007bff; }
+        button.outline:hover, .button-link.outline:hover { background-color: rgba(0, 123, 255, 0.1); }
+
+
+        input[type="text"], input[type="password"], input[type="email"], textarea, select {
+            width: calc(100% - 22px); /* Adjust for padding and border */
+            padding: 10px;
+            margin-bottom: 10px;
+            border: 1px solid #ccc;
+            border-radius: 5px;
+            font-size: 1em;
+        }
+        textarea { height: 80px; resize: vertical; }
+
+        /* Component Styles */
+        .card {
+            background-color: #fff;
+            border: 1px solid #eee;
+            border-radius: 8px;
+            padding: 15px;
+            margin-bottom: 15px;
+            box-shadow: 0 2px 5px rgba(0,0,0,0.05);
+        }
+        .user-card, .message-item, .group-item, .event-item, .notification-item, .skill-item {
+            display: flex;
+            align-items: center;
+            margin-bottom: 15px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid #f0f0f0;
+        }
+        .user-card:last-child, .message-item:last-child, .group-item:last-child, .event-item:last-child, .notification-item:last-child, .skill-item:last-child {
+             border-bottom: none;
+             margin-bottom: 0;
+             padding-bottom: 0;
+        }
+        .avatar {
+            width: 50px;
+            height: 50px;
+            border-radius: 50%;
+            background-color: #e0e0e0;
+            margin-right: 15px;
+            object-fit: cover; /* Make images fit well */
+            flex-shrink: 0;
+        }
+        .content-details { flex-grow: 1; }
+        .content-details h3 { margin: 0 0 5px 0; font-size: 1em; }
+        .content-details p { margin: 0; font-size: 0.9em; color: #777; }
+        .user-card .actions { margin-left: auto; }
+        .interest-tag {
+            display: inline-block;
+            background-color: #e7f3ff;
+            color: #007bff;
+            padding: 3px 8px;
+            border-radius: 15px;
+            font-size: 0.8em;
+            margin: 2px;
+            border: 1px solid #cce5ff;
+        }
+         .interest-category h3 { font-size: 1em; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px;}
+         .interest-category .tags { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 15px;}
+         .interest-category .tag-button {
+             background-color: #f0f0f0;
+             border: 1px solid #ddd;
+             color: #555;
+             padding: 5px 10px;
+             border-radius: 15px;
+             font-size: 0.9em;
+             cursor: pointer;
+         }
+          .interest-category .tag-button.selected {
+             background-color: #007bff;
+             color: white;
+             border-color: #007bff;
+          }
+
+        .chat-bubble {
+            max-width: 75%;
+            padding: 10px 15px;
+            border-radius: 18px;
+            margin-bottom: 10px;
+            line-height: 1.4;
+            word-wrap: break-word;
+        }
+        .chat-bubble.sent {
+            background-color: #007bff;
+            color: white;
+            margin-left: auto;
+            border-bottom-right-radius: 5px;
+        }
+        .chat-bubble.received {
+            background-color: #e9ecef;
+            color: #333;
+            margin-right: auto;
+            border-bottom-left-radius: 5px;
+        }
+        .chat-input {
+            display: flex;
+            padding: 10px;
+            border-top: 1px solid #eee;
+            background: #f8f9fa;
+        }
+        .chat-input input { flex-grow: 1; margin-right: 10px; margin-bottom: 0; }
+
+        .profile-header { text-align: center; margin-bottom: 20px; }
+        .profile-header .avatar { width: 100px; height: 100px; margin: 0 auto 10px auto; }
+        .profile-header h2 { margin: 5px 0; }
+        .profile-header p { color: #666; font-size: 0.9em; }
+
+        .section-title { font-weight: bold; margin-top: 20px; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px;}
+
+        .map-placeholder {
+            width: 100%;
+            height: 300px;
+            background-color: #e0e0e0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            color: #999;
+            font-style: italic;
+            text-align: center;
+            border-radius: 8px;
+            border: 1px dashed #ccc;
+            position: relative;
+        }
+        .map-placeholder::after { /* Simple pin example */
+            content: '📍';
+            font-size: 2em;
+            position: absolute;
+            top: 40%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+        }
+         .map-placeholder p { padding: 0 20px;}
+
+        /* Hide all pages initially */
+        .app-page { display: none; }
+        /* Show the default page (Home) */
+        #page-home { display: block; }
+
+    </style>
+</head>
+<body>
+
+    <div class="phone-mockup">
+        <!-- Dynamic Header will be controlled by JS in a real app -->
+        <!-- Placeholder Header -->
+        <div class="app-header" id="app-header">
+             <!-- Content changes based on active page -->
+        </div>
+
+        <!-- Content Area: Contains all pages -->
+        <div class="app-content">
+
+            <!-- PAGE: Home / Feed -->
+            <div class="app-page" id="page-home">
+                <script>document.getElementById('app-header').innerHTML = '<span>兴趣伙伴</span><a href="#page-notifications" class="icon">🔔</a>';</script>
+                <h2>为你推荐</h2>
+                <div class="card user-card">
+                    <img src="https://via.placeholder.com/50/FFA500/FFFFFF?text=U1" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>张伟 <span style="font-size: 0.8em; color: #777;">(北京大学)</span></h3>
+                        <p>共同兴趣: <span class="interest-tag">摄影</span> <span class="interest-tag">爬山</span></p>
+                        <p style="font-size: 0.8em; color: #999;">"周末想找人一起去奥森拍照~"</p>
+                    </div>
+                    <div class="actions">
+                        <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">打招呼</a>
+                    </div>
+                </div>
+                 <div class="card user-card">
+                    <img src="https://via.placeholder.com/50/4682B4/FFFFFF?text=U2" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>李静 <span style="font-size: 0.8em; color: #777;">(清华大学)</span></h3>
+                        <p>共同兴趣: <span class="interest-tag">日语学习</span> <span class="interest-tag">看动漫</span></p>
+                         <p style="font-size: 0.8em; color: #999;">"寻找N1备考伙伴,互相监督!"</p>
+                    </div>
+                    <div class="actions">
+                        <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">打招呼</a>
+                    </div>
+                </div>
+                <div class="card user-card">
+                    <img src="https://via.placeholder.com/50/32CD32/FFFFFF?text=U3" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>王磊 <span style="font-size: 0.8em; color: #777;">(人民大学)</span></h3>
+                        <p>共同兴趣: <span class="interest-tag">篮球</span> <span class="interest-tag">说唱音乐</span></p>
+                         <p style="font-size: 0.8em; color: #999;">"下午东操场3v3,来不来?"</p>
+                    </div>
+                     <div class="actions">
+                        <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">打招呼</a>
+                    </div>
+                </div>
+                 <h2>附近的活动</h2>
+                 <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/FF6347/FFFFFF?text=E" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>周末狼人杀局</h3>
+                        <p>时间: 周六 14:00</p>
+                        <p>地点: 学校咖啡厅</p>
+                    </div>
+                    <div class="actions">
+                        <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">查看详情</a>
+                    </div>
+                </div>
+            </div>
+
+            <!-- PAGE: Discover / Search -->
+            <div class="app-page" id="page-discover">
+                 <script>document.getElementById('app-header').innerHTML = '<span>发现</span>';</script>
+                 <input type="text" placeholder="搜索用户、兴趣、小组、活动...">
+                 <button style="width: 100%; margin-bottom: 15px;">搜索</button>
+
+                 <h3>按兴趣浏览</h3>
+                 <div>
+                     <span class="interest-tag">摄影</span>
+                     <span class="interest-tag">篮球</span>
+                     <span class="interest-tag">吉他</span>
+                     <span class="interest-tag">Python</span>
+                     <span class="interest-tag">日语学习</span>
+                     <span class="interest-tag">电影</span>
+                     <span class="interest-tag">爬山</span>
+                     <span class="interest-tag">桌游</span>
+                     <a href="#page-interests" style="font-size: 0.9em; color: #007bff;">更多...</a>
+                 </div>
+
+                 <h3 style="margin-top: 20px;">推荐用户</h3>
+                 <!-- User cards like on home page -->
+                 <div class="card user-card">
+                    <img src="https://via.placeholder.com/50/8A2BE2/FFFFFF?text=U4" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>赵敏 <span style="font-size: 0.8em; color: #777;">(北京师范大学)</span></h3>
+                        <p>兴趣: <span class="interest-tag">志愿者活动</span> <span class="interest-tag">阅读</span></p>
+                    </div>
+                    <div class="actions"> <a href="#page-profile-other" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">查看资料</a> </div>
+                </div>
+
+                 <h3 style="margin-top: 20px;">热门小组</h3>
+                 <div class="card group-item">
+                     <img src="https://via.placeholder.com/50/FFD700/000000?text=G1" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                     <div class="content-details">
+                        <h3>北邮吉他社</h3>
+                        <p>128 成员 | 活跃度高</p>
+                     </div>
+                      <div class="actions"> <a href="#page-group-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">加入</a> </div>
+                 </div>
+                 <a href="#page-groups" class="button-link secondary" style="width: 100%; text-align: center; margin-top: 10px;">查看所有小组</a>
+
+                 <h3 style="margin-top: 20px;">推荐活动</h3>
+                  <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/00CED1/FFFFFF?text=E2" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>校园编程马拉松</h3>
+                        <p>时间: 下周五 9:00</p>
+                        <p>地点: 计算机学院楼</p>
+                    </div>
+                    <div class="actions"> <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">感兴趣</a> </div>
+                </div>
+                 <a href="#page-events" class="button-link secondary" style="width: 100%; text-align: center; margin-top: 10px;">查看所有活动</a>
+
+                 <h3 style="margin-top: 20px;">技能交换</h3>
+                 <div class="card skill-item">
+                     <img src="https://via.placeholder.com/50/6495ED/FFFFFF?text=S1" alt="User Avatar" class="avatar">
+                     <div class="content-details">
+                        <h3>刘洋 <span style="font-size: 0.8em; color: #777;">(北京航空航天大学)</span></h3>
+                        <p><strong>提供:</strong> PPT美化 <span style="color: green;">|</span> <strong>需要:</strong> 英语口语练习</p>
+                     </div>
+                     <div class="actions"> <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">联系Ta</a> </div>
+                 </div>
+                  <a href="#page-skills" class="button-link secondary" style="width: 100%; text-align: center; margin-top: 10px;">查看技能交换</a>
+
+                  <h3 style="margin-top: 20px;">附近的人 (地图)</h3>
+                  <a href="#page-map" class="button-link" style="width: 100%; text-align: center;">查看地图模式</a>
+
+
+            </div>
+
+            <!-- PAGE: Messages List -->
+            <div class="app-page" id="page-messages">
+                <script>document.getElementById('app-header').innerHTML = '<span>消息</span><a href="#page-chat" class="icon">+</a>';</script>
+                 <div class="message-item">
+                    <img src="https://via.placeholder.com/50/4682B4/FFFFFF?text=U2" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>李静</h3>
+                        <p>好的,那我们周二晚上图书馆见!</p>
+                    </div>
+                    <span style="font-size: 0.8em; color: #999; margin-left: auto;">10:35</span>
+                 </div>
+                 <div class="message-item">
+                    <img src="https://via.placeholder.com/50/FFA500/FFFFFF?text=U1" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>张伟</h3>
+                        <p>[图片]</p>
+                    </div>
+                    <span style="font-size: 0.8em; color: #999; margin-left: auto;">昨天</span>
+                 </div>
+                 <div class="message-item">
+                    <img src="https://via.placeholder.com/50/FFD700/000000?text=G1" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>北邮吉他社 (群)</h3>
+                        <p>@所有人 周五晚上有迎新演出!</p>
+                    </div>
+                    <span style="font-size: 0.8em; color: red; background: red; color: white; border-radius: 50%; padding: 2px 6px; margin-left: auto;">3</span>
+                 </div>
+            </div>
+
+             <!-- PAGE: Chat / Direct Message -->
+            <div class="app-page" id="page-chat">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-messages" class="back-button">‹</a><span>李静</span><a href="#" class="icon">···</a>';</script>
+                <div style="height: calc(100% - 60px); overflow-y: auto; display: flex; flex-direction: column-reverse; padding-bottom: 10px;"> <!-- Chat history area -->
+                    <!-- Messages would load here, newest at the bottom -->
+                     <div class="chat-bubble sent">好的,那我们周二晚上图书馆见!</div>
+                     <div class="chat-bubble received">太好了!我正好也在复习,可以一起。</div>
+                     <div class="chat-bubble sent">我看到你也选了高数,下周考试一起复习怎么样?地点图书馆?</div>
+                     <div class="chat-bubble received">你好,看到你也喜欢日语学习,我也是!😊</div>
+                     <div class="chat-bubble sent">[系统提示] 你们都对“日语学习”感兴趣,可以聊聊哦~</div>
+                </div>
+                 <div class="chat-input">
+                    <input type="text" placeholder="输入消息...">
+                    <button>发送</button>
+                 </div>
+            </div>
+
+
+            <!-- PAGE: User Profile (Self) -->
+            <div class="app-page" id="page-profile-self">
+                <script>document.getElementById('app-header').innerHTML = '<span>我的资料</span><a href="#page-settings" class="icon">⚙️</a>';</script>
+                <div class="profile-header">
+                    <img src="https://via.placeholder.com/100/007BFF/FFFFFF?text=ME" alt="My Avatar" class="avatar">
+                    <h2>你的昵称</h2>
+                    <p>北京邮电大学 | 计算机科学 | 大二</p>
+                    <p>"努力成为有趣的人 | Coding & Reading"</p>
+                     <a href="#page-interests" class="button-link outline" style="margin-top: 10px;">编辑兴趣标签</a>
+                </div>
+
+                <div class="section-title">我的兴趣</div>
+                <div class="card">
+                    <span class="interest-tag">Python</span>
+                    <span class="interest-tag">机器学习</span>
+                    <span class="interest-tag">科幻电影</span>
+                    <span class="interest-tag">羽毛球</span>
+                    <span class="interest-tag">独立音乐</span>
+                </div>
+
+                <div class="section-title">我的小组</div>
+                 <div class="card group-item">
+                     <img src="https://via.placeholder.com/50/20B2AA/FFFFFF?text=G2" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                     <div class="content-details">
+                        <h3>BUPT算法交流群</h3>
+                        <p>55 成员</p>
+                     </div>
+                 </div>
+                 <a href="#page-groups" class="button-link secondary" style="width: 100%; text-align: center;">查看所有小组</a>
+
+
+                <div class="section-title">我发布的活动</div>
+                <p style="text-align: center; color: #999;">暂无活动</p>
+                <a href="#page-event-post" class="button-link" style="width: 100%; text-align: center;">发布新活动</a>
+
+
+                 <div class="section-title">我的技能/需求</div>
+                  <div class="card skill-item">
+                     <img src="https://via.placeholder.com/50/007BFF/FFFFFF?text=ME" alt="User Avatar" class="avatar">
+                     <div class="content-details">
+                        <p><strong>提供:</strong> Python基础教学 <span style="color: green;">|</span> <strong>需要:</strong> 羽毛球球友</p>
+                     </div>
+                     <div class="actions"> <a href="#page-skills" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">编辑</a> </div>
+                 </div>
+                 <a href="#page-skills" class="button-link" style="width: 100%; text-align: center;">发布技能/需求</a>
+            </div>
+
+            <!-- PAGE: User Profile (Other) -->
+            <div class="app-page" id="page-profile-other">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-discover" class="back-button">‹</a><span>用户资料</span><a href="#" class="icon">···</a>';</script>
+                 <div class="profile-header">
+                    <img src="https://via.placeholder.com/100/FFA500/FFFFFF?text=U1" alt="User Avatar" class="avatar">
+                    <h2>张伟</h2>
+                    <p>北京大学 | 艺术学院 | 大三</p>
+                     <p>"周末想找人一起去奥森拍照~"</p>
+                     <a href="#page-chat" class="button-link" style="margin-top: 10px;">发消息</a>
+                     <button class="secondary">关注</button>
+                </div>
+
+                <div class="section-title">Ta的兴趣</div>
+                 <div class="card">
+                    <span class="interest-tag">摄影</span>
+                    <span class="interest-tag">后期制作</span>
+                    <span class="interest-tag">爬山</span>
+                    <span class="interest-tag">纪录片</span>
+                </div>
+
+                 <div class="section-title">共同兴趣</div>
+                  <div class="card">
+                    <span class="interest-tag">摄影</span>
+                    <span class="interest-tag">爬山</span>
+                </div>
+
+                <div class="section-title">Ta的小组</div>
+                 <p style="text-align: center; color: #999;">用户尚未加入任何小组</p>
+
+                <div class="section-title">Ta发布的活动</div>
+                 <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/FFA500/FFFFFF?text=U1" alt="Host Avatar" class="avatar">
+                    <div class="content-details">
+                        <h3>奥森公园外拍活动</h3>
+                        <p>时间: 本周六 10:00</p>
+                        <p>地点: 奥林匹克森林公园南门</p>
+                    </div>
+                     <div class="actions"> <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">查看详情</a> </div>
+                </div>
+            </div>
+
+            <!-- PAGE: Interest Selection/Editing -->
+            <div class="app-page" id="page-interests">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-profile-self" class="back-button">‹</a><span>选择我的兴趣</span><a href="#page-profile-self" class="icon" style="font-size: 0.9em;">保存</a>';</script>
+                <input type="text" placeholder="搜索兴趣标签...">
+
+                <div class="interest-category">
+                    <h3>运动</h3>
+                    <div class="tags">
+                        <button class="tag-button selected">羽毛球</button>
+                        <button class="tag-button">篮球</button>
+                        <button class="tag-button">跑步</button>
+                        <button class="tag-button">游泳</button>
+                        <button class="tag-button">健身</button>
+                        <button class="tag-button">瑜伽</button>
+                        <button class="tag-button">乒乓球</button>
+                        <button class="tag-button">爬山</button>
+                    </div>
+                </div>
+
+                 <div class="interest-category">
+                    <h3>音乐</h3>
+                    <div class="tags">
+                        <button class="tag-button selected">独立音乐</button>
+                        <button class="tag-button">摇滚</button>
+                        <button class="tag-button">流行</button>
+                        <button class="tag-button">古典</button>
+                        <button class="tag-button">吉他</button>
+                        <button class="tag-button">钢琴</button>
+                    </div>
+                </div>
+
+                 <div class="interest-category">
+                    <h3>学术/技术</h3>
+                    <div class="tags">
+                        <button class="tag-button selected">Python</button>
+                        <button class="tag-button selected">机器学习</button>
+                        <button class="tag-button">Web开发</button>
+                        <button class="tag-button">算法</button>
+                        <button class="tag-button">考研</button>
+                        <button class="tag-button">四六级</button>
+                    </div>
+                </div>
+
+                 <div class="interest-category">
+                    <h3>艺术/生活</h3>
+                    <div class="tags">
+                        <button class="tag-button selected">科幻电影</button>
+                        <button class="tag-button">摄影</button>
+                        <button class="tag-button">阅读</button>
+                        <button class="tag-button">绘画</button>
+                        <button class="tag-button">桌游</button>
+                        <button class="tag-button">美食探店</button>
+                    </div>
+                </div>
+                 <p style="color: #888; font-size: 0.9em;">找不到你的兴趣?可以尝试搜索或 <a href="#">创建新标签</a>。</p>
+            </div>
+
+             <!-- PAGE: Notifications -->
+            <div class="app-page" id="page-notifications">
+                 <script>document.getElementById('app-header').innerHTML = '<a href="#page-home" class="back-button">‹</a><span>通知</span>';</script>
+                 <div class="notification-item">
+                    <img src="https://via.placeholder.com/50/32CD32/FFFFFF?text=U3" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <p><strong>王磊</strong> 赞了你的动态。</p>
+                        <p style="font-size: 0.8em; color: #999;">5分钟前</p>
+                    </div>
+                 </div>
+                 <div class="notification-item">
+                     <img src="https://via.placeholder.com/50/FF6347/FFFFFF?text=E" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <p>你报名的活动 <strong>周末狼人杀局</strong> 将在1小时后开始。</p>
+                        <p style="font-size: 0.8em; color: #999;">15分钟前</p>
+                    </div>
+                 </div>
+                  <div class="notification-item">
+                    <img src="https://via.placeholder.com/50/8A2BE2/FFFFFF?text=U4" alt="User Avatar" class="avatar">
+                    <div class="content-details">
+                        <p><strong>赵敏</strong> 开始关注你了。</p>
+                         <p style="font-size: 0.8em; color: #999;">1小时前</p>
+                    </div>
+                      <div class="actions"> <a href="#page-profile-other" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">回关</a> </div>
+                 </div>
+                  <div class="notification-item">
+                    <img src="https://via.placeholder.com/50/FFD700/000000?text=G1" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <p><strong>北邮吉他社</strong> 有新公告。</p>
+                         <p style="font-size: 0.8em; color: #999;">3小时前</p>
+                    </div>
+                 </div>
+            </div>
+
+            <!-- PAGE: Settings -->
+            <div class="app-page" id="page-settings">
+                 <script>document.getElementById('app-header').innerHTML = '<a href="#page-profile-self" class="back-button">‹</a><span>设置</span>';</script>
+                 <ul style="list-style: none; padding: 0;">
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#page-profile-self" style="text-decoration: none; color: #333;">编辑个人资料</a></li>
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#" style="text-decoration: none; color: #333;">账号与安全</a></li>
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#" style="text-decoration: none; color: #333;">隐私设置</a></li>
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#page-notifications" style="text-decoration: none; color: #333;">通知设置</a></li>
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#" style="text-decoration: none; color: #333;">关于我们</a></li>
+                     <li style="padding: 15px 0; border-bottom: 1px solid #eee;"><a href="#" style="text-decoration: none; color: #333;">帮助与反馈</a></li>
+                 </ul>
+                 <button style="width: 100%; background-color: #dc3545; margin-top: 30px;">退出登录</button>
+            </div>
+
+             <!-- PAGE: Group List -->
+            <div class="app-page" id="page-groups">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-profile-self" class="back-button">‹</a><span>我的小组</span><a href="#" class="icon">+</a>';</script>
+                <!-- Add Tabs for My Groups / Discover Groups -->
+                 <div class="card group-item">
+                     <img src="https://via.placeholder.com/50/20B2AA/FFFFFF?text=G2" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                     <div class="content-details">
+                        <h3>BUPT算法交流群</h3>
+                        <p>55 成员</p>
+                     </div>
+                     <div class="actions"> <a href="#page-group-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">进入</a> </div>
+                 </div>
+                 <div class="card group-item">
+                     <img src="https://via.placeholder.com/50/DAA520/FFFFFF?text=G3" alt="Group Icon" class="avatar" style="border-radius: 8px;">
+                     <div class="content-details">
+                        <h3>电影爱好者协会</h3>
+                        <p>210 成员</p>
+                     </div>
+                     <div class="actions"> <a href="#page-group-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">进入</a> </div>
+                 </div>
+                 <p style="text-align: center; margin-top: 20px;"><a href="#page-discover" style="color: #007bff;">发现更多小组...</a></p>
+            </div>
+
+             <!-- PAGE: Group Detail -->
+            <div class="app-page" id="page-group-detail">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-groups" class="back-button">‹</a><span>BUPT算法交流群</span><a href="#" class="icon">···</a>';</script>
+                 <div style="text-align: center; margin-bottom: 15px;">
+                    <img src="https://via.placeholder.com/80/20B2AA/FFFFFF?text=G2" alt="Group Icon" class="avatar" style="width: 80px; height: 80px; border-radius: 15px; margin: 0 auto 10px auto;">
+                    <h2>BUPT算法交流群</h2>
+                    <p>55 成员 | <a href="#">查看成员</a></p>
+                    <p style="font-size: 0.9em; color: #666;">欢迎对算法、数据结构、ACM竞赛感兴趣的同学加入讨论!</p>
+                    <button class="secondary">已加入</button>
+                 </div>
+                 <hr>
+                 <h3>小组动态</h3>
+                  <div class="card"> <!-- Post within group -->
+                     <div class="user-card" style="border-bottom: none; margin-bottom: 10px; padding-bottom: 0;">
+                         <img src="https://via.placeholder.com/40/CD5C5C/FFFFFF?text=U5" alt="User Avatar" class="avatar" style="width: 40px; height: 40px;">
+                         <div class="content-details">
+                             <h3 style="font-size: 0.9em;">陈晨</h3>
+                             <p style="font-size: 0.8em;">5小时前</p>
+                         </div>
+                     </div>
+                     <p>LeetCode今天这道Hard题有人AC了吗?求思路分享!#每日一题</p>
+                     <div style="font-size: 0.8em; color: #888;">👍 5    💬 2</div>
+                 </div>
+                  <div class="card"> <!-- Post within group -->
+                     <div class="user-card" style="border-bottom: none; margin-bottom: 10px; padding-bottom: 0;">
+                         <img src="https://via.placeholder.com/40/4682B4/FFFFFF?text=U2" alt="User Avatar" class="avatar" style="width: 40px; height: 40px;">
+                         <div class="content-details">
+                             <h3 style="font-size: 0.9em;">李静 (管理员)</h3>
+                             <p style="font-size: 0.8em;">昨天</p>
+                         </div>
+                     </div>
+                     <p>【公告】下周三晚上7点,邀请学长分享蓝桥杯备赛经验,地点教三201,欢迎参加!</p>
+                     <div style="font-size: 0.8em; color: #888;">👍 15    💬 1</div>
+                 </div>
+                 <button style="width: 100%; margin-top: 15px;">发布新动态</button>
+            </div>
+
+             <!-- PAGE: Event Listing -->
+            <div class="app-page" id="page-events">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-discover" class="back-button">‹</a><span>活动广场</span><a href="#page-event-post" class="icon">+</a>';</script>
+                <!-- Add Tabs for Upcoming / My Events / Discover -->
+                <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/FF6347/FFFFFF?text=E" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>周末狼人杀局</h3>
+                        <p>时间: 周六 14:00</p>
+                        <p>地点: 学校咖啡厅 | 发起人: 王磊</p>
+                    </div>
+                    <div class="actions"> <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">详情</a> </div>
+                </div>
+                <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/00CED1/FFFFFF?text=E2" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>校园编程马拉松</h3>
+                        <p>时间: 下周五 9:00 - 周日 17:00</p>
+                        <p>地点: 计算机学院楼 | 主办方: 计算机学院</p>
+                    </div>
+                    <div class="actions"> <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">详情</a> </div>
+                </div>
+                 <div class="card event-item">
+                    <img src="https://via.placeholder.com/50/9370DB/FFFFFF?text=E3" alt="Event Icon" class="avatar" style="border-radius: 8px;">
+                    <div class="content-details">
+                        <h3>草坪音乐分享会</h3>
+                        <p>时间: 周日 16:00</p>
+                        <p>地点: 学校大草坪 | 发起人: 赵敏</p>
+                    </div>
+                    <div class="actions"> <a href="#page-event-detail" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">详情</a> </div>
+                </div>
+            </div>
+
+             <!-- PAGE: Event Detail -->
+            <div class="app-page" id="page-event-detail">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-events" class="back-button">‹</a><span>活动详情</span><a href="#" class="icon">☆</a>';</script>
+                <img src="https://via.placeholder.com/345x150/FF6347/FFFFFF?text=Event+Banner" alt="Event Image" style="width: 100%; height: 150px; object-fit: cover; border-radius: 8px; margin-bottom: 15px;">
+                <h2>周末狼人杀局</h2>
+                <p><strong>时间:</strong> 本周六 14:00 - 18:00</p>
+                <p><strong>地点:</strong> 学校南门咖啡厅二楼</p>
+                <p><strong>发起人:</strong> <a href="#page-profile-other">王磊</a> (人民大学)</p>
+                <p><strong>活动介绍:</strong> 找个地方一起玩狼人杀,新手友好,欢迎各种板子!目前已有3人,还差5-7人。费用AA(场地费)。</p>
+                <p><strong>兴趣标签:</strong> <span class="interest-tag">桌游</span> <span class="interest-tag">狼人杀</span> <span class="interest-tag">线下聚会</span></p>
+                <p><strong>已报名 (3/10):</strong> <img src="https://via.placeholder.com/30/32CD32/FFFFFF?text=U3" class="avatar" style="width:30px; height:30px; margin-right: 5px;"> <img src="https://via.placeholder.com/30/1E90FF/FFFFFF?text=U6" class="avatar" style="width:30px; height:30px; margin-right: 5px;"> <img src="https://via.placeholder.com/30/FF4500/FFFFFF?text=U7" class="avatar" style="width:30px; height:30px; margin-right: 5px;"> <a href="#">查看全部</a></p>
+                <button style="width: 100%;">我要报名</button>
+                 <button class="secondary" style="width: 100%; margin-top: 10px;">分享活动</button>
+            </div>
+
+             <!-- PAGE: Event Posting -->
+            <div class="app-page" id="page-event-post">
+                 <script>document.getElementById('app-header').innerHTML = '<a href="#page-events" class="back-button">‹</a><span>发布新活动</span><a href="#page-events" class="icon" style="font-size: 0.9em;">发布</a>';</script>
+                 <label for="event-title">活动标题:</label>
+                 <input type="text" id="event-title" placeholder="如:周日图书馆约学习">
+                 <label for="event-desc">活动描述:</label>
+                 <textarea id="event-desc" placeholder="详细说明活动内容、目的、要求等..."></textarea>
+                 <label for="event-time">时间:</label>
+                 <input type="text" id="event-time" placeholder="如:本周日 下午2点-5点">
+                 <label for="event-loc">地点:</label>
+                 <input type="text" id="event-loc" placeholder="如:学校图书馆三楼自习室">
+                 <label for="event-tags">关联兴趣标签 (可选):</label>
+                 <input type="text" id="event-tags" placeholder="如:考研, 学习伙伴, 图书馆">
+                 <label for="event-limit">人数限制 (可选):</label>
+                 <input type="number" id="event-limit" placeholder="如:5 (留空表示不限)">
+                 <label for="event-cover">上传封面图 (可选):</label>
+                 <button class="outline">选择图片</button>
+            </div>
+
+
+            <!-- PAGE: Skill Exchange Listing -->
+            <div class="app-page" id="page-skills">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-discover" class="back-button">‹</a><span>技能交换</span><a href="#page-skill-post" class="icon">+</a>';</script>
+                 <!-- Add Tabs: 提供 / 需求 / 浏览 -->
+                 <div class="card skill-item">
+                     <img src="https://via.placeholder.com/50/6495ED/FFFFFF?text=S1" alt="User Avatar" class="avatar">
+                     <div class="content-details">
+                        <h3>刘洋 <span style="font-size: 0.8em; color: #777;">(北航)</span></h3>
+                        <p><strong style="color: green;">提供:</strong> PPT美化, 海报设计</p>
+                        <p><strong style="color: orange;">需要:</strong> 英语口语练习</p>
+                     </div>
+                     <div class="actions"> <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">联系Ta</a> </div>
+                 </div>
+                 <div class="card skill-item">
+                     <img src="https://via.placeholder.com/50/DC143C/FFFFFF?text=S2" alt="User Avatar" class="avatar">
+                     <div class="content-details">
+                        <h3>孙悦 <span style="font-size: 0.8em; color: #777;">(北理)</span></h3>
+                         <p><strong style="color: green;">提供:</strong> 日语N2辅导</p>
+                         <p><strong style="color: orange;">需要:</strong> Python爬虫教学</p>
+                     </div>
+                     <div class="actions"> <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">联系Ta</a> </div>
+                 </div>
+                  <div class="card skill-item">
+                     <img src="https://via.placeholder.com/50/FF8C00/FFFFFF?text=S3" alt="User Avatar" class="avatar">
+                     <div class="content-details">
+                        <h3>周峰 <span style="font-size: 0.8em; color: #777;">(北交大)</span></h3>
+                         <p><strong style="color: green;">提供:</strong> 吉他弹唱教学 (入门)</p>
+                         <p><strong style="color: orange;">需要:</strong> 无</p>
+                     </div>
+                     <div class="actions"> <a href="#page-chat" class="button-link outline" style="padding: 5px 10px; font-size: 0.8em;">联系Ta</a> </div>
+                 </div>
+            </div>
+
+             <!-- PAGE: Skill Post/Edit -->
+            <div class="app-page" id="page-skill-post">
+                 <script>document.getElementById('app-header').innerHTML = '<a href="#page-skills" class="back-button">‹</a><span>发布技能/需求</span><a href="#page-skills" class="icon" style="font-size: 0.9em;">发布</a>';</script>
+                 <label for="skill-provide">我能提供的技能/帮助:</label>
+                 <textarea id="skill-provide" placeholder="详细描述你能提供的技能或帮助,如:Python数据分析入门、日语五十音教学..."></textarea>
+                 <label for="skill-need">我需要的技能/帮助:</label>
+                 <textarea id="skill-need" placeholder="详细描述你需要的技能或帮助,如:寻找英语口语语伴、求PS修图指导..."></textarea>
+                 <p style="font-size: 0.9em; color: #888;">可以只填写一项或两项都填。</p>
+            </div>
+
+            <!-- PAGE: Map View (Conceptual) -->
+            <div class="app-page" id="page-map">
+                <script>document.getElementById('app-header').innerHTML = '<a href="#page-discover" class="back-button">‹</a><span>附近 (<span style="color:red;">隐私保护模式</span>)</span><a href="#" class="icon">⚙️</a>';</script>
+                <p style="font-size: 0.85em; color: #dc3545; text-align: center; border: 1px solid #f5c6cb; background-color: #f8d7da; padding: 8px; border-radius: 5px; margin-bottom: 15px;">
+                    <strong>注意:</strong> 为保护隐私,地图仅显示模糊位置和共同兴趣,不会暴露精确坐标或个人身份。你需要明确授权才能使用此功能。
+                </p>
+                 <div class="map-placeholder">
+                    <p>模拟地图区域<br/>(显示附近有相同兴趣的用户或活动的大致位置)</p>
+                 </div>
+                 <div style="margin-top: 15px;">
+                     <label for="map-filter-interest">筛选兴趣:</label>
+                     <select id="map-filter-interest">
+                         <option>所有兴趣</option>
+                         <option>摄影</option>
+                         <option>篮球</option>
+                         <option>学习伙伴</option>
+                     </select>
+                     <label for="map-filter-radius">范围:</label>
+                     <select id="map-filter-radius">
+                         <option>1公里内</option>
+                         <option>3公里内</option>
+                         <option>5公里内</option>
+                     </select>
+                 </div>
+                 <!-- List nearby items below the map? -->
+                 <p style="text-align: center; color: #999; margin-top: 20px;">地图下方可列出匹配项</p>
+
+            </div>
+
+            <!-- PAGE: Login -->
+            <div class="app-page" id="page-login">
+                <script>document.getElementById('app-header').innerHTML = '<span>登录</span>';</script>
+                <h2 style="text-align: center; margin-top: 40px;">欢迎回来</h2>
+                 <label for="login-email">邮箱/手机号:</label>
+                 <input type="email" id="login-email" placeholder="输入你的注册邮箱或手机号">
+                 <label for="login-password">密码:</label>
+                 <input type="password" id="login-password" placeholder="输入密码">
+                 <button style="width: 100%; margin-top: 20px;">登录</button>
+                 <div style="display: flex; justify-content: space-between; margin-top: 15px; font-size: 0.9em;">
+                     <a href="#" style="color: #007bff;">忘记密码?</a>
+                     <a href="#page-signup" style="color: #007bff;">没有账号?去注册</a>
+                 </div>
+                 <p style="text-align: center; color: #aaa; margin: 30px 0 15px 0;">--- 第三方登录 ---</p>
+                 <div style="text-align: center;">
+                    <button class="outline" style="margin: 5px; border-radius: 50%; width: 50px; height: 50px; font-size: 1.5em;">微</button>
+                    <button class="outline" style="margin: 5px; border-radius: 50%; width: 50px; height: 50px; font-size: 1.5em;">Q</button>
+                     <button class="outline" style="margin: 5px; border-radius: 50%; width: 50px; height: 50px; font-size: 1.5em;"></button>
+                 </div>
+            </div>
+
+             <!-- PAGE: Signup -->
+            <div class="app-page" id="page-signup">
+                <script>document.getElementById('app-header').innerHTML = '<span>注册</span>';</script>
+                 <h2 style="text-align: center; margin-top: 20px;">创建新账号</h2>
+                  <label for="signup-school-email">学校邮箱 (.edu):</label>
+                 <input type="email" id="signup-school-email" placeholder="推荐使用edu邮箱验证身份">
+                  <label for="signup-nickname">昵称:</label>
+                 <input type="text" id="signup-nickname" placeholder="给自己取个名字吧">
+                  <label for="signup-password">设置密码:</label>
+                 <input type="password" id="signup-password" placeholder="至少8位,包含字母和数字">
+                  <label for="signup-password-confirm">确认密码:</label>
+                 <input type="password" id="signup-password-confirm" placeholder="再次输入密码">
+                 <button style="width: 100%; margin-top: 20px;">注册</button>
+                 <p style="font-size: 0.8em; color: #888; margin-top: 10px; text-align: center;">
+                    点击注册表示同意 <a href="#" style="color: #007bff;">用户协议</a> 和 <a href="#" style="color: #007bff;">隐私政策</a>
+                 </p>
+                  <p style="text-align: center; margin-top: 15px;">
+                     <a href="#page-login" style="color: #007bff;">已有账号?去登录</a>
+                 </p>
+            </div>
+
+        </div> <!-- End .app-content -->
+
+        <!-- Footer / Tab Bar -->
+        <div class="app-footer" id="app-footer">
+            <a href="#page-home" class="tab-item active" onclick="setActiveTab(this)"><span>🏠</span>首页</a>
+            <a href="#page-discover" class="tab-item" onclick="setActiveTab(this)"><span>🔍</span>发现</a>
+            <a href="#page-messages" class="tab-item" onclick="setActiveTab(this)"><span>💬</span>消息</a>
+            <a href="#page-profile-self" class="tab-item" onclick="setActiveTab(this)"><span>👤</span>我的</a>
+        </div>
+    </div> <!-- End .phone-mockup -->
+
+<script>
+    // Simple JavaScript for page navigation simulation and tab highlighting
+    const pages = document.querySelectorAll('.app-page');
+    const footerTabs = document.querySelectorAll('.app-footer .tab-item');
+    const header = document.getElementById('app-header');
+
+    function showPage(pageId) {
+        pages.forEach(page => {
+            page.style.display = 'none'; // Hide all pages
+        });
+        const targetPage = document.getElementById(pageId);
+        if (targetPage) {
+            targetPage.style.display = 'block'; // Show the target page
+             // Find the script inside the target page and run it to update the header
+            const headerScript = targetPage.querySelector('script');
+            if (headerScript) {
+                try {
+                    eval(headerScript.innerText); // Execute the script to set the header
+                } catch (e) {
+                    console.error("Error executing header script for page:", pageId, e);
+                    // Set a default header if script fails
+                     header.innerHTML = '<span>App Name</span>';
+                }
+            } else {
+                 // Set a default header if no script found
+                  header.innerHTML = '<span>App Name</span>';
+            }
+
+            // Scroll content to top when changing pages
+            targetPage.parentElement.scrollTop = 0;
+
+        } else {
+             console.warn("Page not found:", pageId);
+             document.getElementById('page-home').style.display = 'block'; // Fallback to home
+             header.innerHTML = '<span>兴趣伙伴</span><a href="#page-notifications" class="icon">🔔</a>';
+        }
+    }
+
+    function setActiveTab(selectedTab) {
+        footerTabs.forEach(tab => {
+            tab.classList.remove('active');
+        });
+        selectedTab.classList.add('active');
+        // Navigate to the page specified in the tab's href
+        const pageId = selectedTab.getAttribute('href').substring(1); // Remove #
+        showPage(pageId);
+    }
+
+    // Handle initial page load based on URL hash, or default to home
+    window.addEventListener('load', () => {
+        const hash = window.location.hash;
+        if (hash) {
+            const pageId = hash.substring(1);
+            showPage(pageId);
+            // Update active tab based on hash
+            footerTabs.forEach(tab => {
+                 if(tab.getAttribute('href') === hash) {
+                     setActiveTab(tab);
+                 } else {
+                      tab.classList.remove('active');
+                 }
+             });
+             // Ensure at least one tab is active if hash matches a main page
+             const activeTabs = document.querySelectorAll('.app-footer .tab-item.active');
+             if(activeTabs.length === 0 && ['page-home', 'page-discover', 'page-messages', 'page-profile-self'].includes(pageId)) {
+                 document.querySelector(`.app-footer .tab-item[href="#${pageId}"]`).classList.add('active');
+             } else if (activeTabs.length === 0) {
+                 document.querySelector('.app-footer .tab-item[href="#page-home"]').classList.add('active'); // Default active tab
+             }
+
+        } else {
+            showPage('page-home'); // Default page
+            document.querySelector('.app-footer .tab-item[href="#page-home"]').classList.add('active'); // Default active tab
+        }
+    });
+
+    // Optional: Handle browser back/forward navigation
+    window.addEventListener('hashchange', () => {
+         const hash = window.location.hash;
+         if (hash) {
+            const pageId = hash.substring(1);
+            showPage(pageId);
+             // Update active tab based on hash
+             footerTabs.forEach(tab => {
+                 if(tab.getAttribute('href') === hash) {
+                     setActiveTab(tab); // This already calls showPage
+                 } else {
+                     tab.classList.remove('active');
+                 }
+             });
+              // Ensure at least one tab is active if hash matches a main page
+             const activeTabs = document.querySelectorAll('.app-footer .tab-item.active');
+             if(activeTabs.length === 0 && ['page-home', 'page-discover', 'page-messages', 'page-profile-self'].includes(pageId)) {
+                 document.querySelector(`.app-footer .tab-item[href="#${pageId}"]`).classList.add('active');
+             } else if (activeTabs.length === 0) {
+                 document.querySelector('.app-footer .tab-item[href="#page-home"]').classList.add('active'); // Default active tab
+             }
+         } else {
+             showPage('page-home'); // Default to home if hash is empty
+             setActiveTab(document.querySelector('.app-footer .tab-item[href="#page-home"]'));
+         }
+    });
+
+    // Make internal links work for navigation
+    document.addEventListener('click', function(event) {
+        let target = event.target;
+        // Traverse up the DOM if the click was inside a link
+        while(target && target.tagName !== 'A') {
+            target = target.parentElement;
+        }
+
+        if (target && target.tagName === 'A' && target.getAttribute('href') && target.getAttribute('href').startsWith('#')) {
+            const href = target.getAttribute('href');
+            // Check if it's a tab bar link (already handled by setActiveTab)
+            if (!target.classList.contains('tab-item')) {
+                event.preventDefault(); // Prevent default anchor jump
+                const pageId = href.substring(1);
+                window.location.hash = pageId; // Change hash to trigger navigation
+            } else {
+                 // If it IS a tab item, ensure setActiveTab is called (covers cases where onclick might fail or be absent)
+                 setActiveTab(target);
+            }
+        }
+    });
+
+
+</script>
+
+</body>
+</html>