zen_admin 0.9.2

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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +76 -0
  3. data/Rakefile +6 -0
  4. data/app/assets/stylesheets/zen_admin/application.css +15 -0
  5. data/app/controllers/zen_admin/application_controller.rb +27 -0
  6. data/app/controllers/zen_admin/dashboard_controller.rb +9 -0
  7. data/app/controllers/zen_admin/sessions_controller.rb +40 -0
  8. data/app/controllers/zen_admin/ui/resources_controller.rb +331 -0
  9. data/app/helpers/zen_admin/application_helper.rb +80 -0
  10. data/app/javascript/controllers/application.js +9 -0
  11. data/app/javascript/controllers/bulk_actions_controller.js +55 -0
  12. data/app/javascript/controllers/clipboard_controller.js +27 -0
  13. data/app/javascript/controllers/index.js +5 -0
  14. data/app/javascript/controllers/layout_controller.js +48 -0
  15. data/app/javascript/controllers/menu_group_controller.js +40 -0
  16. data/app/javascript/controllers/modal_controller.js +31 -0
  17. data/app/javascript/controllers/toast_controller.js +14 -0
  18. data/app/javascript/controllers/tom_select_controller.js +35 -0
  19. data/app/javascript/zen_admin/application.js +12 -0
  20. data/app/jobs/zen_admin/application_job.rb +4 -0
  21. data/app/mailers/zen_admin/application_mailer.rb +6 -0
  22. data/app/models/zen_admin/application_record.rb +5 -0
  23. data/app/models/zen_admin/asset.rb +43 -0
  24. data/app/models/zen_admin/audit_log.rb +116 -0
  25. data/app/models/zen_admin/permission.rb +73 -0
  26. data/app/models/zen_admin/role.rb +45 -0
  27. data/app/models/zen_admin/trash_item.rb +98 -0
  28. data/app/models/zen_admin/user.rb +37 -0
  29. data/app/policies/zen_admin/application_policy.rb +53 -0
  30. data/app/policies/zen_admin/resource_policy.rb +48 -0
  31. data/app/views/layouts/zen_admin/_flash.html.erb +9 -0
  32. data/app/views/layouts/zen_admin/_sidebar.html.erb +115 -0
  33. data/app/views/layouts/zen_admin/application.html.erb +98 -0
  34. data/app/views/zen_admin/builtin/_copy_button.html.erb +9 -0
  35. data/app/views/zen_admin/builtin/_file_link.html.erb +22 -0
  36. data/app/views/zen_admin/dashboard/index.html.erb +27 -0
  37. data/app/views/zen_admin/sessions/new.html.erb +47 -0
  38. data/app/views/zen_admin/ui/resources/_form.html.erb +226 -0
  39. data/app/views/zen_admin/ui/resources/_row.html.erb +170 -0
  40. data/app/views/zen_admin/ui/resources/create.turbo_stream.erb +2 -0
  41. data/app/views/zen_admin/ui/resources/destroy.turbo_stream.erb +2 -0
  42. data/app/views/zen_admin/ui/resources/edit.html.erb +11 -0
  43. data/app/views/zen_admin/ui/resources/index.html.erb +285 -0
  44. data/app/views/zen_admin/ui/resources/new.html.erb +11 -0
  45. data/app/views/zen_admin/ui/resources/show.html.erb +133 -0
  46. data/app/views/zen_admin/ui/resources/update.turbo_stream.erb +2 -0
  47. data/config/importmap.rb +10 -0
  48. data/config/locales/en.yml +107 -0
  49. data/config/locales/zh-CN.yml +110 -0
  50. data/config/routes.rb +24 -0
  51. data/lib/generators/zen_admin/admin_user/admin_user_generator.rb +55 -0
  52. data/lib/generators/zen_admin/install/install_generator.rb +50 -0
  53. data/lib/generators/zen_admin/install/templates/asset.rb +46 -0
  54. data/lib/generators/zen_admin/install/templates/create_zen_admin_assets.rb.erb +9 -0
  55. data/lib/generators/zen_admin/install/templates/create_zen_admin_audit_logs.rb.erb +18 -0
  56. data/lib/generators/zen_admin/install/templates/create_zen_admin_rbac.rb.erb +57 -0
  57. data/lib/generators/zen_admin/install/templates/create_zen_admin_trash_items.rb.erb +13 -0
  58. data/lib/generators/zen_admin/install/templates/zen_admin.rb +17 -0
  59. data/lib/generators/zen_admin/install/templates/zh-CN.yml +15 -0
  60. data/lib/generators/zen_admin/model/model_generator.rb +65 -0
  61. data/lib/generators/zen_admin/model/templates/zen_admin_config.rb.erb +80 -0
  62. data/lib/generators/zen_admin/rbac_install/rbac_install_generator.rb +52 -0
  63. data/lib/generators/zen_admin/rbac_install/templates/create_zen_admin_rbac.rb.erb +42 -0
  64. data/lib/generators/zen_admin/rbac_install/templates/seeds.rb.erb +13 -0
  65. data/lib/tasks/zen_admin_tasks.rake +4 -0
  66. data/lib/zen_admin/authenticatable.rb +44 -0
  67. data/lib/zen_admin/builtin.rb +77 -0
  68. data/lib/zen_admin/configuration.rb +17 -0
  69. data/lib/zen_admin/core/field.rb +34 -0
  70. data/lib/zen_admin/core/filter.rb +16 -0
  71. data/lib/zen_admin/core/resource.rb +175 -0
  72. data/lib/zen_admin/core.rb +3 -0
  73. data/lib/zen_admin/engine.rb +67 -0
  74. data/lib/zen_admin/link_registerable.rb +11 -0
  75. data/lib/zen_admin/registerable.rb +17 -0
  76. data/lib/zen_admin/registry.rb +83 -0
  77. data/lib/zen_admin/schema/serializer.rb +15 -0
  78. data/lib/zen_admin/schema.rb +1 -0
  79. data/lib/zen_admin/version.rb +3 -0
  80. data/lib/zen_admin.rb +51 -0
  81. metadata +233 -0
