diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 60f3aa4..6d07b08 100644 Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ diff --git a/links/forms.py b/links/forms.py index eb71ceb..80b42f1 100644 --- a/links/forms.py +++ b/links/forms.py @@ -2,6 +2,8 @@ from django import forms from .models import Link from simplemde.fields import SimpleMDEField from django.utils.translation import gettext_lazy as _ +from django.core.validators import URLValidator +import re class LinkForm(forms.ModelForm): text = SimpleMDEField() @@ -14,6 +16,28 @@ class LinkForm(forms.ModelForm): 'description': forms.Textarea(attrs={'rows': 3}), } + def clean_original_url(self): + url = self.cleaned_data.get('original_url') + if not url: + return url + + # Extract template parameters + template_params = re.findall(r'\{([^{}]*)\}', url) + + # Temporarily replace template parameters with placeholder + temp_url = url + for param in template_params: + param_full = '{' + param + '}' + temp_url = temp_url.replace(param_full, 'template-param') + + # Validate the URL with placeholders + try: + URLValidator()(temp_url) + except forms.ValidationError: + raise forms.ValidationError(_("Please enter a valid URL. Template parameters are allowed in the format {param_name,default=value}.")) + + return url + def clean(self): cleaned_data = super().clean() link_type = cleaned_data.get('link_type') diff --git a/links/migrations/0008_alter_link_original_url.py b/links/migrations/0008_alter_link_original_url.py new file mode 100644 index 0000000..3941461 --- /dev/null +++ b/links/migrations/0008_alter_link_original_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-11-02 11:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0007_link_description'), + ] + + operations = [ + migrations.AlterField( + model_name='link', + name='original_url', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/links/models.py b/links/models.py index 54d9f20..a1fd61b 100644 --- a/links/models.py +++ b/links/models.py @@ -2,6 +2,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.urls import reverse import logging +import re logger = logging.getLogger(__name__) @@ -11,8 +12,8 @@ class Link(models.Model): 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 + original_url = models.TextField(blank=True, null=True) + text = models.TextField(blank=True, null=True) link_type = models.CharField( max_length=10, choices=LinkType.choices, @@ -23,6 +24,71 @@ class Link(models.Model): updated_at = models.DateTimeField(auto_now=True) description = models.TextField(blank=True, null=True, verbose_name=_("Description")) + def get_template_parameters(self): + """Extract template parameters and their default values from original_url""" + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, self.original_url) + params = {} + + for match in matches: + param_str = match.group(1) + if ',' in param_str: + param_name, default_value = param_str.split(',', 1) + default_value = default_value.strip() + if default_value.startswith('default='): + default_value = default_value[8:].strip('"\'') + params[param_name.strip()] = default_value + else: + params[param_str.strip()] = None + + return params + + def get_processed_url(self, **kwargs): + """Process template URL with provided parameters""" + if not self.original_url: + return None + + processed_url = self.original_url + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, self.original_url) + + # 收集所有参数及其默认值 + params = {} + for match in matches: + param_str = match.group(1) + param_full = '{' + param_str + '}' + + # 处理参数字符串 + param_parts = [p.strip() for p in param_str.split(',', 1)] + param_name = param_parts[0].strip() + default_value = None + + if len(param_parts) > 1: + # 处理默认值部分 + default_part = param_parts[1].strip() + if default_part.startswith('default='): + default_value = default_part[8:].strip() # 移除 'default=' 前缀 + # 移除引号(如果存在) + if (default_value.startswith('"') and default_value.endswith('"')) or \ + (default_value.startswith("'") and default_value.endswith("'")): + default_value = default_value[1:-1] + + params[param_name] = { + 'default': default_value, + 'full_match': param_full + } + + # 替换所有参数 + for param_name, param_info in params.items(): + value = kwargs.get(param_name, param_info['default']) + if value is None: + raise ValueError(f"No value provided for parameter {param_name}") + + # 使用完整的匹配模式进行替换 + processed_url = processed_url.replace(param_info['full_match'], str(value)) + + return processed_url + def __str__(self): return self.alias diff --git a/links/patches.py b/links/patches.py new file mode 100644 index 0000000..e137f1c --- /dev/null +++ b/links/patches.py @@ -0,0 +1,4 @@ +import sys +from django import urls + +sys.modules['django.core.urlresolvers'] = urls diff --git a/links/templates/links/help.html b/links/templates/links/help.html new file mode 100644 index 0000000..d4ac24b --- /dev/null +++ b/links/templates/links/help.html @@ -0,0 +1,141 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load markdown_extras %} + +{% block extra_css %} + + + +{% endblock %} + +{% block content %} +
+
+ {% filter markdown %} +# Frequently Asked Questions (FAQ) + +## Link Types + +GoLinks supports three types of links: Normal Links, Template Links, and Custom Pages. Each type serves different purposes and has its own use cases. + +### 1. Normal Links + +Normal links are the simplest type - they provide direct redirection to a target URL. + +**Example:** +- Original URL: `https://github.com/microsoft/vscode` +- Alias: `vscode` +- Access: `http://go/vscode` → Redirects to VS Code GitHub repository + +**Use Cases:** +- Quick access to frequently visited websites +- Shortening long URLs +- Creating memorable aliases for complex URLs + +### 2. Template Links + +Template links allow you to create dynamic URLs that can generate the final URL based on parameters. This is particularly useful for search links, API calls, or any URL that needs dynamic parameters. + +**Basic Syntax:** +Use the syntax `{parameter_name, default=default_value}` to define variables in your URL: `https://google.com?q={query, default=hello}`, and set the alias to `google`. + +**Examples:** +- Use the syntax `{query, default=default_value}` to define variables in your URL: `https://google.com?q={query, default=hello}`, and set the alias to `google`. + + Then you can access the link by `http://go/google/`, and the URL will be `https://google.com?q=`. + + When you access the url with `http://go/google/`, the URL will be `https://google.com?q=hello`, which will leverage the `default` value. + +### 3. Custom Pages + +Custom pages allow you to create markdown-based web pages that can serve as documentation, link collections, or any other content you need. + +**Key Features:** +- Write content in Markdown format +- Support for headings, lists, code blocks, and links +- Can include multiple sections and navigation +- Perfect for creating documentation or resource collections + +**Example Use Cases:** + Check http://go.junv.cc/custom/today/ + {% endfilter %} +
+
+{% endblock %} diff --git a/links/templates/links/link_detail.html b/links/templates/links/link_detail.html index f60f7a0..f1eaec3 100644 --- a/links/templates/links/link_detail.html +++ b/links/templates/links/link_detail.html @@ -8,7 +8,11 @@

