yanleafadmin 2.0.2__tar.gz → 2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {yanleafadmin-2.0.2/yanleafadmin.egg-info → yanleafadmin-2.0.4}/PKG-INFO +17 -1
  2. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/README.md +16 -0
  3. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/apps.py +8 -2
  4. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/views.py +5 -2
  5. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/apps.py +10 -0
  6. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/context_processors.py +9 -0
  7. yanleafadmin-2.0.4/apps/theme/forms.py +24 -0
  8. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/core.js +1 -1
  9. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/base.html +3 -1
  10. yanleafadmin-2.0.4/apps/theme/templates/admin/base_site.html +15 -0
  11. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/change_list.html +1 -1
  12. yanleafadmin-2.0.4/apps/theme/templates/admin/dashboard_index.html +394 -0
  13. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/login.html +2 -0
  14. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/setup.py +1 -1
  15. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4/yanleafadmin.egg-info}/PKG-INFO +17 -1
  16. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin.egg-info/SOURCES.txt +2 -1
  17. yanleafadmin-2.0.2/apps/theme/forms.py +0 -13
  18. yanleafadmin-2.0.2/apps/theme/static/yanleafadmin/vendor/mermaid/mermaid.min.js +0 -3405
  19. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/LICENSE +0 -0
  20. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/MANIFEST.in +0 -0
  21. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/__init__.py +0 -0
  22. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/__init__.py +0 -0
  23. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/apps.py +0 -0
  24. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/services.py +0 -0
  25. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/static/ai/ai-search.js +0 -0
  26. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/templates/ai/fullpage.html +0 -0
  27. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/urls.py +0 -0
  28. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/ai_assistant/views.py +0 -0
  29. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/__init__.py +0 -0
  30. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/admin.py +0 -0
  31. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/migrations/__init__.py +0 -0
  32. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/models.py +0 -0
  33. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/templates/admin/base_site.html +0 -0
  34. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/templates/admin/dashboard_index.html +0 -0
  35. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/tests.py +0 -0
  36. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/dashboard_engine/views.py +0 -0
  37. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/__init__.py +0 -0
  38. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/apps.py +0 -0
  39. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/sql_parser.py +0 -0
  40. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/static/erd/er-diagram.js +0 -0
  41. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/templates/erd/er_diagram.html +0 -0
  42. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/erd_engine/urls.py +0 -0
  43. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/__init__.py +0 -0
  44. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/admin.py +0 -0
  45. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/components/__init__.py +0 -0
  46. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/components/actions.py +0 -0
  47. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/components/charts.py +0 -0
  48. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/migrations/__init__.py +0 -0
  49. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/models.py +0 -0
  50. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/settings.py +0 -0
  51. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/admin.css +0 -0
  52. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/change-form.css +0 -0
  53. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/change-list.css +0 -0
  54. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/dashboard.css +0 -0
  55. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/login.css +0 -0
  56. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/password-form.css +0 -0
  57. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/css/user-change-form.css +0 -0
  58. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/components.js +0 -0
  59. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/filter-widget.js +0 -0
  60. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/i18n/datatables.en.json +0 -0
  61. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/i18n/datatables.zh-hans.json +0 -0
  62. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/login.js +0 -0
  63. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/password-check.js +0 -0
  64. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/smart-chart.js +0 -0
  65. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/js/user-password-field.js +0 -0
  66. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/bulma/bulma.min.css +0 -0
  67. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/bulma-calendar/bulma-calendar.min.css +0 -0
  68. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/bulma-calendar/bulma-calendar.min.js +0 -0
  69. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/css/buttons.bulma.min.css +0 -0
  70. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/css/dataTables.bulma.min.css +0 -0
  71. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/js/buttons.html5.min.js +0 -0
  72. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/js/buttons.print.min.js +0 -0
  73. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/js/dataTables.bulma.min.js +0 -0
  74. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/js/dataTables.buttons.min.js +0 -0
  75. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/datatables/js/jquery.dataTables.min.js +0 -0
  76. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/dropzone/dropzone-min.js +0 -0
  77. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/dropzone/dropzone.css +0 -0
  78. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/echarts/echarts.min.js +0 -0
  79. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/fontawesome/css/all.min.css +0 -0
  80. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-brands-400.woff2 +0 -0
  81. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-regular-400.woff2 +0 -0
  82. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-solid-900.woff2 +0 -0
  83. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/gojs/go.js +0 -0
  84. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/jquery/jquery.min.js +0 -0
  85. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/jszip/jszip.min.js +0 -0
  86. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/select2/css/select2.min.css +0 -0
  87. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/select2/js/select2.min.js +0 -0
  88. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/static/yanleafadmin/vendor/sweetalert2/sweetalert2.all.min.js +0 -0
  89. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/app_index.html +0 -0
  90. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/auth/user/add_form.html +0 -0
  91. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/auth/user/change_form.html +0 -0
  92. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/auth/user/change_password.html +0 -0
  93. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/change_form.html +0 -0
  94. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/change_list_results.html +0 -0
  95. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/edit_inline/tabular.html +0 -0
  96. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/admin/pagination.html +0 -0
  97. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/registration/password_change_done.html +0 -0
  98. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templates/registration/password_change_form.html +0 -0
  99. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templatetags/__init__.py +0 -0
  100. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templatetags/yla_charts.py +0 -0
  101. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/templatetags/yla_components.py +0 -0
  102. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/tests.py +0 -0
  103. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/urls.py +0 -0
  104. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/theme/views.py +0 -0
  105. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/__init__.py +0 -0
  106. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/admin.py +0 -0
  107. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/apps.py +0 -0
  108. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/management/__init__.py +0 -0
  109. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/management/commands/__init__.py +0 -0
  110. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/management/commands/seed_demo_data.py +0 -0
  111. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/migrations/0001_initial.py +0 -0
  112. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/migrations/__init__.py +0 -0
  113. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/models.py +0 -0
  114. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/tests.py +0 -0
  115. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/apps/users/views.py +0 -0
  116. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/requirements.txt +0 -0
  117. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/setup.cfg +0 -0
  118. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin/__init__.py +0 -0
  119. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin/apps.py +0 -0
  120. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin.egg-info/dependency_links.txt +0 -0
  121. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin.egg-info/requires.txt +0 -0
  122. {yanleafadmin-2.0.2 → yanleafadmin-2.0.4}/yanleafadmin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: yanleafadmin
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: 极简白色现代 Django Admin 主题体系 — 基于 Django + Bulma CSS
5
5
  Home-page: https://github.com/zhouyanye/yanleafadmin