@@ -0,0 +1,53 @@
1
+ module ZenAdmin
2
+ class ApplicationPolicy
3
+ attr_reader :user, :record
4
+
5
+ def initialize(user, record)
6
+ @user = user
7
+ @record = record
8
+ end
9
+
10
+ def index?
11
+ false
12
+ end
13
+
14
+ def show?
15
+ false
16
+ end
17
+
18
+ def create?
19
+ false
20
+ end
21
+
22
+ def new?
23
+ create?
24
+ end
25
+
26
+ def update?
27
+ false
28
+ end
29
+
30
+ def edit?
31
+ update?
32
+ end
33
+
34
+ def destroy?
35
+ false
36
+ end
37
+
38
+ class Scope
39
+ def initialize(user, scope)
40
+ @user = user
41
+ @scope = scope
42
+ end
43
+
44
+ def resolve
45
+ scope.all
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :user, :scope
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ module ZenAdmin
2
+ class ResourcePolicy < ApplicationPolicy
3
+ def index?
4
+ check_permission("index")
5
+ end
6
+
7
+ def show?
8
+ check_permission("show")
9
+ end
10
+
11
+ def create?
12
+ check_permission("create")
13
+ end
14
+
15
+ def new?
16
+ create?
17
+ end
18
+
19
+ def update?
20
+ check_permission("update")
21
+ end
22
+
23
+ def edit?
24
+ update?
25
+ end
26
+
27
+ def destroy?
28
+ check_permission("destroy")
29
+ end
30
+
31
+ private
32
+
33
+ def check_permission(action)
34
+ return true if user.respond_to?(:super_admin?) && user.super_admin?
35
+
36
+ # Handle both class (index) and instance (member)
37
+ klass = record.is_a?(Class) ? record : record.class
38
+ resource_name = klass.name.underscore.pluralize.gsub('/', '_')
39
+
40
+ permission = "#{resource_name}:#{action}"
41
+ result = user.can?(permission)
42
+
43
+ Rails.logger.debug "[ZenAdmin] Auth Check: User=#{user.username} | Permission=#{permission} | Result=#{result}"
44
+
45
+ result
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ <% flash.each do |key, value| %>
2
+ <% alert_class = key.to_s == 'notice' ? 'success' : 'danger' %>
3
+ <% icon_class = key.to_s == 'notice' ? 'check' : 'ban' %>
4
+ <div class="alert alert-<%= alert_class %> alert-dismissible" data-controller="toast" data-toast-target="element">
5
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
6
+ <h5><i class="icon fas fa-<%= icon_class %>"></i> <%= key.to_s.humanize %>!</h5>
7
+ <%= value %>
8
+ </div>
9
+ <% end %>
@@ -0,0 +1,115 @@
1
+ <aside class="main-sidebar sidebar-dark-primary elevation-4">
2
+ <!-- Brand Logo -->
3
+ <a href="<%= zen_admin.root_path %>" class="brand-link">
4
+ <span class="brand-text font-weight-light px-3">ZenAdmin</span>
5
+ </a>
6
+
7
+ <!-- Sidebar -->
8
+ <div class="sidebar">
9
+ <!-- Sidebar user panel -->
10
+ <div class="user-panel mt-3 pb-3 mb-3 d-flex align-items-center">
11
+ <div class="image">
12
+ <img src="https://ui-avatars.com/api/?name=<%= current_admin_user.username %>&background=random" class="img-circle elevation-2" alt="User Image">
13
+ </div>
14
+ <div class="info flex-grow-1">
15
+ <a href="#" class="d-block text-truncate"><%= current_admin_user.username %></a>
16
+ </div>
17
+ <div class="logout-btn pr-3">
18
+ <%= link_to zen_admin.logout_path, data: { turbo_method: :delete }, class: "text-danger", title: "退出登录" do %>
19
+ <i class="fas fa-sign-out-alt"></i>
20
+ <% end %>
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Sidebar Menu -->
25
+ <nav class="mt-2">
26
+ <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
27
+
28
+ <li class="nav-item">
29
+ <a href="<%= zen_admin.root_path %>" class="nav-link <%= 'active' if request.path == zen_admin.root_path %>">
30
+ <i class="nav-icon fas fa-tachometer-alt"></i>
31
+ <p>Dashboard</p>
32
+ </a>
33
+ </li>
34
+
35
+ <li class="nav-header">MENU</li>
36
+
37
+ <% ZenAdmin::Registry.instance.menu_items.each do |item| %>
38
+ <% next unless menu_item_visible?(item, current_admin_user) %>
39
+
40
+ <% if item[:type] == :group %>
41
+ <li class="nav-item" data-controller="menu-group">
42
+ <a href="#" class="nav-link" data-action="click->menu-group#toggle">
43
+ <i class="nav-icon fas fa-folder"></i>
44
+ <p>
45
+ <%= item[:label] %>
46
+ <i class="right fas fa-angle-left"></i>
47
+ </p>
48
+ </a>
49
+ <ul class="nav nav-treeview" data-menu-group-target="treeview" style="display: none;">
50
+ <% item[:items].each do |sub_item| %>
51
+ <% next unless menu_item_visible?(sub_item, current_admin_user) %>
52
+
53
+ <% if sub_item[:type] == :resource %>
54
+ <% resource = sub_item[:object] %>
55
+ <li class="nav-item">
56
+ <a href="<%= zen_admin.ui_resource_index_path(resource.name) %>" class="nav-link <%= 'active' if params[:resource_name] == resource.name %>">
57
+ <i class="nav-icon <%= resource.menu_options[:icon] || 'far fa-circle' %>"></i>
58
+ <p>
59
+ <%
60
+ menu_label = resource.menu_options[:label]
61
+ label = if menu_label.present?
62
+ menu_label.is_a?(Symbol) ? t(menu_label) : menu_label
63
+ else
64
+ resource.label_plural
65
+ end
66
+ %>
67
+ <%= label %>
68
+ </p>
69
+ </a>
70
+ </li>
71
+ <% elsif sub_item[:type] == :link %>
72
+ <% link = sub_item[:object] %>
73
+ <li class="nav-item">
74
+ <a href="<%= link[:path] %>" class="nav-link">
75
+ <i class="nav-icon <%= link[:icon] %>"></i>
76
+ <p><%= link[:label] %></p>
77
+ </a>
78
+ </li>
79
+ <% end %>
80
+ <% end %>
81
+ </ul>
82
+ </li>
83
+ <% elsif item[:type] == :resource %>
84
+ <% resource = item[:object] %>
85
+ <li class="nav-item">
86
+ <a href="<%= zen_admin.ui_resource_index_path(resource.name) %>" class="nav-link <%= 'active' if params[:resource_name] == resource.name %>">
87
+ <i class="nav-icon <%= resource.menu_options[:icon] || 'fas fa-circle' %>"></i>
88
+ <p>
89
+ <%
90
+ menu_label = resource.menu_options[:label]
91
+ label = if menu_label.present?
92
+ menu_label.is_a?(Symbol) ? t(menu_label) : menu_label
93
+ else
94
+ resource.label_plural
95
+ end
96
+ %>
97
+ <%= label %>
98
+ </p>
99
+ </a>
100
+ </li>
101
+ <% else %>
102
+ <% link = item[:object] %>
103
+ <li class="nav-item">
104
+ <a href="<%= link[:path] %>" class="nav-link">
105
+ <i class="nav-icon <%= link[:icon] %>"></i>
106
+ <p><%= link[:label] %></p>
107
+ </a>
108
+ </li>
109
+ <% end %>
110
+ <% end %>
111
+
112
+ </ul>
113
+ </nav>
114
+ </div>
115
+ </aside>
@@ -0,0 +1,98 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>ZenAdmin</title>
7
+
8
+ <!-- Google Font & Icons -->
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+
12
+ <!-- Theme style (AdminLTE) -->
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
14
+
15
+ <!-- Tom Select CSS -->
16
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/tom-select/2.2.2/css/tom-select.bootstrap5.min.css" rel="stylesheet">
17
+
18
+ <%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
19
+
20
+ <% # 核心:必须使用 Rails 标准标签来加载环境,确保 CSRF 和 Turbo 正常工作 %>
21
+ <%= csrf_meta_tags %>
22
+ <%= csp_meta_tag %>
23
+
24
+ <%= javascript_importmap_tags "zen_admin/application" %>
25
+ <%= yield :head %>
26
+
27
+ <style>
28
+ :root {
29
+ --primary-color: #165DFF;
30
+ --text-color: #1D2129;
31
+ --text-secondary: #4E5969;
32
+ --border-color: #E5E6EB;
33
+ --bg-color: #F2F3F5;
34
+ }
35
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; color: var(--text-color); background-color: var(--bg-color); }
36
+ .content-wrapper { background-color: var(--bg-color); }
37
+ .card { border: none; border-radius: 8px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); margin-bottom: 24px; border-top: 3px solid var(--primary-color); }
38
+ .btn-action { padding: 4px 8px; font-size: 12px; border-radius: 4px; }
39
+
40
+ /* Hide logout button when sidebar is collapsed (but not hovered) */
41
+ body.sidebar-mini.sidebar-collapse .main-sidebar:not(:hover) .user-panel > .logout-btn {
42
+ display: none !important;
43
+ }
44
+ </style>
45
+ </head>
46
+ <body class="hold-transition sidebar-mini layout-fixed" data-controller="layout">
47
+ <div class="wrapper">
48
+
49
+ <div id="flash" style="position: fixed; top: 1rem; right: 1rem; z-index: 1050; width: 350px;">
50
+ <%= render "layouts/zen_admin/flash" %>
51
+ </div>
52
+
53
+ <!-- Navbar -->
54
+ <nav class="main-header navbar navbar-expand navbar-white navbar-light border-bottom-0 shadow-sm">
55
+ <ul class="navbar-nav">
56
+ <li class="nav-item">
57
+ <a class="nav-link" href="#" role="button" data-action="click->layout#toggleSidebar">
58
+ <i class="fas fa-bars text-muted"></i>
59
+ </a>
60
+ </li>
61
+ </ul>
62
+ <ul class="navbar-nav ml-auto">
63
+ <li class="nav-item">
64
+ <a class="nav-link" href="#" role="button" data-action="click->layout#toggleFullscreen">
65
+ <i class="fas fa-expand-arrows-alt text-muted"></i>
66
+ </a>
67
+ </li>
68
+ </ul>
69
+ </nav>
70
+
71
+ <%= render "layouts/zen_admin/sidebar" %>
72
+
73
+ <div class="content-wrapper pt-3">
74
+ <div class="content">
75
+ <div class="container-fluid">
76
+ <div class="d-flex align-items-center justify-content-between mb-4">
77
+ <h2 class="fw-bold mb-0"><%= content_for?(:title) ? yield(:title) : "Dashboard" %></h2>
78
+ <div class="page-actions">
79
+ <%= yield :page_actions %>
80
+ </div>
81
+ </div>
82
+ <%= yield %>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <footer class="main-footer border-top-0 small text-muted">
88
+ <strong>Copyright &copy; <%= Time.now.year %> <a href="#" class="text-decoration-none">ZenAdmin</a>.</strong>
89
+ </footer>
90
+
91
+ <%= turbo_frame_tag "modal" %>
92
+ </div>
93
+
94
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tom-select/2.2.2/js/tom-select.complete.min.js"></script>
95
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" data-turbo-track="reload"></script>
96
+
97
+ </body>
98
+ </html>
@@ -0,0 +1,9 @@
1
+ <div data-controller="clipboard" class="d-inline-block">
2
+ <button class="btn btn-outline-primary py-1 px-2"
3
+ type="button"
4
+ data-action="clipboard#copy"
5
+ data-clipboard-text="<%= url %>"
6
+ title="<%= t('zen_admin.actions.copy') rescue '复制' %>">
7
+ <i class="fas fa-copy"></i>
8
+ </button>
9
+ </div>
@@ -0,0 +1,22 @@
1
+ <div class="input-group" data-controller="clipboard">
2
+ <input type="text" class="form-control" value="<%= url %>" readonly data-clipboard-target="input">
3
+ <div class="input-group-append">
4
+ <button class="btn btn-outline-primary" type="button" data-action="clipboard#copy">
5
+ <i class="fas fa-copy"></i> 复制链接
6
+ </button>
7
+ </div>
8
+ </div>
9
+ <div class="mt-3 text-center">
10
+ <% # 兼容 Attachment, Blob 和 Asset %>
11
+ <% blob = if @record.is_a?(ActiveStorage::Attachment)
12
+ @record.blob
13
+ elsif @record.respond_to?(:file) && @record.file.attached?
14
+ @record.file.blob
15
+ else
16
+ @record
17
+ end %>
18
+ <% if blob.is_a?(ActiveStorage::Blob) && blob.representable? %>
19
+ <% # 使用原图预览,避免产生变体记录 %>
20
+ <img src="<%= main_app.url_for(blob) %>" class="img-fluid rounded shadow-sm border" style="max-height: 600px;">
21
+ <% end %>
22
+ </div>
@@ -0,0 +1,27 @@
1
+ <div style="font-family: sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
2
+ <h1 style="color: #2c3e50;">ZenAdmin 挂载成功!</h1>
3
+ <p style="color: #7f8c8d; font-size: 1.1em;">
4
+ 您现在看到的页面是由 <strong>ZenAdmin Engine</strong> 渲染的。
5
+ </p>
6
+ <hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
7
+ <div style="text-align: left; background: #f8f9fa; padding: 15px; border-radius: 4px;">
8
+ <p><strong>当前配置:</strong></p>
9
+ <ul style="list-style: none; padding: 0;">
10
+ <li>✅ UI 状态: <code><%= ZenAdmin.configuration.enable_ui %></code></li>
11
+ <li>✅ 管理路径: <code><%= ZenAdmin.configuration.admin_path %></code></li>
12
+ </ul>
13
+
14
+ <p class="mt-3"><strong>已注册资源:</strong></p>
15
+ <ul style="padding-left: 20px;">
16
+ <% ZenAdmin::Registry.instance.all.each do |resource| %>
17
+ <li style="margin-bottom: 5px;">
18
+ <%= link_to resource.model.name, ui_resource_index_path(resource.name), style: "color: #3498db; text-decoration: none; font-weight: bold;" %>
19
+ <span style="color: #999; font-size: 0.9em;">(<%= resource.name %>)</span>
20
+ </li>
21
+ <% end %>
22
+ </ul>
23
+ <% if ZenAdmin::Registry.instance.all.empty? %>
24
+ <p style="color: #999; font-style: italic;">暂无注册资源。</p>
25
+ <% end %>
26
+ </div>
27
+ </div>
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>ZenAdmin | 登录</title>
7
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
10
+ </head>
11
+ <body class="hold-transition login-page">
12
+ <div class="login-box">
13
+ <div class="card card-outline card-primary">
14
+ <div class="card-header text-center">
15
+ <a href="#" class="h1"><b>Zen</b>Admin</a>
16
+ </div>
17
+ <div class="card-body">
18
+ <p class="login-box-msg">请登录以开启管理后台</p>
19
+
20
+ <% if flash[:alert] %>
21
+ <div class="alert alert-danger"><%= flash[:alert] %></div>
22
+ <% end %>
23
+
24
+ <%= form_with url: login_path, method: :post, local: true do |f| %>
25
+ <div class="input-group mb-3">
26
+ <%= f.text_field :username, class: "form-control", placeholder: "用户名" %>
27
+ <div class="input-group-append">
28
+ <div class="input-group-text"><span class="fas fa-user"></span></div>
29
+ </div>
30
+ </div>
31
+ <div class="input-group mb-3">
32
+ <%= f.password_field :password, class: "form-control", placeholder: "密码" %>
33
+ <div class="input-group-append">
34
+ <div class="input-group-text"><span class="fas fa-lock"></span></div>
35
+ </div>
36
+ </div>
37
+ <div class="row">
38
+ <div class="col-12">
39
+ <%= f.submit "登录", class: "btn btn-primary btn-block" %>
40
+ </div>
41
+ </div>
42
+ <% end %>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </body>
47
+ </html>