{% trans "Link Details" %} - {% if link.link_type == 'LINK' %} + {% if '{' in link.original_url and '}' in link.original_url %} + + {% trans "Template" %} + + {% elif link.link_type == 'LINK' %} {% trans "Link" %} @@ -19,7 +23,7 @@ {% endif %}

- + diff --git a/links/templates/links/link_form.html b/links/templates/links/link_form.html index d9d204b..75421ab 100644 --- a/links/templates/links/link_form.html +++ b/links/templates/links/link_form.html @@ -45,6 +45,14 @@ {% trans "Custom type allows you to create a link with custom HTML content. Use this when you want to display a message or custom content instead of redirecting." %}

+
+

+ {% trans "Template type allows you to create dynamic URLs with parameters. Use {query, default=value} syntax in the URL." %} +

+

+ {% trans "Example: https://google.com/search?q={query, default=hello}" %} +

+
diff --git a/links/templates/links/link_list.html b/links/templates/links/link_list.html index a373c5e..05a4c06 100644 --- a/links/templates/links/link_list.html +++ b/links/templates/links/link_list.html @@ -105,16 +105,25 @@ - {% if link.link_type == 'LINK' %} + {% if '{' in link.original_url and '}' in link.original_url %} + + Template + {% elif link.link_type == 'LINK' %} + Link {% else %} + Custom {% endif %} {{ link.click_count }} diff --git a/links/templates/links/search.html b/links/templates/links/search.html index 7cc4a1b..a3d08fa 100644 --- a/links/templates/links/search.html +++ b/links/templates/links/search.html @@ -3,93 +3,143 @@ {% block content %}
-

