diff --git a/README.md b/README.md
index 7680786..9e704f3 100644
--- a/README.md
+++ b/README.md
@@ -124,3 +124,12 @@ To run the URL Manager locally:
9. Open your browser and go to `http://127.0.0.1:8000`
Remember to keep both the Tailwind CSS build process and the Django development server running while you're developing.
+
+## Tools
+
+The URL Manager includes a Tools page with the following features:
+
+- Export all aliases as a JSON file
+- Import aliases from a JSON file
+
+To access the Tools page, click on the "Tools" link in the navigation bar.
diff --git a/data/db.sqlite3 b/data/db.sqlite3
index 91e3d72..ab521e1 100644
Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ
diff --git a/links/__pycache__/urls.cpython-312.pyc b/links/__pycache__/urls.cpython-312.pyc
index 2334921..dfbf32f 100644
Binary files a/links/__pycache__/urls.cpython-312.pyc and b/links/__pycache__/urls.cpython-312.pyc differ
diff --git a/links/__pycache__/views.cpython-312.pyc b/links/__pycache__/views.cpython-312.pyc
index 5ed4140..4c90855 100644
Binary files a/links/__pycache__/views.cpython-312.pyc and b/links/__pycache__/views.cpython-312.pyc differ
diff --git a/links/templates/links/link_list.html b/links/templates/links/link_list.html
index c0bd92a..aa18f2d 100644
--- a/links/templates/links/link_list.html
+++ b/links/templates/links/link_list.html
@@ -19,10 +19,10 @@
-
+
-
+
{% trans "Alias" %}
{% if current_sort == 'alias' %}
@@ -32,8 +32,8 @@
{% endif %}
- {% trans "Original URL" %}
-
+ {% trans "Original URL" %}
+
{% trans "Clicks" %}
{% if current_sort == 'clicks' %}
@@ -43,7 +43,7 @@
{% endif %}
-
+
{% trans "Created At" %}
{% if current_sort == 'created_at' %}
@@ -53,7 +53,7 @@
{% endif %}
-
+
{% trans "Updated At" %}
{% if current_sort == 'updated_at' %}
@@ -63,7 +63,7 @@
{% endif %}
- {% trans "Actions" %}
+ {% trans "Actions" %}
@@ -76,7 +76,9 @@
{{ link.alias }}
- {{ link.original_url }}
+
{{ link.click_count }}
{{ link.created_at|date:"Y-m-d H:i" }}
diff --git a/links/templates/links/tools.html b/links/templates/links/tools.html
new file mode 100644
index 0000000..c0252ed
--- /dev/null
+++ b/links/templates/links/tools.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
{% trans "Tools" %}
+
+
+
{% trans "Export Aliases" %}
+
+
+
{% trans "Import Aliases" %}
+
+
+
+
+{% endblock %}
diff --git a/links/urls.py b/links/urls.py
index 43b081a..3e3964b 100644
--- a/links/urls.py
+++ b/links/urls.py
@@ -9,5 +9,6 @@ urlpatterns = [
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('tools/', views.ToolsView.as_view(), name='tools'), # Add this line
path('/', views.redirect_to_original, name='redirect_to_original'),
]
diff --git a/links/views.py b/links/views.py
index 786d838..7601980 100644
--- a/links/views.py
+++ b/links/views.py
@@ -1,5 +1,6 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView, TemplateView
+from django.views import View # Add this import
from django.urls import reverse_lazy, reverse # Add 'reverse' here
from django.db.models import F, Count, Q
from django.db.models.functions import TruncDate
@@ -13,6 +14,10 @@ from django.utils.translation import gettext as _
from django.core.exceptions import ValidationError
from django.http import JsonResponse
+from django.http import JsonResponse, HttpResponse
+from django.core.serializers.json import DjangoJSONEncoder
+from django.utils.dateparse import parse_datetime
+
class LinkListView(ListView):
model = Link
template_name = 'links/link_list.html'
@@ -146,3 +151,50 @@ class SearchView(TemplateView):
context['links'] = links
context['query'] = query
return context
+
+class ToolsView(View):
+ template_name = 'links/tools.html'
+
+ def get(self, request):
+ return render(request, self.template_name)
+
+ def post(self, request):
+ if 'export' in request.POST:
+ links = Link.objects.all().values('alias', 'original_url', 'created_at', 'updated_at')
+ response = HttpResponse(json.dumps(list(links), cls=DjangoJSONEncoder), content_type='application/json')
+ response['Content-Disposition'] = 'attachment; filename="links_export.json"'
+ return response
+ elif 'import' in request.POST:
+ try:
+ json_file = request.FILES['json_file']
+ data = json.load(json_file)
+
+ if not isinstance(data, list):
+ raise ValueError(_("Invalid JSON format. Expected a list of objects."))
+
+ imported_count = 0
+ skipped_count = 0
+ for item in data:
+ if not isinstance(item, dict) or 'alias' not in item:
+ raise ValueError(_("Invalid item in JSON. Each item must be an object with an 'alias' field."))
+
+ if not Link.objects.filter(alias=item['alias']).exists():
+ Link.objects.create(
+ alias=item['alias'],
+ original_url=item.get('original_url', ''),
+ created_at=parse_datetime(item.get('created_at')) if item.get('created_at') else None,
+ updated_at=parse_datetime(item.get('updated_at')) if item.get('updated_at') else None
+ )
+ imported_count += 1
+ else:
+ skipped_count += 1
+
+ messages.success(request, _(f"{imported_count} aliases have been imported successfully. {skipped_count} were skipped."))
+ except json.JSONDecodeError:
+ messages.error(request, _("Invalid JSON file. Please check the file format."))
+ except ValueError as e:
+ messages.error(request, str(e))
+ except Exception as e:
+ messages.error(request, _(f"An error occurred during import: {str(e)}"))
+
+ return render(request, self.template_name)
diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo
index 326b3c8..079e0c2 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 b2ab5dc..a224fc7 100644
--- a/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/locale/zh_Hans/LC_MESSAGES/django.po
@@ -114,6 +114,33 @@ msgstr "未找到结果。"
msgid "Advanced Search"
msgstr "高级搜索"
+msgid "Tools"
+msgstr "工具"
+
+msgid "Export Aliases"
+msgstr "导出别名"
+
+msgid "Export as JSON"
+msgstr "导��为JSON"
+
+msgid "Import Aliases"
+msgstr "导入别名"
+
+msgid "Import from JSON"
+msgstr "从JSON导入"
+
+msgid "{} aliases have been imported successfully."
+msgstr "{}个别名已成功导入。"
+
+msgid "An error occurred during import: {}"
+msgstr "导入过程中发生错误:{}"
+
+msgid "Invalid JSON format. Expected a list of objects."
+msgstr "无效的JSON格式。预期是对象列表。"
+
+msgid "Invalid item in JSON. Each item must be an object with an 'alias' field."
+msgstr "JSON中存在无效项。每个项目必须是包含'alias'字段的对象。"
+
#: 3.12/lib/python3.12/site-packages/django/forms/fields.py:95
msgid "This field is required."
msgstr ""
diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css
index e5ef0ae..2373b97 100644
--- a/new_theme/static/css/dist/styles.css
+++ b/new_theme/static/css/dist/styles.css
@@ -944,6 +944,22 @@ select {
width: 16rem;
}
+.w-10 {
+ width: 2.5rem;
+}
+
+.w-1\/12 {
+ width: 8.333333%;
+}
+
+.w-1\/3 {
+ width: 33.333333%;
+}
+
+.w-1\/6 {
+ width: 16.666667%;
+}
+
.min-w-full {
min-width: 100%;
}
@@ -956,6 +972,10 @@ select {
max-width: 56rem;
}
+.max-w-xs {
+ max-width: 20rem;
+}
+
.flex-grow {
flex-grow: 1;
}
@@ -1033,6 +1053,12 @@ select {
overflow-x: auto;
}
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.whitespace-nowrap {
white-space: nowrap;
}
@@ -1115,6 +1141,11 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
+.bg-green-500 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
+}
+
.p-2 {
padding: 0.5rem;
}
@@ -1306,6 +1337,11 @@ select {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
+.hover\:bg-green-600:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(22 163 74 / var(--tw-bg-opacity));
+}
+
.hover\:text-blue-800:hover {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
diff --git a/templates/base.html b/templates/base.html
index 3719aea..6175cd4 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -36,6 +36,7 @@
{% trans "URL Manager" %}