这是一个基于 Django 5.2 的文件存储系统。下面将从零开始,逐文件、逐行梳理每一处代码,帮助新手小白彻底理解整个项目的运作方式。
用户浏览器 ──请求──▶ Django (manage.py 启动服务)
│
▼
┌──────────────┐
│ urls.py │ ← 路由分发:把 URL 分配到对应的视图函数
└──────┬───────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌──────────┐
│ views.py │ │forms.py│ │ models.py│
│ (视图) │ │ (表单) │ │ (数据库) │
└────┬─────┘ └────────┘ └────┬─────┘
│ │
▼ ▼
┌────────────┐ ┌───────────┐
│ templates/ │ │ MySQL DB │
│ (HTML模板) │ │ (数据库) │
└────────────┘ └───────────┘
│
▼
┌────────────┐
│ static/ │ ← CSS / JS / 图片等静态资源
└────────────┘
file_own/requirements.txt — 项目依赖清单
pip install -r requirements.txt,就能一次性安装所有依赖。
| 1 | Django>=5.2 | ← 安装 Django 框架,版本至少 5.2。Django 是整个网站的后端骨架,帮你处理路由、数据库、模板等一切 |
| 2 | PyMySQL>=1.0.2 | ← 安装 PyMySQL,它是 Python 连接 MySQL 数据库的驱动。没有它,Django 无法和 MySQL 通信 |
| 3 | django-crispy-forms>=2.0 | ← 安装 crispy-forms,它能让 Django 自动生成的表单变得更好看,省去手写 HTML 表单样式的麻烦 |
| 4 | crispy-bootstrap5>=0.7 | ← 安装 Bootstrap5 主题包,配合上一行的 crispy-forms 使用,让表单自动套用 Bootstrap5 的样式 |
file_own/manage.py — 项目启动入口 / 管理工具
python manage.py runserver(启动服务器)、python manage.py migrate(同步数据库)等命令,都是通过它来调度的。一般不需要修改它。
| 1 | #!/usr/bin/env python | ← Shebang 行(Linux/Mac 专用)。告诉系统用哪个 Python 解释器来执行这个脚本,Windows 下会忽略这行 |
| 2 | """Django's command-line utility for administrative tasks.""" | ← 模块的文档字符串(docstring),简单描述这个文件的用途:Django 的命令行管理工具 |
| 3 | import os | ← 导入 Python 内置的 os 模块,它提供操作系统相关功能,比如读取环境变量(后面会用到) |
| 4 | import sys | ← 导入 Python 内置的 sys 模块,它提供命令行参数(sys.argv)和 Python 解释器相关的信息 |
| 5 | import pymysql | ← 导入 PyMySQL 库。因为项目使用 MySQL 数据库,而 Django 默认只支持 PostgreSQL 等,需要 PyMySQL 来桥接 |
| 6 | pymysql.install_as_MySQLdb() | ← 关键操作!把 PyMySQL 伪装成 MySQLdb(MySQL 官方 Python 驱动),这样 Django 以为自己在用 MySQLdb,实际用的是 PyMySQL |
| 7 | ||
| 8 | ||
| 9 | def main(): | ← 定义主函数 main(),整个脚本的逻辑从这里开始执行 |
| 10 | """Run administrative tasks.""" | ← 函数的文档说明:运行管理任务 |
| 11 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'file_own.settings') | ← 设置环境变量:告诉 Django 去 file_own/settings.py 里读取配置。setdefault 的意思是"如果没设置过就设成这个值" |
| 12 | try: | ← 开始尝试(try-except 错误处理):接下来导入 Django,如果导入失败就给出友好提示 |
| 13 | from django.core.management import execute_from_command_line | ← 从 Django 核心管理模块中导入命令行执行函数。这个函数能解析你在终端输入的命令(如 runserver、migrate 等)并执行 |
| 14 | except ImportError as exc: | ← 如果导入失败(说明 Django 没安装),捕获这个 ImportError 异常 |
| 15 | raise ImportError( | ← 重新抛出一个更友好的错误提示,告诉用户该怎么做 |
| 16 | "Couldn't import Django. Are you sure it's installed and " | ← 错误信息:无法导入 Django |
| 17 | "available on your PYTHONPATH environment variable? Did you " | ← 提示用户检查 Python 路径 |
| 18 | "forget to activate a virtual environment?" | ← 提示用户是否忘记激活虚拟环境 |
| 19 | ) from exc | ← from exc 保留原始异常信息,方便调试 |
| 20 | execute_from_command_line(sys.argv) | ← 核心!把命令行参数(如 ['manage.py', 'runserver'])传给 Django,让它执行对应的命令 |
| 21 | ||
| 22 | ||
| 23 | if __name__ == '__main__': | ← Python 经典写法:判断这个文件是否被直接运行(而不是被其他文件 import)。如果是直接运行,就执行下面的代码 |
| 24 | main() | ← 调用上面定义的 main() 函数,启动 Django 管理命令 |
file_own/file_own/settings.py — 项目全局配置中心
| 1 | """ | ← 三引号开始一段多行文档字符串 |
| 2 | Django settings for file_own project. | ← 说明这是 file_own 项目的设置文件 |
| 3-11 | ...(省略文档说明)... | ← Django 自动生成的文档链接,提供参考地址 |
| 12 | """ | ← 文档字符串结束 |
| 13 | import os | ← 导入 os 模块,后面用于拼接文件路径(如静态文件目录) |
| 14 | from pathlib import Path | ← 导入 pathlib 的 Path 类。Path 是现代 Python 处理路径的推荐方式,比字符串拼接更安全、更方便 |
| 15 | ||
| 16 | BASE_DIR = Path(__file__).resolve().parent.parent | ← 【重要】获取项目根目录的绝对路径。__file__ 是当前文件(settings.py)的路径,resolve() 解析为绝对路径,两次 .parent 就跳到了项目根目录 |
| 17 | ||
| 18 | # SECURITY WARNING: keep the secret key used in production secret! | ← 安全警告:上线时必须更换这个密钥! |
| 19 | SECRET_KEY = 'django-insecure-5g1^(6f...' | ← Django 的加密密钥,用于 Session、CSRF 等安全机制的加密签名。开发时用默认值即可,上线必须换成随机字符串 |
| 20 | ||
| 21 | DEBUG = True | ← 开启调试模式。True 时出错会显示详细错误页面(方便开发);上线必须改为 False(否则泄露敏感信息) |
| 22 | ||
| 23 | ALLOWED_HOSTS = ['*'] | ← 允许访问的域名/IP 列表。'*' 表示允许所有(开发环境),上线应改为具体域名如 ['www.example.com'] |
| 24 | ||
| 25 | # Application definition | ← 下面开始定义"安装了哪些应用" |
| 26 | INSTALLED_APPS = [ | ← 【核心配置】注册所有已安装的 App。Django 启动时会加载列表中的每个 App |
| 27 | 'django.contrib.admin', | ← Django 自带的后台管理系统,访问 /admin/ 就能管理数据库数据 |
| 28 | 'django.contrib.auth', | ← Django 自带的用户认证系统,提供登录、注册、权限管理等功能 |
| 29 | 'django.contrib.contenttypes', | ← 内容类型框架,让 Django 能追踪所有模型的元数据(一般不用管它) |
| 30 | 'django.contrib.sessions', | ← Session 会话管理,用于在用户浏览器和服务端之间保持登录状态等 |
| 31 | 'django.contrib.messages', | ← 消息框架,用于在页面间传递一次性提示(如"上传成功!") |
| 32 | 'django.contrib.staticfiles', | ← 静态文件管理,帮助收集和分发 CSS、JS、图片等静态资源 |
| 33 | 'File_app', | ← 【我们自己写的应用】文件管理 App,包含了文件上传、下载、删除等所有业务逻辑 |
| 34 | ] | ← INSTALLED_APPS 列表结束 |
| 35 | CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | ← 告诉 crispy-forms 允许使用 Bootstrap5 的模板包 |
| 36 | CRISPY_TEMPLATE_PACK = "bootstrap5" | ← 设置 crispy-forms 默认使用 Bootstrap5 样式渲染表单 |
| 37 | ||
| 38 | MIDDLEWARE = [ | ← 【中间件列表】中间件就像"过滤器",每个请求进来和响应出去时都会经过这些中间件处理 |
| 39 | 'django.middleware.security.SecurityMiddleware', | ← 安全中间件:处理 HTTPS 重定向、安全头部等 |
| 40 | 'django.contrib.sessions.middleware.SessionMiddleware', | ← Session 中间件:为每个请求关联 Session(保持登录状态) |
| 41 | 'django.middleware.common.CommonMiddleware', | ← 通用中间件:处理 URL 末尾斜杠等通用逻辑 |
| 42 | 'django.middleware.csrf.CsrfViewMiddleware', | ← CSRF 防护中间件:防止跨站请求伪造攻击(表单里必须加 {% csrf_token %}) |
| 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | ← 认证中间件:把登录用户信息绑定到 request.user 上 |
| 44 | 'django.contrib.messages.middleware.MessageMiddleware', | ← 消息中间件:支持一次性消息提示功能 |
| 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', | ← 点击劫持防护:防止网站被嵌入到恶意 iframe 中 |
| 46 | ] | ← 中间件列表结束 |
| 47 | ||
| 48 | ROOT_URLCONF = 'file_own.urls' | ← 指定主路由文件的位置。所有 URL 请求首先到 file_own/urls.py 中匹配 |
| 49 | ||
| 50 | TEMPLATES = [ | ← 模板引擎配置,告诉 Django 去哪里找 HTML 模板、用什么引擎渲染 |
| 51 | { | |
| 52 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | ← 使用 Django 自带的模板引擎(还有 Jinja2 等可选) |
| 53 | 'DIRS': [BASE_DIR / 'templates'], | ← 模板目录:告诉 Django 去项目根目录下的 templates/ 文件夹找模板文件 |
| 54 | 'APP_DIRS': True, | ← 允许 Django 自动去每个 App 内部的 templates/ 子目录找模板 |
| 55 | 'OPTIONS': { | ← 模板引擎的额外选项 |
| 56 | 'context_processors': [ | ← 上下文处理器:在每个模板渲染前自动注入一些变量 |
| 57 | 'django.template.context_processors.debug', | ← 在模板中注入 DEBUG 变量(调试用) |
| 58 | 'django.template.context_processors.request', | ← 在模板中注入 request 变量(可以在模板中访问请求信息) |
| 59 | 'django.contrib.auth.context_processors.auth', | ← 在模板中注入 user 变量(可以在模板中判断用户是否登录) |
| 60 | 'django.contrib.messages.context_processors.messages', | ← 在模板中注入 messages 变量(显示提示消息) |
| 61 | ], | |
| 62 | }, | |
| 63 | }, | |
| 64 | ] | ← 模板配置结束 |
| 65 | ||
| 66 | WSGI_APPLICATION = 'file_own.wsgi.application' | ← 指定 WSGI 应用入口,部署到生产服务器(如 Nginx + Gunicorn)时会用到 |
| 67 | ||
| 68 | # Database | ← 下面配置数据库连接 |
| 69 | DATABASES = { | ← 【核心配置】数据库设置字典 |
| 70 | 'default': { | ← 'default' 是默认数据库的配置名,Django 支持多数据库 |
| 71 | 'ENGINE': 'django.db.backends.mysql', | ← 数据库引擎:使用 MySQL(通过前面安装的 PyMySQL 驱动) |
| 72 | 'NAME': 'file_p', | ← 数据库名称:连接到 MySQL 中名为 file_p 的数据库 |
| 73 | 'USER': 'root', | ← 数据库用户名:使用 root 账户(生产环境应使用专用账户) |
| 74 | 'PASSWORD': 'MySQL102346', | ← 数据库密码(生产环境应使用环境变量,不要硬编码) |
| 75 | 'HOST': '127.0.0.1', | ← 数据库主机地址:127.0.0.1 表示本机(localhost) |
| 76 | 'PORT': 3306, | ← 数据库端口号:3306 是 MySQL 的默认端口,必须是整数不能加引号 |
| 77 | } | |
| 78 | } | ← 数据库配置结束 |
| 79 | ||
| 80 | AUTH_PASSWORD_VALIDATORS = [ | ← 密码验证器列表:用户注册/改密码时,Django 会用这些规则检查密码强度 |
| 81 | {'NAME': '...UserAttributeSimilarityValidator'}, | ← 密码不能和用户名太相似(防止用用户名当密码) |
| 82 | {'NAME': '...MinimumLengthValidator'}, | ← 密码最小长度验证(默认至少 8 位) |
| 83 | {'NAME': '...CommonPasswordValidator'}, | ← 密码不能是常见密码(如 12345678、password) |
| 84 | {'NAME': '...NumericPasswordValidator'}, | ← 密码不能全是数字(如 12345678) |
| 85 | ] | |
| 86 | ||
| 87 | LANGUAGE_CODE = 'zh-hans' | ← 网站语言设为简体中文(Django 内置的管理页面、错误提示都会变成中文) |
| 88 | TIME_ZONE = 'Asia/Shanghai' | ← 时区设为上海(东八区),所有时间显示和存储都按这个时区 |
| 89 | USE_I18N = True | ← 开启国际化支持(Internationalization),允许翻译文本 |
| 90 | USE_TZ = True | ← 开启时区支持,数据库内部用 UTC 存储,显示时自动转换为上海时间 |
| 91 | ||
| 92 | STATIC_URL = 'static/' | ← 静态文件的 URL 前缀。访问 CSS/JS/图片时用 /static/css/style.css 这样的路径 |
| 93 | STATICFILES_DIRS = [ | ← 额外的静态文件目录列表(开发时用) |
| 94 | BASE_DIR / "static" | ← 告诉 Django 去项目根目录下的 static/ 文件夹找静态文件 |
| 95 | ] | |
| 96 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") | ← 收集静态文件的目标目录(运行 collectstatic 命令后,所有静态文件会复制到这里,给生产服务器用) |
| 97 | ||
| 98 | MEDIA_URL = '/media/' | ← 用户上传文件的 URL 前缀。访问上传的文件时用 /media/2026/05/17/xxx.exe |
| 99 | MEDIA_ROOT = BASE_DIR / 'media' | ← 用户上传文件的存储目录。文件实际保存在项目根目录下的 media/ 文件夹中 |
| 100 | ||
| 101 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' | ← 模型默认的自增主键类型。BigAutoField 支持更大的 ID 范围(防止数据量超大时 ID 不够用) |
| 102 | ||
| 103 | LOGIN_REDIRECT_URL = '/' | ← 登录成功后跳转到哪里:'/' 表示跳转到首页 |
| 104 | LOGOUT_REDIRECT_URL = '/' | ← 退出登录后跳转到哪里:'/' 表示跳转到首页 |
| 105 | LOGIN_URL = 'login' | ← 未登录用户访问需要登录的页面时,自动跳转到 'login' 这个 URL |
| 106 | ||
| 107 | DATA_UPLOAD_MAX_MEMORY_SIZE = 2147483648 | ← 请求体最大内存限制:2GB(2147483648 字节 = 2 × 1024³),允许上传大文件 |
| 108 | FILE_UPLOAD_MAX_MEMORY_SIZE = 2147483648 | ← 单个文件上传最大限制:2GB。小于此值的文件存在内存中,大于此值的文件会临时写入磁盘 |
file_own/file_own/urls.py — 项目主路由(URL 总调度中心)
| 1 | from django.contrib import admin | ← 导入 Django 的后台管理模块,用于注册 /admin/ 路由 |
| 2 | from django.urls import path, include | ← 导入 path(定义 URL 路径)和 include(引入其他路由文件) |
| 3 | from django.conf.urls.static import static | ← 导入 static 辅助函数,用于在开发模式下提供静态文件和媒体文件的访问 |
| 4 | from django.conf import settings | ← 导入项目配置(settings.py 中的所有设置),后面用来判断是否为 DEBUG 模式 |
| 5 | ||
| 6 | urlpatterns = [ | ← 【核心】URL 路由列表。Django 从上到下依次匹配,匹配到第一个就停止 |
| 7 | path('admin/', admin.site.urls), | ← 访问 /admin/ 开头的所有 URL → 交给 Django 后台管理系统处理(自动提供增删改查界面) |
| 8 | path('', include('File_app.urls')), | ← 访问根路径 '' 开头的 URL → 交给 File_app/urls.py 处理。空字符串表示"所有请求都先交给它",相当于默认应用 |
| 9 | path('accounts/', include('django.contrib.auth.urls')) | ← 访问 /accounts/ 开头的 URL → 交给 Django 内置认证系统处理(自动提供 /accounts/login/、/accounts/logout/ 等路由) |
| 10 | ] | ← 路由列表结束 |
| 11 | ||
| 12 | if settings.DEBUG: | ← 只在调试模式(DEBUG=True)下执行下面的代码,生产环境不会走这里 |
| 13 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) | ← 开发模式下,让 /media/xxx 这样的 URL 能直接访问用户上传的文件(生产环境应由 Nginx 处理) |
| 14 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) | ← 开发模式下,让 /static/xxx 这样的 URL 能直接访问静态文件(CSS/JS/图片) |
file_own/file_own/wsgi.py — WSGI 部署接口
| 1 | import os | ← 导入 os 模块,用于设置环境变量 |
| 2 | from django.core.wsgi import get_wsgi_application | ← 导入 Django 的 WSGI 应用工厂函数 |
| 3 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'file_own.settings') | ← 告诉 Django 配置文件在哪里(和 manage.py 中一样的操作) |
| 4 | application = get_wsgi_application() | ← 创建 WSGI 应用实例。生产服务器(如 Gunicorn)会调用这个 application 对象来处理 HTTP 请求 |
file_own/file_own/asgi.py — ASGI 异步部署接口
| 1 | import os | ← 导入 os 模块 |
| 2 | from django.core.asgi import get_asgi_application | ← 导入 Django 的 ASGI 应用工厂函数(和 WSGI 类似,但支持异步) |
| 3 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'file_own.settings') | ← 设置配置文件路径 |
| 4 | application = get_asgi_application() | ← 创建 ASGI 应用实例,供异步服务器(如 Daphne、Uvicorn)使用 |
file_own/File_app/apps.py — 应用配置信息
| 1 | from django.apps import AppConfig | ← 导入 Django 的应用配置基类 AppConfig,所有 App 的配置类都要继承它 |
| 2 | ||
| 3 | class FileAppConfig(AppConfig): | ← 定义 File_app 的配置类,继承自 AppConfig |
| 4 | default_auto_field = 'django.db.models.BigAutoField' | ← 该应用下所有模型默认使用的自增主键类型(BigAutoField = 64位整数,范围超大) |
| 5 | name = 'File_app' | ← 应用的名称,必须和文件夹名一致。Django 通过这个名字来识别和加载应用 |
file_own/File_app/models.py — 数据模型(数据库表结构定义)
| 1 | from django.db import models | ← 【必须】导入 Django 的 models 模块。所有数据库模型(数据表)都必须继承 models.Model 基类 |
| 2 | import os | ← 导入操作系统接口模块,后面用于处理文件路径(如获取文件扩展名 os.path.splitext) |
| 3 | import uuid | ← 导入 UUID 模块。UUID 能生成全球唯一的 ID(如 a1b2c3d4-e5f6-...),用作文件记录的主键,避免 ID 冲突和被猜测 |
| 4 | from django.contrib.auth.models import User | ← 导入 Django 内置的 User 模型。用于关联"哪个用户上传了这个文件"(外键关系) |
| 5 | from django.utils import timezone | ← 导入 Django 的时区工具,用于获取当前时间(带时区信息的 now()) |
| 6 | ||
| 7 | ||
| 8 | def generate_upload_path(instance, filename): | ← 定义一个函数,用于生成上传文件的存储路径。Django 的 FileField 会调用它来决定文件存到哪 |
| 9 | """生成上传路径""" | ← 函数的文档说明 |
| 10 | ext = filename.split('.')[-1] | ← 从原始文件名中提取扩展名。比如 "报告.pdf" 按 '.' 分割后取最后一个元素 → "pdf" |
| 11 | filename = f"{uuid.uuid4().hex}.{ext}" | ← 用 UUID 生成全新的文件名(如 a1b2c3d4e5f6.pdf),防止文件名冲突和中文路径问题 |
| 12 | return os.path.join( | ← 拼接最终路径,返回类似 "2026/05/25/a1b2c3d4.pdf" 的相对路径 |
| 13 | timezone.now().strftime('%Y/%m/%d'), | ← 按当前日期生成目录,如 "2026/05/25"。这样文件按日期分文件夹存放,方便管理 |
| 14 | filename | ← UUID 文件名 |
| 15 | ) | ← 最终路径示例:media/2026/05/25/a1b2c3d4e5f6.pdf |
| 16 | ||
| 17 | ||
| 18 | class File(models.Model): | ← 【核心】定义 File 模型类 = 数据库中的 file_file 表。继承 models.Model 后,Django 会自动帮你建表 |
| 18 | class File(models.Model): | ← 【核心】定义 File 模型类 = 数据库中的 file_file 表。继承 models.Model 后,Django 会自动帮你建表 |
| 19 | # 文件映射类型 | ← 下面定义一个"元组列表",用来限定文件的分类。每个元组 (值, 显示名) 对应下拉选项的一项 |
| 20 | FILE_TYPES = ( | ← 【常量】FILE_TYPES 是一个元组,包含所有支持的文件类型。在表单下拉框和管理后台中都会用到 |
| 21 | ('image', '图片'), | ← 数据库中存 'image',页面上显示"图片" |
| 22 | ('document', '文档'), | ← 文档类型:Word、PDF、PPT 等 |
| 23 | ('audio', '音频'), | ← 音频类型:MP3、WAV 等 |
| 24 | ('video', '视频'), | ← 视频类型:MP4、AVI 等 |
| 25 | ('application', '应用'), | ← 应用程序类型:EXE、APK 等安装包 |
| 26 | ('text', '文本'), | ← 纯文本/代码文件:.py、.js、.html 等 |
| 27 | ('other', '其他') | ← 兜底类型:不属于以上分类的都归为"其他" |
| 28 | ) | ← FILE_TYPES 元组结束 |
| 29 | ||
| 30 | # 支持的文件拓展名 | ← 下面是一个字典,键=文件类型,值=该类型允许的文件扩展名列表 |
| 31 | SUPPORTED_EXTENSIONS = { | ← 【常量】用于上传时校验文件扩展名是否合法,相当于"白名单" |
| 32 | 'image': ['.jpg', '.jpeg', '.png', ...], | ← 图片类允许的扩展名列表 |
| 33 | 'document': ['.doc', '.docx', '.pdf', ...], | ← 文档类允许的扩展名列表 |
| 34 | 'audio': ['.mp3', '.wav', '.flac', ...], | ← 音频类允许的扩展名列表 |
| 35 | 'video': ['.mp4', '.avi', '.mkv', ...], | ← 视频类允许的扩展名列表 |
| 36 | 'application': ['.apk', '.exe', ...], | ← 应用程序类允许的扩展名列表 |
| 37 | 'text': ['.txt', '.py', '.js', ...], | ← 文本/代码类允许的扩展名列表 |
| 38 | 'other': [...] # 兜底 | ← 其他类型包含所有已知扩展名作为兜底匹配 |
| 39 | } | ← SUPPORTED_EXTENSIONS 字典结束 |
| 40 | ||
| 41 | # ===== 数据库字段定义 ===== | ← 下面每一行对应数据库表中的一列(一个字段) |
| 42 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | ← 【主键】用 UUID 做主键而非自增 ID。好处:ID 不可预测(安全)、全球唯一(不会冲突)。editable=False 表示后台不可编辑 |
| 43 | name = models.CharField(max_length=255, verbose_name='文件名') | ← 文件名字段,最大 255 个字符。verbose_name 是后台管理/表单中显示的名称 |
| 44 | file = models.FileField(upload_to=generate_upload_path, verbose_name='文件') | ← 文件字段,存储实际的文件。upload_to 调用前面的函数来决定文件存放路径(如 media/2026/05/25/xxx.pdf) |
| 45 | file_size = models.PositiveIntegerField(verbose_name='文件大小 (字节)') | ← 文件大小字段,正整数类型。存储文件的字节数,后面有方法将其格式化为 KB/MB |
| 46 | file_type = models.CharField(max_length=20, choices=FILE_TYPES, verbose_name='文件类型') | ← 文件类型字段,choices=FILE_TYPES 意味着只能从前面定义的 7 种类型中选一个 |
| 47 | uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name='上传时间') | ← 上传时间字段。auto_now_add=True 表示创建记录时自动填入当前时间,之后不可更改 |
| 48 | uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='上传者') | ← 【外键】关联到 User 表,表示"谁上传的"。CASCADE 表示如果用户被删除,该用户上传的所有文件记录也一起删除 |
| 49 | description = models.TextField(blank=True, verbose_name='描述') | ← 文件描述字段,TextField 支持大段文本。blank=True 表示表单中可以为空不填 |
| 50 | download_count = models.PositiveIntegerField(default=0, verbose_name='下载次数') | ← 下载次数计数器,默认为 0。每次有人下载文件时 +1 |
| 51 | is_deleted = models.BooleanField(default=False, verbose_name='是否已删除') | ← 软删除标记。True 表示文件已被"删除"(移到回收站),但数据库记录还在 |
| 52 | deleted_at = models.DateTimeField(null=True, blank=True, verbose_name='删除时间') | ← 删除时间字段。null=True 表示数据库中可以为空(未删除时就没有删除时间) |
| 53 | ||
| 54 | class Meta: | ← 【元数据类】Meta 是 Django 模型的特殊内部类,用来配置模型的额外信息(如排序、显示名等),不会生成数据库字段 |
| 55 | verbose_name = '文件' | ← 单数形式显示名,在 Admin 后台侧边栏显示 |
| 56 | verbose_name_plural = '文件' | ← 复数形式显示名。Django 默认会加 's',中文不需要加 |
| 57 | ordering = ['-uploaded_at'] | ← 默认排序:按上传时间倒序(最新的排最前面)。'-' 前缀表示降序 |
| 58 | ||
| 59 | def __str__(self): | ← 定义模型的字符串表示。当 print(file) 或在 Admin 中显示时,会调用这个方法返回文件名 |
| 60 | return self.name | ← 返回文件名作为字符串表示 |
| 61 | ||
| 62 | @property | ← @property 装饰器:让方法可以像属性一样调用(file.extension 而不是 file.extension()) |
| 63 | def extension(self): | ← 获取文件扩展名的属性方法 |
| 64 | return os.path.splitext(self.name)[1].lower() | ← splitext 将文件名按 '.' 分成 (名字, 扩展名),取 [1] 即扩展名,lower() 转小写 |
| 65 | ||
| 66 | @property | ← 又一个属性装饰器 |
| 67 | def formatted_size(self): | ← 将文件大小(字节)转换为人类可读的格式(如 "2.35 MB") |
| 68 | size = self.file_size | ← 获取文件大小的原始字节数 |
| 69 | for unit in ['B', 'KB', 'MB', 'GB', 'TB']: | ← 依次尝试 B → KB → MB → GB → TB |
| 70 | if size < 1024.0: | ← 如果当前值小于 1024,就用当前单位 |
| 71 | return f"{size:.2f} {unit}" | ← 返回格式化字符串,如 "2.35 MB"(保留 2 位小数) |
| 72 | size /= 1024.0 | ← 除以 1024,升到下一个单位 |
| 73 | return f"{size:.2f} TB" | ← 兜底:如果文件超过 1024 TB(不太可能),就显示 TB |
| 74 | ||
| 75 | @property | ← 属性装饰器 |
| 76 | def icon_class(self): | ← 根据文件类型返回对应的 Bootstrap Icons 图标 CSS 类名 |
| 77 | type_icons = { | ← 字典映射:文件类型 → Bootstrap Icons 类名 |
| 78 | 'image': 'bi bi-image', | ← 图片类型 → 图片图标 |
| 79 | 'document': 'bi bi-file-earmark-text', | ← 文档类型 → 文档图标 |
| 80 | 'audio': 'bi bi-music-note-beamed', | ← 音频类型 → 音乐图标 |
| 81 | 'video': 'bi bi-film', | ← 视频类型 → 胶卷图标 |
| 82 | 'application': 'bi bi-app', | ← 应用类型 → 应用图标 |
| 83 | 'text': 'bi bi-file-earmark-code', | ← 文本类型 → 代码文件图标 |
| 84 | 'other': 'bi bi-file-earmark' | ← 其他类型 → 通用文件图标 |
| 85 | } | ← 字典结束 |
| 86 | return type_icons.get(self.file_type, 'bi bi-file-earmark') | ← 用 get 方法安全获取图标类名,找不到类型时返回默认的通用文件图标 |
| 87 | ||
| 88 | @classmethod | ← @classmethod 装饰器:让方法属于类而非实例。调用方式:File.get_file_type('test.jpg') |
| 89 | def get_file_type(cls, filename): | ← 根据文件名自动判断文件类型。上传时如果没有手动选择类型,就调用这个方法 |
| 90 | ext = os.path.splitext(filename)[1].lower() | ← 提取文件扩展名并转小写 |
| 91 | for file_type, extensions in cls.SUPPORTED_EXTENSIONS.items(): | ← 遍历每种文件类型及其允许的扩展名列表 |
| 92 | if ext in extensions: | ← 如果扩展名匹配到了某个类型 |
| 93 | return file_type | ← 返回该类型名(如 'image'、'document') |
| 94-100 | # (备用 if/elif 硬编码匹配逻辑) | ← 如果上面的字典匹配失败(理论上不会),用硬编码的 if/elif 再匹配一遍作为兜底 |
| 101 | else: return 'other' | ← 实在无法识别就归为 'other'(其他) |
| 102 | ||
| 103 | ||
| 104 | class DeletedFile(models.Model): | ← 【第二个模型】DeletedFile 表 = 回收站。当用户"删除"文件时,不是真删,而是在这张表里创建一条记录 |
| 105 | """已删除文件记录(回收站)""" | ← 文档字符串:说明这是回收站模型 |
| 106 | file_name = models.CharField(max_length=255, verbose_name='文件名') | ← 被删除的文件名(复制过来的副本信息) |
| 107 | original_path = models.CharField(max_length=500, verbose_name='原文件路径') | ← 文件的物理存储路径(如 media/2026/05/25/xxx.pdf),恢复时需要用它找到文件 |
| 108 | file_size = models.PositiveIntegerField(verbose_name='文件大小 (字节)') | ← 文件大小 |
| 109 | file_type = models.CharField(max_length=20, verbose_name='文件类型') | ← 文件类型(如 image、document) |
| 110 | description = models.TextField(blank=True, verbose_name='描述') | ← 文件描述(从原文件记录复制过来的) |
| 111 | deleted_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='删除者') | ← 外键关联用户表,记录"是谁删除的"。用于限制用户只能恢复/删除自己的回收站文件 |
| 112 | deleted_at = models.DateTimeField(auto_now_add=True, verbose_name='删除时间') | ← 删除时间,记录自动填入 |
| 113 | ||
| 114 | class Meta: | ← 元数据配置 |
| 115 | verbose_name = '回收站' | ← 后台显示名 |
| 116 | verbose_name_plural = '回收站' | ← 后台复数显示名 |
| 117 | ordering = ['-deleted_at'] | ← 按删除时间倒序排列(最近删除的排最前面) |
| 118 | ||
| 119 | def __str__(self): | ← 字符串表示 |
| 120 | return self.file_name | ← 返回被删除的文件名 |
file_own/File_app/forms.py — 表单定义(用户输入的界面控件)
| 1 | from django import forms | ← 导入 Django 的表单模块,用于创建 HTML 表单 |
| 2 | from .models import File | ← 从当前目录的 models.py 中导入 File 模型 |
| 3 | from django.contrib.auth.models import User | ← 导入 Django 自带的用户模型,用于注册表单 |
| 4 | ||
| 5 | ||
| 6 | class FileUploadForm(forms.ModelForm): | ← 【文件上传表单】继承 ModelForm,自动根据 File 模型生成表单字段,省去手写字段的麻烦 |
| 7 | file_type = forms.ChoiceField( | ← 额外添加一个"文件类型"下拉框字段(模型中虽然有这个字段,但我们想自定义选项) |
| 8 | choices=[('', '自动识别')] + list(File.FILE_TYPES), | ← 下拉选项:第一项是"自动识别"(空值),后面拼接模型中定义的 7 种文件类型 |
| 9 | required=False, | ← 非必填,用户可以不选(默认"自动识别") |
| 10 | label='文件类型', | ← 表单中显示的标签文字 |
| 11 | widget=forms.Select(attrs={'class': 'form-select'}) | ← 使用 <select> 下拉框控件,添加 Bootstrap 的 form-select 样式类 |
| 12 | ) | ← file_type 字段定义结束 |
| 13 | ||
| 14 | class Meta: | ← 内部配置类,告诉 Django 这个表单关联哪个模型、显示哪些字段 |
| 15 | model = File | ← 关联 File 模型 |
| 16 | fields = ['file', 'description'] | ← 表单只显示这两个字段(file 文件选择框 + description 描述文本框) |
| 17 | widgets = { | ← 自定义字段的 HTML 控件样式 |
| 18 | 'file': forms.FileInput(attrs={ | ← file 字段用文件上传控件(<input type="file">) |
| 19 | 'class': 'form-control', | ← 添加 Bootstrap 的 form-control 样式 |
| 20 | 'accept': '.jpg,.png,.pdf,.exe,...' | ← 设置 accept 属性,文件选择框默认只显示这些格式(用户仍可切换为"所有文件") |
| 21 | }), | ← file 字段控件配置结束 |
| 22 | 'description': forms.Textarea(attrs={ | ← description 字段用多行文本域(<textarea>) |
| 23 | 'class': 'form-control', | ← Bootstrap 样式 |
| 24 | 'rows': 3, | ← 文本框高度为 3 行 |
| 25 | 'placeholder': '添加文件描述.........' | ← 灰色占位提示文字 |
| 26 | }) | ← description 字段控件配置结束 |
| 27 | } | ← widgets 字典结束 |
| 28 | ||
| 29 | def clean_file(self): | ← 【钩子函数】Django 表单验证的"钩子",命名规则:clean_ + 字段名。验证 file 字段时自动调用 |
| 30 | file = self.cleaned_data.get('file') | ← 从已清理的数据中获取用户上传的文件对象 |
| 31 | if file: | ← 检查文件是否存在(防止空数据报错) |
| 32 | if file.size > 2048 * 1024 * 1024: | ← 检查文件大小:2048×1024×1024 = 2GB。超过就报错 |
| 33 | raise forms.ValidationError('文件大小不能超过2GB') | ← 抛出验证错误,错误信息会自动显示在表单上方 |
| 34 | ext = file.name.split('.')[-1] | ← 提取文件后缀名(如 image.jpg → jpg) |
| 35 | allowed_extensions = set() | ← 创建空集合,用于存放所有允许的扩展名 |
| 36 | for extensions in File.SUPPORTED_EXTENSIONS.values(): | ← 遍历模型中定义的所有类型的扩展名列表 |
| 37 | allowed_extensions.update(extensions) | ← 将每种类型的扩展名列表合并到集合中 |
| 38 | if f".{ext}" not in allowed_extensions: | ← 检查文件后缀是否在白名单中 |
| 39 | raise forms.ValidationError(f"不支持的文件类型:.{ext}") | ← 不在白名单就报错 |
| 40 | return file | ← 【重要】验证通过后必须返回 file 对象,否则数据丢失无法保存 |
| 41 | ||
| 42 | ||
| 43 | class UserRegistrationForm(forms.ModelForm): | ← 【用户注册表单】继承 ModelForm,基于 Django 内置的 User 模型 |
| 44 | """用户注册表单""" | ← 文档说明 |
| 45 | password = forms.CharField(widget=forms.PasswordInput, label='密码') | ← 密码字段,使用 PasswordInput 控件(输入时显示圆点而非明文) |
| 46 | password_confirm = forms.CharField(widget=forms.PasswordInput, label='确认密码') | ← 确认密码字段,用于让用户再输入一次密码,防止输错 |
| 47 | ||
| 48 | class Meta: | ← 表单元数据配置 |
| 49 | model = User | ← 关联 Django 内置的 User 模型 |
| 50 | fields = ['username', 'email', 'password', 'password_confirm'] | ← 表单包含 4 个字段:用户名、邮箱、密码、确认密码 |
| 51 | widgets = { ... } | ← 为每个字段设置 Bootstrap 样式和 placeholder 提示文字(结构与上传表单类似,省略) |
| 52 | ||
| 53 | def clean(self): | ← 【全局验证钩子】clean() 会在所有字段验证完后调用,用于跨字段校验 |
| 54 | cleaned_data = super().clean() | ← 先调用父类的 clean() 获取已清理的数据 |
| 55 | password = cleaned_data.get('password') | ← 获取密码 |
| 56 | password_confirm = cleaned_data.get('password_confirm') | ← 获取确认密码 |
| 57 | if password and password_confirm and password != password_confirm: | ← 如果两次密码不一致 |
| 58 | raise forms.ValidationError('两次输入的密码不一致') | ← 抛出错误提示 |
| 59 | return cleaned_data | ← 返回清理后的数据 |
| 60 | ||
| 61 | def save(self, commit=True): | ← 重写 save 方法:因为密码不能直接存储明文,需要调用 set_password() 加密 |
| 62 | user = super().save(commit=False) | ← 先创建 User 对象但不保存到数据库(commit=False) |
| 63 | user.set_password(self.cleaned_data['password']) | ← 【关键】用 set_password 对密码进行哈希加密后再存储(绝不能存明文密码!) |
| 64 | if commit: user.save() | ← 如果 commit=True,才真正写入数据库 |
| 65 | return user | ← 返回创建好的用户对象 |
file_own/File_app/views.py — 视图函数(业务逻辑的核心)
| 1 | from django.shortcuts import render, redirect, get_object_or_404 | ← render:渲染模板返回 HTML;redirect:重定向到其他 URL;get_object_or_404:查数据库,找不到就返回 404 |
| 2 | from django.http import HttpResponse, Http404, FileResponse, JsonResponse | ← Http404:抛出 404 错误;FileResponse:返回文件下载流;JsonResponse:返回 JSON 数据(AJAX 用) |
| 3 | from django.contrib import messages | ← 消息框架:在页面间传递一次性提示(如"上传成功!") |
| 4 | from django.contrib.auth.decorators import login_required | ← 登录验证装饰器:加在视图函数上面,未登录用户访问时自动跳转到登录页 |
| 5 | from django.core.paginator import Paginator | ← 分页器:将大量数据分成多页显示 |
| 6 | from django.db.models import Q | ← Q 对象:用于构建复杂的数据库查询条件(如 OR 查询) |
| 7 | from django.utils.encoding import escape_uri_path | ← 对文件名进行 URL 编码,防止中文文件名在下载时乱码 |
| 8 | from django.contrib.auth import login | ← login 函数:在注册完成后自动登录用户 |
| 9 | from .models import File, DeletedFile | ← 导入我们定义的两个模型 |
| 10 | from django.utils import timezone | ← 时区工具,用于获取当前时间(软删除时记录删除时间) |
| 11 | from .forms import FileUploadForm, UserRegistrationForm | ← 导入前面定义的两个表单类 |
| 12 | import os | ← 导入 os 模块,用于文件操作(检查文件是否存在、删除文件等) |
| 13 | ||
| 14 | ||
| 15 | def home(request): | ← 【主页视图】显示所有文件列表,支持搜索、按类型过滤、分页 |
| 16 | files = File.objects.filter(is_deleted=False).order_by('-uploaded_at') | ← 查询所有未被删除的文件,按上传时间倒序排列 |
| 17 | search_query = request.GET.get('search', '') | ← 获取搜索关键词(GET 参数 ?search=xxx),没有则返回空字符串 |
| 18 | if search_query: | ← 如果有搜索内容 |
| 19 | files = files.filter( | ← 在已有查询结果上进一步过滤 |
| 20 | Q(name__icontains=search_query) | | ← Q 对象:文件名包含关键词(icontains = 不区分大小写的包含) |
| 21 | Q(description__icontains=search_query) | | ← 或者描述包含关键词(| 表示 OR 关系) |
| 22 | Q(file_type__icontains=search_query) | ← 或者文件类型包含关键词 |
| 23 | ) | ← 三个条件满足任一即可(OR 查询) |
| 24 | file_type = request.GET.get('type', '') | ← 获取类型过滤参数(GET 参数 ?type=image) |
| 25 | if file_type: | ← 如果选择了特定类型 |
| 26 | files = files.filter(file_type=file_type) | ← 只保留该类型的文件 |
| 27 | paginator = Paginator(files, 12) | ← 创建分页器:每页显示 12 条记录 |
| 28 | page_number = request.GET.get('page') | ← 获取当前页码(GET 参数 ?page=2) |
| 29 | page_obj = paginator.get_page(page_number) | ← 获取当前页的数据对象(包含数据、页码信息等) |
| 30 | context = { 'page_obj': page_obj, ... } | ← 构造传递给模板的上下文字典 |
| 31 | return render(request, 'home.html', context) | ← 用 home.html 模板渲染,返回完整的 HTML 页面 |
| 32 | ||
| 33 | @login_required | ← 【装饰器】要求用户必须登录,否则跳转到登录页 |
| 34 | def upload_file(request): | ← 【上传文件视图】处理文件上传的 GET(显示表单)和 POST(提交文件)请求 |
| 35 | if request.method == 'POST': | ← 如果是 POST 请求(用户提交了文件) |
| 36 | form = FileUploadForm(request.POST, request.FILES) | ← 用提交的数据填充表单(POST 数据 + FILES 上传文件) |
| 37 | if form.is_valid(): | ← 验证表单数据是否合法(文件大小、扩展名等) |
| 38 | file_instance = form.save(commit=False) | ← 创建 File 对象但暂不保存到数据库(还要补充额外字段) |
| 39 | file_instance.uploaded_by = request.user | ← 设置上传者 = 当前登录用户 |
| 40 | file_instance.name = request.FILES['file'].name | ← 设置文件名 = 上传文件的原始文件名 |
| 41 | file_instance.file_size = request.FILES['file'].size | ← 设置文件大小 = 上传文件的字节数 |
| 42 | selected_type = form.cleaned_data.get('file_type') | ← 获取用户选择的文件类型(可能为空 = "自动识别") |
| 43 | if selected_type: | ← 如果用户手动选了类型 |
| 44 | file_instance.file_type = selected_type | ← 使用用户选择的类型 |
| 45 | else: | ← 否则(选了"自动识别") |
| 46 | file_instance.file_type = File.get_file_type(file_instance.name) | ← 调用模型中的方法自动判断类型 |
| 47 | file_instance.save() | ← 现在所有字段都填好了,保存到数据库 |
| 48 | messages.success(request, '文件上传成功!') | ← 添加成功提示消息(下次页面加载时显示) |
| 49 | return redirect('home') | ← 重定向到首页 |
| 50 | else: | ← 如果是 GET 请求(首次打开上传页面) |
| 51 | form = FileUploadForm() | ← 创建一个空表单 |
| 52 | return render(request, 'upload.html', {'form': form}) | ← 渲染上传页面模板 |
| 53 | ||
| 54 | def download_file(request, file_id): | ← 【下载文件视图】根据 file_id 查找文件并以附件形式返回给浏览器 |
| 55 | file_instance = get_object_or_404(File, id=file_id) | ← 根据 UUID 查找文件记录,找不到则返回 404 |
| 56 | file_instance.download_count += 1 | ← 下载计数 +1 |
| 57 | file_instance.save(update_fields=['download_count']) | ← 只更新 download_count 字段(优化:不更新其他字段) |
| 58 | if not os.path.exists(file_instance.file.path): | ← 检查物理文件是否还在磁盘上 |
| 59 | raise Http404("文件不存在") | ← 文件不在就抛 404 |
| 60 | response = FileResponse( | ← 创建文件响应:以二进制流的形式返回文件内容 |
| 61 | open(file_instance.file.path, 'rb'), | ← 以二进制只读模式打开文件 |
| 62 | content_type='application/octet-stream' | ← 设置 Content-Type 为二进制流,触发浏览器下载而不是在线预览 |
| 63 | ) | ← FileResponse 结束 |
| 64 | response['Content-Disposition'] = f'attachment; filename="{escape_uri_path(file_instance.name)}"' | ← 设置下载时的文件名(escape_uri_path 防止中文乱码) |
| 65 | response['Content-Length'] = file_instance.file_size | ← 设置文件大小,浏览器能显示下载进度 |
| 66 | return response | ← 返回文件流 |
| 67 | ||
| 68 | def file_detail(request, file_id): | ← 【文件详情视图】(当前模板 detail.html 未创建,预留功能) |
| 69 | file_instance = get_object_or_404(File, id=file_id) | ← 根据 ID 获取文件 |
| 70 | return render(request, 'detail.html', {'file': file_instance}) | ← 渲染详情页模板 |
| 71 | ||
| 72 | def file_list_by_type(request, file_type): | ← 【按类型筛选视图】根据 URL 中传入的 file_type 参数,只显示该类型的文件列表 |
| 73 | """按类型列出文件""" | ← 函数的文档说明 |
| 74 | files = File.objects.filter(file_type=file_type).order_by('-uploaded_at') | ← 查询指定类型的所有文件,按上传时间倒序排列(最新的在前) |
| 75 | ||
| 76 | paginator = Paginator(files, 12) | ← 创建分页器,每页显示 12 条记录 |
| 77 | page_number = request.GET.get('page') | ← 获取 URL 中的页码参数(如 ?page=2) |
| 78 | page_obj = paginator.get_page(page_number) | ← 获取当前页的数据(包含文件列表、页码、是否有上/下一页等信息) |
| 79 | ||
| 80 | context = { | ← 构造传给模板的上下文数据 |
| 81 | 'page_obj': page_obj, | ← 当前页的数据对象 |
| 82 | 'file_type': file_type, | ← 当前的文件类型标识(如 'image') |
| 83 | 'file_type_display': dict(File.FILE_TYPES).get(file_type, '未知类型'), | ← 把 'image' 转成 '图片' 这样的中文显示名(用 dict 将元组转为字典查找) |
| 84 | } | |
| 85 | return render(request, 'type_list.html', context) | ← 用 type_list.html 模板渲染分类文件列表页面 |
| 86 | ||
| 87 | def register(request): | ← 【用户注册视图】处理用户注册的 GET(显示注册表单)和 POST(提交注册信息)请求 |
| 88 | """用户注册视图""" | ← 函数的文档说明 |
| 89 | if request.method == 'POST': | ← 如果是 POST 请求(用户提交了注册表单) |
| 90 | form = UserRegistrationForm(request.POST) | ← 用提交的数据填充注册表单 |
| 91 | if form.is_valid(): | ← 验证表单数据(用户名是否合法、两次密码是否一致等) |
| 92 | user = form.save() | ← 保存用户到数据库(form.save 内部会调用 set_password 加密密码) |
| 93 | login(request, user) | ← 注册成功后自动登录该用户(不用跳转到登录页再输一遍密码) |
| 94 | messages.success(request, '注册成功!') | ← 显示"注册成功"的提示消息 |
| 95 | return redirect('home') | ← 重定向到首页 |
| 96 | else: | ← 如果是 GET 请求(首次打开注册页面) |
| 97 | form = UserRegistrationForm() | ← 创建一个空的注册表单 |
| 98 | ||
| 99 | return render(request, 'registration/register.html', {'form': form}) | ← 渲染注册页面模板 |
| 100 | ||
| 101 | @login_required | ← 必须登录才能访问修改类型页面 |
| 102 | def change_file_type(request, file_id): | ← 【修改文件类型视图】允许文件上传者更改文件的分类 |
| 103 | """修改文件类型""" | |
| 104 | file_instance = get_object_or_404(File, id=file_id) | ← 根据 UUID 查找文件记录 |
| 105 | ||
| 106 | # 检查是否为文件上传者 | ← 权限检查:只有上传者本人才能修改文件类型 |
| 107 | if file_instance.uploaded_by != request.user: | ← 如果当前登录用户 ≠ 文件上传者 |
| 108 | messages.error(request, '您只能修改自己上传的文件!') | ← 显示错误提示 |
| 109 | return redirect('home') | ← 直接跳转回首页,拒绝操作 |
| 110 | ||
| 111 | if request.method == 'POST': | ← 如果是 POST 请求(用户提交了新类型) |
| 112 | new_type = request.POST.get('file_type') | ← 从表单获取用户选择的新类型 |
| 113 | if new_type in dict(File.FILE_TYPES): | ← 验证新类型是否合法(必须在预定义的 7 种类型中) |
| 114 | file_instance.file_type = new_type | ← 更新文件类型 |
| 115 | file_instance.save() | ← 保存到数据库 |
| 116 | messages.success(request, '文件类型已更新!') | ← 显示成功提示 |
| 117 | else: | ← 如果类型不合法 |
| 118 | messages.error(request, '无效的文件类型!') | ← 显示错误提示 |
| 119 | return redirect('home') | ← 不管成功失败都返回首页 |
| 120 | ||
| 121 | return render(request, 'file_app/change_type.html', { | ← GET 请求时渲染修改类型页面 |
| 122 | 'file': file_instance, | ← 传递当前文件信息(显示文件名、当前类型) |
| 123 | 'file_types': File.FILE_TYPES | ← 传递所有可选的文件类型列表 |
| 124 | }) | |
| 125 | ||
| 126 | @login_required | ← 必须登录才能删除文件 |
| 127 | def delete_file(request, file_id): | ← 【删除文件视图】软删除:不真正删除文件,而是标记 is_deleted=True 并移入回收站 |
| 128 | """删除文件(软删除,移入回收站)""" | |
| 129 | file_instance = get_object_or_404(File, id=file_id) | ← 查找要删除的文件 |
| 130 | ||
| 131 | if file_instance.uploaded_by != request.user: | ← 权限检查:只能删除自己上传的文件 |
| 132 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← 判断是否为 AJAX 请求(前端 JS 发来的异步请求) |
| 133 | return JsonResponse({'success': False, 'message': '权限不足'}) | ← AJAX 请求返回 JSON 格式的错误信息 |
| 134 | messages.error(request, '您只能删除自己上传的文件!') | ← 普通请求用 messages 显示错误 |
| 135 | return redirect('home') | ← 返回首页 |
| 136 | ||
| 137 | if request.method == 'POST': | ← 只接受 POST 请求(防止误触 GET 链接导致删除) |
| 138 | # 创建回收站记录 | ← 在 DeletedFile 表中创建一条记录,保存被删文件的信息 |
| 139 | DeletedFile.objects.create( | ← 创建回收站记录 |
| 140 | file_name=file_instance.name, | ← 记录文件名 |
| 141 | original_path=file_instance.file.path, | ← 记录文件的物理路径(恢复时需要) |
| 142 | file_size=file_instance.file_size, | ← 记录文件大小 |
| 143 | file_type=file_instance.file_type, | ← 记录文件类型 |
| 144 | description=file_instance.description, | ← 记录文件描述 |
| 145 | deleted_by=request.user | ← 记录是谁删除的 |
| 146 | ) | |
| 147 | ||
| 148 | file_instance.is_deleted = True | ← 将文件标记为"已删除"(软删除核心逻辑) |
| 149 | file_instance.deleted_at = timezone.now() | ← 记录删除时间 |
| 150 | file_instance.save() | ← 保存修改 |
| 151 | ||
| 152 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← 如果是 AJAX 请求(前端 JS 无刷新删除) |
| 153 | return JsonResponse({'success': True}) | ← 返回 JSON 成功响应,前端 JS 据此移除卡片 |
| 154 | ||
| 155 | messages.success(request, '文件已移至回收站!') | ← 普通请求显示成功消息 |
| 156 | ||
| 157 | return redirect('home') | ← 返回首页 |
| 158 | ||
| 159 | @login_required | ← 必须登录才能恢复文件 |
| 160 | def restore_file(request, deleted_id): | ← 【恢复文件视图】从回收站中恢复被软删除的文件 |
| 161 | """恢复回收站中的文件""" | |
| 162 | deleted_file = get_object_or_404(DeletedFile, id=deleted_id) | ← 根据 deleted_id 查找回收站记录 |
| 163 | ||
| 164 | if deleted_file.deleted_by != request.user: | ← 权限检查:只能恢复自己删除的文件 |
| 165 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 请求返回 JSON 错误 |
| 166 | return JsonResponse({'success': False, 'message': '权限不足'}) | |
| 167 | messages.error(request, '您只能恢复自己删除的文件!') | ← 普通请求显示错误消息 |
| 168 | return redirect('recycle_bin') | ← 返回回收站页面 |
| 169 | ||
| 170 | if request.method == 'POST': | ← 只接受 POST 请求 |
| 171 | if os.path.exists(deleted_file.original_path): | ← 先检查磁盘上的物理文件是否还在 |
| 172 | try: | ← 尝试恢复文件 |
| 173 | file_instance = File.objects.get(name=deleted_file.file_name) | ← 在 File 表中找到对应的文件记录(虽然被软删但记录还在) |
| 174 | file_instance.is_deleted = False | ← 取消删除标记 |
| 175 | file_instance.deleted_at = None | ← 清除删除时间 |
| 176 | file_instance.save() | ← 保存修改 → 文件恢复成功 |
| 177 | deleted_file.delete() | ← 从回收站表中删除该条记录 |
| 178 | ||
| 179 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 返回成功 |
| 180 | return JsonResponse({'success': True}) | |
| 181 | messages.success(request, '文件已恢复!') | ← 普通请求显示成功消息 |
| 182 | except File.DoesNotExist: | ← 如果 File 表中的原始记录已不存在 |
| 183 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 返回错误 |
| 184 | return JsonResponse({'success': False, 'message': '原文件记录不存在'}) | |
| 185 | messages.error(request, '原文件记录不存在!') | ← 普通请求显示错误 |
| 186 | else: | ← 如果物理文件已不存在(磁盘上被删了) |
| 187 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 返回错误 |
| 188 | return JsonResponse({'success': False, 'message': '物理文件已丢失'}) | |
| 189 | messages.error(request, '物理文件已被删除,无法恢复!') | ← 普通请求显示错误 |
| 190 | ||
| 191 | return redirect('recycle_bin') | ← 返回回收站页面 |
| 192 | ||
| 193 | @login_required | ← 必须登录才能彻底删除 |
| 194 | def permanent_delete(request, deleted_id): | ← 【彻底删除视图】从回收站中永久删除文件(物理文件 + 数据库记录全部删除) |
| 195 | """彻底删除文件(从回收站永久删除)""" | |
| 196 | deleted_file = get_object_or_404(DeletedFile, id=deleted_id) | ← 查找回收站记录 |
| 197 | ||
| 198 | if deleted_file.deleted_by != request.user: | ← 权限检查 |
| 199 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 错误响应 |
| 200 | return JsonResponse({'success': False, 'message': '权限不足'}) | |
| 201 | messages.error(request, '您只能删除自己的回收站文件!') | |
| 202 | return redirect('recycle_bin') | |
| 203 | ||
| 204 | if request.method == 'POST': | ← 只接受 POST |
| 205 | if os.path.exists(deleted_file.original_path): | ← 检查磁盘上文件是否还在 |
| 206 | os.remove(deleted_file.original_path) | ← 【危险操作】删除磁盘上的物理文件(不可恢复!) |
| 207 | ||
| 208 | deleted_file.delete() | ← 删除回收站中的记录 |
| 209 | ||
| 210 | try: | ← 尝试删除 File 表中对应的记录 |
| 211 | file_instance = File.objects.get(name=deleted_file.file_name, is_deleted=True) | ← 找到已标记删除的原始记录 |
| 212 | file_instance.delete() | ← 从数据库中彻底删除该记录 |
| 213 | except File.DoesNotExist: | ← 如果记录已经不存在,跳过 |
| 214 | pass | ← 什么都不做 |
| 215 | ||
| 216 | if request.headers.get('x-requested-with') == 'XMLHttpRequest': | ← AJAX 成功响应 |
| 217 | return JsonResponse({'success': True}) | |
| 218 | messages.success(request, '文件已彻底删除!') | ← 普通请求显示成功 |
| 219 | ||
| 220 | return redirect('recycle_bin') | ← 返回回收站 |
| 221 | ||
| 222 | @login_required | ← 必须登录才能查看回收站 |
| 223 | def recycle_bin(request): | ← 【回收站视图】显示当前用户已删除的文件列表 |
| 224 | """回收站视图""" | |
| 225 | deleted_files = DeletedFile.objects.filter(deleted_by=request.user) | ← 只查询当前用户删除的文件(每人只看自己的回收站) |
| 226 | ||
| 227 | paginator = Paginator(deleted_files, 12) | ← 分页,每页 12 条 |
| 228 | page_number = request.GET.get('page') | ← 获取页码 |
| 229 | page_obj = paginator.get_page(page_number) | ← 获取当前页数据 |
| 230 | ||
| 231 | return render(request, 'file_app/recycle_bin.html', {'page_obj': page_obj}) | ← 渲染回收站模板 |
file_own/File_app/urls.py — 应用路由(File_app 的 URL 分发器)
| 1 | from django.urls import path | ← 导入 path 函数,用于定义 URL 路由规则 |
| 2 | from . import views | ← 从当前目录(File_app)导入 views.py 中的所有视图函数 |
| 3 | ||
| 4 | urlpatterns = [ | ← 【路由列表】Django 按顺序从上到下匹配 URL,匹配到第一个就停止 |
| 5 | path('', views.home, name='home'), | ← 访问根路径 / → 调用 home 视图(首页文件列表)。name='home' 可在模板中用 {% url 'home' %} 反向解析 |
| 6 | path('upload/', views.upload_file, name='upload_file'), | ← 访问 /upload/ → 调用上传文件视图 |
| 7 | path('download/<uuid:file_id>/', views.download_file, name='download_file'), | ← 访问 /download/xxx-uuid/ → 下载文件。<uuid:file_id> 是路径参数,Django 自动校验 UUID 格式并传给视图函数 |
| 8 | path('file/<uuid:file_id>/', views.file_detail, name='file_detail'), | ← 访问 /file/xxx-uuid/ → 查看文件详情 |
| 9 | path('type/<str:file_type>/', views.file_list_by_type, name='file_list_by_type'), | ← 访问 /type/image/ → 按类型筛选文件。<str:file_type> 是字符串参数 |
| 10 | path('register/', views.register, name='register'), | ← 访问 /register/ → 用户注册 |
| 11 | path('file/<uuid:file_id>/change_type/', views.change_file_type, name='change_file_type'), | ← 访问 /file/xxx/change_type/ → 修改文件类型 |
| 12 | path('file/<uuid:file_id>/delete/', views.delete_file, name='delete_file'), | ← 访问 /file/xxx/delete/ → 软删除文件(移入回收站) |
| 13 | path('recycle-bin/', views.recycle_bin, name='recycle_bin'), | ← 访问 /recycle-bin/ → 查看回收站 |
| 14 | path('recycle-bin/<int:deleted_id>/restore/', views.restore_file, name='restore_file'), | ← 访问 /recycle-bin/123/restore/ → 恢复文件。<int:deleted_id> 是整数参数 |
| 15 | path('recycle-bin/<int:deleted_id>/permanent-delete/', views.permanent_delete, name='permanent_delete') | ← 访问 /recycle-bin/123/permanent-delete/ → 彻底删除文件 |
| 16 | ] | ← 路由列表结束 |
file_own/File_app/admin.py — Django 后台管理配置
| 1 | from django.contrib import admin | ← 导入 Django 的后台管理模块 |
| 2 | from .models import File | ← 导入我们要管理的 File 模型 |
| 3 | ||
| 4 | @admin.register(File) | ← 【装饰器】将 File 模型注册到后台管理。等价于 admin.site.register(File, FileAdmin) |
| 5 | class FileAdmin(admin.ModelAdmin): | ← 自定义 File 模型在后台的行为和显示方式 |
| 6 | list_display = ['name', 'file_type', 'formatted_size', 'uploaded_by', 'uploaded_at', 'download_count'] | ← 后台列表页显示哪些列(文件名、类型、大小、上传者、时间、下载次数) |
| 7 | list_filter = ['file_type', 'uploaded_at', 'uploaded_by'] | ← 后台右侧的过滤器:可按类型、时间、上传者筛选 |
| 8 | search_fields = ['name', 'description'] | ← 后台顶部的搜索框:可按文件名和描述搜索 |
| 9 | readonly_fields = ['id', 'file_size', 'download_count', 'uploaded_at'] | ← 只读字段:这些字段在编辑页面中显示但不能修改(如 UUID、文件大小等自动生成的值) |
| 10 | date_hierarchy = 'uploaded_at' | ← 在列表页顶部添加日期导航条(可按年/月/日快速筛选) |
| 11 | ||
| 12 | fieldsets = ( | ← 将编辑页面的字段分组显示,让后台更整洁 |
| 13 | ('基本信息', { | ← 第一组:基本信息 |
| 14 | 'fields': ('id', 'name', 'file', 'description', 'file_type') | ← 这组包含的字段 |
| 15 | }), | |
| 16 | ('统计信息', { | ← 第二组:统计信息 |
| 17 | 'fields': ('file_size', 'download_count', 'uploaded_at', 'uploaded_by'), | ← 这组包含的字段 |
| 18 | 'classes': ('collapse',) | ← 默认折叠这组字段(点击展开),因为统计信息不常修改 |
| 19 | }), | |
| 20 | ) | |
| 21 | ||
| 22 | def formatted_size(self, obj): | ← 自定义列:调用模型的 formatted_size 属性显示人类可读的文件大小(如 "2.5 MB") |
| 23 | return obj.formatted_size | ← 返回 @property 计算好的格式化大小 |
| 24 | ||
| 25 | formatted_size.short_description = '文件大小' | ← 设置该列在后台的表头名称 |
file_own/File_app/migrations/0001_initial.py — 初始数据库迁移
python manage.py migrate 时,Django 就会根据这个文件在数据库中创建对应的表。永远不要手动修改迁移文件!
| 1 | # Generated by Django 5.2.x | ← 注释:由 Django 自动生成 |
| 2 | from django.conf import settings | ← 导入项目配置(需要知道 User 模型的位置) |
| 3 | from django.db import migrations, models | ← 导入迁移工具和模型字段类型 |
| 4 | import django.db.models.deletion | ← 导入外键的删除行为模块(CASCADE 等) |
| 5 | import uuid | ← 导入 uuid 模块(因为 File 模型用 UUID 做主键) |
| 6 | ||
| 7 | class Migration(migrations.Migration): | ← 迁移类,继承自 migrations.Migration |
| 8 | initial = True | ← 标记为初始迁移(第一次建表) |
| 9 | dependencies = [ | ← 依赖关系:这个迁移必须在哪些迁移之后执行 |
| 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ← 依赖 User 模型的迁移(因为 File 表有外键指向 User 表,必须先建 User 表) |
| 11 | ] | |
| 12 | operations = [ | ← 操作列表:要执行的数据库操作 |
| 13 | migrations.CreateModel( | ← 创建一张新表 |
| 14 | name='File', | ← 表名(对应 File 模型) |
| 15 | fields=[ | ← 字段列表(对应模型中的每个字段) |
| 16 | ('id', models.UUIDField(...)), | ← UUID 主键字段 |
| 17 | ('name', models.CharField(max_length=255)), | ← 文件名字段 |
| 18 | ('file', models.FileField(...)), | ← 文件路径字段 |
| 19 | # ... 其余字段省略 ... | ← 每个模型字段都会在这里对应一条 |
| 20 | ], | |
| 21 | ), | |
| 22 | ] | ← 操作列表结束 |
file_own/File_app/migrations/0002_*.py — 添加软删除字段迁移
is_deleted 和 deleted_at 字段后,运行 makemigrations 就会生成这个迁移文件。它告诉数据库:"请在已有的 File 表上新增这两列"。
| 1 | class Migration(migrations.Migration): | ← 第二次迁移 |
| 2 | dependencies = [ | ← 依赖上一次迁移(0001_initial) |
| 3 | ('File_app', '0001_initial'), | ← 必须在 0001 之后执行 |
| 4 | ] | |
| 5 | operations = [ | ← 操作列表 |
| 6 | migrations.AddField(model_name='file', name='is_deleted', ...), | ← 给 File 表添加 is_deleted 列(布尔型,默认 False) |
| 7 | migrations.AddField(model_name='file', name='deleted_at', ...), | ← 给 File 表添加 deleted_at 列(日期时间型,允许为空) |
| 8 | migrations.AlterModelOptions(name='file', ...), | ← 修改模型的 Meta 选项(如排序方式) |
| 9 | ] |
file_own/File_app/migrations/0003_*.py — 字段修改迁移
| 1 | class Migration(migrations.Migration): | ← 第三次迁移 |
| 2 | dependencies = [ | |
| 3 | ('File_app', '0002_alter_file_...'), | ← 依赖第二次迁移 |
| 4 | ] | |
| 5 | operations = [ | ← 操作列表 |
| 6 | migrations.AlterField(model_name='file', name='file_size', ...), | ← 修改 file_size 字段的属性 |
| 7 | migrations.AlterModelOptions(...), | ← 修改模型的 Meta 选项 |
| 8 | ] |
file_own/templates/base.html — 基础模板(所有页面的"母版")
{% extends 'base.html' %} 继承它,这样就无需在每个页面重复写导航栏、页脚、CSS/JS 引用。Django 的模板继承机制让代码复用变得非常简单。
| 1 | {% load static %} | ← 加载 Django 的静态文件标签库。加载后才能使用 {% static '...' %} 来引用 CSS/JS/图片 |
| 2 | <!DOCTYPE html> | ← 声明文档类型为 HTML5 |
| 3 | <html lang="zh-CN"> | ← HTML 根标签,lang="zh-CN" 告诉浏览器这是简体中文页面 |
| 4 | <head> | ← 头部区域:包含元信息、CSS 引用、页面标题等(不会显示在页面上) |
| 5 | <meta charset="UTF-8"> | ← 字符编码设为 UTF-8,支持中文等多语言字符 |
| 6 | <meta name="viewport" ...> | ← 视口设置:让页面在手机/平板上也能正确显示(响应式设计的基础) |
| 7 | <title>文件存储系统</title> | ← 浏览器标签页上显示的默认标题 |
| 8 | <!-- Bootstrap 5 CSS --> | ← 引入 Bootstrap 5 的样式表(CDN 方式),提供现成的按钮、卡片、导航栏等组件样式 |
| 9 | <link rel="stylesheet" href="https://cdn...bootstrap.min.css"> | ← Bootstrap 5 CSS 文件链接 |
| 10 | <!-- Bootstrap Icons --> | ← 引入 Bootstrap 图标库,提供 1000+ 免费图标(如 bi-upload、bi-download 等) |
| 11 | <link rel="stylesheet" href="https://cdn...bootstrap-icons.css"> | ← Bootstrap Icons CSS 文件链接 |
| 12 | <link rel="stylesheet" href="{% static 'css/style.css' %}"> | ← 引入项目自定义的 CSS 文件。{% static %} 标签会自动解析正确的 URL 路径 |
| 13 | <link rel="shortcut icon" href="{% static 'images/ico.png' %}"> | ← 设置浏览器标签页的小图标(favicon) |
| 14 | <title>{% block title %}文件存储系统{% endblock %}</title> | ← 【模板块】子页面可以用 {% block title %}新标题{% endblock %} 覆盖默认标题 |
| 15 | </head> | ← 头部区域结束 |
| 16 | <body> | ← 页面主体开始(所有可见内容都在这里) |
| 17 | <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> | ← 【导航栏】Bootstrap 的响应式导航栏组件。navbar-dark=浅色文字,bg-primary=蓝色背景 |
| 18 | <div class="container"> | ← Bootstrap 容器:居中且限宽,让内容不会撑满整个屏幕 |
| 19 | <a class="navbar-brand" href="{% url 'home' %}"> | ← 导航栏的品牌/Logo 链接,点击回到首页。{% url 'home' %} 自动解析为 '/' |
| 20 | <i class="bi bi-cloud-arrow-up"></i>文件存储系统 | ← 图标 + 文字(Bootstrap Icons 的云上传图标) |
| 21 | </a> | |
| 22 | <button class="navbar-toggler" ...> | ← 手机端的"汉堡菜单"按钮(小屏幕下折叠导航栏) |
| 23 | <div class="collapse navbar-collapse"> | ← 可折叠的导航内容区域 |
| 24 | <ul class="navbar-nav me-auto"> | ← 左侧导航链接列表 |
| 25 | <li><a href="{% url 'home' %}">首页</a></li> | ← "首页"链接 |
| 26 | <li><a href="{% url 'upload_file' %}">上传文件</a></li> | ← "上传文件"链接 |
| 27 | {% if user.is_authenticated %} | ← 【条件判断】如果用户已登录,才显示"回收站"链接 |
| 28 | <li><a href="{% url 'recycle_bin' %}">回收站</a></li> | ← "回收站"链接(仅登录可见) |
| 29 | {% endif %} | ← 条件判断结束 |
| 30 | </ul> | ← 左侧导航结束 |
| 31 | <ul class="navbar-nav"> | ← 右侧导航链接(用户信息区域) |
| 32 | {% if user.is_authenticated %} | ← 如果用户已登录 |
| 33 | <span>欢迎,{{ user.username }}</span> | ← 显示"欢迎,用户名"。{{ user.username }} 是 Django 模板变量语法 |
| 34 | <form action="{% url 'logout' %}" method="post"> | ← 退出登录表单(用 POST 防止 CSRF 攻击) |
| 35 | {% csrf_token %} | ← CSRF 令牌:Django 的安全机制,防止跨站请求伪造 |
| 36 | <button type="submit">退出</button> | ← 退出按钮 |
| 37 | </form> | |
| 38 | {% else %} | ← 如果用户未登录 |
| 39 | <a href="{% url 'login' %}">登录</a> | ← 显示"登录"链接 |
| 40 | <a href="{% url 'register' %}">注册</a> | ← 显示"注册"链接 |
| 41 | {% endif %} | ← 条件判断结束 |
| 42 | </ul> | ← 右侧导航结束 |
| 43 | </div> | |
| 44 | </div> | |
| 45 | </nav> | ← 导航栏结束 |
| 46 | ||
| 47 | <main class="container mt-4"> | ← 主内容区域。container=居中限宽,mt-4=上边距 4 级 |
| 48 | {% if messages %} | ← 如果有消息(如"上传成功"、"删除失败"等提示) |
| 49 | {% for message in messages %} | ← 遍历所有消息 |
| 50 | <div class="alert alert-{{ message.tags }}"> | ← 消息提示框。message.tags 会是 success/error/warning 等,对应不同颜色 |
| 51 | {{ message }} | ← 显示消息内容 |
| 52 | <button class="btn-close"></button> | ← 关闭按钮(Bootstrap 自带关闭动画) |
| 53 | </div> | |
| 54 | {% endfor %} | ← 循环结束 |
| 55 | {% endif %} | ← 条件结束 |
| 56 | ||
| 57 | {% block content %} | ← 【核心】内容块占位。子页面的内容会填充到这里(如首页的文件列表、上传页的表单等) |
| 58 | {% endblock %} | ← 内容块结束 |
| 59 | </main> | ← 主内容区域结束 |
| 60 | ||
| 61 | <footer class="bg-light text-center"> | ← 页脚区域 |
| 62 | <p>©2026 文件储存系统</p> | ← 版权信息 |
| 63 | </footer> | ← 页脚结束 |
| 64 | ||
| 65 | <form id="csrf-form" style="display:none;"> | ← 隐藏的 CSRF 表单:供 AJAX 请求获取 CSRF Token 使用 |
| 66 | {% csrf_token %} | ← 生成一个隐藏的 CSRF input 标签 |
| 67 | </form> | |
| 68 | ||
| 69 | <script src="https://cdn...bootstrap.bundle.min.js"></script> | ← 引入 Bootstrap JS(包含下拉菜单、模态框等交互功能) |
| 70 | <script src="{% static 'js/script.js' %}"></script> | ← 引入项目自定义的 JavaScript 文件 |
| 71 | {% block extra_js %}{% endblock %} | ← 【额外 JS 块】子页面可以在这里添加自己独有的 JavaScript 代码 |
| 72 | </body> | ← 页面主体结束 |
| 73 | </html> | ← HTML 文档结束 |
file_own/templates/home.html — 首页模板(文件列表 + 搜索 + 分页)
| 1 | {% extends 'base.html' %} | ← 继承 base.html 基础模板。本页面只需填充 content 块,导航栏/页脚等由 base 提供 |
| 2 | ||
| 3 | {% block title %}文件列表 - 文件存储系统{% endblock %} | ← 覆盖 base.html 中的 title 块,设置本页标题 |
| 4 | ||
| 5 | {% block content %} | ← 开始填充内容块(以下所有内容都会出现在 base.html 的 {% block content %} 位置) |
| 6 | <div class="row mb-4"> | ← Bootstrap 栅格系统:row 是一行,mb-4=下边距 |
| 7 | <div class="col-md-8"> | ← 左侧 8/12 宽度区域:显示标题和描述 |
| 8 | <h1><i class="bi bi-files"></i> 文件存储系统</h1> | ← 页面大标题 + 文件图标 |
| 9 | <p>安全,高效的文件存储和分享平台</p> | ← 副标题描述文字 |
| 10 | </div> | |
| 11 | <div class="col-md-4"> | ← 右侧 4/12 宽度区域:上传按钮 |
| 12 | <a href="{% url 'upload_file' %}" class="btn btn-primary btn-lg"> | ← 上传按钮,点击跳转到上传页面 |
| 13 | 上传文件 | |
| 14 | </a> | |
| 15 | </div> | |
| 16 | </div> | |
| 17 | ||
| 18 | {# 搜索和过滤 #} | ← Django 模板注释:搜索和过滤区域 |
| 19 | <div class="card mb-4"> | ← Bootstrap 卡片组件,包裹搜索框 |
| 20 | <form method="GET" class="row g-3"> | ← 搜索表单,用 GET 方法(参数会显示在 URL 中,方便分享和书签) |
| 21 | <input type="text" name="search" value="{{ search_query }}"> | ← 搜索输入框。value="{{ search_query }}" 保持搜索词不丢失 |
| 22 | <select name="type"> | ← 类型筛选下拉框 |
| 23 | <option value="">所有类型</option> | ← 默认选项:不过滤类型 |
| 24 | {% for key, display in file_types %} | ← 遍历所有文件类型(7 种) |
| 25 | <option value="{{ key }}">{{ display }}</option> | ← 每个类型生成一个 option(如 value="image" 显示"图片") |
| 26 | {% endfor %} | ← 循环结束 |
| 27 | </select> | |
| 28 | <button type="submit">搜索</button> | ← 搜索按钮 |
| 29 | </form> | |
| 30 | </div> | |
| 31 | ||
| 32 | {# 文件列表 #} | ← 文件卡片网格区域 |
| 33 | <div class="row"> | ← 一行文件卡片 |
| 34 | {% for file in page_obj %} | ← 遍历当前页的文件列表(page_obj 是分页后的数据) |
| 35 | <div class="col-xl-3 col-lg-4 col-md-6 mb-4" id="file-card-{{ file.id }}"> | ← 响应式列:大屏4列/中屏3列/小屏2列。id 用于 AJAX 删除时定位卡片 |
| 36 | <div class="card h-100 shadow-sm"> | ← 文件卡片。h-100=等高,shadow-sm=轻微阴影 |
| 37 | <i class="{{ file.icon_class }}"></i> | ← 文件图标。icon_class 是模型的 @property,根据类型返回不同图标类名 |
| 38 | <h6>{{ file.name|truncatechars:20 }}</h6> | ← 文件名。truncatechars:20 是模板过滤器,超过 20 字符截断显示"..." |
| 39 | <small>{{ file.formatted_size }}</small> | ← 文件大小(如 "2.5 MB"),调用模型的 @property |
| 40 | <p>{{ file.description|default:"无描述" }}</p> | ← 文件描述。default 过滤器:如果描述为空,显示"无描述" |
| 41 | <small>{{ file.uploaded_at|date:'m-d H:i' }}</small> | ← 上传时间,date 过滤器格式化为"月-日 时:分" |
| 42 | <a href="{% url 'download_file' file.id %}">下载</a> | ← 下载按钮。file.id 是 UUID,自动传给 URL 路由 |
| 43 | {% if user == file.uploaded_by %} | ← 只有上传者本人才能看到修改/删除按钮 |
| 44 | <a href="{% url 'change_file_type' file.id %}">修改类型</a> | ← 修改类型按钮 |
| 45 | <button onclick="confirmDelete('{{ file.id }}', '{{ file.name }}')">删除</button> | ← 删除按钮:点击调用 JS 的 confirmDelete 函数,弹出确认框 |
| 46 | {% endif %} | ← 权限判断结束 |
| 47 | </div> | |
| 48 | </div> | |
| 49 | {% empty %} | ← 【空状态】如果文件列表为空(没有任何文件或搜索无结果),显示以下内容 |
| 50 | <h4>暂无文件</h4> | ← 空状态提示 |
| 51 | {% endfor %} | ← for 循环结束 |
| 52 | </div> | |
| 53 | ||
| 54 | {# 分页 #} | ← 分页导航区域 |
| 55 | {% if page_obj.has_other_pages %} | ← 如果总页数 > 1 页才显示分页 |
| 56 | <nav> | ← HTML5 导航语义标签 |
| 57 | <ul class="pagination"> | ← Bootstrap 分页组件 |
| 58 | {% if page_obj.has_previous %} | ← 如果有上一页 |
| 59 | <a href="?page={{ page_obj.previous_page_number }}...">上一页</a> | ← 上一页链接(URL 中保留搜索和类型参数) |
| 60 | {% endif %} | |
| 61 | <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span> | ← 显示"第 X / Y 页" |
| 62 | {% if page_obj.has_next %} | ← 如果有下一页 |
| 63 | <a href="?page={{ page_obj.next_page_number }}...">下一页</a> | ← 下一页链接 |
| 64 | {% endif %} | |
| 65 | </ul> | |
| 66 | </nav> | |
| 67 | {% endif %} | |
| 68 | ||
| 69 | {# 删除确认模态框 #} | ← Bootstrap 模态框弹窗(确认删除用) |
| 70 | <div class="modal fade" id="deleteModal"> | ← 模态框容器。fade=淡入动画,id 用于 JS 控制显示/隐藏 |
| 71 | <h5>确认删除</h5> | ← 弹窗标题 |
| 72 | <p>确定要将 "<strong id="modal-file-name"></strong>" 移至回收站吗?</p> | ← 弹窗内容。JS 会动态填入文件名 |
| 73 | <button id="btn-confirm-delete">移至回收站</button> | ← 确认删除按钮(JS 绑定 AJAX 删除逻辑) |
| 74 | </div> | |
| 75 | {% endblock %} | ← content 块结束 |
| 76 | ||
| 77 | {% block extra_js %} | ← 开始填充额外 JS 块(首页独有的 JavaScript 代码) |
| 78 | <script> | |
| 79 | let currentFileId = null; | ← 全局变量:存储当前要删除的文件 ID |
| 80 | let deleteModal = null; | ← 全局变量:存储 Bootstrap 模态框实例 |
| 81 | ||
| 82 | function confirmDelete(fileId, fileName) { | ← 点击删除按钮时调用:打开确认弹窗 |
| 83 | currentFileId = fileId; | ← 记住要删除的文件 ID |
| 84 | document.getElementById('modal-file-name').textContent = fileName; | ← 在弹窗中显示文件名 |
| 85 | deleteModal.show(); | ← 显示模态框 |
| 86 | } | |
| 87 | ||
| 88 | function executeDelete() { | ← 点击"确认删除"按钮时执行 AJAX 删除 |
| 89 | const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; | ← 从隐藏表单中获取 CSRF Token |
| 90 | fetch(`/file/${currentFileId}/delete/`, { | ← 发送 AJAX POST 请求到删除接口 |
| 91 | method: 'POST', | ← 请求方法为 POST |
| 92 | headers: { | ← 请求头 |
| 93 | 'X-CSRFToken': csrfToken, | ← 携带 CSRF Token(安全验证) |
| 94 | 'X-Requested-With': 'XMLHttpRequest' | ← 告诉后端这是 AJAX 请求 |
| 95 | } | |
| 96 | }) | |
| 97 | .then(data => { | ← 请求成功后 |
| 98 | const card = document.getElementById(`file-card-${currentFileId}`); | ← 找到对应的文件卡片 DOM 元素 |
| 99 | card.style.opacity = '0'; | ← 淡出动画:透明度变为 0 |
| 100 | setTimeout(() => card.remove(), 400); | ← 400ms 后从 DOM 中移除卡片(无刷新删除效果) |
| 101 | }); | |
| 102 | } | |
| 103 | </script> | |
| 104 | {% endblock %} | ← extra_js 块结束 |
file_own/templates/upload.html — 文件上传页面
| 1 | {% extends 'base.html' %} | ← 继承基础模板 |
| 2 | ||
| 3 | {% block title %}上传文件 - 文件存储系统{% endblock %} | ← 页面标题 |
| 4 | ||
| 5 | {% block content %} | ← 开始内容块 |
| 6 | <div class="row justify-content-center"> | ← 居中布局 |
| 7 | <div class="col-md-8"> | ← 内容宽度 8/12 |
| 8 | <div class="card shadow"> | ← 卡片容器 |
| 9 | <div class="card-header bg-primary text-white"> | ← 卡片头部:蓝色背景白色文字 |
| 10 | <h4>上传文件</h4> | ← 标题 |
| 11 | </div> | |
| 12 | <div class="card-body"> | ← 卡片内容区域 |
| 13 | <form method="post" enctype="multipart/form-data"> | ← 【关键】enctype="multipart/form-data" 是文件上传表单必须的属性,否则文件无法提交 |
| 14 | {% csrf_token %} | ← CSRF 安全令牌 |
| 15 | <label>选择文件</label> | ← 文件选择标签 |
| 16 | {{ form.file }} | ← Django 自动渲染的文件上传控件(<input type="file">) |
| 17 | <div class="form-text"> | ← 帮助文字 |
| 18 | 支持的文件格式:图片、文档、音频、视频...最大 2GB | ← 告诉用户支持哪些格式和大小限制 |
| 19 | </div> | |
| 20 | {{ form.file_type }} | ← 文件类型选择下拉框(含"自动识别"选项) |
| 21 | {{ form.description }} | ← 文件描述多行文本框 |
| 22 | <a href="{% url 'home' %}">返回</a> | ← 返回按钮 |
| 23 | <button type="submit">上传文件</button> | ← 提交按钮 |
| 24 | </form> | |
| 25 | </div> | |
| 26 | </div> | |
| 27 | </div> | |
| 28 | </div> | |
| 29 | {% endblock %} | ← 内容块结束 |
file_own/templates/type_list.html — 按类型分类的文件列表页
| 1 | {% extends 'base.html' %} | ← 继承基础模板 |
| 2 | {% load static %} | ← 加载静态文件标签 |
| 3 | ||
| 4 | {% block content %} | ← 内容块开始 |
| 5 | <h2>{{ category_name }}</h2> | ← 页面标题:显示分类名称(如"图片") |
| 6 | <p>共有 {{ files|length }} 个文件</p> | ← 显示文件总数。|length 是模板过滤器,返回列表长度 |
| 7 | <a href="{% url 'home' %}">返回总览</a> | ← 返回首页链接 |
| 8 | ||
| 9 | {% if files %} | ← 如果有文件 |
| 10 | {% for file in files %} | ← 遍历文件列表 |
| 11 | <div class="card"> | ← 每个文件一个卡片 |
| 12 | {% if file.file_type == 'Image' %} | ← 如果是图片类型 |
| 13 | <img src="{{ file.file.url }}"> | ← 直接显示图片缩略图 |
| 14 | {% else %} | ← 其他类型 |
| 15 | <i class="bi bi-file-earmark"></i> | ← 显示通用文件图标 |
| 16 | {% endif %} | |
| 17 | <h5>{{ file.original_name }}</h5> | ← 显示文件名 |
| 18 | <a href="{% url 'download_file' file.id %}">下载文件</a> | ← 下载按钮 |
| 19 | </div> | |
| 20 | {% endfor %} | ← 文件列表循环结束 |
| 21 | {% else %} | ← 【空状态】如果该分类下没有文件 |
| 22 | <p>该分类下暂无文件</p> | ← 空状态提示文字 |
| 23 | <a href="{% url 'upload_file' %}">去上传</a> | ← 引导用户去上传页面 |
| 24 | {% endif %} | ← 条件判断结束 |
| 25 | ||
| 26 | <style> | ← 页面内嵌样式(仅在此页面生效) |
| 27 | .hover-shadow:hover { | ← 鼠标悬停时的阴影效果 |
| 28 | transform: translateY(-5px); | ← 向上移动 5px(卡片"浮起"效果) |
| 29 | transition: all 0.3s ease; | ← 过渡动画 0.3 秒(平滑过渡) |
| 30 | } | |
| 31 | </style> | |
| 32 | {% endblock %} | ← content 块结束 |
file_own/templates/registration/login.html — 用户登录页面
registration/ 目录下是 Django 的惯例——Django 内置的认证系统(auth)会自动查找这个路径下的 login.html。用户输入用户名和密码后提交表单,Django 会自动验证并登录。
| 1 | {% extends 'base.html' %} | ← 继承基础模板,复用导航栏和页脚 |
| 2 | ||
| 3 | {% block content %} | ← 开始内容块 |
| 4 | <div class="row justify-content-center"> | ← Bootstrap 栅格:居中显示登录卡片 |
| 5 | <div class="col-md-6"> | ← 占屏幕宽度的一半(6/12) |
| 6 | <div class="card"> | ← Bootstrap 卡片组件 |
| 7 | <div class="card-header"> | ← 卡片头部 |
| 8 | <h3>用户登录</h3> | ← 页面标题 |
| 9 | </div> | |
| 10 | <div class="card-body"> | ← 卡片内容区域 |
| 11 | {% if form.errors %} | ← 如果表单有错误(用户名或密码不对) |
| 12 | <div class="alert alert-danger"> | ← 红色警告提示框 |
| 13 | 用户名或密码错误,请重试。 | ← 错误提示文字 |
| 14 | </div> | |
| 15 | {% endif %} | ← 条件结束 |
| 16 | <form method="post" action="{% url 'login' %}"> | ← 表单提交到 Django 内置的 login 视图(POST 方式) |
| 17 | {% csrf_token %} | ← CSRF 安全令牌(防止跨站请求伪造) |
| 18 | <input type="hidden" name="next" value="{{ next }}"> | ← 隐藏字段:登录后自动跳转回之前访问的页面(Django 自动传入) |
| 19 | <label>用户名</label> | ← 用户名标签 |
| 20 | <input type="text" name="username" required> | ← 用户名输入框。name="username" 是 Django auth 要求的字段名 |
| 21 | <label>密码</label> | ← 密码标签 |
| 22 | <input type="password" name="password" required> | ← 密码输入框。type="password" 让输入内容显示为圆点 |
| 23 | <button type="submit">登录</button> | ← 登录按钮 |
| 24 | </form> | |
| 25 | </div> | |
| 26 | <div class="card-footer text-center"> | ← 卡片底部(居中对齐) |
| 27 | <p>还没有账号?<a href="{% url 'register' %}">注册</a></p> | ← 引导未注册用户去注册页面 |
| 28 | </div> | |
| 29 | </div> | |
| 30 | </div> | |
| 31 | </div> | |
| 32 | {% endblock %} | ← content 块结束 |
file_own/templates/registration/register.html — 用户注册页面
| 1 | {% extends 'base.html' %} | ← 继承基础模板 |
| 2 | ||
| 3 | {% block content %} | ← 内容块开始 |
| 4 | <div class="row justify-content-center"> | ← 居中布局 |
| 5 | <div class="col-md-6"> | ← 半宽卡片 |
| 6 | <div class="card"> | ← Bootstrap 卡片 |
| 7 | <div class="card-header"> | |
| 8 | <h3>用户注册</h3> | ← 页面标题 |
| 9 | </div> | |
| 10 | <div class="card-body"> | |
| 11 | {% if form.errors %} | ← 如果表单验证有错误 |
| 12 | <div class="alert alert-danger"> | ← 红色错误提示框 |
| 13 | <ul> | ← 错误列表(可能有多条错误,如"密码不一致"、"用户名已存在"等) |
| 14 | {% for field in form %} | ← 遍历表单的每个字段 |
| 15 | {% for error in field.errors %} | ← 遍历当前字段的所有错误 |
| 16 | <li>{{ error }}</li> | ← 显示每条错误信息 |
| 17 | {% endfor %} | |
| 18 | {% endfor %} | |
| 19 | {% for error in form.non_field_errors %} | ← 非字段级别的错误(如表单整体的验证错误) |
| 20 | <li>{{ error }}</li> | ← 显示非字段错误 |
| 21 | {% endfor %} | |
| 22 | </ul> | |
| 23 | </div> | |
| 24 | {% endif %} | ← 错误判断结束 |
| 25 | <form method="post"> | ← 注册表单(POST 提交到 register 视图) |
| 26 | {% csrf_token %} | ← CSRF 令牌 |
| 27 | <label>用户名</label> | ← 用户名标签 |
| 28 | {{ form.username }} | ← Django 自动渲染用户名输入框(UserRegistrationForm 中定义的字段) |
| 29 | <label>邮箱</label> | ← 邮箱标签 |
| 30 | {{ form.email }} | ← 邮箱输入框 |
| 31 | <label>密码</label> | ← 密码标签 |
| 32 | {{ form.password }} | ← 密码输入框 |
| 33 | <label>确认密码</label> | ← 确认密码标签 |
| 34 | {{ form.password_confirm }} | ← 确认密码输入框(后端会校验两次密码是否一致) |
| 35 | <button type="submit">注册</button> | ← 注册按钮 |
| 36 | </form> | |
| 37 | </div> | |
| 38 | <div class="card-footer text-center"> | ← 卡片底部 |
| 39 | <p>已有账号?<a href="{% url 'login' %}">登录</a></p> | ← 引导已有用户去登录页面 |
| 40 | </div> | |
| 41 | </div> | |
| 42 | </div> | |
| 43 | </div> | |
| 44 | {% endblock %} | ← content 块结束 |
file_own/templates/file_app/change_type.html — 修改文件类型页面
| 1 | {% extends 'base.html' %} | ← 继承基础模板 |
| 2 | ||
| 3 | {% block content %} | ← 内容块开始 |
| 4 | <div class="row justify-content-center"> | ← 居中布局 |
| 5 | <div class="col-md-6"> | ← 半宽卡片 |
| 6 | <div class="card"> | ← Bootstrap 卡片 |
| 7 | <div class="card-header"> | |
| 8 | <h4>修改文件类型</h4> | ← 页面标题 |
| 9 | </div> | |
| 10 | <div class="card-body"> | |
| 11 | <p><strong>文件名:</strong>{{ file.name }}</p> | ← 显示当前文件名 |
| 12 | <p><strong>当前类型:</strong>{{ file.get_file_type_display }}</p> | ← 显示当前类型的中文名称(调用 Django 的 get_FOO_display 方法) |
| 13 | <form method="post"> | ← 表单提交(POST 方式) |
| 14 | {% csrf_token %} | ← CSRF 安全令牌 |
| 15 | <label>选择新类型</label> | ← 下拉框标签 |
| 16 | <select name="file_type"> | ← 文件类型下拉选择框 |
| 17 | {% for key, display in file_types %} | ← 遍历所有可选的文件类型(7 种) |
| 18 | <option value="{{ key }}" | ← 每个 option 的 value 是类型代码 |
| 19 | {% if file.file_type == key %}selected{% endif %}> | ← 如果当前类型和这个 option 匹配,加上 selected(默认选中) |
| 20 | {{ display }} | ← 显示中文类型名(如"图片"、"视频"等) |
| 21 | </option> | |
| 22 | {% endfor %} | ← 循环结束 |
| 23 | </select> | |
| 24 | <a href="{% url 'home' %}">取消</a> | ← 取消按钮:回到首页 |
| 25 | <button type="submit">保存</button> | ← 保存按钮:提交修改 |
| 26 | </form> | |
| 27 | </div> | |
| 28 | </div> | |
| 29 | </div> | |
| 30 | </div> | |
| 31 | {% endblock %} | ← content 块结束 |
file_own/templates/file_app/recycle_bin.html — 回收站页面(恢复 + 彻底删除)
| 1 | {% extends 'base.html' %} | ← 继承基础模板 |
| 2 | ||
| 3 | {% block content %} | ← 内容块开始 |
| 4 | <div class="row mb-4"> | ← 页面头部行 |
| 5 | <div class="col-md-8"> | ← 左侧:标题和描述 |
| 6 | <h1><i class="bi bi-trash"></i> 回收站</h1> | ← 回收站标题 + 垃圾桶图标 |
| 7 | <p>已删除的文件会保留在这里,您可以恢复或彻底删除</p> | ← 说明文字 |
| 8 | </div> | |
| 9 | <div class="col-md-4 text-end"> | ← 右侧:返回按钮(右对齐) |
| 10 | <a href="{% url 'home' %}">返回主页</a> | ← 返回首页链接 |
| 11 | </div> | |
| 12 | </div> | |
| 13 | ||
| 14 | <div class="row" id="recycle-list"> | ← 回收站文件列表容器。id 用于 JS 定位 |
| 15 | {% for deleted_file in page_obj %} | ← 遍历当前页的已删除文件(分页数据) |
| 16 | <div class="col-xl-3 col-lg-4 col-md-6 mb-4" id="file-card-{{ deleted_file.id }}"> | ← 响应式卡片列。id 用于 AJAX 操作后定位并移除卡片 |
| 17 | <div class="card h-100 shadow-sm border-danger"> | ← 文件卡片。border-danger=红色边框(表示已删除) |
| 18 | <div class="card-body text-center"> | ← 卡片内容(居中排版) |
| 19 | <i class="bi bi-file-earmark-x"></i> | ← "已删除文件"图标(带 X 的文件图标) |
| 20 | <h6>{{ deleted_file.file_name|truncatechars:20 }}</h6> | ← 文件名(最多 20 字符) |
| 21 | <small>{{ deleted_file.file_size|filesizeformat }}</small> | ← 文件大小。filesizeformat 过滤器自动转为 KB/MB/GB |
| 22 | <p>{{ deleted_file.description|truncatechars:50|default:"无描述" }}</p> | ← 文件描述(最多 50 字符,空则显示"无描述") |
| 23 | <small>{{ deleted_file.deleted_at|date:'Y-m-d H:i' }}</small> | ← 删除时间 |
| 24 | </div> | |
| 25 | <div class="card-footer bg-white"> | ← 卡片底部:操作按钮区域 |
| 26 | <form class="restore-form" data-id="{{ deleted_file.id }}"> | ← 恢复表单。data-id 存储文件 ID(JS 通过 dataset.id 获取) |
| 27 | {% csrf_token %} | ← CSRF 令牌 |
| 28 | <button type="submit" class="btn btn-success">恢复</button> | ← 绿色恢复按钮(JS 拦截表单提交,改用 AJAX) |
| 29 | </form> | |
| 30 | <button onclick="showPermanentDeleteModal('{{ deleted_file.id }}', '{{ deleted_file.file_name }}')"> | ← 红色彻底删除按钮(弹出确认框) |
| 31 | </div> | |
| 32 | </div> | |
| 33 | </div> | |
| 34 | {% empty %} | ← 【空状态】回收站为空时显示 |
| 35 | <h4>回收站为空</h4> | ← 空状态提示 |
| 36 | <p>没有已删除的文件</p> | |
| 37 | {% endfor %} | ← 循环结束 |
| 38 | </div> | |
| 39 | ||
| 40 | {% if page_obj.has_other_pages %} | ← 分页导航(和首页类似) |
| 41 | <nav> | |
| 42 | <ul class="pagination justify-content-center"> | ← 居中的分页组件 |
| 43 | {% if page_obj.has_previous %} | |
| 44 | <a href="?page={{ page_obj.previous_page_number }}">上一页</a> | ← 上一页 |
| 45 | {% endif %} | |
| 46 | <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span> | ← 页码信息 |
| 47 | {% if page_obj.has_next %} | |
| 48 | <a href="?page={{ page_obj.next_page_number }}">下一页</a> | ← 下一页 |
| 49 | {% endif %} | |
| 50 | </ul> | |
| 51 | </nav> | |
| 52 | {% endif %} | |
| 53 | ||
| 54 | {# 彻底删除确认模态框 #} | ← Bootstrap 模态框:确认彻底删除的弹窗 |
| 55 | <div class="modal fade" id="permanentDeleteModal"> | ← 模态框容器 |
| 56 | <h5>彻底删除</h5> | ← 弹窗标题 |
| 57 | <button class="btn-close" data-bs-dismiss="modal"></button> | ← 关闭按钮(Bootstrap 内置属性,点击关闭弹窗) |
| 58 | <p>确定要永久删除 "<strong id="permanent-file-name"></strong>" 吗?</p> | ← 弹窗内容(JS 动态填入文件名) |
| 59 | <p class="text-danger fw-bold">此操作不可恢复!</p> | ← 红色警告:提醒用户不可恢复 |
| 60 | <button data-bs-dismiss="modal">取消</button> | ← 取消按钮 |
| 61 | <button id="confirm-permanent-delete-btn">彻底删除</button> | ← 确认彻底删除按钮(JS 绑定 AJAX 请求) |
| 62 | </div> | |
| 63 | {% endblock %} | ← content 块结束 |
| 64 | ||
| 65 | {% block extra_js %} | ← 额外 JS 块开始(回收站独有的 JavaScript 代码) |
| 66 | <script> | |
| 67 | function getCookie(name) { | ← 辅助函数:从浏览器 Cookie 中获取指定值 |
| 68 | let cookieValue = null; | ← 初始值为 null |
| 69 | if (document.cookie && document.cookie !== '') { | ← 如果浏览器有 Cookie |
| 70 | const cookies = document.cookie.split(';'); | ← 将所有 Cookie 按分号拆分为数组 |
| 71 | for (let i = 0; i < cookies.length; i++) { | ← 遍历每个 Cookie |
| 72 | const cookie = cookies[i].trim(); | ← 去除前后空格 |
| 73 | if (cookie.substring(0, name.length + 1) === (name + '=')) { | ← 如果 Cookie 名匹配 |
| 74 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); | ← 解码并获取 Cookie 值 |
| 75 | break; | ← 找到后跳出循环 |
| 76 | } | |
| 77 | } | |
| 78 | } | |
| 79 | return cookieValue; | ← 返回 Cookie 值 |
| 80 | } | |
| 81 | const csrftoken = getCookie('csrftoken'); | ← 获取 Django 设置的 CSRF Cookie(AJAX 请求需要携带) |
| 82 | ||
| 83 | // --- 恢复功能 AJAX --- | ← 恢复文件的 AJAX 逻辑 |
| 84 | document.querySelectorAll('.restore-form').forEach(form => { | ← 找到所有恢复表单并逐个绑定事件 |
| 85 | form.addEventListener('submit', function(e) { | ← 监听表单提交事件 |
| 86 | e.preventDefault(); | ← 阻止默认的表单提交行为(改用 AJAX) |
| 87 | const fileId = this.dataset.id; | ← 从 data-id 属性获取文件 ID |
| 88 | const btn = this.querySelector('.restore-btn'); | ← 获取按钮元素 |
| 89 | const originalText = btn.innerHTML; | ← 保存按钮原始文字(用于恢复) |
| 90 | btn.disabled = true; | ← 禁用按钮(防止重复点击) |
| 91 | btn.innerHTML = '加载动画...'; | ← 显示加载动画(spinner-border 旋转圈) |
| 92 | fetch(`/recycle-bin/${fileId}/restore/`, { | ← 发送 AJAX POST 请求到恢复接口 |
| 93 | method: 'POST', | ← POST 方法 |
| 94 | headers: { | |
| 95 | 'X-CSRFToken': csrftoken, | ← CSRF Token |
| 96 | 'X-Requested-With': 'XMLHttpRequest' | ← 标记为 AJAX 请求 |
| 97 | } | |
| 98 | }) | |
| 99 | .then(data => { | ← 请求成功后 |
| 100 | if (data.success) { | ← 如果后端返回 success=true |
| 101 | const card = document.getElementById(`file-card-${fileId}`); | ← 找到对应的卡片 |
| 102 | card.style.transition = 'all 0.5s ease'; | ← 设置过渡动画 |
| 103 | card.style.opacity = '0'; | ← 淡出效果 |
| 104 | setTimeout(() => card.remove(), 500); | ← 500ms 后从 DOM 移除(无刷新消失) |
| 105 | } else { | ← 如果恢复失败 |
| 106 | alert('恢复失败'); | ← 弹出错误提示 |
| 107 | btn.innerHTML = originalText; | ← 恢复按钮文字 |
| 108 | btn.disabled = false; | ← 重新启用按钮 |
| 109 | } | |
| 110 | }); | |
| 111 | }); | |
| 112 | }); | |
| 113 | ||
| 114 | // --- 彻底删除功能 AJAX --- | ← 彻底删除的 AJAX 逻辑 |
| 115 | let currentPermanentDeleteId = null; | ← 存储当前要彻底删除的文件 ID |
| 116 | const permDeleteModal = new bootstrap.Modal(...); | ← 创建 Bootstrap 模态框实例 |
| 117 | ||
| 118 | function showPermanentDeleteModal(fileId, fileName) { | ← 点击删除按钮时调用 |
| 119 | currentPermanentDeleteId = fileId; | ← 记住要删除的文件 ID |
| 120 | document.getElementById('permanent-file-name').textContent = fileName; | ← 在弹窗中显示文件名 |
| 121 | permDeleteModal.show(); | ← 打开弹窗 |
| 122 | } | |
| 123 | ||
| 124 | document.getElementById('confirm-permanent-delete-btn') | ← 获取"彻底删除"确认按钮 |
| 125 | .addEventListener('click', function() { | ← 点击时执行彻底删除 |
| 126 | const btn = this; | ← 按钮引用 |
| 127 | btn.disabled = true; | ← 禁用按钮 |
| 128 | btn.innerHTML = '删除中...'; | ← 显示加载状态 |
| 129 | fetch(`/recycle-bin/${currentPermanentDeleteId}/permanent-delete/`, { | ← 发送 AJAX POST 请求到彻底删除接口 |
| 130 | method: 'POST', | |
| 131 | headers: { 'X-CSRFToken': csrftoken } | ← 携带 CSRF Token |
| 132 | }) | |
| 133 | .then(data => { | |
| 134 | if (data.success) { | ← 删除成功 |
| 135 | const card = document.getElementById(`file-card-${currentPermanentDeleteId}`); | ← 找到卡片 |
| 136 | card.style.opacity = '0'; | ← 淡出 |
| 137 | card.style.transform = 'scale(0.9)'; | ← 缩小效果(增强视觉反馈) |
| 138 | setTimeout(() => card.remove(), 500); | ← 500ms 后移除 |
| 139 | permDeleteModal.hide(); | ← 关闭弹窗 |
| 140 | } else { | ← 删除失败 |
| 141 | alert('删除失败'); | ← 弹出错误提示 |
| 142 | } | |
| 143 | }) | |
| 144 | .finally(() => { | ← 无论成功或失败都执行 |
| 145 | btn.disabled = false; | ← 恢复按钮可用 |
| 146 | btn.innerHTML = originalText; | ← 恢复按钮文字 |
| 147 | }); | |
| 148 | }); | |
| 149 | </script> | |
| 150 | {% endblock %} | ← extra_js 块结束 |
file_own/static/css/style.css — 项目自定义样式表
| 1 | /* 自定义样式 */ | ← CSS 注释:标记以下为自定义样式 |
| 2 | body { | ← body 标签的样式(应用到整个页面) |
| 3 | background-color: #f8f9fa; | ← 页面背景色:浅灰色(比纯白更柔和) |
| 4 | } | |
| 5 | ||
| 6 | .file-icon { | ← 文件图标的容器样式 |
| 7 | width: 80px; | ← 宽度 80px |
| 8 | height: 80px; | ← 高度 80px |
| 9 | margin: 0 auto; | ← 水平居中(margin: 0 auto 是经典的居中技巧) |
| 10 | display: flex; | ← Flexbox 弹性布局 |
| 11 | align-items: center; | ← 垂直居中 |
| 12 | justify-content: center; | ← 水平居中(Flex 方式) |
| 13 | border-radius: 10px; | ← 圆角 10px |
| 14 | background-color: rgba(13, 110, 253, 0.1); | ← 浅蓝色半透明背景(Bootstrap 主色的 10% 透明度) |
| 15 | } | |
| 16 | ||
| 17 | .card { | ← 所有卡片的样式 |
| 18 | transition: transform 0.2s, box-shadow 0.2s; | ← 过渡动画:变形和阴影变化各 0.2 秒(让动画平滑) |
| 19 | } | |
| 20 | ||
| 21 | .card:hover { | ← 鼠标悬停在卡片上时的样式(伪类选择器) |
| 22 | transform: translateY(-5px); | ← 向上移动 5px(卡片"浮起"效果) |
| 23 | box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important; | ← 更深的阴影(增加立体感)。!important 覆盖 Bootstrap 默认值 |
| 24 | } | |
| 25 | ||
| 26 | .navbar-brand { | ← 导航栏品牌文字样式 |
| 27 | font-weight: bold; | ← 加粗 |
| 28 | } | |
| 29 | ||
| 30 | .btn { | ← 所有按钮的样式 |
| 31 | border-radius: 8px; | ← 圆角 8px(比 Bootstrap 默认的 4px 更圆润) |
| 32 | } | |
| 33 | ||
| 34 | .form-control:focus, | ← 输入框获得焦点时的样式(用户点击输入框时) |
| 35 | .form-select:focus { | ← 下拉框获得焦点时的样式 |
| 36 | border-color: #86b7fe; | ← 边框变为浅蓝色 |
| 37 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); | ← 蓝色发光效果(提示用户当前正在编辑哪个输入框) |
| 38 | } | |
| 39 | ||
| 40 | /* 响应式设计 */ | ← 媒体查询:针对小屏幕设备的样式调整 |
| 41 | @media (max-width: 768px) { | ← 当屏幕宽度 ≤ 768px(手机/小平板)时应用以下样式 |
| 42 | .file-icon { | ← 小屏幕上缩小文件图标 |
| 43 | width: 60px; height: 60px; | ← 缩小到 60px |
| 44 | } | |
| 45 | .card-body { | ← 小屏幕上缩小卡片内边距 |
| 46 | padding: 1rem; | ← 内边距缩小为 1rem |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | /* 加载动画 */ | ← 按钮加载状态的旋转动画 |
| 51 | .loading { | ← 加载指示器样式 |
| 52 | display: inline-block; | ← 行内块元素(可以和文字同行) |
| 53 | width: 20px; height: 20px; | ← 大小 20px |
| 54 | border: 3px solid rgba(255,255,255,.3); | ← 半透明白色边框(圆环底色) |
| 55 | border-radius: 50%; | ← 50% 圆角 → 变成圆形 |
| 56 | border-top-color: #fff; | ← 顶部边框为纯白色(旋转时产生"缺口"效果) |
| 57 | animation: spin 1s ease-in-out infinite; | ← 应用 spin 动画:1 秒一圈,无限循环 |
| 58 | } | |
| 59 | ||
| 60 | @keyframes spin { | ← 定义旋转动画 |
| 61 | to { transform: rotate(360deg); } | ← 从 0° 旋转到 360°(一圈) |
| 62 | } | |
| 63 | ||
| 64 | /* 进度条样式 */ | ← 上传进度条的样式 |
| 65 | .progress { | ← 进度条外框 |
| 66 | height: 10px; | ← 高度 10px |
| 67 | border-radius: 5px; | ← 圆角 |
| 68 | } | |
| 69 | .progress-bar { | ← 进度条内部填充 |
| 70 | border-radius: 5px; | ← 同样的圆角(保持外观一致) |
| 71 | } |
file_own/static/js/script.js — 项目公共 JavaScript 脚本
file_own/static/js/script.js — 项目公共 JavaScript 脚本
| 1 | // 文件上传进度处理 | ← 注释:说明本段代码的功能 |
| 2 | document.addEventListener('DOMContentLoaded', function() { | ← 【核心事件】当 HTML 文档完全加载完毕后执行回调函数(类似 jQuery 的 $(document).ready()) |
| 3 | // 文件上传表单处理 | ← 注释:处理上传表单 |
| 4 | const uploadForm = document.querySelector('form[method="post"]'); | ← 查找页面中 method="post" 的表单(即上传表单)。querySelector 返回第一个匹配的元素 |
| 5 | if(uploadForm) { | ← 如果找到了上传表单(只有在上传页才会存在) |
| 6 | uploadForm.addEventListener('submit', function(e) { | ← 监听表单的 submit(提交)事件 |
| 7 | const submitBtn = this.querySelector('button[type="submit"]'); | ← 找到表单中的提交按钮 |
| 8 | if(submitBtn) { | ← 如果按钮存在 |
| 9 | submitBtn.innerHTML = '<span class="loading"></span> 上传中...'; | ← 替换按钮内容为加载动画 + "上传中..."文字(利用 style.css 中定义的 .loading 旋转动画) |
| 10 | submitBtn.disabled = true; | ← 禁用按钮(防止用户重复点击导致多次提交) |
| 11 | } | |
| 12 | }); | |
| 13 | } | |
| 14 | ||
| 15 | // 文件预览 | ← 注释:文件选择后的预览/调试功能 |
| 16 | const fileInput = document.querySelector('input[type="file"]'); | ← 查找文件上传控件(<input type="file">) |
| 17 | if(fileInput) { | ← 如果找到了文件输入框 |
| 18 | fileInput.addEventListener('change', function(e) { | ← 监听 change 事件(用户选择文件后触发) |
| 19 | const file = e.target.files[0]; | ← 获取用户选择的第一个文件(File 对象,包含 name、size 等属性) |
| 20 | if(file) { | ← 如果确实选择了文件 |
| 21 | console.log(`选择了文件: ${file.name}, 大小: ${file.size} bytes`); | ← 在浏览器控制台输出文件信息(调试用)。模板字符串用反引号和 ${} 插入变量 |
| 22 | } | |
| 23 | }); | |
| 24 | } | |
| 25 | }); | ← DOMContentLoaded 事件回调结束 |
| 26 | ||
| 27 | // 确认下载 | ← 注释:下载确认功能 |
| 28 | function confirmDownload(url, fileName) { | ← 全局函数:下载前弹出确认框。可在 HTML 的 onclick 中调用 |
| 29 | if(confirm(`确定要下载文件 "${fileName}" 吗?`)) { | ← confirm() 弹出浏览器原生确认对话框,返回 true/false |
| 30 | window.location.href = url; | ← 用户确认后,跳转到下载 URL(触发文件下载) |
| 31 | } | |
| 32 | } | ← 函数结束 |
| 核心模块 | models.py → views.py → urls.py → templates/ |
| 数据流向 | 用户请求 → URL 路由 → 视图函数 → 模型操作数据库 → 模板渲染响应 |
| 安全机制 | CSRF Token + 登录验证 @login_required + 软删除模式 |
| 前端交互 | Bootstrap 5 组件 + fetch API (AJAX) + CSS 动画 |
| 文件总数 | 25 个核心文件(已全部注释完毕 ✅) |
— 文档完毕 · 共 25 个文件 · 逐行中文注释 —