{% trans "Advanced Search" %}

+ +
+

{% trans "Advanced Search" %}

+

{% trans "Search through all links by alias or URL" %}

+
- -
-
- -
- -
- - - {% if query %} -

{% trans "Search results for" %}: {{ query }}

- - {% if links %} -
- + +
+

{% trans "Try searching by alias, URL, or any related keywords" %}

+
+ +
+ + + + {% if query %} +
+
+

+ {% trans "Search results for" %}: {{ query }} +

- {% if is_paginated %} - - {% endif %} + {% if links %} + - {% else %} -

{% trans "No results found." %}

- {% endif %} + {% if is_paginated %} + + {% endif %} + {% else %} +
+ + + +

{% trans "No results found" %}

+

{% trans "Try adjusting your search terms." %}

+
+ {% endif %} +
{% else %} -

{% trans "Enter a search query to find links." %}

+
+
+ + + +

{% trans "Start searching" %}

+

{% trans "Enter a search query to find links. You can search by alias, URL, or any related keywords." %}

+
+
{% endif %} {% endblock %} diff --git a/links/templates/links/tools.html b/links/templates/links/tools.html index c3ea0a3..897150f 100644 --- a/links/templates/links/tools.html +++ b/links/templates/links/tools.html @@ -7,28 +7,61 @@

{% trans "Tools" %}

-
-

{% trans "Export Aliases" %}

-
- {% csrf_token %} - -
- -

{% trans "Import Aliases" %}

-
-

{% trans "Import aliases from a JSON file. The file should contain a list of objects with the following fields:" %}

- +
+ +
+
+

{% trans "Export Aliases" %}

+
+
+
+ {% csrf_token %} + +
+
-
-

{% trans "Example JSON file content:" %}

-
+
+      
+      
+
+

{% trans "Database Export" %}

+
+
+

{% trans "Export the entire database as a compressed file." %}

+ + + + + {% trans "Export Database" %} + +
+
+ + +
+
+

{% trans "Import Aliases" %}

+
+
+
+

{% trans "Import aliases from a JSON file. The file should contain a list of objects with the following fields:" %}

+
    +
  • {% trans "alias (required): The short alias for the URL" %}
  • +
  • {% trans "original_url (required): The original URL" %}
  • +
  • {% trans "created_at (optional): Creation timestamp (ISO format)" %}
  • +
  • {% trans "updated_at (optional): Last update timestamp (ISO format)" %}
  • +
+
+ +
+

{% trans "Example JSON file content:" %}

+
 [
   {
     "alias": "example",
@@ -41,18 +74,25 @@
     "original_url": "https://www.google.com"
   }
 ]