6
6
  Author: zhouyanye
@@ -219,6 +219,22 @@ yanleaf_admin_project/
219
219
  └── README.md
220
220
  ```
221
221
 
222
+ ### URL 配置
223
+
224
+ 如果使用了 ER 图、AI 助手等功能,需要在 `urls.py` 中添加对应路由:
225
+
226
+ ```python
227
+ from django.urls import path, include
228
+
229
+ urlpatterns = [
230
+ path('i18n/', include('django.conf.urls.i18n')), # 语言切换(必须)
231
+ path('admin/', admin.site.urls),
232
+ path('admin/erd/', include('apps.erd_engine.urls')), # ER 图
233
+ path('api/ai/', include('apps.ai_assistant.urls')), # AI 助手
234
+ path('yla-api/', include('apps.theme.urls')), # SmartChart 等
235
+ ]
236
+ ```
237
+
222
238
  ## YANLEAF_ADMIN 完整配置项
223
239
 
224
240
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -200,6 +200,22 @@ yanleaf_admin_project/
200
200
  └── README.md
201
201
  ```
202
202
 
203
+ ### URL 配置
204
+
205
+ 如果使用了 ER 图、AI 助手等功能,需要在 `urls.py` 中添加对应路由:
206
+
207
+ ```python
208
+ from django.urls import path, include
209
+
210
+ urlpatterns = [
211
+ path('i18n/', include('django.conf.urls.i18n')), # 语言切换(必须)
212
+ path('admin/', admin.site.urls),
213
+ path('admin/erd/', include('apps.erd_engine.urls')), # ER 图
214
+ path('api/ai/', include('apps.ai_assistant.urls')), # AI 助手
215
+ path('yla-api/', include('apps.theme.urls')), # SmartChart 等
216
+ ]
217
+ ```
218
+
203
219
  ## YANLEAF_ADMIN 完整配置项
204
220
 
205
221
  | 配置项 | 类型 | 默认值 | 说明 |
@@ -8,8 +8,14 @@ class DashboardEngineConfig(AppConfig):
8
8
  def ready(self):
9
9
  from django.contrib import admin
10
10
  from django.contrib.auth import get_user_model
11
- import psutil
12
11
  import json
12
+
13
+ try:
14
+ import psutil
15
+ _has_psutil = True
16
+ except ImportError:
17
+ _has_psutil = False
18
+
13
19
  original_index = admin.site.index
14
20
 
15
21
  def custom_index(request, extra_context=None):
@@ -23,7 +29,7 @@ class DashboardEngineConfig(AppConfig):
23
29
  today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
24
30
  today_count = User.objects.filter(date_joined__gte=today_start).count()
25
31
 
26
- memory_percent = psutil.virtual_memory().percent
32
+ memory_percent = psutil.virtual_memory().percent if _has_psutil else 0.0
27
33
  except Exception:
28
34
  total_users = 0
29
35
  active_users = 0
@@ -145,8 +145,11 @@ def er_word_export(request):
145
145
  except (json.JSONDecodeError, TypeError):
146
146
  return HttpResponse('Invalid data', status=400)
147
147
 
148
- from docx import Document
149
- from docx.shared import Pt
148
+ try:
149
+ from docx import Document
150
+ from docx.shared import Pt
151
+ except ImportError:
152
+ return HttpResponse('Word 导出需要安装 python-docx:pip install yanleafadmin[word]', status=500)
150
153
  from docx.enum.text import WD_ALIGN_PARAGRAPH
151
154
  from docx.oxml.ns import qn
152
155
  from docx.oxml import OxmlElement
@@ -23,6 +23,16 @@ class ThemeConfig(AppConfig):
23
23
  # 保留 per_page 参数补丁
24
24
  self._patch_changelist_per_page()
25
25
 
26
+ # 仪表盘首页(pip install 时自动启用)
27
+ try:
28
+ from apps.dashboard_engine.apps import DashboardEngineConfig
29
+ import apps.dashboard_engine
30
+ dashboard_cfg = DashboardEngineConfig(apps.dashboard_engine.__name__, apps.dashboard_engine)
31
+ dashboard_cfg.path = __import__('os').path.dirname(apps.dashboard_engine.__file__)
32
+ dashboard_cfg.ready()
33
+ except Exception:
34
+ pass # 仪表盘可选
35
+
26
36
  def _patch_changelist_per_page(self):
27
37
  from django.contrib.admin.views.main import ChangeList
28
38
  from django.core.paginator import Paginator
@@ -4,9 +4,18 @@ from .settings import get_config
4
4
 
5
5
  def yanleaf_settings(request):
6
6
  config = get_config()
7
+ # 安全检测 set_language URL 是否可用
8
+ try:
9
+ from django.urls import reverse
10
+ reverse('set_language')
11
+ has_i18n = True
12
+ except Exception:
13
+ has_i18n = False
14
+
7
15
  return {
8
16
  'YANLEAF_CONFIG': config,
9
17
  'YANLEAF_SHOW_CREDIT': config.get('show_credit', True),
10
18
  'YANLEAF_SIDEBAR_WIDTH': config.get('sidebar_width', '250px'),
11
19
  'YANLEAF_THEME_COLOR': config.get('theme_color', '#485fc7'),
20
+ 'YLA_HAS_SET_LANGUAGE': has_i18n,
12
21
  }
@@ -0,0 +1,24 @@
1
+ from django import forms
2
+ from django.contrib.auth.forms import AuthenticationForm
3
+
4
+ try:
5
+ from captcha.fields import CaptchaField
6
+ _has_captcha = True
7
+ except ImportError:
8
+ _has_captcha = False
9
+ CaptchaField = None
10
+
11
+
12
+ class YanleafAdminLoginForm(AuthenticationForm):
13
+ """重写 Admin 登录表单,可选 captcha"""
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ if _has_captcha and CaptchaField:
18
+ self.fields['captcha'] = CaptchaField(
19
+ label="验证码",
20
+ error_messages={
21
+ 'invalid': '验证码输入错误,请重新输入',
22
+ 'required': '请输入验证码'
23
+ }
24
+ )
@@ -72,7 +72,7 @@
72
72
 
