Initial commit

This commit is contained in:
2025-01-05 10:45:32 +08:00
commit 7634e66541
55 changed files with 2922 additions and 0 deletions

BIN
db.sqlite3 Normal file

Binary file not shown.

0
fileshare/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
fileshare/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for fileshare 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.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fileshare.settings")
application = get_asgi_application()

139
fileshare/settings.py Normal file
View File

@@ -0,0 +1,139 @@
"""
Django settings for fileshare project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
# 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.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-c$znmkzi@t&!x)r!7(-g6h++fu3hi!ml&_nqmvcigt2$x=6xi&"
# 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",
"main",
]
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 = "fileshare.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / 'templates'],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "fileshare.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/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.1/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.1/howto/static-files/
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Login redirect
LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'home'
# CSRF settings
CSRF_COOKIE_SECURE = False # Set to True in production with HTTPS
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_TRUSTED_ORIGINS = ['http://localhost:8000']
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'

31
fileshare/urls.py Normal file
View File

@@ -0,0 +1,31 @@
"""
URL configuration for fileshare project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from main import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('main.urls')),
path('accounts/', include([
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
path('register/', views.register, name='register'),
])),
]

16
fileshare/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for fileshare 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.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fileshare.settings")
application = get_wsgi_application()

3
input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

0
main/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
main/admin.py Normal file
View File

