diff --git a/.gitignore b/.gitignore
index a83cc85..c39b2e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,3 +71,6 @@ node_modules/
/data
**/__pycache__
+links/__pycache__/
+
+staticfiles/
diff --git a/data/db.sqlite3 b/data/db.sqlite3
index 8dcfc9a..0e914eb 100644
Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ
diff --git a/links/__pycache__/forms.cpython-312.pyc b/links/__pycache__/forms.cpython-312.pyc
deleted file mode 100644
index 50ca4ef..0000000
Binary files a/links/__pycache__/forms.cpython-312.pyc and /dev/null differ
diff --git a/links/__pycache__/models.cpython-312.pyc b/links/__pycache__/models.cpython-312.pyc
deleted file mode 100644
index b290253..0000000
Binary files a/links/__pycache__/models.cpython-312.pyc and /dev/null differ
diff --git a/links/__pycache__/urls.cpython-312.pyc b/links/__pycache__/urls.cpython-312.pyc
deleted file mode 100644
index 2e7ba5e..0000000
Binary files a/links/__pycache__/urls.cpython-312.pyc and /dev/null differ
diff --git a/links/forms.py b/links/forms.py
index 0178869..d9ce794 100644
--- a/links/forms.py
+++ b/links/forms.py
@@ -1,13 +1,26 @@
from django import forms
from .models import Link
-from django.utils.translation import gettext_lazy as _
+from simplemde.fields import SimpleMDEField
class LinkForm(forms.ModelForm):
+ text = SimpleMDEField()
+
class Meta:
model = Link
- fields = ['alias', 'original_url']
- error_messages = {
- 'alias': {
- 'unique': _("This alias already exists. Please choose a different one."),
- }
+ fields = ['alias', 'link_type', 'original_url', 'text']
+ widgets = {
+ 'link_type': forms.RadioSelect(),
}
+
+ def clean(self):
+ cleaned_data = super().clean()
+ link_type = cleaned_data.get('link_type')
+ original_url = cleaned_data.get('original_url')
+ text = cleaned_data.get('text')
+
+ if link_type == Link.LinkType.LINK and not original_url:
+ raise forms.ValidationError(_("Original URL is required for Link type."))
+ elif link_type == Link.LinkType.CUSTOM and not text:
+ raise forms.ValidationError(_("Text is required for Custom type."))
+
+ return cleaned_data
diff --git a/links/migrations/0004_link_link_type_link_text_alter_link_alias_and_more.py b/links/migrations/0004_link_link_type_link_text_alter_link_alias_and_more.py
new file mode 100644
index 0000000..d17db07
--- /dev/null
+++ b/links/migrations/0004_link_link_type_link_text_alter_link_alias_and_more.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.0.8 on 2024-10-05 06:26
+
+import ckeditor.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('links', '0003_clicklog'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='link',
+ name='link_type',
+ field=models.CharField(choices=[('LINK', 'Link'), ('CUSTOM', 'Custom')], default='LINK', max_length=10),
+ ),
+ migrations.AddField(
+ model_name='link',
+ name='text',
+ field=ckeditor.fields.RichTextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='link',
+ name='alias',
+ field=models.SlugField(max_length=100, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='link',
+ name='original_url',
+ field=models.URLField(blank=True, null=True),
+ ),
+ ]
diff --git a/links/migrations/0005_alter_link_text.py b/links/migrations/0005_alter_link_text.py
new file mode 100644
index 0000000..0047f05
--- /dev/null
+++ b/links/migrations/0005_alter_link_text.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.8 on 2024-10-05 07:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('links', '0004_link_link_type_link_text_alter_link_alias_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='link',
+ name='text',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/links/migrations/__pycache__/0001_initial.cpython-312.pyc b/links/migrations/__pycache__/0001_initial.cpython-312.pyc
deleted file mode 100644
index bb3e09f..0000000
Binary files a/links/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ
diff --git a/links/migrations/__pycache__/__init__.cpython-312.pyc b/links/migrations/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 995ab75..0000000
Binary files a/links/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/links/models.py b/links/models.py
index bff1f2f..76df526 100644
--- a/links/models.py
+++ b/links/models.py
@@ -1,11 +1,22 @@
from django.db import models
+from django.utils.translation import gettext_lazy as _
class Link(models.Model):
- alias = models.CharField(max_length=50, unique=True, db_index=True)
- original_url = models.URLField()
+ class LinkType(models.TextChoices):
+ LINK = 'LINK', _('Link')
+ CUSTOM = 'CUSTOM', _('Custom')
+
+ alias = models.SlugField(max_length=100, unique=True)
+ original_url = models.URLField(blank=True, null=True)
+ text = models.TextField(blank=True, null=True) # 改为 TextField
+ link_type = models.CharField(
+ max_length=10,
+ choices=LinkType.choices,
+ default=LinkType.LINK,
+ )
+ click_count = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
- click_count = models.IntegerField(default=0)
def __str__(self):
return self.alias
diff --git a/links/templates/links/link_form.html b/links/templates/links/link_form.html
index 1010cda..dc61875 100644
--- a/links/templates/links/link_form.html
+++ b/links/templates/links/link_form.html
@@ -1,61 +1,140 @@
{% extends 'base.html' %}
{% load i18n %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
{% block content %}
-
+
-
-
- {% if form.instance.pk %}{% trans "Edit Link" %}{% else %}{% trans "Create New Link" %}{% endif %}
-
-
-
-
+
+{% endblock %}
+
+{% block extra_js %}
+
+
{% endblock %}
diff --git a/links/views.py b/links/views.py
index eef889b..47731b4 100644
--- a/links/views.py
+++ b/links/views.py
@@ -63,7 +63,7 @@ class LinkListView(ListView):
class LinkCreateView(CreateView):
model = Link
form_class = LinkForm
- template_name = 'links/link_form.html'
+ template_name = 'links/link_form.html' # 确保这里的路径是正确的
success_url = reverse_lazy('link_list')
def get_initial(self):
@@ -74,9 +74,8 @@ class LinkCreateView(CreateView):
return initial
def form_valid(self, form):
- # 将输入转换为小写
form.instance.alias = form.instance.alias.lower()
- alias = form.cleaned_data.get('alias').lower() # 确保检查时使用小写
+ alias = form.cleaned_data.get('alias').lower()
if not self.is_valid_alias(alias):
form.add_error('alias', _("Alias cannot contain special characters."))
return self.form_invalid(form)
@@ -86,8 +85,7 @@ class LinkCreateView(CreateView):
return super().form_valid(form)
def is_valid_alias(self, alias):
- # 检查别名是否包含特殊字符
- return alias.isalnum() # 只允许字母和数字
+ return alias.isalnum()
class LinkUpdateView(UpdateView):
model = Link
@@ -96,9 +94,8 @@ class LinkUpdateView(UpdateView):
success_url = reverse_lazy('link_list')
def form_valid(self, form):
- # 将输入转换为小写
form.instance.alias = form.instance.alias.lower()
- alias = form.cleaned_data.get('alias').lower() # 确保检查时使用小写
+ alias = form.cleaned_data.get('alias').lower()
if not self.is_valid_alias(alias):
form.add_error('alias', _("Alias cannot contain special characters."))
return self.form_invalid(form)
@@ -113,6 +110,9 @@ class LinkUpdateView(UpdateView):
messages.error(self.request, f"{error}")
return super().form_invalid(form)
+ def is_valid_alias(self, alias):
+ return alias.isalnum()
+
class LinkDeleteView(DeleteView):
model = Link
template_name = 'links/link_confirm_delete.html'
diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css
index 4d8685f..085ebea 100644
--- a/new_theme/static/css/dist/styles.css
+++ b/new_theme/static/css/dist/styles.css
@@ -717,6 +717,18 @@ video {
margin-top: 2rem;
}
+.ml-3 {
+ margin-left: 0.75rem;
+}
+
+.mr-3 {
+ margin-right: 0.75rem;
+}
+
+.ml-2 {
+ margin-left: 0.5rem;
+}
+
.block {
display: block;
}
@@ -757,6 +769,10 @@ video {
height: 100vh;
}
+.h-6 {
+ height: 1.5rem;
+}
+
.w-1\/12 {
width: 8.333333%;
}
@@ -785,6 +801,10 @@ video {
width: 100%;
}
+.w-11 {
+ width: 2.75rem;
+}
+
.min-w-full {
min-width: 100%;
}
@@ -809,6 +829,10 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
+.cursor-pointer {
+ cursor: pointer;
+}
+
.select-all {
-webkit-user-select: all;
-moz-user-select: all;
@@ -869,6 +893,18 @@ video {
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
+.space-y-6 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
+}
+
+.space-y-1 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
+}
+
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@@ -914,6 +950,10 @@ video {
border-radius: 0.375rem;
}
+.rounded-full {
+ border-radius: 9999px;
+}
+
.rounded-l-md {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
@@ -936,6 +976,14 @@ video {
border-top-width: 1px;
}
+.border-b-2 {
+ border-bottom-width: 2px;
+}
+
+.border-l-4 {
+ border-left-width: 4px;
+}
+
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@@ -956,6 +1004,25 @@ video {
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
+.border-transparent {
+ border-color: transparent;
+}
+
+.border-blue-400 {
+ --tw-border-opacity: 1;
+ border-color: rgb(96 165 250 / var(--tw-border-opacity));
+}
+
+.border-blue-600 {
+ --tw-border-opacity: 1;
+ border-color: rgb(37 99 235 / var(--tw-border-opacity));
+}
+
+.border-green-400 {
+ --tw-border-opacity: 1;
+ border-color: rgb(74 222 128 / var(--tw-border-opacity));
+}
+
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
@@ -1000,6 +1067,26 @@ video {
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
}
+.bg-blue-600 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(37 99 235 / var(--tw-bg-opacity));
+}
+
+.bg-yellow-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(254 252 232 / var(--tw-bg-opacity));
+}
+
+.bg-blue-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity));
+}
+
+.bg-green-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(240 253 244 / var(--tw-bg-opacity));
+}
+
.p-2 {
padding: 0.5rem;
}
@@ -1047,6 +1134,11 @@ video {
padding-bottom: 1rem;
}
+.py-5 {
+ padding-top: 1.25rem;
+ padding-bottom: 1.25rem;
+}
+
.text-left {
text-align: left;
}
@@ -1176,6 +1268,26 @@ video {
color: rgb(161 98 7 / var(--tw-text-opacity));
}
+.text-gray-900 {
+ --tw-text-opacity: 1;
+ color: rgb(17 24 39 / var(--tw-text-opacity));
+}
+
+.text-yellow-600 {
+ --tw-text-opacity: 1;
+ color: rgb(202 138 4 / var(--tw-text-opacity));
+}
+
+.text-blue-700 {
+ --tw-text-opacity: 1;
+ color: rgb(29 78 216 / var(--tw-text-opacity));
+}
+
+.text-green-700 {
+ --tw-text-opacity: 1;
+ color: rgb(21 128 61 / var(--tw-text-opacity));
+}
+
.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);
@@ -1221,6 +1333,75 @@ video {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
+.after\:absolute::after {
+ content: var(--tw-content);
+ position: absolute;
+}
+
+.after\:left-\[2px\]::after {
+ content: var(--tw-content);
+ left: 2px;
+}
+
+.after\:top-\[2px\]::after {
+ content: var(--tw-content);
+ top: 2px;
+}
+
+.after\:h-5::after {
+ content: var(--tw-content);
+ height: 1.25rem;
+}
+
+.after\:w-5::after {
+ content: var(--tw-content);
+ width: 1.25rem;
+}
+
+.after\:rounded-full::after {
+ content: var(--tw-content);
+ border-radius: 9999px;
+}
+
+.after\:border::after {
+ content: var(--tw-content);
+ border-width: 1px;
+}
+
+.after\:border-gray-300::after {
+ content: var(--tw-content);
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
+}
+
+.after\:bg-white::after {
+ content: var(--tw-content);
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
+}
+
+.after\:transition-all::after {
+ content: var(--tw-content);
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.after\:content-\[\'\'\]::after {
+ --tw-content: '';
+ content: var(--tw-content);
+}
+
+.hover\:border-gray-300:hover {
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
+}
+
+.hover\:border-blue-700:hover {
+ --tw-border-opacity: 1;
+ border-color: rgb(29 78 216 / var(--tw-border-opacity));
+}
+
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
@@ -1246,6 +1427,11 @@ video {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
+.hover\:bg-blue-700:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
+}
+
.hover\:text-blue-800:hover {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
@@ -1276,6 +1462,16 @@ video {
color: rgb(127 29 29 / var(--tw-text-opacity));
}
+.hover\:text-gray-700:hover {
+ --tw-text-opacity: 1;
+ color: rgb(55 65 81 / var(--tw-text-opacity));
+}
+
+.hover\:text-blue-700:hover {
+ --tw-text-opacity: 1;
+ color: rgb(29 78 216 / var(--tw-text-opacity));
+}
+
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -1285,6 +1481,11 @@ video {
border-color: rgb(147 197 253 / var(--tw-border-opacity));
}
+.focus\:border-indigo-300:focus {
+ --tw-border-opacity: 1;
+ border-color: rgb(165 180 252 / var(--tw-border-opacity));
+}
+
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
@@ -1312,10 +1513,57 @@ video {
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
+.focus\:ring-gray-500:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
+}
+
+.focus\:ring-indigo-200:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(199 210 254 / var(--tw-ring-opacity));
+}
+
.focus\:ring-opacity-50:focus {
--tw-ring-opacity: 0.5;
}
+.focus\:ring-offset-2:focus {
+ --tw-ring-offset-width: 2px;
+}
+
+.peer:checked ~ .peer-checked\:bg-blue-600 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(37 99 235 / var(--tw-bg-opacity));
+}
+
+.peer:checked ~ .peer-checked\:after\:translate-x-full::after {
+ content: var(--tw-content);
+ --tw-translate-x: 100%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.peer:checked ~ .peer-checked\:after\:border-white::after {
+ content: var(--tw-content);
+ --tw-border-opacity: 1;
+ border-color: rgb(255 255 255 / var(--tw-border-opacity));
+}
+
+.peer:focus ~ .peer-focus\:outline-none {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.peer:focus ~ .peer-focus\:ring-4 {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.peer:focus ~ .peer-focus\:ring-blue-300 {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
+}
+
@media (min-width: 640px) {
.sm\:inline {
display: inline;
@@ -1324,6 +1572,15 @@ video {
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+
+ .sm\:p-6 {
+ padding: 1.5rem;
+ }
+
+ .sm\:px-6 {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
}
@media (min-width: 768px) {
@@ -1340,4 +1597,26 @@ video {
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
+
+ .lg\:px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:border-gray-600 {
+ --tw-border-opacity: 1;
+ border-color: rgb(75 85 99 / var(--tw-border-opacity));
+ }
+
+ .dark\:bg-gray-700 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(55 65 81 / var(--tw-bg-opacity));
+ }
+
+ .peer:focus ~ .dark\:peer-focus\:ring-blue-800 {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity));
+ }
}
diff --git a/requirements.txt b/requirements.txt
index ed96b18..f930391 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ django-tailwind==3.8.0
fontawesome-free==5.15.3
gunicorn==22.0.0
whitenoise==5.3.0
+django-simplemde-x==1.2.0
diff --git a/templates/base.html b/templates/base.html
index 2449cf3..a8242c3 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -49,6 +49,8 @@
cursor: text;
}
+ {{ form.media }}
+ {% block extra_css %}{% endblock %}