yanleafadmin 2.0.8__tar.gz → 2.1.0__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.
- {yanleafadmin-2.0.8/yanleafadmin.egg-info → yanleafadmin-2.1.0}/PKG-INFO +16 -5
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/README.md +14 -3
- yanleafadmin-2.1.0/apps/ai_assistant/migrations/0001_initial.py +32 -0
- yanleafadmin-2.1.0/apps/ai_assistant/migrations/0002_aichatsession_aichatmessage_session_and_more.py +49 -0
- yanleafadmin-2.1.0/apps/ai_assistant/models.py +34 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/services.py +1 -2
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/static/ai/ai-search.js +35 -18
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/templates/ai/fullpage.html +71 -1
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/urls.py +3 -0
- yanleafadmin-2.1.0/apps/ai_assistant/views.py +116 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/apps.py +4 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/change-list.css +9 -1
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/login.css +5 -3
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/change_list.html +20 -0
- yanleafadmin-2.1.0/apps/users/migrations/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/setup.py +3 -2
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0/yanleafadmin.egg-info}/PKG-INFO +16 -5
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin.egg-info/SOURCES.txt +4 -0
- yanleafadmin-2.0.8/apps/ai_assistant/views.py +0 -40
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/LICENSE +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/MANIFEST.in +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/ai_assistant/apps.py +0 -0
- {yanleafadmin-2.0.8/apps/dashboard_engine → yanleafadmin-2.1.0/apps/ai_assistant/migrations}/__init__.py +0 -0
- {yanleafadmin-2.0.8/apps/dashboard_engine/migrations → yanleafadmin-2.1.0/apps/dashboard_engine}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/admin.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/apps.py +0 -0
- {yanleafadmin-2.0.8/apps/erd_engine → yanleafadmin-2.1.0/apps/dashboard_engine/migrations}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/models.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/templates/admin/base_site.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/templates/admin/dashboard_index.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/tests.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/dashboard_engine/views.py +0 -0
- {yanleafadmin-2.0.8/apps/theme → yanleafadmin-2.1.0/apps/erd_engine}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/apps.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/sql_parser.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/static/erd/er-diagram.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/templates/erd/er_diagram.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/urls.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/erd_engine/views.py +0 -0
- {yanleafadmin-2.0.8/apps/theme/components → yanleafadmin-2.1.0/apps/theme}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/admin.py +0 -0
- {yanleafadmin-2.0.8/apps/theme/migrations → yanleafadmin-2.1.0/apps/theme/components}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/components/actions.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/components/charts.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/context_processors.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/forms.py +0 -0
- {yanleafadmin-2.0.8/apps/theme/templatetags → yanleafadmin-2.1.0/apps/theme/migrations}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/models.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/settings.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/admin.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/change-form.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/dashboard.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/password-form.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/user-change-form.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/components.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/core.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/filter-widget.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/i18n/datatables.en.json +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/i18n/datatables.zh-hans.json +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/login.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/password-check.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/smart-chart.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/js/user-password-field.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/bulma/bulma.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/bulma-calendar/bulma-calendar.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/bulma-calendar/bulma-calendar.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/css/buttons.bulma.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/css/dataTables.bulma.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/js/buttons.html5.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/js/buttons.print.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/js/dataTables.bulma.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/js/dataTables.buttons.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/datatables/js/jquery.dataTables.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/dropzone/dropzone-min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/dropzone/dropzone.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/echarts/echarts.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/fontawesome/css/all.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-brands-400.woff2 +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-regular-400.woff2 +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/fontawesome/webfonts/fa-solid-900.woff2 +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/gojs/go.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/jquery/jquery.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/jszip/jszip.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/select2/css/select2.min.css +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/select2/js/select2.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/vendor/sweetalert2/sweetalert2.all.min.js +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/app_index.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/auth/user/add_form.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/auth/user/change_form.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/auth/user/change_password.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/base.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/base_site.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/change_form.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/change_list_results.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/dashboard_index.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/edit_inline/tabular.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/login.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/admin/pagination.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/registration/password_change_done.html +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templates/registration/password_change_form.html +0 -0
- {yanleafadmin-2.0.8/apps/users → yanleafadmin-2.1.0/apps/theme/templatetags}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templatetags/yla_charts.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/templatetags/yla_components.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/tests.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/urls.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/views.py +0 -0
- {yanleafadmin-2.0.8/apps/users/management → yanleafadmin-2.1.0/apps/users}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/admin.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/apps.py +0 -0
- {yanleafadmin-2.0.8/apps/users/management/commands → yanleafadmin-2.1.0/apps/users/management}/__init__.py +0 -0
- {yanleafadmin-2.0.8/apps/users/migrations → yanleafadmin-2.1.0/apps/users/management/commands}/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/management/commands/seed_demo_data.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/migrations/0001_initial.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/models.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/tests.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/users/views.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/requirements.txt +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/setup.cfg +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin/__init__.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin/apps.py +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin.egg-info/dependency_links.txt +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin.egg-info/requires.txt +0 -0
- {yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/yanleafadmin.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: yanleafadmin
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: 极简白色现代 Django Admin 主题体系 — 基于 Django + Bulma CSS
|
|
5
5
|
Home-page: https://github.com/zhouyanye/yanleafadmin
|
|
6
|
-
Author: zhouyanye
|
|
6
|
+
Author: zhouyanye,YanLeaf AI Lab
|
|
7
7
|
Classifier: Framework :: Django
|
|
8
8
|
Classifier: Framework :: Django :: 5.0
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -29,6 +29,8 @@ License-File: LICENSE
|
|
|
29
29
|
- 侧边栏导航 + 顶部面包屑 + 用户菜单
|
|
30
30
|
- 登录页验证码(django-simple-captcha)
|
|
31
31
|
|
|
32
|
+

|
|
33
|
+
|
|
32
34
|
### 组件与交互
|
|
33
35
|
- **DataTables**:列表页一键导出 Excel / CSV / 打印
|
|
34
36
|
- **SweetAlert2**:编辑/删除确认弹窗,Toast 弱提示
|
|
@@ -36,12 +38,16 @@ License-File: LICENSE
|
|
|
36
38
|
- **Dropzone**:拖拽文件上传
|
|
37
39
|
- **穿梭框重写**:Bulma 卡片式左右选择器
|
|
38
40
|
|
|
41
|
+

|
|
42
|
+
|
|
39
43
|
### 可视化引擎
|
|
40
44
|
- **SmartChart**:根据 Model 字段类型自动生成 ECharts 图表(趋势/饼图/柱状图)
|
|
41
45
|
- **仪表盘热力图**:近半年操作活跃度(类 GitHub 绿格子墙)
|
|
42
46
|
- **模块玫瑰图**:近 7 天模块活跃度分布
|
|
43
47
|
- **系统动态时间线**:10 条最新操作记录,Badge 四级颜色标记,展开变更 Diff
|
|
44
48
|
|
|
49
|
+

|
|
50
|
+
|
|
45
51
|
### ER 图引擎
|
|
46
52
|
- **Django 模型直读**:一键生成当前项目所有表的实体关系图
|
|
47
53
|
- **SQL DDL 解析**:粘贴建表语句自动渲染 ER 图
|
|
@@ -50,14 +56,18 @@ License-File: LICENSE
|
|
|
50
56
|
- **中英文切换**:中文优先 COMMENT 注释,英文用原名
|
|
51
57
|
- **导出 PNG** + **导出 Word 三线表 (.docx)**
|
|
52
58
|
|
|
59
|
+

|
|
60
|
+
|
|
53
61
|
### AI 数据助手
|
|
54
62
|
- **右下角悬浮聊天机器人**,点击即可对话
|
|
55
63
|
- **自然语言查询**:基于 DeepSeek 驱动,自动转 Django ORM
|
|
56
64
|
- **ECharts 图表**:查询结果原地渲染折线图/饼图/柱状图
|
|
57
65
|
- **多模型支持**:DeepSeek Chat / Reasoner / GPT-4o / 通义千问
|
|
58
66
|
- **配置保存在浏览器**:API Key 不上传服务器
|
|
59
|
-
-
|
|
60
|
-
-
|
|
67
|
+
- **聊天历史**:数据库持久化,支持多会话管理,会话 UUID 可分享
|
|
68
|
+
- **全屏展开**:新标签页全屏对话,历史记录下拉切换
|
|
69
|
+
|
|
70
|
+

|
|
61
71
|
|
|
62
72
|
### 开发者体验
|
|
63
73
|
- **pip install**:`pip install yanleafadmin`
|
|
@@ -229,6 +239,7 @@ yanleaf_admin_project/
|
|
|
229
239
|
├── yanleafadmin/ # pip 入口包(代理 apps/theme)
|
|
230
240
|
│ ├── __init__.py
|
|
231
241
|
│ └── apps.py
|
|
242
|
+
├── screenshots/ # 截图
|
|
232
243
|
├── docs/ # 文档与设计规格
|
|
233
244
|
├── setup.py # pip 安装包
|
|
234
245
|
├── MANIFEST.in
|
|
@@ -243,7 +254,7 @@ yanleaf_admin_project/
|
|
|
243
254
|
from django.urls import path, include
|
|
244
255
|
|
|
245
256
|
urlpatterns = [
|
|
246
|
-
path('i18n/', include('django.conf.urls.i18n')), #
|
|
257
|
+
path('i18n/', include('django.conf.urls.i18n')), # 语言切换
|
|
247
258
|
path('admin/', admin.site.urls),
|
|
248
259
|
path('admin/erd/', include('apps.erd_engine.urls')), # ER 图
|
|
249
260
|
path('api/ai/', include('apps.ai_assistant.urls')), # AI 助手
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
- 侧边栏导航 + 顶部面包屑 + 用户菜单
|
|
11
11
|
- 登录页验证码(django-simple-captcha)
|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
### 组件与交互
|
|
14
16
|
- **DataTables**:列表页一键导出 Excel / CSV / 打印
|
|
15
17
|
- **SweetAlert2**:编辑/删除确认弹窗,Toast 弱提示
|
|
@@ -17,12 +19,16 @@
|
|
|
17
19
|
- **Dropzone**:拖拽文件上传
|
|
18
20
|
- **穿梭框重写**:Bulma 卡片式左右选择器
|
|
19
21
|
|
|
22
|
+

|
|
23
|
+
|
|
20
24
|
### 可视化引擎
|
|
21
25
|
- **SmartChart**:根据 Model 字段类型自动生成 ECharts 图表(趋势/饼图/柱状图)
|
|
22
26
|
- **仪表盘热力图**:近半年操作活跃度(类 GitHub 绿格子墙)
|
|
23
27
|
- **模块玫瑰图**:近 7 天模块活跃度分布
|
|
24
28
|
- **系统动态时间线**:10 条最新操作记录,Badge 四级颜色标记,展开变更 Diff
|
|
25
29
|
|
|
30
|
+

|
|
31
|
+
|
|
26
32
|
### ER 图引擎
|
|
27
33
|
- **Django 模型直读**:一键生成当前项目所有表的实体关系图
|
|
28
34
|
- **SQL DDL 解析**:粘贴建表语句自动渲染 ER 图
|
|
@@ -31,14 +37,18 @@
|
|
|
31
37
|
- **中英文切换**:中文优先 COMMENT 注释,英文用原名
|
|
32
38
|
- **导出 PNG** + **导出 Word 三线表 (.docx)**
|
|
33
39
|
|
|
40
|
+

|
|
41
|
+
|
|
34
42
|
### AI 数据助手
|
|
35
43
|
- **右下角悬浮聊天机器人**,点击即可对话
|
|
36
44
|
- **自然语言查询**:基于 DeepSeek 驱动,自动转 Django ORM
|
|
37
45
|
- **ECharts 图表**:查询结果原地渲染折线图/饼图/柱状图
|
|
38
46
|
- **多模型支持**:DeepSeek Chat / Reasoner / GPT-4o / 通义千问
|
|
39
47
|
- **配置保存在浏览器**:API Key 不上传服务器
|
|
40
|
-
-
|
|
41
|
-
-
|
|
48
|
+
- **聊天历史**:数据库持久化,支持多会话管理,会话 UUID 可分享
|
|
49
|
+
- **全屏展开**:新标签页全屏对话,历史记录下拉切换
|
|
50
|
+
|
|
51
|
+

|
|
42
52
|
|
|
43
53
|
### 开发者体验
|
|
44
54
|
- **pip install**:`pip install yanleafadmin`
|
|
@@ -210,6 +220,7 @@ yanleaf_admin_project/
|
|
|
210
220
|
├── yanleafadmin/ # pip 入口包(代理 apps/theme)
|
|
211
221
|
│ ├── __init__.py
|
|
212
222
|
│ └── apps.py
|
|
223
|
+
├── screenshots/ # 截图
|
|
213
224
|
├── docs/ # 文档与设计规格
|
|
214
225
|
├── setup.py # pip 安装包
|
|
215
226
|
├── MANIFEST.in
|
|
@@ -224,7 +235,7 @@ yanleaf_admin_project/
|
|
|
224
235
|
from django.urls import path, include
|
|
225
236
|
|
|
226
237
|
urlpatterns = [
|
|
227
|
-
path('i18n/', include('django.conf.urls.i18n')), #
|
|
238
|
+
path('i18n/', include('django.conf.urls.i18n')), # 语言切换
|
|
228
239
|
path('admin/', admin.site.urls),
|
|
229
240
|
path('admin/erd/', include('apps.erd_engine.urls')), # ER 图
|
|
230
241
|
path('api/ai/', include('apps.ai_assistant.urls')), # AI 助手
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated by Django 5.2.12 on 2026-05-28 01:53
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='AiChatMessage',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('role', models.CharField(max_length=10)),
|
|
22
|
+
('content', models.TextField()),
|
|
23
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
24
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
25
|
+
],
|
|
26
|
+
options={
|
|
27
|
+
'verbose_name': 'AI 聊天记录',
|
|
28
|
+
'verbose_name_plural': 'AI 聊天记录',
|
|
29
|
+
'ordering': ['created_at'],
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
]
|
yanleafadmin-2.1.0/apps/ai_assistant/migrations/0002_aichatsession_aichatmessage_session_and_more.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Generated by Django 5.2.12 on 2026-05-28 02:10
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('ai_assistant', '0001_initial'),
|
|
12
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='AiChatSession',
|
|
18
|
+
fields=[
|
|
19
|
+
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
|
20
|
+
('title', models.CharField(default='新对话', max_length=100)),
|
|
21
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
22
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
23
|
+
],
|
|
24
|
+
options={
|
|
25
|
+
'verbose_name': 'AI 对话会话',
|
|
26
|
+
'verbose_name_plural': 'AI 对话会话',
|
|
27
|
+
'ordering': ['-updated_at'],
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
migrations.AddField(
|
|
31
|
+
model_name='aichatmessage',
|
|
32
|
+
name='session',
|
|
33
|
+
field=models.CharField(db_index=True, default='default', max_length=36),
|
|
34
|
+
),
|
|
35
|
+
migrations.AddField(
|
|
36
|
+
model_name='aichatmessage',
|
|
37
|
+
name='title',
|
|
38
|
+
field=models.CharField(blank=True, default='', max_length=100),
|
|
39
|
+
),
|
|
40
|
+
migrations.AddIndex(
|
|
41
|
+
model_name='aichatmessage',
|
|
42
|
+
index=models.Index(fields=['user', 'session'], name='ai_assistan_user_id_562e9d_idx'),
|
|
43
|
+
),
|
|
44
|
+
migrations.AddField(
|
|
45
|
+
model_name='aichatsession',
|
|
46
|
+
name='user',
|
|
47
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
|
48
|
+
),
|
|
49
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""AI Chat History model"""
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AiChatMessage(models.Model):
|
|
7
|
+
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
8
|
+
session = models.CharField(max_length=36, default='default', db_index=True)
|
|
9
|
+
role = models.CharField(max_length=10)
|
|
10
|
+
content = models.TextField()
|
|
11
|
+
title = models.CharField(max_length=100, blank=True, default='')
|
|
12
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
ordering = ['created_at']
|
|
16
|
+
verbose_name = 'AI 聊天记录'
|
|
17
|
+
verbose_name_plural = 'AI 聊天记录'
|
|
18
|
+
indexes = [
|
|
19
|
+
models.Index(fields=['user', 'session']),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AiChatSession(models.Model):
|
|
24
|
+
"""对话会话"""
|
|
25
|
+
id = models.CharField(max_length=36, primary_key=True)
|
|
26
|
+
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
27
|
+
title = models.CharField(max_length=100, default='新对话')
|
|
28
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
29
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
30
|
+
|
|
31
|
+
class Meta:
|
|
32
|
+
ordering = ['-updated_at']
|
|
33
|
+
verbose_name = 'AI 对话会话'
|
|
34
|
+
verbose_name_plural = 'AI 对话会话'
|
|
@@ -158,10 +158,9 @@ class AIQueryService:
|
|
|
158
158
|
})
|
|
159
159
|
result_data = list(qs[:parsed.get('limit', 30)])
|
|
160
160
|
else:
|
|
161
|
-
# Normalize order_by: AI might
|
|
161
|
+
# Normalize order_by: AI might return "-count" but annotation is "_count"
|
|
162
162
|
order = parsed.get('order_by', '-_count')
|
|
163
163
|
if order in ('count', '-count'):
|
|
164
|
-
order = '-' + order if order.startswith('-') else order
|
|
165
164
|
order = order.replace('count', '_count')
|
|
166
165
|
qs = qs.values(gb_field).annotate(_count=Count('id')).order_by(order)
|
|
167
166
|
chart_data = []
|
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
function loadCfg() { try { return JSON.parse(localStorage.getItem('yla_ai_config') || '{}'); } catch(e) { return {}; } }
|
|
14
14
|
function saveCfg(c) { localStorage.setItem('yla_ai_config', JSON.stringify(c)); }
|
|
15
|
+
function sessionId() {
|
|
16
|
+
var id = localStorage.getItem('yla_ai_session');
|
|
17
|
+
if (!id) { id = 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); localStorage.setItem('yla_ai_session', id); }
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
var T = '#485fc7';
|
|
21
|
+
var T = '#485fc7';
|
|
17
22
|
|
|
18
23
|
var html = '' +
|
|
19
24
|
'<div id="yla-ai-widget">' +
|
|
@@ -139,13 +144,15 @@
|
|
|
139
144
|
$('#yla-ai-settings-back').on('click', function() { $settings.hide(); $panel.show(); $input.focus(); });
|
|
140
145
|
// 新窗口展开
|
|
141
146
|
$('#yla-ai-expand').on('click', function() {
|
|
142
|
-
window.open('/api/ai/full/', '_blank');
|
|
147
|
+
window.open('/api/ai/full/' + sessionId() + '/', '_blank');
|
|
143
148
|
});
|
|
144
149
|
|
|
145
150
|
$('#yla-ai-clear-chat').on('click', function() {
|
|
146
151
|
$body.find('.yla-ai-msg').remove();
|
|
147
152
|
$body.append('<div class="yla-ai-msg ai-bot"><div class="yla-ai-bubble">对话已清空。<br><br>你可以继续问我数据问题,比如:<br>• "最近 7 天新增了多少用户?"<br>• "每个组有多少用户?"<br>• "活跃用户占比是多少?"</div></div>');
|
|
148
153
|
$body.scrollTop($body[0].scrollHeight);
|
|
154
|
+
// Clear from server
|
|
155
|
+
$.ajax({ url: '/api/ai/history/?session=' + sessionId(), method: 'DELETE', headers: { 'X-CSRFToken': window.YLA.csrfToken || '' } });
|
|
149
156
|
});
|
|
150
157
|
|
|
151
158
|
function loadCfgForm() {
|
|
@@ -177,7 +184,7 @@
|
|
|
177
184
|
var label = type === 'bot' ? '<span class="ai-gen-label">AI 生成</span>' : '';
|
|
178
185
|
$body.append('<div class="yla-ai-msg ' + cls + '"><div class="yla-ai-bubble">' + html + label + '</div></div>');
|
|
179
186
|
$body.scrollTop($body[0].scrollHeight);
|
|
180
|
-
|
|
187
|
+
_queueSave();
|
|
181
188
|
}
|
|
182
189
|
function addTyping() {
|
|
183
190
|
var $t = $('<div class="yla-ai-msg ai-bot"><div class="yla-ai-typing"><span></span><span></span><span></span></div></div>');
|
|
@@ -228,31 +235,41 @@
|
|
|
228
235
|
}
|
|
229
236
|
}
|
|
230
237
|
|
|
231
|
-
|
|
238
|
+
var _saveTimer = null;
|
|
239
|
+
function _queueSave() {
|
|
240
|
+
clearTimeout(_saveTimer);
|
|
241
|
+
_saveTimer = setTimeout(_doSave, 500);
|
|
242
|
+
}
|
|
243
|
+
function _doSave() {
|
|
232
244
|
var msgs = [];
|
|
245
|
+
var title = '';
|
|
233
246
|
$('#yla-ai-body .yla-ai-msg').each(function() {
|
|
234
247
|
var type = $(this).hasClass('ai-user') ? 'user' : 'bot';
|
|
235
248
|
var html = $(this).find('.yla-ai-bubble').html();
|
|
236
|
-
|
|
249
|
+
if (!title && type === 'user') title = $(this).find('.yla-ai-bubble').text().substring(0, 50);
|
|
250
|
+
msgs.push({ role: type, content: html });
|
|
237
251
|
});
|
|
238
|
-
// Keep only last 50 messages
|
|
239
252
|
if (msgs.length > 50) msgs = msgs.slice(-50);
|
|
240
|
-
|
|
253
|
+
if (!msgs.length) return;
|
|
254
|
+
$.ajax({
|
|
255
|
+
url: '/api/ai/history/', method: 'POST',
|
|
256
|
+
contentType: 'application/json',
|
|
257
|
+
data: JSON.stringify({ session: sessionId(), title: title, messages: msgs }),
|
|
258
|
+
headers: { 'X-CSRFToken': window.YLA.csrfToken || '' }
|
|
259
|
+
});
|
|
241
260
|
}
|
|
242
261
|
|
|
243
262
|
function loadHistory() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
263
|
+
$.getJSON('/api/ai/history/?session=' + sessionId(), function(data) {
|
|
264
|
+
if (data.messages && data.messages.length) {
|
|
265
|
+
$body.empty();
|
|
266
|
+
data.messages.forEach(function(m) {
|
|
267
|
+
var cls = m.role === 'user' ? 'ai-user' : 'ai-bot';
|
|
268
|
+
var label = m.role === 'bot' ? '<span class="ai-gen-label">AI 生成</span>' : '';
|
|
269
|
+
$body.append('<div class="yla-ai-msg ' + cls + '"><div class="yla-ai-bubble">' + (m.content || '') + label + '</div></div>');
|
|
270
|
+
});
|
|
249
271
|
$body.scrollTop($body[0].scrollHeight);
|
|
250
272
|
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function addMsgSilent(type, html) {
|
|
255
|
-
var cls = type === 'user' ? 'ai-user' : 'ai-bot';
|
|
256
|
-
$body.append('<div class="yla-ai-msg ' + cls + '"><div class="yla-ai-bubble">' + html + '</div></div>');
|
|
273
|
+
});
|
|
257
274
|
}
|
|
258
275
|
})(jQuery);
|
|
@@ -33,12 +33,25 @@
|
|
|
33
33
|
.theme-dark .ai-full-msg.bot .ai-full-bubble { background:#2a2a3a;color:#ddd; }
|
|
34
34
|
.theme-dark #ai-full-input { background:#2a2a3a;border-color:#444;color:#ddd; }
|
|
35
35
|
.theme-dark .ai-full-example { background:#2a2a3a; }
|
|
36
|
+
.ai-session-bar { display:flex;align-items:center;gap:8px;padding:6px 14px;background:var(--yla-bg-card,#fff);border:1px solid var(--yla-border,#e5e5e5);border-bottom:none;border-radius:12px 12px 0 0;font-size:.78rem }
|
|
37
|
+
.ai-session-bar select { padding:4px 8px;border:1px solid var(--yla-border,#ddd);border-radius:4px;font-size:.78rem;background:var(--yla-bg-input);color:var(--yla-text-primary) }
|
|
38
|
+
.ai-session-bar a { color:var(--yla-text-muted);text-decoration:none;font-size:.75rem }
|
|
39
|
+
.ai-session-bar a:hover { color:#485fc7 }
|
|
40
|
+
.theme-dark .ai-session-bar { background:#1e1e2e;border-color:#333 }
|
|
41
|
+
.theme-dark .ai-session-bar select { background:#2a2a3a;border-color:#444;color:#ddd }
|
|
36
42
|
</style>
|
|
37
43
|
{% endblock %}
|
|
38
44
|
|
|
39
45
|
{% block content %}
|
|
40
46
|
<div class="ai-full-wrap">
|
|
41
|
-
<div
|
|
47
|
+
<div class="ai-session-bar" id="ai-session-bar">
|
|
48
|
+
<span style="font-weight:600;">历史对话</span>
|
|
49
|
+
<select id="ai-session-select" onchange="loadSession(this.value)">
|
|
50
|
+
<option value="">新对话</option>
|
|
51
|
+
</select>
|
|
52
|
+
<a href="/api/ai/full/" style="margin-left:auto;">+ 新建</a>
|
|
53
|
+
</div>
|
|
54
|
+
<div id="ai-full-body" class="ai-full-body" style="border-radius:0 0 12px 12px;">
|
|
42
55
|
<div class="ai-full-empty">
|
|
43
56
|
<i class="fas fa-robot"></i>
|
|
44
57
|
<p style="font-size:1.1rem;font-weight:600;">AI 数据助手</p>
|
|
@@ -66,6 +79,7 @@
|
|
|
66
79
|
var $input = document.getElementById('ai-full-input');
|
|
67
80
|
var $body = document.getElementById('ai-full-body');
|
|
68
81
|
var hasMsg = false;
|
|
82
|
+
var sid = '{{ session_id|default:"" }}';
|
|
69
83
|
|
|
70
84
|
function loadCfg() {
|
|
71
85
|
try { return JSON.parse(localStorage.getItem('yla_ai_config') || '{}'); } catch(e) { return {}; }
|
|
@@ -79,6 +93,24 @@
|
|
|
79
93
|
d.innerHTML = '<div class="ai-full-bubble">' + html + label + '</div>';
|
|
80
94
|
$body.appendChild(d);
|
|
81
95
|
$body.scrollTop = $body.scrollHeight;
|
|
96
|
+
if (sid) _queueSave();
|
|
97
|
+
}
|
|
98
|
+
var _fst = null;
|
|
99
|
+
function _queueSave() { clearTimeout(_fst); _fst = setTimeout(_doSave, 500); }
|
|
100
|
+
function _doSave() {
|
|
101
|
+
var msgs = [], title = '';
|
|
102
|
+
[].forEach.call($body.querySelectorAll('.ai-full-msg'), function(el) {
|
|
103
|
+
var ty = el.classList.contains('user') ? 'user' : 'bot';
|
|
104
|
+
var h = el.querySelector('.ai-full-bubble').innerHTML;
|
|
105
|
+
if (!title && ty === 'user') title = el.querySelector('.ai-full-bubble').textContent.substring(0, 50);
|
|
106
|
+
msgs.push({ role: ty, content: h });
|
|
107
|
+
});
|
|
108
|
+
if (!msgs.length) return;
|
|
109
|
+
var x = new XMLHttpRequest();
|
|
110
|
+
x.open('POST', '/api/ai/history/');
|
|
111
|
+
x.setRequestHeader('Content-Type', 'application/json');
|
|
112
|
+
x.setRequestHeader('X-CSRFToken', window.YLA.csrfToken || '');
|
|
113
|
+
x.send(JSON.stringify({ session: sid, title: title, messages: msgs }));
|
|
82
114
|
}
|
|
83
115
|
function addTyping() {
|
|
84
116
|
if (!hasMsg) { $body.innerHTML = ''; hasMsg = true; }
|
|
@@ -138,6 +170,44 @@
|
|
|
138
170
|
window.quickAsk = function(el) { $input.value = el.textContent; send(); };
|
|
139
171
|
document.getElementById('ai-full-send').onclick = send;
|
|
140
172
|
$input.onkeydown = function(e) { if (e.key === 'Enter') send(); };
|
|
173
|
+
// 加载会话列表
|
|
174
|
+
function loadSessions() {
|
|
175
|
+
var x = new XMLHttpRequest();
|
|
176
|
+
x.open('GET', '/api/ai/sessions/');
|
|
177
|
+
x.onload = function() {
|
|
178
|
+
try { var d = JSON.parse(x.responseText);
|
|
179
|
+
var sel = document.getElementById('ai-session-select');
|
|
180
|
+
if (d.sessions) d.sessions.forEach(function(s) {
|
|
181
|
+
var o = document.createElement('option');
|
|
182
|
+
o.value = s.id; o.textContent = s.title; o.selected = (s.id === sid);
|
|
183
|
+
sel.appendChild(o);
|
|
184
|
+
});
|
|
185
|
+
} catch(e) {}
|
|
186
|
+
}; x.send();
|
|
187
|
+
}
|
|
188
|
+
window.loadSession = function(id) {
|
|
189
|
+
if (id) { window.location.href = '/api/ai/full/' + id + '/'; }
|
|
190
|
+
else { window.location.href = '/api/ai/full/'; }
|
|
191
|
+
};
|
|
192
|
+
loadSessions();
|
|
193
|
+
|
|
194
|
+
if (sid) {
|
|
195
|
+
var xh = new XMLHttpRequest();
|
|
196
|
+
xh.open('GET', '/api/ai/history/?session=' + sid);
|
|
197
|
+
xh.onload = function() {
|
|
198
|
+
try { var d = JSON.parse(xh.responseText); if (d.messages && d.messages.length) {
|
|
199
|
+
$body.innerHTML = ''; hasMsg = true;
|
|
200
|
+
d.messages.forEach(function(m) {
|
|
201
|
+
var cls = m.role === 'user' ? 'user' : 'bot';
|
|
202
|
+
var label = m.role === 'bot' ? '<span class="ai-gen-tag">AI 生成</span>' : '';
|
|
203
|
+
var el = document.createElement('div'); el.className = 'ai-full-msg ' + cls;
|
|
204
|
+
el.innerHTML = '<div class="ai-full-bubble">' + (m.content || '') + label + '</div>';
|
|
205
|
+
$body.appendChild(el);
|
|
206
|
+
});
|
|
207
|
+
$body.scrollTop = $body.scrollHeight;
|
|
208
|
+
}} catch(e) {}
|
|
209
|
+
}; xh.send();
|
|
210
|
+
}
|
|
141
211
|
})();
|
|
142
212
|
</script>
|
|
143
213
|
{% endblock %}
|
|
@@ -6,4 +6,7 @@ app_name = 'ai_assistant'
|
|
|
6
6
|
urlpatterns = [
|
|
7
7
|
path('query/', views.ai_query, name='ai_query'),
|
|
8
8
|
path('full/', views.ai_fullpage, name='ai_fullpage'),
|
|
9
|
+
path('full/<str:session_id>/', views.ai_fullpage, name='ai_fullpage_session'),
|
|
10
|
+
path('history/', views.ai_history, name='ai_history'),
|
|
11
|
+
path('sessions/', views.ai_sessions, name='ai_sessions'),
|
|
9
12
|
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from django.http import JsonResponse
|
|
3
|
+
from django.shortcuts import render
|
|
4
|
+
from django.views.decorators.http import require_POST
|
|
5
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
|
6
|
+
from .services import AIQueryService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@staff_member_required
|
|
10
|
+
def ai_fullpage(request, session_id=None):
|
|
11
|
+
"""Full-page AI chat view, optionally with session history"""
|
|
12
|
+
from .models import AiChatSession
|
|
13
|
+
title = 'AI 数据助手'
|
|
14
|
+
if session_id:
|
|
15
|
+
try:
|
|
16
|
+
s = AiChatSession.objects.get(id=session_id, user=request.user)
|
|
17
|
+
title = s.title or title
|
|
18
|
+
except AiChatSession.DoesNotExist:
|
|
19
|
+
pass
|
|
20
|
+
return render(request, 'ai/fullpage.html', {
|
|
21
|
+
'title': title,
|
|
22
|
+
'session_id': session_id or '',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@staff_member_required
|
|
27
|
+
@require_POST
|
|
28
|
+
def ai_query(request):
|
|
29
|
+
"""AI natural language query endpoint"""
|
|
30
|
+
try:
|
|
31
|
+
body = json.loads(request.body)
|
|
32
|
+
question = body.get('question', '').strip()
|
|
33
|
+
api_key = body.get('api_key', '') or None
|
|
34
|
+
model = body.get('model', '') or None
|
|
35
|
+
api_base = body.get('api_base', '') or None
|
|
36
|
+
except (json.JSONDecodeError, AttributeError):
|
|
37
|
+
return JsonResponse({'error': '请求格式不正确'}, status=400)
|
|
38
|
+
|
|
39
|
+
if not question:
|
|
40
|
+
return JsonResponse({'error': '请输入问题'}, status=400)
|
|
41
|
+
if len(question) > 500:
|
|
42
|
+
return JsonResponse({'error': '问题过长(最多500字)'}, status=400)
|
|
43
|
+
|
|
44
|
+
service = AIQueryService()
|
|
45
|
+
try:
|
|
46
|
+
result = service.query(question, api_key_override=api_key, model_override=model, api_base_override=api_base)
|
|
47
|
+
return JsonResponse(result)
|
|
48
|
+
except ValueError as e:
|
|
49
|
+
return JsonResponse({'error': str(e)}, status=400)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return JsonResponse({'error': f'AI 调用失败: {str(e)}'}, status=500)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@staff_member_required
|
|
55
|
+
def ai_history(request):
|
|
56
|
+
"""Chat history CRUD with session support"""
|
|
57
|
+
from .models import AiChatMessage, AiChatSession
|
|
58
|
+
session_id = request.GET.get('session', 'default')
|
|
59
|
+
|
|
60
|
+
if request.method == 'GET':
|
|
61
|
+
msgs = AiChatMessage.objects.filter(
|
|
62
|
+
user=request.user, session=session_id
|
|
63
|
+
).order_by('created_at')[:100]
|
|
64
|
+
return JsonResponse({
|
|
65
|
+
'messages': [{'role': m.role, 'content': m.content} for m in msgs]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if request.method == 'POST':
|
|
69
|
+
try:
|
|
70
|
+
body = json.loads(request.body)
|
|
71
|
+
except (json.JSONDecodeError, AttributeError):
|
|
72
|
+
return JsonResponse({'error': '格式错误'}, status=400)
|
|
73
|
+
|
|
74
|
+
# 兼容两种格式:纯列表 或 {session, title, messages}
|
|
75
|
+
if isinstance(body, list):
|
|
76
|
+
msg_list = body
|
|
77
|
+
sid = session_id
|
|
78
|
+
title = ''
|
|
79
|
+
else:
|
|
80
|
+
msg_list = body.get('messages', [])
|
|
81
|
+
sid = body.get('session', session_id)
|
|
82
|
+
title = body.get('title', '')
|
|
83
|
+
|
|
84
|
+
# Save session
|
|
85
|
+
if sid and sid != 'default':
|
|
86
|
+
import django.utils.timezone
|
|
87
|
+
AiChatSession.objects.update_or_create(
|
|
88
|
+
id=sid, user=request.user,
|
|
89
|
+
defaults={'title': title or '对话 ' + sid[:8], 'updated_at': django.utils.timezone.now()}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Save messages
|
|
93
|
+
AiChatMessage.objects.filter(user=request.user, session=sid).delete()
|
|
94
|
+
for m in msg_list[-50:]:
|
|
95
|
+
AiChatMessage.objects.create(
|
|
96
|
+
user=request.user, session=sid,
|
|
97
|
+
role=m.get('role', 'bot'), content=m.get('content', ''),
|
|
98
|
+
title=title or ''
|
|
99
|
+
)
|
|
100
|
+
return JsonResponse({'status': 'ok', 'session': sid})
|
|
101
|
+
|
|
102
|
+
if request.method == 'DELETE':
|
|
103
|
+
AiChatMessage.objects.filter(user=request.user, session=session_id).delete()
|
|
104
|
+
return JsonResponse({'status': 'ok'})
|
|
105
|
+
|
|
106
|
+
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@staff_member_required
|
|
110
|
+
def ai_sessions(request):
|
|
111
|
+
"""List user's chat sessions"""
|
|
112
|
+
from .models import AiChatSession
|
|
113
|
+
sessions = AiChatSession.objects.filter(user=request.user).order_by('-updated_at')[:20]
|
|
114
|
+
return JsonResponse({
|
|
115
|
+
'sessions': [{'id': s.id, 'title': s.title, 'updated': s.updated_at.isoformat()} for s in sessions]
|
|
116
|
+
})
|
|
@@ -12,6 +12,10 @@ class ThemeConfig(AppConfig):
|
|
|
12
12
|
|
|
13
13
|
config = get_config()
|
|
14
14
|
|
|
15
|
+
# 登出后重定向到登录页
|
|
16
|
+
if not getattr(django_settings, 'LOGOUT_REDIRECT_URL', None):
|
|
17
|
+
django_settings.LOGOUT_REDIRECT_URL = '/admin/login/'
|
|
18
|
+
|
|
15
19
|
# 自动注入 context processor(pip 用户不用手动配置)
|
|
16
20
|
cp_path = 'apps.theme.context_processors.yanleaf_settings'
|
|
17
21
|
for tpl in django_settings.TEMPLATES:
|
{yanleafadmin-2.0.8 → yanleafadmin-2.1.0}/apps/theme/static/yanleafadmin/css/change-list.css
RENAMED
|
@@ -135,9 +135,17 @@
|
|
|
135
135
|
background: #450a0a; border-color: #7f1d1d; color: #fca5a5;
|
|
136
136
|
}
|
|
137
137
|
.theme-dark #changelist-filter .clear-filters:hover { background: #7f1d1d; color: #fecaca; }
|
|
138
|
-
#changelist-filter h3 { font-size: 0.72rem; font-weight: 600; color: var(--yla-text-primary); margin: 0.65rem 0 0.3rem; }
|
|
138
|
+
#changelist-filter h3 { font-size: 0.72rem; font-weight: 600; color: var(--yla-text-primary); margin: 0.65rem 0 0.3rem; cursor: pointer; user-select: none; }
|
|
139
|
+
#changelist-filter h3::after { content: ' ▾'; font-size: 0.6rem; }
|
|
140
|
+
#changelist-filter h3.collapsed::after { content: ' ▸'; }
|
|
139
141
|
#changelist-filter ul { margin: 0; padding: 0; list-style: none; }
|
|
142
|
+
#changelist-filter ul.collapsed li:nth-child(n+6) { display: none; }
|
|
143
|
+
#changelist-filter ul.collapsed + .yla-filter-toggle { display: block; }
|
|
144
|
+
#changelist-filter .yla-filter-toggle { display: none; font-size: 0.65rem; color: var(--yla-text-muted); cursor: pointer; padding: 2px 4px; }
|
|
145
|
+
#changelist-filter .yla-filter-toggle:hover { color: var(--yla-text-primary); }
|
|
140
146
|
#changelist-filter li { padding: 0.08rem 0; }
|
|
147
|
+
#changelist-filter li:nth-child(n+6) { display: none; }
|
|
148
|
+
#changelist-filter ul.expanded li { display: block; }
|
|
141
149
|
#changelist-filter a {
|
|
142
150
|
font-size: 0.75rem; color: var(--yla-text-secondary); text-decoration: none;
|
|
143
151
|
display: block; padding: 0.18rem 0.4rem; border-radius: 4px;
|
|
@@ -40,11 +40,13 @@
|
|
|
40
40
|
.login-page .captcha-field-wrapper { display: flex; align-items: center; gap: 12px; }
|
|
41
41
|
.login-page .captcha-field-wrapper img.captcha {
|
|
42
42
|
border-radius: var(--yla-radius-sm); border: 1px solid var(--yla-border);
|
|
43
|
-
cursor: pointer; height:
|
|
43
|
+
cursor: pointer; height: 40px; flex-shrink: 0; width: auto;
|
|
44
44
|
}
|
|
45
|
+
.login-page .captcha-field-wrapper br { display: none; }
|
|
45
46
|
|
|
46
|
-
.yla-captcha-input {
|
|
47
|
-
flex: 1; height: 40px;
|
|
47
|
+
.yla-captcha-input, #id_captcha_1 {
|
|
48
|
+
flex: 1; height: 40px; width: 100%;
|
|
49
|
+
max-width: 100%;
|
|
48
50
|
padding: 0.5em 0.75em;
|
|
49
51
|
border: 1px solid var(--yla-border, #dbdbdb);
|
|
50
52
|
border-radius: 8px;
|