@@ -0,0 +1,34 @@
from django.contrib import admin
from .models import File, Message, PublicMessage, PublicFile, Friendship
@admin.register(File)
class FileAdmin(admin.ModelAdmin):
list_display = ('file', 'owner', 'created_at')
list_filter = ('owner', 'created_at')
search_fields = ('file', 'description')
raw_id_fields = ('shared_with',)
@admin.register(Message)
class MessageAdmin(admin.ModelAdmin):
list_display = ('sender', 'created_at')
list_filter = ('sender', 'created_at')
search_fields = ('content',)
raw_id_fields = ('recipients',)
@admin.register(PublicMessage)
class PublicMessageAdmin(admin.ModelAdmin):
list_display = ('name', 'created_at', 'ip_address')
list_filter = ('created_at',)
search_fields = ('content',)
@admin.register(PublicFile)
class PublicFileAdmin(admin.ModelAdmin):
list_display = ('name', 'file', 'created_at', 'ip_address')
list_filter = ('created_at',)
search_fields = ('file', 'description')
@admin.register(Friendship)
class FriendshipAdmin(admin.ModelAdmin):
list_display = ('from_user', 'to_user', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('from_user__username', 'to_user__username')

6
main/apps.py Normal file
View File

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

112
main/forms.py Normal file
View File

@@ -0,0 +1,112 @@
from django import forms
from django.core import validators
from .models import File, Message, PublicMessage, PublicFile, Friendship
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
labels = {
'username': '用户名',
'password1': '密码',
'password2': '确认密码'
}
help_texts = {
'username': '150个字符以内。可以包含中文、字母、数字和 @/./+/-/_。',
'password1': '您的密码不能与其他个人信息太相似。\n您的密码必须包含至少 8 个字符。\n您的密码不能是常用密码。\n您的密码不能全部是数字。',
'password2': '再次输入密码以确认。'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 修改用户名字段的正则验证
self.fields['username'].validators = [
validators.RegexValidator(
r'^[\w\u4e00-\u9fa5@.+-_]+$',
'用户名只能包含中文、字母、数字和 @/./+/-/_。'
)
]
class FileUploadForm(forms.ModelForm):
is_public = forms.BooleanField(
required=False,
label='设为公开文件',
help_text='如果勾选,此文件将对所有用户可见'
)
class Meta:
model = File
fields = ['file', 'description', 'shared_with', 'is_public']
labels = {
'file': '文件',
'description': '描述',
'shared_with': '共享用户'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['shared_with'].required = False
class MessageForm(forms.ModelForm):
class Meta:
model = Message
fields = ['recipients', 'content', 'file']
labels = {
'recipients': '收件人',
'content': '消息内容',
'file': '附件'
}
class PublicMessageForm(forms.ModelForm):
class Meta:
model = PublicMessage
fields = ['content']
labels = {
'content': '留言内容'
}
widgets = {
'content': forms.Textarea(attrs={'rows': 3, 'placeholder': '您的留言...'}),
}
class FriendRequestForm(forms.Form):
username = forms.CharField(
label='用户名',
max_length=150,
widget=forms.TextInput(attrs={
'placeholder': '输入要添加的好友用户名',
'class': 'w-full px-3 py-2 border rounded-md'
})
)
def clean_username(self):
username = self.cleaned_data['username']
User = get_user_model()
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError('用户不存在')
return user
class FriendActionForm(forms.Form):
ACTION_CHOICES = [
('accept', '接受'),
('reject', '拒绝'),
]
action = forms.ChoiceField(
choices=ACTION_CHOICES,
widget=forms.RadioSelect,
label='操作'
)
class PublicFileForm(forms.ModelForm):
class Meta:
model = PublicFile
fields = ['file', 'description']
labels = {
'file': '文件',
'description': '描述'
}
widgets = {
'description': forms.Textarea(attrs={'rows': 3, 'placeholder': '文件描述...'}),
}

View File

@@ -0,0 +1,92 @@
# Generated by Django 5.1.4 on 2025-01-03 14:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="File",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("file", models.FileField(upload_to="uploads/")),
("created_at", models.DateTimeField(auto_now_add=True)),
("description", models.TextField(blank=True)),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to=settings.AUTH_USER_MODEL,
),
),
(
"shared_with",
models.ManyToManyField(
blank=True,
related_name="shared_files",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="Message",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"file",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.file",
),
),
(
"recipients",
models.ManyToManyField(
related_name="received_messages", to=settings.AUTH_USER_MODEL
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at"],
},
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.1.4 on 2025-01-04 12:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="PublicMessage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(blank=True, max_length=100)),
("content", models.TextField()),
(
"file",
models.FileField(
blank=True, null=True, upload_to="public_uploads/"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
],
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.1.4 on 2025-01-04 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0002_publicmessage"),
]
operations = [
migrations.CreateModel(
name="PublicFile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(blank=True, max_length=100)),
("file", models.FileField(upload_to="public_uploads/")),
("description", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name="publicmessage",
name="file",
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.4 on 2025-01-04 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0003_publicfile_remove_publicmessage_file"),
]
operations = [
migrations.AddField(
model_name="publicfile",
name="ip_address",
field=models.GenericIPAddressField(blank=True, null=True),
),
migrations.AddField(
model_name="publicmessage",
name="ip_address",
field=models.GenericIPAddressField(blank=True, null=True),
),
migrations.AlterField(
model_name="publicfile",
name="name",
field=models.CharField(default="公共", editable=False, max_length=100),
),
migrations.AlterField(
model_name="publicmessage",
name="name",
field=models.CharField(default="公共", editable=False, max_length=100),
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.1.4 on 2025-01-04 15:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0004_publicfile_ip_address_publicmessage_ip_address_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Friendship",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.CharField(
choices=[
("pending", "等待中"),
("accepted", "已接受"),
("rejected", "已拒绝"),
],
default="pending",
max_length=10,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"from_user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="friendship_requests_sent",
to=settings.AUTH_USER_MODEL,
),
),
(
"to_user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="friendship_requests_received",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at"],
"unique_together": {("from_user", "to_user")},
},
),
]

View File

Binary file not shown.

63
main/models.py Normal file
View File

@@ -0,0 +1,63 @@
from django.db import models
from django.contrib.auth.models import User
class File(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='files')
file = models.FileField(upload_to='uploads/')
shared_with = models.ManyToManyField(User, related_name='shared_files', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
description = models.TextField(blank=True)
def __str__(self):
return f"{self.file.name}{self.owner.username}"
class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
recipients = models.ManyToManyField(User, related_name='received_messages')
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
file = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self):
return f"来自 {self.sender.username} 的消息"
class Meta:
ordering = ['-created_at']
class PublicMessage(models.Model):
name = models.CharField(max_length=100, default="公共", editable=False)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
def __str__(self):
return f"来自 {self.name or '匿名用户'} 的公开留言"
class PublicFile(models.Model):
name = models.CharField(max_length=100, default="公共", editable=False)
file = models.FileField(upload_to='public_uploads/')
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
def __str__(self):
return f"来自 {self.name or '匿名用户'} 的公开文件"
class Friendship(models.Model):
STATUS_CHOICES = [
('pending', '等待中'),
('accepted', '已接受'),
('rejected', '已拒绝'),
]
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_requests_sent')
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_requests_received')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('from_user', 'to_user')
ordering = ['-created_at']
def __str__(self):
return f"{self.from_user.username} -> {self.to_user.username} ({self.status})"

View File

@@ -0,0 +1,89 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-3xl font-bold mb-6">您的仪表板</h1>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">您的文件</h2>
<div class="bg-white p-6 rounded-lg shadow">
{% if files %}
<ul class="divide-y divide-gray-200">
{% for file in files %}
<li class="py-4 flex items-center justify-between">
<div>
<a href="{{ file.file.url }}" class="text-blue-500 hover:text-blue-700">
{{ file.file.name }}
</a>
<p class="text-gray-600 text-sm">{{ file.description }}</p>
</div>
<form action="{% url 'delete_file' file.id %}" method="post">
{% csrf_token %}
<button type="submit" class="text-red-500 hover:text-red-700">
Delete
</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">尚未上传文件。</p>
{% endif %}
<div class="mt-4 flex space-x-4">
<a href="{% url 'upload' %}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
上传新文件
</a>
<a href="{% url 'send_message' %}" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
发送消息
</a>
</div>
</div>
</div>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">共享文件</h2>
<div class="bg-white p-6 rounded-lg shadow">
{% if shared_files %}
<ul class="divide-y divide-gray-200">
{% for file in shared_files %}
<li class="py-4">
<a href="{{ file.file.url }}" class="text-blue-500 hover:text-blue-700">
{{ file.file.name }}
</a>
<p class="text-gray-600 text-sm">Shared by {{ file.owner.username }}</p>
<p class="text-gray-600 text-sm">{{ file.description }}</p>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">没有共享文件。</p>
{% endif %}
</div>
</div>
<div>
<h2 class="text-xl font-semibold mb-4">消息</h2>
<div class="bg-white p-6 rounded-lg shadow">
{% if messages %}
<ul class="divide-y divide-gray-200">
{% for message in messages %}
<li class="py-4">
<p class="text-gray-800">{{ message.content }}</p>
<p class="text-gray-600 text-sm">From {{ message.sender.username }}</p>
{% if message.file %}
<p class="text-gray-600 text-sm">
File: <a href="{{ message.file.file.url }}" class="text-blue-500 hover:text-blue-700">
{{ message.file.file.name }}
</a>
</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">没有消息。</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,132 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-3xl font-bold mb-6">欢迎使用文件共享系统</h1>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">公共留言</h2>
<div class="bg-gray-50 p-6 rounded-lg shadow mb-8">
{% if public_messages %}
<ul class="divide-y divide-gray-200">
{% for message in public_messages %}
<li class="py-4">
<div class="flex justify-between items-start">
<div>
<p class="font-medium">{{ message.name|default:"匿名" }}</p>
<p class="text-gray-600">{{ message.content }}</p>
<p class="text-xs text-gray-400 mt-1">IP: {{ message.ip_address }}</p>
{% if message.file %}
<a href="{{ message.file.url }}" class="text-blue-500 hover:text-blue-700 mt-2 inline-block">
下载附件: {{ message.file.name }}
</a>
{% endif %}
</div>
<span class="text-sm text-gray-500">{{ message.created_at|date:"Y-m-d H:i" }}</span>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">还没有留言。</p>
{% endif %}
</div>
<h2 class="text-xl font-semibold mb-4">公共文件</h2>
<div class="bg-gray-50 p-6 rounded-lg shadow">
{% if public_files %}
<ul class="divide-y divide-gray-200">
{% for file in public_files %}
<li class="py-4">
<div class="flex justify-between items-start">
<div>
<a href="{{ file.file.url }}" class="text-blue-500 hover:text-blue-700">
{{ file.file.name }}
</a>
<p class="text-gray-600 text-sm">上传者:{{ file.name|default:"匿名用户" }}</p>
<p class="text-gray-600 text-sm">{{ file.description }}</p>
<p class="text-xs text-gray-400 mt-1">IP: {{ file.ip_address }}</p>
</div>
<span class="text-sm text-gray-500">{{ file.created_at|date:"Y-m-d H:i" }}</span>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">没有可用的公共文件。</p>
{% endif %}
</div>
<h2 class="text-xl font-semibold mb-4">提交留言</h2>
<div class="bg-blue-50 p-6 rounded-lg shadow mb-8">
<form method="post" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="submit_message" value="1">
<div>
<label for="id_name" class="block text-sm font-medium text-gray-700 mb-1">
姓名
</label>
<input type="text" name="name" id="id_name" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="id_content" class="block text-sm font-medium text-gray-700 mb-1">
留言内容
</label>
<textarea name="content" id="id_content" rows="4" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<button type="submit"
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors duration-300">
提交留言
</button>
</form>
</div>
<h2 class="text-xl font-semibold mb-4">提交文件</h2>
<div class="bg-blue-50 p-6 rounded-lg shadow mb-8">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="submit_file" value="1">
<div>
<label for="id_name" class="block text-sm font-medium text-gray-700 mb-1">
姓名
</label>
<input type="text" name="name" id="id_name" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="id_description" class="block text-sm font-medium text-gray-700 mb-1">
文件描述
</label>
<textarea name="description" id="id_description" rows="3" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div>
<label for="id_file" class="block text-sm font-medium text-gray-700 mb-1">
选择文件
</label>
<input type="file" name="file" id="id_file" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100">
</div>
<button type="submit"
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors duration-300">
提交文件
</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">好友管理</h1>
<div class="space-y-8">
<!-- 待处理的好友请求 -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">待处理的好友请求</h2>
{% if pending_requests %}
<ul class="divide-y divide-gray-200">
{% for request in pending_requests %}
<li class="py-4">
<div class="flex justify-between items-center">
<div>
<p class="font-medium">{{ request.from_user.username }}</p>
<p class="text-sm text-gray-500">{{ request.created_at|date:"Y-m-d H:i" }}</p>
</div>
<form method="post" action="{% url 'handle_friend_request' request.id %}">
{% csrf_token %}
<div class="flex space-x-2">
<button type="submit" name="action" value="accept"
class="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600">
接受
</button>
<button type="submit" name="action" value="reject"
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
拒绝
</button>
</div>
</form>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">没有待处理的好友请求。</p>
{% endif %}
</div>
<!-- 好友列表 -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">好友列表</h2>
{% if friends %}
<ul class="divide-y divide-gray-200">
{% for friend in friends %}
<li class="py-4">
<div class="flex justify-between items-center">
<div>
<p class="font-medium">
{% if friend.from_user == request.user %}
{{ friend.to_user.username }}
{% else %}
{{ friend.from_user.username }}
{% endif %}
</p>
<p class="text-sm text-gray-500">成为好友时间:{{ friend.created_at|date:"Y-m-d H:i" }}</p>
</div>
<form method="post" action="{% url 'remove_friend' friend.id %}">
{% csrf_token %}
<button type="submit"
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
删除好友
</button>
</form>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-gray-600">您还没有好友。</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Search Files</h1>
<form method="get" class="mb-8">
<div class="flex">
<input type="text" name="q" value="{{ query }}"
class="flex-1 px-4 py-2 border rounded-l-lg focus:outline-none"
placeholder="搜索文件名...">
<button type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold px-4 rounded-r-lg">
Search
</button>
</div>
</form>
{% if results %}
<div class="bg-white p-6 rounded-lg shadow">
<ul class="divide-y divide-gray-200">
{% for file in results %}
<li class="py-4">
<a href="{{ file.file.url }}" class="text-blue-500 hover:text-blue-700">
{{ file.file.name }}
</a>
<p class="text-gray-600 text-sm">Uploaded by {{ file.owner.username }}</p>
<p class="text-gray-600 text-sm">{{ file.description }}</p>
</li>
{% endfor %}
</ul>
</div>
{% elif query %}
<p class="text-gray-600">No files found matching "{{ query }}".</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">发送好友请求</h1>
<div class="bg-white p-6 rounded-lg shadow">
<form method="post">
{% csrf_token %}
<div class="space-y-4">
{{ form.as_p }}
</div>
<button type="submit"
class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
发送请求
</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Send Message</h1>
<form method="post" class="bg-white p-6 rounded-lg shadow">
{% csrf_token %}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
收件人
</label>
{{ form.recipients }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
消息内容
</label>
{{ form.content }}
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
附加文件(可选)
</label>
{{ form.file }}
</div>
<div class="flex items-center justify-between">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit">
Send
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-4xl mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">Upload File</h1>
<form method="post" enctype="multipart/form-data" class="bg-white p-6 rounded-lg shadow">
{% csrf_token %}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
文件
</label>
{{ form.file }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
描述
</label>
{{ form.description }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
与特定用户共享(可选)
</label>
{{ form.shared_with }}
</div>
<div class="mb-6">
<label class="flex items-center space-x-2">
{{ form.is_public }}
<span class="text-gray-700 text-sm">设为公开文件</span>
</label>
<p class="text-gray-500 text-xs mt-1">公开文件对所有用户可见</p>
</div>
<div class="flex items-center justify-between">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit">
Upload
</button>
</div>
</form>
</div>
{% endblock %}

3
main/tests.py Normal file
View File

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

14
main/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('dashboard/', views.dashboard, name='dashboard'),
path('upload/', views.upload_file, name='upload'),
path('delete/<int:file_id>/', views.delete_file, name='delete_file'),
path('send-message/', views.send_message, name='send_message'),
path('search/', views.search_files, name='search'),
path('friends/', views.manage_friends, name='manage_friends'),
path('friends/send-request/', views.send_friend_request, name='send_friend_request'),
path('friends/handle-request/<int:request_id>/', views.handle_friend_request, name='handle_friend_request'),
]

176
main/views.py Normal file
View File

@@ -0,0 +1,176 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from .forms import CustomUserCreationForm
from django.contrib.auth.decorators import login_required
from .models import File, Message, PublicMessage, PublicFile, Friendship
from .forms import FileUploadForm, MessageForm, PublicMessageForm, PublicFileForm, FriendRequestForm, FriendActionForm
def home(request):
public_files = PublicFile.objects.all().order_by('-created_at')
public_messages = PublicMessage.objects.all().order_by('-created_at')
if request.method == 'POST':
if 'submit_message' in request.POST:
form = PublicMessageForm(request.POST)
if form.is_valid():
message = form.save(commit=False)
message.ip_address = request.META.get('REMOTE_ADDR')
message.save()
return redirect('home')
elif 'submit_file' in request.POST:
file_form = PublicFileForm(request.POST, request.FILES)
if file_form.is_valid():
public_file = file_form.save(commit=False)
public_file.ip_address = request.META.get('REMOTE_ADDR')
public_file.save()
return redirect('home')
else:
form = PublicMessageForm()
file_form = PublicFileForm()
return render(request, 'main/home.html', {
'public_files': public_files,
'public_messages': public_messages,
'form': form,
'file_form': file_form
})
@login_required
def dashboard(request):
files = File.objects.filter(owner=request.user)
shared_files = File.objects.filter(shared_with=request.user)
messages = Message.objects.filter(recipients=request.user)
return render(request, 'main/dashboard.html', {
'files': files,
'shared_files': shared_files,
'messages': messages
})
@login_required
def upload_file(request):
if request.method == 'POST':
form = FileUploadForm(request.POST, request.FILES)
if form.is_valid():
file = form.save(commit=False)
file.owner = request.user
# Handle public file sharing
if form.cleaned_data['is_public']:
file.shared_with.clear()
file.save()
form.save_m2m()
return redirect('dashboard')
else:
form = FileUploadForm()
return render(request, 'main/upload.html', {'form': form})
@login_required
def delete_file(request, file_id):
file = get_object_or_404(File, id=file_id, owner=request.user)
if request.method == 'POST':
file.delete()
return redirect('dashboard')
@login_required
def send_message(request):
if request.method == 'POST':
form = MessageForm(request.POST)
if form.is_valid():
message = form.save(commit=False)
message.sender = request.user
message.save()
form.save_m2m()
return redirect('dashboard')
else:
form = MessageForm()
return render(request, 'main/send_message.html', {'form': form})
def register(request):
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('dashboard')
else:
# 添加错误提示
return render(request, 'registration/register.html', {
'form': form,
'error': '注册失败,请检查输入信息'
})
else:
form = CustomUserCreationForm()
return render(request, 'registration/register.html', {'form': form})
@login_required
def search_files(request):
query = request.GET.get('q')
results = []
if query:
results = File.objects.filter(
file__icontains=query,
owner=request.user
) | File.objects.filter(
file__icontains=query,
shared_with=request.user
)
return render(request, 'main/search.html', {
'results': results,
'query': query
})
@login_required
def send_friend_request(request):
if request.method == 'POST':
form = FriendRequestForm(request.POST)
if form.is_valid():
to_user = form.cleaned_data['username']
# 检查是否已经存在好友请求
if not Friendship.objects.filter(
from_user=request.user,
to_user=to_user
).exists():
Friendship.objects.create(
from_user=request.user,
to_user=to_user,
status='pending'
)
return redirect('manage_friends')
else:
form = FriendRequestForm()
return render(request, 'main/send_friend_request.html', {'form': form})
@login_required
def manage_friends(request):
# 获取所有待处理的好友请求
pending_requests = Friendship.objects.filter(
to_user=request.user,
status='pending'
)
# 获取已接受的好友列表
friends = Friendship.objects.filter(
models.Q(from_user=request.user) | models.Q(to_user=request.user),
status='accepted'
).select_related('from_user', 'to_user')
return render(request, 'main/manage_friends.html', {
'pending_requests': pending_requests,
'friends': friends
})
@login_required
def handle_friend_request(request, request_id):
friendship = get_object_or_404(Friendship, id=request_id, to_user=request.user)
if request.method == 'POST':
form = FriendActionForm(request.POST)
if form.is_valid():
action = form.cleaned_data['action']
if action == 'accept':
friendship.status = 'accepted'
else:
friendship.status = 'rejected'
friendship.save()
return redirect('manage_friends')

22
manage.py Normal file
View File

@@ -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", "fileshare.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()

568
output.css Normal file
View File

@@ -0,0 +1,568 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
.static {
position: static;
}
.block {
display: block;
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}

Binary file not shown.

856
static/css/output.css Normal file
View File

@@ -0,0 +1,856 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
.visible {
visibility: visible;
}
.static {
position: static;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-4 {
margin-top: 1rem;
}
.block {
display: block;
}
.flex {
display: flex;
}
.w-full {
width: 100%;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-6xl {
max-width: 72rem;
}
.max-w-md {
max-width: 28rem;
}
.flex-1 {
flex: 1 1 0%;
}
.appearance-none {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1));
}
.rounded {
border-radius: 0.25rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-l-lg {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.rounded-r-lg {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.border {
border-width: 1px;
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.p-6 {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.leading-tight {
line-height: 1.25;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
}
.hover\:bg-green-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1));
}
.hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity, 1));
}
.hover\:text-gray-900:hover {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.hover\:text-red-700:hover {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./templates/**/*.html',
'./main/templates/**/*.html',
],
theme: {
extend: {},
},
plugins: [],
}

BIN
tailwindcss-windows-x64.exe Normal file

Binary file not shown.

57
templates/base.html Normal file
View File

@@ -0,0 +1,57 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Share System</title>
<link href="{% static 'css/output.css' %}" rel="stylesheet">
</head>
<body class="bg-gray-100">
<nav class="bg-white shadow">
<div class="max-w-6xl mx-auto px-4">
<div class="flex justify-between">
<div class="flex space-x-4">
<div>
<a href="{% url 'home' %}" class="flex items-center py-5 px-2 text-gray-700 hover:text-gray-900">
首页
</a>
</div>
{% if user.is_authenticated %}
<div>
<a href="{% url 'search' %}" class="flex items-center py-5 px-2 text-gray-700 hover:text-gray-900">
搜索
</a>
</div>
{% endif %}
</div>
<div class="flex items-center space-x-1">
{% if user.is_authenticated %}
<a href="{% url 'dashboard' %}" class="py-5 px-3 text-gray-700 hover:text-gray-900">仪表盘</a>
<a href="{% url 'upload' %}" class="py-5 px-3 text-gray-700 hover:text-gray-900">上传文件</a>
{% if user.is_authenticated %}
<a href="{% url 'admin:index' %}" class="py-5 px-3 text-gray-700 hover:text-gray-900">管理后台</a>
{% endif %}
<span class="py-5 px-3 text-gray-700">{{ user.username }}</span>
<form action="{% url 'logout' %}" method="post" class="inline" onsubmit="return confirmLogout()">
{% csrf_token %}
<button type="submit" class="py-5 px-3 text-gray-700 hover:text-gray-900">退出</button>
</form>
<script>
function confirmLogout() {
return confirm('确定要退出登录吗?');
}
</script>
{% else %}
<a href="{% url 'login' %}" class="py-5 px-3 text-gray-700 hover:text-gray-900">登录</a>
<a href="{% url 'register' %}" class="py-5 px-3 text-gray-700 hover:text-gray-900">注册</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% block content %}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-md mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">登录</h1>
{% if form.errors %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
用户名或密码错误,请重试
</div>
{% endif %}
<form method="post" class="bg-white p-6 rounded-lg shadow">
{% csrf_token %}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
用户名
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
type="text" name="username" required>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
密码
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
type="password" name="password" required>
</div>
<div class="flex items-center justify-between">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit">
登录
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-md mx-auto py-8 text-center">
<h1 class="text-2xl font-bold mb-6">您已成功退出</h1>
<p class="mb-6">正在跳转到首页...</p>
<div class="spinner"></div>
</div>
<script>
setTimeout(function() {
window.location.href = "{% url 'home' %}";
}, 2000);
</script>
<style>
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-md mx-auto py-8">
<h1 class="text-2xl font-bold mb-6">注册</h1>
{% if error %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{{ error }}
</div>
{% endif %}
<form method="post" class="bg-white p-6 rounded-lg shadow">
{% csrf_token %}
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
用户名
</label>
{{ form.username }}
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password1">
密码
</label>
{{ form.password1 }}
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password2">
确认密码
</label>
{{ form.password2 }}
</div>
<div class="flex items-center justify-between">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit">
注册
</button>
</div>
</form>
</div>
{% endblock %}