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 %} -

-
-
-
- {% csrf_token %} - {% for field in form %} +
+

+ {% if form.instance.pk %}{% trans "Edit Link" %}{% else %}{% trans "Create New Link" %}{% endif %} +

+
+
+
- - {% if field.errors %} -
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} - - {% if field.help_text %} -

{{ field.help_text }}

- {% endif %} +
+ + +
- {% endfor %} -
- - {% trans "Cancel" %} - - -
- -

{% trans "Note:" %}

-
-

- {% trans "Please note that special characters are not allowed in the alias when creating or updating a link." %} -
- {% trans "For example:" %} -

    -
  • {% trans "Valid alias: helloworld" %}
  • -
  • {% trans "Invalid alias: hello@world!" %}
  • -
  • {% trans "Accessing 'helloworld' as: hello-world or hello_world" %}
  • -
-

+ + + + + +
+ {% csrf_token %} + + {% for field in form %} + {% if field.name != 'link_type' %} + + {% endif %} + {% endfor %} +
+ + {% trans "Cancel" %} + + +
+
-
-
+
+{% 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 %}