73
73
  window.YLA.initDataTable = function(tableSelector, options) {
74
74
  var langCode = window.YLA.lang || 'zh-hans';
75
- var langUrl = '/static/yanleafadmin/js/i18n/datatables.' + langCode + '.json';
75
+ var langUrl = (window.YLA.staticUrl || '/static/yanleafadmin/') + 'js/i18n/datatables.' + langCode + '.json';
76
76
  var defaults = {
77
77
  language: { url: langUrl },
78
78
  pageLength: 25,
@@ -142,6 +142,7 @@
142
142
 
143
143
  <!-- 语言切换 -->
144
144
  {% block language_switcher %}
145
+ {% if YLA_HAS_SET_LANGUAGE %}
145
146
  <div class="dropdown is-right" id="lang-dropdown">
146
147
  <div class="dropdown-trigger">
147
148
  <button class="icon-btn" title="{% translate '切换语言' %}">
@@ -167,6 +168,7 @@
167
168
  </div>
168
169
  </div>
169
170
  </div>
171
+ {% endif %}
170
172
  {% endblock %}
171
173
 
172
174
  <!-- 用户菜单 -->
@@ -250,7 +252,7 @@
250
252
  <!-- ============================================================
251
253
  YanLeafAdmin 核心脚本
252
254
  ============================================================ -->
253
- <script>window.YLA=window.YLA||{};window.YLA.lang='{{ request.LANGUAGE_CODE|default:"zh-hans" }}';</script>
255
+ <script>window.YLA=window.YLA||{};window.YLA.lang='{{ request.LANGUAGE_CODE|default:"zh-hans" }}';window.YLA.staticUrl='{% static 'yanleafadmin/' %}';</script>
254
256
  <script>window.YLA.aiEnabled = {{ YANLEAF_CONFIG.ai_assistant_enabled|default:True|yesno:'true,false' }};</script>
255
257
  <script src="{% static 'yanleafadmin/js/core.js' %}"></script>
256
258
  <script src="{% static 'yanleafadmin/js/components.js' %}"></script>
@@ -0,0 +1,15 @@
1
+ {% extends "admin/base.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | YanleafAdmin{% endblock %}
5
+
6
+ {% block sidebar_brand %}
7
+ <div class="sidebar-brand">
8
+ <a href="{% url 'admin:index' %}" style="display:flex;align-items:center;gap:0.5rem;text-decoration:none;">
9
+ <span class="brand-icon"><i class="fas fa-leaf"></i></span>
10
+ <span class="brand-text">
11
+ Yanleaf<span class="brand-sub">Admin</span>
12
+ </span>
13
+ </a>
14
+ </div>
15
+ {% endblock %}
@@ -167,7 +167,7 @@
167
167
  { extend: 'print', text: '打印' }
168
168
  ],
169
169
  language: {
170
- url: '/static/yanleafadmin/js/i18n/datatables.' + langCode + '.json'
170
+ url: (window.YLA.staticUrl || '/static/yanleafadmin/') + 'js/i18n/datatables.' + langCode + '.json'
171
171
  }
172
172
  });
173
173
 