-        
-
-
- {% csrf_token %} -
- +
+
+ + + {% csrf_token %} +
+ +
+ +
- - - {% trans "Export Links" %} +
diff --git a/links/templatetags/markdown_extras.py b/links/templatetags/markdown_extras.py new file mode 100644 index 0000000..a4186bf --- /dev/null +++ b/links/templatetags/markdown_extras.py @@ -0,0 +1,8 @@ +from django import template +import markdown as md + +register = template.Library() + +@register.filter +def markdown(value): + return md.markdown(value, extensions=['fenced_code', 'tables']) diff --git a/links/urls.py b/links/urls.py index 44bd7d4..af8f5fb 100644 --- a/links/urls.py +++ b/links/urls.py @@ -8,13 +8,17 @@ urlpatterns = [ path('delete//', views.LinkDeleteView.as_view(), name='link_delete'), path('delete-selected/', views.delete_selected, name='delete_selected'), path('detail//', views.LinkDetailView.as_view(), name='link_detail'), - path('search/', views.SearchView.as_view(), name='search'), + path('ui/search/', views.SearchView.as_view(), name='search'), path('link//', views.LinkDetailView.as_view(), name='link_detail'), path('link//edit/', views.LinkUpdateView.as_view(), name='link_update'), - path('tools/', views.ToolsView.as_view(), name='tools'), - path('/', views.redirect_to_original, name='redirect_to_original'), + path('ui/tools/', views.ToolsView.as_view(), name='tools'), + path('tools/export-database/', views.export_database, name='export_database'), + path('ui/help/', views.HelpView.as_view(), name='help'), path('export/', views.export_links, name='export_links'), - # New route for link detail view by alias + # Aliases + path('/', views.redirect_to_original, name='redirect_to_original'), + path('//', views.redirect_to_original, name='redirect_to_original_with_param'), path('alias//', views.LinkDetailView.as_view(), name='link_detail_by_alias'), + ] diff --git a/links/views.py b/links/views.py index 4376f45..f73997b 100644 --- a/links/views.py +++ b/links/views.py @@ -18,6 +18,15 @@ import random from django.utils.text import slugify import markdown import logging +import re +import os +import tarfile +import tempfile +from datetime import datetime +from django.conf import settings +from django.http import FileResponse +from wsgiref.util import FileWrapper +from django.contrib.auth.decorators import user_passes_test logger = logging.getLogger(__name__) @@ -178,16 +187,40 @@ def delete_selected(request): Link.objects.filter(id__in=selected_ids).delete() return redirect('link_list') -def redirect_to_original(request, alias): - # 将别名转换为小写并移除特殊字符 +def redirect_to_original(request, alias, param=None): processed_alias = ''.join(e for e in alias.lower() if e.isalnum()) try: link = Link.objects.get(alias=processed_alias) link.click_count = F('click_count') + 1 link.save() - ClickLog.objects.create(link=link) # 记录点击 - return redirect(link.original_url) + ClickLog.objects.create(link=link) + + try: + # 如果是模板 URL 并且提供了参数 + if param: + # 从 URL 中提取参数名 + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, link.original_url) + for match in matches: + param_str = match.group(1) + if ',' in param_str: + param_name = param_str.split(',')[0].strip() + else: + param_name = param_str.strip() + # 使用提供的参数值 + url = link.get_processed_url(**{param_name: param}) + return redirect(url) + + # 如果没有提供参数,使用默认值 + url = link.get_processed_url() + if url is None: + url = link.original_url + return redirect(url) + except ValueError as e: + messages.error(request, str(e)) + return redirect('link_list') + except Link.DoesNotExist: messages.warning(request, _("The alias '{}' doesn't exist. Do you want to create a new one?").format(processed_alias)) return redirect(reverse('link_create') + f'?alias={processed_alias}') @@ -365,3 +398,37 @@ class CustomLinkView(DetailView): # Render markdown text as HTML context['rendered_text'] = markdown.markdown(self.object.text) return context + +def export_database(request): + """Export the entire SQLite database as a tarball""" + try: + # 获取数据库文件路径 + db_path = settings.DATABASES['default']['NAME'] + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + # 创建带时间戳的文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + tar_filename = f'database_backup_{timestamp}.tar.gz' + tar_filepath = os.path.join(temp_dir, tar_filename) + + # 创建tar文件 + with tarfile.open(tar_filepath, 'w:gz') as tar: + # 添加数据库文件到tar + tar.add(db_path, arcname=os.path.basename(db_path)) + + # 打开文件准备下载 + wrapper = FileWrapper(open(tar_filepath, 'rb')) + response = FileResponse(wrapper, content_type='application/x-gzip') + response['Content-Disposition'] = f'attachment; filename="{tar_filename}"' + response['Content-Length'] = os.path.getsize(tar_filepath) + + return response + + except Exception as e: + logger.error(f"Database export failed: {str(e)}") + messages.error(request, _("Failed to export database: {}").format(str(e))) + return redirect('tools') + +class HelpView(TemplateView): + template_name = 'links/help.html' diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo index 19ebde9..5ef2d40 100644 Binary files a/locale/zh_Hans/LC_MESSAGES/django.mo and b/locale/zh_Hans/LC_MESSAGES/django.mo differ diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po index cd3e47e..0d99c24 100644 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -1856,7 +1856,7 @@ msgstr "无效的JSON格式。预期是对象列表。" #: links/views.py:282 msgid "" "Invalid item in JSON. Each item must be an object with an 'alias' field." -msgstr "JSON中存在无效项。每个项目必须是包含'alias'字段的对象。" +msgstr "JSON中存在无效项。每个项必须是包含'alias'字段的对象。" #: links/views.py:295 #, fuzzy, python-brace-format @@ -1910,3 +1910,21 @@ msgstr "简体中文" msgid "Description" msgstr "描述" + +msgid "Please enter a valid URL. Template parameters are allowed in the format {param_name,default=value}." +msgstr "请输入有效的URL。允许使用模板参数,格式为 {参数名,default=默认值}。" + +msgid "Database Export" +msgstr "数据库导出" + +msgid "Export the entire database as a compressed file. This feature is only available to administrators." +msgstr "将整个数据库导出为压缩文件。此功能仅管理员可用。" + +msgid "Export Database" +msgstr "导出数据库" + +msgid "Only administrators can export the database." +msgstr "只有管理员可以导出数据库。" + +msgid "Failed to export database: {}" +msgstr "导出数据库失败:{}" diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index 41b8910..ed31bb6 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -670,6 +670,14 @@ video { margin-right: auto; } +.-ml-px { + margin-left: -1px; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + .mb-2 { margin-bottom: 0.5rem; } @@ -694,10 +702,6 @@ video { margin-left: 0.5rem; } -.ml-3 { - margin-left: 0.75rem; -} - .ml-4 { margin-left: 1rem; } @@ -774,6 +778,14 @@ video { display: none; } +.h-12 { + height: 3rem; +} + +.h-16 { + height: 4rem; +} + .h-4 { height: 1rem; } @@ -786,6 +798,14 @@ video { height: 100vh; } +.w-12 { + width: 3rem; +} + +.w-16 { + width: 4rem; +} + .w-4 { width: 1rem; } @@ -802,6 +822,10 @@ video { width: 100%; } +.min-w-0 { + min-width: 0px; +} + .min-w-full { min-width: 100%; } @@ -822,6 +846,10 @@ video { max-width: 32rem; } +.max-w-md { + max-width: 28rem; +} + .max-w-none { max-width: none; } @@ -913,6 +941,12 @@ video { row-gap: 2rem; } +.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-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.75rem * var(--tw-space-x-reverse)); @@ -949,6 +983,12 @@ video { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } +.space-y-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(2rem * 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))); @@ -978,6 +1018,10 @@ video { white-space: nowrap; } +.break-all { + word-break: break-all; +} + .rounded { border-radius: 0.25rem; } @@ -994,6 +1038,16 @@ video { border-radius: 0.375rem; } +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-r-md { + border-top-right-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; +} + .border { border-width: 1px; } @@ -1039,6 +1093,11 @@ video { border-color: rgb(74 222 128 / var(--tw-border-opacity)); } +.border-purple-400 { + --tw-border-opacity: 1; + border-color: rgb(192 132 252 / var(--tw-border-opacity)); +} + .border-transparent { border-color: transparent; } @@ -1098,6 +1157,21 @@ video { background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } +.bg-purple-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 232 255 / var(--tw-bg-opacity)); +} + +.bg-purple-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 245 255 / var(--tw-bg-opacity)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity)); +} + .bg-red-500 { --tw-bg-opacity: 1; background-color: rgb(239 68 68 / var(--tw-bg-opacity)); @@ -1126,6 +1200,10 @@ video { padding: 0.5rem; } +.p-3 { + padding: 0.75rem; +} + .p-4 { padding: 1rem; } @@ -1169,6 +1247,11 @@ video { padding-bottom: 0.25rem; } +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -1225,6 +1308,11 @@ video { line-height: 1; } +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + .text-lg { font-size: 1.125rem; line-height: 1.75rem; @@ -1297,6 +1385,11 @@ video { color: rgb(30 64 175 / var(--tw-text-opacity)); } +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -1352,6 +1445,26 @@ video { color: rgb(79 70 229 / var(--tw-text-opacity)); } +.text-purple-500 { + --tw-text-opacity: 1; + color: rgb(168 85 247 / var(--tw-text-opacity)); +} + +.text-purple-600 { + --tw-text-opacity: 1; + color: rgb(147 51 234 / var(--tw-text-opacity)); +} + +.text-purple-700 { + --tw-text-opacity: 1; + color: rgb(126 34 206 / var(--tw-text-opacity)); +} + +.text-purple-800 { + --tw-text-opacity: 1; + color: rgb(107 33 168 / var(--tw-text-opacity)); +} + .text-red-600 { --tw-text-opacity: 1; color: rgb(220 38 38 / var(--tw-text-opacity)); @@ -1380,12 +1493,6 @@ video { text-decoration-line: line-through; } -.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); -} - .shadow-md { --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); @@ -1421,14 +1528,65 @@ video { transition-duration: 150ms; } +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-150 { transition-duration: 150ms; } +.duration-200 { + transition-duration: 200ms; +} + .ease-in-out { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } +.file\:mr-4::file-selector-button { + margin-right: 1rem; +} + +.file\:rounded-md::file-selector-button { + border-radius: 0.375rem; +} + +.file\:border-0::file-selector-button { + border-width: 0px; +} + +.file\:bg-blue-50::file-selector-button { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity)); +} + +.file\:px-4::file-selector-button { + padding-left: 1rem; + padding-right: 1rem; +} + +.file\:py-2::file-selector-button { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.file\:text-sm::file-selector-button { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.file\:font-medium::file-selector-button { + font-weight: 500; +} + +.file\:text-blue-700::file-selector-button { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); +} + .hover\:border-blue-700:hover { --tw-border-opacity: 1; border-color: rgb(29 78 216 / var(--tw-border-opacity)); @@ -1474,6 +1632,11 @@ video { background-color: rgb(21 128 61 / var(--tw-bg-opacity)); } +.hover\:bg-purple-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(147 51 234 / var(--tw-bg-opacity)); +} + .hover\:bg-red-600:hover { --tw-bg-opacity: 1; background-color: rgb(220 38 38 / var(--tw-bg-opacity)); @@ -1533,6 +1696,11 @@ video { text-decoration-line: underline; } +.hover\:file\:bg-blue-100::file-selector-button:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + .focus\:border-blue-300:focus { --tw-border-opacity: 1; border-color: rgb(147 197 253 / var(--tw-border-opacity)); @@ -1563,6 +1731,12 @@ video { box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-1:focus { + --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(1px + 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); +} + .focus\:ring-2:focus { --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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); @@ -1611,10 +1785,6 @@ video { grid-column: span 2 / span 2; } - .sm\:mt-0 { - margin-top: 0px; - } - .sm\:block { display: block; } @@ -1665,20 +1835,14 @@ video { margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); } - .sm\: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))); - } - .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0px * var(--tw-space-y-reverse)); } - .sm\:rounded-md { - border-radius: 0.375rem; + .sm\:p-12 { + padding: 3rem; } .sm\:p-6 { diff --git a/templates/base.html b/templates/base.html index a8242c3..6571431 100644 --- a/templates/base.html +++ b/templates/base.html @@ -70,6 +70,7 @@ diff --git a/url_manager/settings.py b/url_manager/settings.py index 5fa1db0..a47df01 100644 --- a/url_manager/settings.py +++ b/url_manager/settings.py @@ -1,6 +1,7 @@ import os from pathlib import Path from django.utils.translation import gettext_lazy as _ +import links.patches # 添加这一行在文件最上方 # 构建路径,如 BASE_DIR / 'subdir' BASE_DIR = Path(__file__).resolve().parent.parent @@ -17,6 +18,7 @@ INSTALLED_APPS = [ 'tailwind', 'new_theme', 'simplemde', + 'markdown', # 只需要基本的markdown包 ] ROOT_URLCONF = 'url_manager.urls'