@@ -0,0 +1,394 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block extrastyle %}
5
+ {{ block.super }}
6
+ <link rel="stylesheet" href="{% static 'yanleafadmin/css/dashboard.css' %}">
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ <div>
11
+
12
+ <div class="mb-6">
13
+ <h1 class="title is-3 has-text-weight-bold mb-2">{% translate '控制台' %}</h1>
14
+ <p class="subtitle is-6 has-text-grey-light">{% translate '欢迎回来' %},{{ user.username }}。{% translate '以下是系统核心组件的实时运行指标。' %}</p>
15
+ </div>
16
+
17
+ <div class="columns is-desktop is-multiline mb-6">
18
+
19
+ <div class="column is-3-desktop is-6-tablet">
20
+ <div class="box dashboard-stat-card p-5" style="height:110px;display:flex;flex-direction:column;justify-content:space-between;">
21
+ <p class="heading mb-1"><i class="fas fa-users"></i> {% translate '总注册用户' %}</p>
22
+ <div>
23
+ <span class="title is-3 has-text-weight-bold" style="margin-bottom:0;white-space:nowrap;">{{ total_users_count }}</span>
24
+ <div class="trend-tag" style="font-size:0.7rem;color:#22c55e;margin-top:0.15rem;">
25
+ <i class="fas fa-arrow-up"></i> 本周 +{{ week_new }}
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="column is-3-desktop is-6-tablet">
32
+ <div class="box dashboard-stat-card p-5" style="height:110px;display:flex;flex-direction:column;justify-content:space-between;">
33
+ <p class="heading mb-1"><i class="fas fa-user-plus"></i> {% translate '今日新增' %}</p>
34
+ <div>
35
+ <span class="title is-3 has-text-weight-bold" style="margin-bottom:0;white-space:nowrap;">{{ today_users_count }}</span>
36
+ {% if today_vs_yesterday != 0 %}
37
+ <div class="trend-tag" style="font-size:0.7rem;{% if today_vs_yesterday > 0 %}color:#22c55e;{% else %}color:#ef4444;{% endif %}margin-top:0.15rem;">
38
+ <i class="fas fa-arrow-{% if today_vs_yesterday > 0 %}up{% else %}down{% endif %}"></i>
39
+ {% if today_vs_yesterday > 0 %}+{% endif %}{{ today_vs_yesterday }} ({% if today_pct > 0 %}+{% endif %}{{ today_pct }}%)
40
+ </div>
41
+ {% endif %}
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="column is-3-desktop is-6-tablet">
47
+ <div class="box dashboard-stat-card p-5" style="height:110px;display:flex;flex-direction:column;justify-content:space-between;">
48
+ <p class="heading mb-1"><i class="fas fa-user-check"></i> {% translate '活跃用户' %}</p>
49
+ <div>
50
+ <span class="title is-3 has-text-weight-bold" style="margin-bottom:0;white-space:nowrap;">{{ active_users_count }}</span>
51
+ <div class="trend-tag" style="font-size:0.7rem;color:var(--yla-text-muted);margin-top:0.15rem;">
52
+ 占比 {{ active_pct }}%
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="column is-3-desktop is-6-tablet">
59
+ <div class="box dashboard-stat-card p-5" style="height:110px;display:flex;flex-direction:column;justify-content:space-between;">
60
+ <p class="heading mb-1"><i class="fas fa-microchip"></i> {% translate '服务器内存负载' %}</p>
61
+ <div>
62
+ <span class="title is-3 has-text-weight-bold" style="margin-bottom:0;white-space:nowrap;letter-spacing:-0.03em;">{{ memory_usage }}</span>
63
+ <div class="trend-tag" style="font-size:0.7rem;color:var(--yla-text-muted);margin-top:0.15rem;">
64
+ 系统运行中
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ </div>
71
+
72
+ <div class="columns">
73
+ <div class="column is-4">
74
+ <div class="box p-5 dashboard-stat-card" style="max-height: 420px; overflow-y: auto;">
75
+ <h3 class="subtitle is-6 has-text-weight-bold mb-3">{% translate '业务模块导航' %}</h3>
76
+
77
+ {% if app_list %}
78
+ <div class="is-flex is-flex-direction-column gap-3">
79
+ {% for app in app_list %}
80
+ <div class="mb-3">
81
+ <p class="is-size-7 has-text-weight-semibold has-text-grey-light uppercase tracking-wider mb-2"
82
+ style="letter-spacing: 0.05em;">
83
+ {{ app.name }}
84
+ </p>
85
+
86
+ <div class="is-flex is-flex-direction-column gap-2" style="padding-left: 4px;">
87
+ {% for model in app.models %}
88
+ <div class="quick-link-item p-2 is-flex is-justify-content-space-between is-align-items-center">
89
+ <div>
90
+ {% if model.admin_url %}
91
+ <a href="{{ model.admin_url }}" class="has-text-weight-medium is-size-6">
92
+ <i class="fas {% if 'user' in model.object_name|lower %}fa-user{% elif 'group' in model.object_name|lower %}fa-users{% elif 'permission' in model.object_name|lower or 'perm' in model.object_name|lower %}fa-shield-alt{% elif 'log' in model.object_name|lower %}fa-history{% else %}fa-table{% endif %} mr-1" style="opacity:0.5;font-size:0.8rem;"></i>
93
+ {{ model.name }}
94
+ </a>
95
+ {% else %}
96
+ <span class="has-text-grey-light is-size-6">{{ model.name }}</span>
97
+ {% endif %}
98
+ </div>
99
+
100
+ <div class="is-flex gap-2">
101
+ {% if model.add_url %}
102
+ <a href="{{ model.add_url }}" class="button is-small is-light is-rounded py-0 px-2" title="{% translate '新增' %}">
103
+ <span class="has-text-grey">+</span>
104
+ </a>
105
+ {% endif %}
106
+ </div>
107
+ </div>
108
+ {% endfor %}
109
+ </div>
110
+ </div>
111
+ {% endfor %}
112
+ </div>
113
+ {% else %}
114
+ <div class="has-text-centered py-5">
115
+ <p class="has-text-grey-light is-size-6">{% translate '暂无可用业务模块' %}</p>
116
+ </div>
117
+ {% endif %}
118
+ </div>
119
+ </div>
120
+
121
+ <div class="column is-8">
122
+ <div class="box p-5 dashboard-stat-card" style="min-height:380px;overflow:hidden;">
123
+ <h3 class="subtitle is-6 has-text-weight-bold mb-2">
124
+ <i class="fas fa-chart-line"></i> {% translate '最近 7 天注册趋势' %}
125
+ </h3>
126
+ <div id="main-chart" style="width:100%;height:330px;"></div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ {% if recent_logs %}
132
+ <div class="box dashboard-stat-card p-5" style="margin-top:1.5rem;">
133
+ <div class="is-flex is-justify-content-space-between is-align-items-center mb-3">
134
+ <h3 class="subtitle is-6 has-text-weight-bold" style="margin-bottom:0;">
135
+ <i class="fas fa-history"></i> {% translate '系统动态' %}
136
+ </h3>
137
+ <div class="is-flex is-align-items-center" style="gap:0.5rem;font-size:0.72rem;color:var(--yla-text-muted);">
138
+ <i class="fab {{ os_icon }}"></i>
139
+ <span>{{ client_ip|default:'—' }}</span>
140
+ <i class="fab {{ browser_icon }}" style="margin-left:0.5rem;"></i>
141
+ </div>
142
+ </div>
143
+ <div class="timeline">
144
+ {% for log in recent_logs %}
145
+ <div class="timeline-item yla-log-row" style="display:flex;align-items:flex-start;gap:0.75rem;padding:0.6rem 0;{% if not forloop.last %}border-bottom:1px solid var(--yla-border-light);{% endif %}" data-log-id="{{ log.id }}">
146
+ <div class="timeline-dot" style="flex-shrink:0;width:8px;height:8px;border-radius:50%;background:{% if log.badge == 'success' %}#22c55e{% elif log.badge == 'danger' %}#ef4444{% elif log.badge == 'warning' %}#f59e0b{% else %}#3b82f6{% endif %};margin-top:0.45rem;"></div>
147
+ <div style="flex:1;min-width:0;">
148
+ <div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
149
+ <strong style="font-size:0.82rem;">{{ log.user }}</strong>
150
+ <span class="yla-badge yla-badge-{{ log.badge }}" style="font-size:0.7rem;">{{ log.action }}</span>
151
+ {% if log.module %}
152
+ <span style="font-size:0.7rem;color:var(--yla-text-muted);">{{ log.module }}</span>
153
+ {% endif %}
154
+ </div>
155
+ <div style="font-size:0.82rem;color:var(--yla-text-primary);margin-top:2px;">
156
+ {{ log.object_name }}
157
+ </div>
158
+ {% if log.has_diff %}
159
+ <div class="yla-log-diff" style="display:none;margin-top:6px;padding:8px 10px;background:var(--yla-bg-hover);border-radius:6px;font-size:0.75rem;">
160
+ <div class="yla-diff-loading has-text-grey">{% translate '加载变更详情...' %}</div>
161
+ </div>
162
+ <a href="#" class="yla-log-expand is-size-7" style="display:inline-block;margin-top:4px;" data-log-id="{{ log.id }}">
163
+ <i class="fas fa-chevron-down"></i> {% translate '展开变更' %}
164
+ </a>
165
+ {% endif %}
166
+ <span style="font-size:0.7rem;color:var(--yla-text-muted);margin-left:0.5rem;white-space:nowrap;">{{ log.time|date:"m-d H:i" }}</span>
167
+ </div>
168
+ </div>
169
+ {% endfor %}
170
+ </div>
171
+ </div>
172
+ {% endif %}
173
+
174
+ <div class="columns" style="margin-top:1.5rem;">
175
+ <div class="column is-8">
176
+ <div class="box dashboard-stat-card p-5" style="min-height:200px;overflow:hidden;">
177
+ <h3 class="subtitle is-6 has-text-weight-bold mb-2">
178
+ <i class="fas fa-calendar-alt"></i> {% translate '近半年操作热力图' %}
179
+ </h3>
180
+ <div id="heatmap-chart" style="width:100%;height:220px;"></div>
181
+ </div>
182
+ </div>
183
+ <div class="column is-4">
184
+ <div class="box dashboard-stat-card p-5" style="min-height:200px;overflow:hidden;">
185
+ <h3 class="subtitle is-6 has-text-weight-bold mb-2">
186
+ <i class="fas fa-chart-pie"></i> {% translate '近7天模块活跃度' %}
187
+ </h3>
188
+ <div id="module-pie-chart" style="width:100%;height:220px;"></div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ </div>
194
+ {% endblock %}
195
+
196
+ {% block extra_js %}
197
+ {{ block.super }}
198
+ <script>
199
+ (function() {
200
+ var chartDom = document.getElementById('main-chart');
201
+ if (!chartDom) return;
202
+ var chart = null;
203
+
204
+ function getTheme() {
205
+ var d = document.documentElement.classList.contains('theme-dark');
206
+ return {
207
+ isDark: d,
208
+ text: d ? '#94a3b8' : '#64748b',
209
+ line: d ? '#60a5fa' : '#3b82f6',
210
+ grid: d ? '#1e293b' : '#f1f5f9',
211
+ axis: d ? '#334155' : '#e2e8f0',
212
+ bg: d ? '#1e293b' : '#fff',
213
+ bgBorder: d ? '#334155' : '#e2e8f0',
214
+ tipText: d ? '#e2e8f0' : '#0f172a',
215
+ area: d
216
+ ? new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(96,165,250,.25)'},{offset:1,color:'rgba(96,165,250,0)'}])
217
+ : new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(59,130,246,.15)'},{offset:1,color:'rgba(59,130,246,0)'}])
218
+ };
219
+ }
220
+
221
+ function buildOption(t) {
222
+ return {
223
+ tooltip: {
224
+ trigger: 'axis',
225
+ backgroundColor: t.bg, borderColor: t.bgBorder,
226
+ textStyle: { color: t.tipText, fontSize: 13 },
227
+ formatter: function(p) { return p[0].axisValue + ' 新增 <b>' + p[0].value + '</b> 人'; }
228
+ },
229
+ grid: { top: 15, right: 20, bottom: 25, left: 45 },
230
+ xAxis: {
231
+ type: 'category', boundaryGap: false,
232
+ data: {{ chart_labels|safe }},
233
+ axisLine: { lineStyle: { color: t.axis } },
234
+ axisTick: { show: false },
235
+ axisLabel: { color: t.text, fontSize: 11 }
236
+ },
237
+ yAxis: {
238
+ type: 'value', minInterval: 1,
239
+ splitLine: { lineStyle: { color: t.grid } },
240
+ axisLabel: { color: t.text, fontSize: 11 }
241
+ },
242
+ series: [{
243
+ data: {{ chart_data|safe }},
244
+ type: 'line', smooth: true,
245
+ symbol: 'circle', symbolSize: 6,
246
+ lineStyle: { color: t.line, width: 2.5 },
247
+ itemStyle: { color: t.line },
248
+ areaStyle: { color: t.area },
249
+ }]
250
+ };
251
+ }
252
+
253
+ function render() {
254
+ if (chart) chart.dispose();
255
+ chart = echarts.init(chartDom);
256
+ chart.setOption(buildOption(getTheme()));
257
+ }
258
+
259
+ render();
260
+ window.addEventListener('resize', function() { if (chart) chart.resize(); });
261
+ var observer = new MutationObserver(function() { render(); });
262
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
263
+
264
+ })();
265
+
266
+ // ── Activity heatmap ──
267
+ (function() {
268
+ var dom = document.getElementById('heatmap-chart');
269
+ if (!dom) return;
270
+ var chart = echarts.init(dom);
271
+ var data = {{ heatmap_data_json|default:'[]'|safe }};
272
+
273
+ function buildHeatmap() {
274
+ var d = document.documentElement.classList.contains('theme-dark');
275
+ var today = new Date();
276
+ var startDate = new Date(today.getTime() - 180 * 24 * 3600 * 1000);
277
+
278
+ // Build full date range
279
+ var dateMap = {};
280
+ data.forEach(function(item) { dateMap[item[0]] = item[1]; });
281
+
282
+ var seriesData = [];
283
+ for (var dt = new Date(startDate); dt <= today; dt.setDate(dt.getDate() + 1)) {
284
+ var ds = dt.toISOString().slice(0, 10);
285
+ seriesData.push([ds, dateMap[ds] || 0]);
286
+ }
287
+
288
+ var option = {
289
+ tooltip: {
290
+ formatter: function(p) { return p.value[0] + '<br/>{% translate "操作" %}: <b>' + p.value[1] + '</b> {% translate "次" %}'; }
291
+ },
292
+ visualMap: {
293
+ min: 0, max: Math.max.apply(null, data.map(function(i){return i[1];}).concat([1])),
294
+ type: 'piecewise', orient: 'horizontal', left: 'center', bottom: 0,
295
+ textStyle: { color: d ? '#94a3b8' : '#555' },
296
+ pieces: [
297
+ { value: 0, color: d ? '#1a1a2e' : '#e8e8e8' },
298
+ { gt: 0, lte: 2, color: d ? '#1a4a1a' : '#6bc470' },
299
+ { gt: 2, lte: 5, color: d ? '#2d6a1e' : '#2ea84a' },
300
+ { gt: 5, lte: 10, color: d ? '#3d7b24' : '#1d8c36' },
301
+ { gt: 10, color: d ? '#4d8c2a' : '#156b2a' }
302
+ ]
303
+ },
304
+ calendar: {
305
+ top: 30, left: 40, right: 20, bottom: 30,
306
+ range: [(new Date(Date.now() - 180*86400000)).toISOString().slice(0,10), today.toISOString().slice(0,10)],
307
+ cellSize: [13, 13],
308
+ yearLabel: { show: false },
309
+ dayLabel: { fontSize: 9, color: d ? '#667' : '#999', firstDay: 1 },
310
+ monthLabel: { fontSize: 10, color: d ? '#667' : '#888', position: 'start', margin: 8 },
311
+ itemStyle: { borderColor: d ? '#2a2a3a' : '#ddd', borderWidth: 2 }
312
+ },
313
+ series: [{
314
+ type: 'heatmap', coordinateSystem: 'calendar',
315
+ data: seriesData,
316
+ label: { show: false }
317
+ }]
318
+ };
319
+ chart.setOption(option);
320
+ }
321
+ buildHeatmap();
322
+ window.addEventListener('resize', function() { chart.resize(); });
323
+ new MutationObserver(function() { buildHeatmap(); }).observe(document.documentElement, {attributes:true,attributeFilter:['class']});
324
+ })();
325
+
326
+ // ── Module rose chart ──
327
+ (function() {
328
+ var dom2 = document.getElementById('module-pie-chart');
329
+ if (!dom2) return;
330
+ var chart2 = echarts.init(dom2);
331
+ var moduleData = {{ module_stats_json|default:'[]'|safe }};
332
+
333
+ function buildRose() {
334
+ var d = document.documentElement.classList.contains('theme-dark');
335
+ chart2.setOption({
336
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ' + '次' + ' ({d}%)' },
337
+ legend: { bottom: 0, textStyle: { fontSize: 9, color: d ? '#94a3b8' : '#64748b' } },
338
+ series: [{
339
+ type: 'pie',
340
+ roseType: 'area',
341
+ radius: ['15%', '70%'],
342
+ center: ['50%', '45%'],
343
+ data: moduleData.length ? moduleData : [{name: '无数据', value: 0}],
344
+ label: { fontSize: 9, color: d ? '#94a3b8' : '#555' },
345
+ emphasis: { label: { fontSize: 13, fontWeight: 'bold' } },
346
+ itemStyle: { borderRadius: 4, borderColor: d ? '#1a1a2e' : '#fff', borderWidth: 2 }
347
+ }]
348
+ });
349
+ }
350
+ buildRose();
351
+ window.addEventListener('resize', function() { chart2.resize(); });
352
+ new MutationObserver(function() { buildRose(); }).observe(document.documentElement, {attributes:true,attributeFilter:['class']});
353
+ })();
354
+
355
+ // ── Diff expand/collapse ──
356
+ (function($) {
357
+ $(document).on('click', '.yla-log-expand', function(e) {
358
+ e.preventDefault();
359
+ var $link = $(this);
360
+ var logId = $link.data('log-id');
361
+ var $row = $link.closest('.yla-log-row');
362
+ var $diff = $row.find('.yla-log-diff');
363
+
364
+ if ($diff.is(':visible')) {
365
+ $diff.slideUp(150);
366
+ $link.html('<i class="fas fa-chevron-down"></i> ' + '{% translate "展开变更" %}');
367
+ } else {
368
+ $diff.slideDown(150);
369
+ $link.html('<i class="fas fa-chevron-up"></i> ' + '{% translate "收起" %}');
370
+
371
+ // Load diff data if not yet loaded
372
+ if ($diff.find('.yla-diff-loading').length) {
373
+ $.getJSON('/admin/log-diff/' + logId + '/', function(data) {
374
+ if (data.error) {
375
+ $diff.html('<span class="has-text-grey">' + data.error + '</span>');
376
+ } else {
377
+ var html = data.changes.map(function(c) {
378
+ return '<div style="margin-bottom:3px;"><span style="color:var(--yla-text-muted);">' + c.field + '</span>: ' +
379
+ '<span style="text-decoration:line-through;color:#ef4444;">' + (c.old || '—') + '</span> ' +
380
+ '<i class="fas fa-arrow-right has-text-grey" style="font-size:0.6rem;"></i> ' +
381
+ '<span style="color:#22c55e;font-weight:500;">' + (c.new || '—') + '</span></div>';
382
+ }).join('');
383
+ if (!html) html = '<span class="has-text-grey">{% translate "无字段级变更记录" %}</span>';
384
+ $diff.html(html);
385
+ }
386
+ }).fail(function() {
387
+ $diff.html('<span class="has-text-grey">{% translate "加载失败" %}</span>');
388
+ });
389
+ }
390
+ }
391
+ });
392
+ })(jQuery);
393
+ </script>
394
+ {% endblock %}
@@ -69,6 +69,7 @@
69
69
  {{ form.password.errors }}
70
70
  </div>
71
71
 
72
+ {% if form.captcha %}
72
73
  <div class="field mb-5">
73
74
  <label class="label is-size-7 has-text-weight-semibold">{% translate '安全验证' %}</label>
74
75
  <div class="captcha-field-wrapper">
@@ -76,6 +77,7 @@
76
77
  </div>
77
78
  {{ form.captcha.errors }}
78
79
  </div>
80
+ {% endif %}
79
81
 
80
82
  <div class="field mt-5">
81
83
  <button class="button is-dark is-fullwidth has-text-weight-medium" type="submit">
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
3
3
 
4
4
  setup(
5
5
  name='yanleafadmin',
6
- version='2.0.2',
6
+ version='2.0.4',
7
7
  description='极简白色现代 Django Admin 主题体系 — 基于 Django + Bulma CSS',
8
8
  long_description=open('README.md', encoding='utf-8').read() if __import__('os').path.exists('README.md') else '',
9
9
  long_description_content_type='text/markdown',