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.
- checksums.yaml +7 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/zen_admin/application.css +15 -0
- data/app/controllers/zen_admin/application_controller.rb +27 -0
- data/app/controllers/zen_admin/dashboard_controller.rb +9 -0
- data/app/controllers/zen_admin/sessions_controller.rb +40 -0
- data/app/controllers/zen_admin/ui/resources_controller.rb +331 -0
- data/app/helpers/zen_admin/application_helper.rb +80 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/bulk_actions_controller.js +55 -0
- data/app/javascript/controllers/clipboard_controller.js +27 -0
- data/app/javascript/controllers/index.js +5 -0
- data/app/javascript/controllers/layout_controller.js +48 -0
- data/app/javascript/controllers/menu_group_controller.js +40 -0
- data/app/javascript/controllers/modal_controller.js +31 -0
- data/app/javascript/controllers/toast_controller.js +14 -0
- data/app/javascript/controllers/tom_select_controller.js +35 -0
- data/app/javascript/zen_admin/application.js +12 -0
- data/app/jobs/zen_admin/application_job.rb +4 -0
- data/app/mailers/zen_admin/application_mailer.rb +6 -0
- data/app/models/zen_admin/application_record.rb +5 -0
- data/app/models/zen_admin/asset.rb +43 -0
- data/app/models/zen_admin/audit_log.rb +116 -0
- data/app/models/zen_admin/permission.rb +73 -0
- data/app/models/zen_admin/role.rb +45 -0
- data/app/models/zen_admin/trash_item.rb +98 -0
- data/app/models/zen_admin/user.rb +37 -0
- data/app/policies/zen_admin/application_policy.rb +53 -0
- data/app/policies/zen_admin/resource_policy.rb +48 -0
- data/app/views/layouts/zen_admin/_flash.html.erb +9 -0
- data/app/views/layouts/zen_admin/_sidebar.html.erb +115 -0
- data/app/views/layouts/zen_admin/application.html.erb +98 -0
- data/app/views/zen_admin/builtin/_copy_button.html.erb +9 -0
- data/app/views/zen_admin/builtin/_file_link.html.erb +22 -0
- data/app/views/zen_admin/dashboard/index.html.erb +27 -0
- data/app/views/zen_admin/sessions/new.html.erb +47 -0
- data/app/views/zen_admin/ui/resources/_form.html.erb +226 -0
- data/app/views/zen_admin/ui/resources/_row.html.erb +170 -0
- data/app/views/zen_admin/ui/resources/create.turbo_stream.erb +2 -0
- data/app/views/zen_admin/ui/resources/destroy.turbo_stream.erb +2 -0
- data/app/views/zen_admin/ui/resources/edit.html.erb +11 -0
- data/app/views/zen_admin/ui/resources/index.html.erb +285 -0
- data/app/views/zen_admin/ui/resources/new.html.erb +11 -0
- data/app/views/zen_admin/ui/resources/show.html.erb +133 -0
- data/app/views/zen_admin/ui/resources/update.turbo_stream.erb +2 -0
- data/config/importmap.rb +10 -0
- data/config/locales/en.yml +107 -0
- data/config/locales/zh-CN.yml +110 -0
- data/config/routes.rb +24 -0
- data/lib/generators/zen_admin/admin_user/admin_user_generator.rb +55 -0
- data/lib/generators/zen_admin/install/install_generator.rb +50 -0
- data/lib/generators/zen_admin/install/templates/asset.rb +46 -0
- data/lib/generators/zen_admin/install/templates/create_zen_admin_assets.rb.erb +9 -0
- data/lib/generators/zen_admin/install/templates/create_zen_admin_audit_logs.rb.erb +18 -0
- data/lib/generators/zen_admin/install/templates/create_zen_admin_rbac.rb.erb +57 -0
- data/lib/generators/zen_admin/install/templates/create_zen_admin_trash_items.rb.erb +13 -0
- data/lib/generators/zen_admin/install/templates/zen_admin.rb +17 -0
- data/lib/generators/zen_admin/install/templates/zh-CN.yml +15 -0
- data/lib/generators/zen_admin/model/model_generator.rb +65 -0
- data/lib/generators/zen_admin/model/templates/zen_admin_config.rb.erb +80 -0
- data/lib/generators/zen_admin/rbac_install/rbac_install_generator.rb +52 -0
- data/lib/generators/zen_admin/rbac_install/templates/create_zen_admin_rbac.rb.erb +42 -0
- data/lib/generators/zen_admin/rbac_install/templates/seeds.rb.erb +13 -0
- data/lib/tasks/zen_admin_tasks.rake +4 -0
- data/lib/zen_admin/authenticatable.rb +44 -0
- data/lib/zen_admin/builtin.rb +77 -0
- data/lib/zen_admin/configuration.rb +17 -0
- data/lib/zen_admin/core/field.rb +34 -0
- data/lib/zen_admin/core/filter.rb +16 -0
- data/lib/zen_admin/core/resource.rb +175 -0
- data/lib/zen_admin/core.rb +3 -0
- data/lib/zen_admin/engine.rb +67 -0
- data/lib/zen_admin/link_registerable.rb +11 -0
- data/lib/zen_admin/registerable.rb +17 -0
- data/lib/zen_admin/registry.rb +83 -0
- data/lib/zen_admin/schema/serializer.rb +15 -0
- data/lib/zen_admin/schema.rb +1 -0
- data/lib/zen_admin/version.rb +3 -0
- data/lib/zen_admin.rb +51 -0
- metadata +233 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Authenticatable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
helper_method :current_admin_user if respond_to?(:helper_method)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def current_admin_user
|
|
12
|
+
@current_admin_user ||= if session[:zen_admin_user_id] == "config_admin"
|
|
13
|
+
# 定义一个简单的类来模拟拥有 can? 方法的用户
|
|
14
|
+
Class.new do
|
|
15
|
+
def username; ZenAdmin.configuration.username; end
|
|
16
|
+
def can?(_permission); true; end # 硬编码管理员拥有所有权限
|
|
17
|
+
def super_admin?; true; end
|
|
18
|
+
def id; "config_admin"; end
|
|
19
|
+
end.new
|
|
20
|
+
elsif session[:zen_admin_user_id]
|
|
21
|
+
ZenAdmin::User.find_by(id: session[:zen_admin_user_id])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def authenticate_admin!
|
|
26
|
+
unless current_admin_user
|
|
27
|
+
redirect_to zen_admin.login_path
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 手动记录审计日志的快捷方法
|
|
32
|
+
def zen_admin_audit(resource, action, note: nil, changes: nil)
|
|
33
|
+
@zen_admin_audited = true # 标记已手动审计
|
|
34
|
+
ZenAdmin::AuditLog.record(
|
|
35
|
+
current_admin_user,
|
|
36
|
+
resource,
|
|
37
|
+
action,
|
|
38
|
+
note: note,
|
|
39
|
+
changes: changes,
|
|
40
|
+
request: request
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Builtin
|
|
3
|
+
def self.register_active_storage_blob
|
|
4
|
+
return unless defined?(ActiveStorage::Attachment)
|
|
5
|
+
return unless ActiveRecord::Base.connection.table_exists?("active_storage_attachments")
|
|
6
|
+
|
|
7
|
+
# 锁定保护
|
|
8
|
+
ActiveStorage::Attachment.send(:define_method, :zen_admin_editable?) { false }
|
|
9
|
+
ActiveStorage::Blob.send(:define_method, :zen_admin_editable?) { false }
|
|
10
|
+
|
|
11
|
+
# 1. 注册 Blob (完全隐藏,仅作为底层支撑)
|
|
12
|
+
ZenAdmin.register(ActiveStorage::Blob) do |resource|
|
|
13
|
+
resource.menu visible: false
|
|
14
|
+
resource.show do
|
|
15
|
+
field :id, label: "文件链接" do |blob|
|
|
16
|
+
url = Rails.application.routes.url_helpers.rails_blob_url(blob, host: "localhost:3000") rescue ""
|
|
17
|
+
render "zen_admin/builtin/file_link", url: url
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 2. Register System Attachments
|
|
23
|
+
ZenAdmin.register(ActiveStorage::Attachment) do |resource|
|
|
24
|
+
resource.label :'zen_admin.builtin.attachments', :'zen_admin.builtin.attachments'
|
|
25
|
+
resource.menu label: :'zen_admin.builtin.attachments', icon: "fas fa-robot", position: 101, group: "系统管理"
|
|
26
|
+
resource.soft_delete = false
|
|
27
|
+
resource.includes :blob, :record
|
|
28
|
+
resource.enable_batch_actions
|
|
29
|
+
resource.batch_action :delete, label: :'zen_admin.actions.delete', type: :danger, confirm: I18n.t('zen_admin.ui.confirm_bulk_delete') do |records|
|
|
30
|
+
records.destroy_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
resource.list do
|
|
34
|
+
field :id, label: :'zen_admin.models.active_storage/attachment.fields.id' do |att|
|
|
35
|
+
if att.blob.representable?
|
|
36
|
+
url = main_app.rails_blob_path(att.blob, only_path: true)
|
|
37
|
+
full_url = main_app.url_for(att.blob)
|
|
38
|
+
tag.div(class: "thumbnail-box border rounded cursor-pointer shadow-xs",
|
|
39
|
+
style: "width: 35px; height: 35px; overflow: hidden;",
|
|
40
|
+
data: { action: "click->layout#previewImage", preview_url: full_url }) do
|
|
41
|
+
image_tag(url, style: "width: 100%; height: 100%; object-fit: cover;")
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
# 精细化文件图标显示
|
|
45
|
+
tag.i(class: "fas #{case att.blob.content_type
|
|
46
|
+
when /pdf/ then 'fa-file-pdf text-danger'
|
|
47
|
+
when /word|officedocument/ then 'fa-file-word text-primary'
|
|
48
|
+
when /excel|sheet/ then 'fa-file-excel text-success'
|
|
49
|
+
else 'fa-file-alt'
|
|
50
|
+
end} fa-lg")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
field :record_type, label: :'zen_admin.models.active_storage/attachment.fields.record_type'
|
|
54
|
+
field :name, label: :'zen_admin.models.active_storage/attachment.fields.name'
|
|
55
|
+
field :blob_filename, label: :'zen_admin.models.active_storage/attachment.fields.blob_filename' do |att|
|
|
56
|
+
att.blob.filename.to_s
|
|
57
|
+
end
|
|
58
|
+
field :blob_id, label: :'zen_admin.models.active_storage/attachment.fields.blob_id' do |att|
|
|
59
|
+
url = Rails.application.routes.url_helpers.rails_blob_url(att.blob, host: "localhost:3000") rescue "#"
|
|
60
|
+
render "zen_admin/builtin/copy_button", url: url
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
resource.show do
|
|
65
|
+
field :record, label: :'zen_admin.models.active_storage/attachment.fields.record'
|
|
66
|
+
field :blob, label: :'zen_admin.models.active_storage/attachment.fields.blob' do |att|
|
|
67
|
+
url = Rails.application.routes.url_helpers.rails_blob_url(att.blob, host: "localhost:3000") rescue ""
|
|
68
|
+
render "zen_admin/builtin/file_link", url: url
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 3. 显式加载我们的手动资源库模型
|
|
74
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/asset")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :enable_ui, :admin_path, :username, :password, :rbac_enable, :default_locale, :custom_dashboard, :audit_log_enable, :time_zone
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@enable_ui = true
|
|
7
|
+
@admin_path = "/admin"
|
|
8
|
+
@username = "admin"
|
|
9
|
+
@password = "password"
|
|
10
|
+
@rbac_enable = false
|
|
11
|
+
@default_locale = :"zh-CN"
|
|
12
|
+
@custom_dashboard = nil
|
|
13
|
+
@audit_log_enable = true
|
|
14
|
+
@time_zone = "Beijing"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Core
|
|
3
|
+
class Field
|
|
4
|
+
attr_accessor :name, :type, :options, :required, :condition
|
|
5
|
+
|
|
6
|
+
def initialize(name, type: :string, options: {}, required: false, condition: nil, label: nil)
|
|
7
|
+
@name = name
|
|
8
|
+
@type = type
|
|
9
|
+
@options = options
|
|
10
|
+
@options[:label] = label if label
|
|
11
|
+
@required = required
|
|
12
|
+
@condition = condition
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def label
|
|
16
|
+
val = @options[:label]
|
|
17
|
+
if val.is_a?(Symbol)
|
|
18
|
+
I18n.t(val)
|
|
19
|
+
else
|
|
20
|
+
val || @name.to_s.humanize
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
name: @name.to_s,
|
|
27
|
+
type: @type.to_s,
|
|
28
|
+
required: @required,
|
|
29
|
+
options: []
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Core
|
|
3
|
+
class Resource
|
|
4
|
+
attr_accessor :name, :list_fields, :form_fields, :show_fields, :search_fields, :filters, :actions, :menu_options, :includes_list, :batch_actions, :member_actions, :resource_label_singular, :resource_label_plural, :soft_delete, :model_name, :scopes_list, :scopes_proc, :exportable
|
|
5
|
+
|
|
6
|
+
def initialize(model)
|
|
7
|
+
@model_name = model.to_s
|
|
8
|
+
if model.respond_to?(:name)
|
|
9
|
+
@name = model.name.underscore.pluralize
|
|
10
|
+
else
|
|
11
|
+
@name = model.to_s.underscore.pluralize
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
@resource_label_singular = model.model_name.human
|
|
16
|
+
@resource_label_plural = model.model_name.human.pluralize
|
|
17
|
+
rescue
|
|
18
|
+
@resource_label_singular = model.to_s.humanize
|
|
19
|
+
@resource_label_plural = @resource_label_singular.pluralize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@list_fields = []
|
|
23
|
+
@form_fields = []
|
|
24
|
+
@show_fields = []
|
|
25
|
+
@search_fields = []
|
|
26
|
+
@filters = []
|
|
27
|
+
@actions = [:index, :new, :create, :edit, :update, :show, :destroy]
|
|
28
|
+
@menu_options = { visible: true, label: nil, icon: nil, position: 10 }
|
|
29
|
+
@includes_list = []
|
|
30
|
+
@batch_actions = {}
|
|
31
|
+
@member_actions = {}
|
|
32
|
+
@scopes_list = []
|
|
33
|
+
@scopes_proc = nil
|
|
34
|
+
@soft_delete = true
|
|
35
|
+
@exportable = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def label(singular = nil, plural = nil)
|
|
39
|
+
if singular
|
|
40
|
+
@resource_label_singular = singular
|
|
41
|
+
@resource_label_plural = plural || singular
|
|
42
|
+
else
|
|
43
|
+
translate_if_symbol(@resource_label_singular)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def label_plural
|
|
48
|
+
translate_if_symbol(@resource_label_plural)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def translate_if_symbol(val)
|
|
52
|
+
if val.is_a?(Symbol)
|
|
53
|
+
I18n.t(val, default: [model.model_name.human, val.to_s.split('.').last.humanize])
|
|
54
|
+
else
|
|
55
|
+
val
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def exportable!
|
|
60
|
+
@exportable = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scope(name_or_proc = nil, label: nil, query: {}, default: false, &block)
|
|
64
|
+
if block_given?
|
|
65
|
+
@scopes_proc = block
|
|
66
|
+
elsif name_or_proc.is_a?(Proc)
|
|
67
|
+
@scopes_proc = name_or_proc
|
|
68
|
+
else
|
|
69
|
+
@scopes_list << { name: name_or_proc.to_sym, label: label, query: query, default: default }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def computed_scopes(context = nil)
|
|
74
|
+
results = @scopes_list.dup
|
|
75
|
+
if @scopes_proc
|
|
76
|
+
dynamic_results = context ? context.instance_exec(&@scopes_proc) : @scopes_proc.call
|
|
77
|
+
results += Array(dynamic_results)
|
|
78
|
+
end
|
|
79
|
+
results
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def batch_actions; @batch_actions || {}; end
|
|
83
|
+
def member_actions; @member_actions || {}; end
|
|
84
|
+
|
|
85
|
+
def member_action(name_or_array, label: nil, type: :default, icon: nil, confirm: nil, method: nil, url: nil, handler: nil, &block)
|
|
86
|
+
if name_or_array.is_a?(Array)
|
|
87
|
+
name_or_array.each do |config|
|
|
88
|
+
name = config[:name]
|
|
89
|
+
@member_actions[name.to_sym] = {
|
|
90
|
+
name: name.to_sym,
|
|
91
|
+
label: config[:label],
|
|
92
|
+
icon: config[:icon],
|
|
93
|
+
type: config[:type] || :default,
|
|
94
|
+
confirm: config[:confirm],
|
|
95
|
+
method: config[:method],
|
|
96
|
+
url: config[:url],
|
|
97
|
+
handler: config[:handler]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
@member_actions[name_or_array.to_sym] = {
|
|
102
|
+
name: name_or_array.to_sym,
|
|
103
|
+
label: label,
|
|
104
|
+
icon: icon,
|
|
105
|
+
type: type,
|
|
106
|
+
confirm: confirm,
|
|
107
|
+
method: method,
|
|
108
|
+
url: url,
|
|
109
|
+
handler: handler || block
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def enable_batch_actions(enabled = true)
|
|
115
|
+
@batch_actions = {} if enabled && (@batch_actions.nil? || @batch_actions == false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def batch_action(name_or_array, label: nil, type: :default, confirm: nil, method: nil, url: nil, handler: nil, &block)
|
|
119
|
+
if name_or_array.is_a?(Array)
|
|
120
|
+
name_or_array.each do |config|
|
|
121
|
+
name = config[:name]
|
|
122
|
+
@batch_actions[name.to_sym] = {
|
|
123
|
+
name: name.to_sym,
|
|
124
|
+
label: config[:label],
|
|
125
|
+
type: config[:type] || :default,
|
|
126
|
+
confirm: config[:confirm],
|
|
127
|
+
method: config[:method],
|
|
128
|
+
url: config[:url],
|
|
129
|
+
handler: config[:handler]
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
@batch_actions[name_or_array.to_sym] = {
|
|
134
|
+
name: name_or_array.to_sym,
|
|
135
|
+
label: label,
|
|
136
|
+
type: type,
|
|
137
|
+
confirm: confirm,
|
|
138
|
+
method: method,
|
|
139
|
+
url: url,
|
|
140
|
+
handler: handler || block
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def search(&block); end
|
|
146
|
+
def includes(*args); @includes_list.concat(args); end
|
|
147
|
+
|
|
148
|
+
def menu(visible: true, label: nil, icon: nil, position: 10, group: nil)
|
|
149
|
+
@menu_options = { visible: visible, label: label, icon: icon, position: position, group: group }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def model; @model_name.constantize; end
|
|
153
|
+
def show(&block); dsl = FieldDSL.new(self, :show_fields); dsl.instance_eval(&block); end
|
|
154
|
+
def list(&block); dsl = FieldDSL.new(self, :list_fields); dsl.instance_eval(&block); end
|
|
155
|
+
def form(&block); dsl = FieldDSL.new(self, :form_fields); dsl.instance_eval(&block); end
|
|
156
|
+
def filters(&block)
|
|
157
|
+
if block_given?
|
|
158
|
+
dsl = FieldDSL.new(self, :filters); dsl.instance_eval(&block)
|
|
159
|
+
else
|
|
160
|
+
@filters || []
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
def filter_fields; @filters || []; end
|
|
164
|
+
|
|
165
|
+
class FieldDSL
|
|
166
|
+
def initialize(resource, target_attr); @resource = resource; @target = target_attr; end
|
|
167
|
+
def field(name, type: :string, required: false, condition: nil, **options, &block)
|
|
168
|
+
field_obj = ZenAdmin::Core::Field.new(name, type: type, options: options, required: required, condition: condition)
|
|
169
|
+
field_obj.options[:formatter] = block if block_given?
|
|
170
|
+
@resource.send(@target) << field_obj
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace ZenAdmin
|
|
4
|
+
|
|
5
|
+
initializer "zen_admin.i18n" do |app|
|
|
6
|
+
app.config.i18n.default_locale = ZenAdmin.configuration.default_locale
|
|
7
|
+
# Add engine locales to host app
|
|
8
|
+
app.config.i18n.load_path += Dir[root.join("config", "locales", "**", "*.{rb,yml}")]
|
|
9
|
+
|
|
10
|
+
# 设置 Rails 时区
|
|
11
|
+
Time.zone = ZenAdmin.configuration.time_zone
|
|
12
|
+
Groupdate.time_zone = Time.zone if defined?(Groupdate)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "zen_admin.active_storage_csrf_fix" do
|
|
16
|
+
ActiveSupport.on_load(:active_storage_direct_uploads_controller) do
|
|
17
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "zen_admin.assets" do |app|
|
|
22
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
initializer "zen_admin.importmap", before: "importmap" do |app|
|
|
26
|
+
if app.config.respond_to?(:importmap)
|
|
27
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
28
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
initializer "zen_admin.auto_discover_models" do
|
|
33
|
+
Rails.application.config.to_prepare do
|
|
34
|
+
# 1. 加载内置资源逻辑
|
|
35
|
+
require_dependency ZenAdmin::Engine.root.join("lib/zen_admin/builtin")
|
|
36
|
+
ZenAdmin::Builtin.register_active_storage_blob rescue nil
|
|
37
|
+
|
|
38
|
+
# 2. 显式加载所有内置模型 (确保注册到 Registry)
|
|
39
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/asset")
|
|
40
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/trash_item")
|
|
41
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/audit_log")
|
|
42
|
+
|
|
43
|
+
if ZenAdmin.configuration.rbac_enable
|
|
44
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/user")
|
|
45
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/role")
|
|
46
|
+
require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/permission")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 3. 扫描并加载宿主应用的模型
|
|
50
|
+
files = Dir.glob(Rails.root.join("app/models/**/*.rb")) + Dir.glob(Rails.root.join("app/controllers/**/*.rb"))
|
|
51
|
+
files.each do |file|
|
|
52
|
+
next if File.directory?(file)
|
|
53
|
+
begin
|
|
54
|
+
require_dependency file
|
|
55
|
+
rescue => e
|
|
56
|
+
Rails.logger.debug "[ZenAdmin] Failed to load #{file}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 4. 自动同步权限点到数据库
|
|
61
|
+
if ZenAdmin.configuration.rbac_enable
|
|
62
|
+
ZenAdmin::Permission.sync_from_registry! rescue nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module LinkRegisterable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def zen_admin_link(label, path, icon: "fas fa-link", visible: true, position: 10, permission: nil)
|
|
7
|
+
ZenAdmin.register_link(label, path, icon: icon, visible: visible, position: position, permission: permission)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Registerable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def zen_admin(&block)
|
|
7
|
+
ZenAdmin.register(self, &block)
|
|
8
|
+
|
|
9
|
+
# 自动授权 Ransack 搜索权限
|
|
10
|
+
if respond_to?(:ransackable_attributes)
|
|
11
|
+
define_singleton_method(:ransackable_attributes) { |auth_object = nil| column_names }
|
|
12
|
+
define_singleton_method(:ransackable_associations) { |auth_object = nil| reflect_on_all_associations.map { |a| a.name.to_s } }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "singleton"
|
|
2
|
+
|
|
3
|
+
module ZenAdmin
|
|
4
|
+
class Registry
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@resources = {}
|
|
9
|
+
@links = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(resource)
|
|
13
|
+
@resources[resource.model_name.to_s] = resource
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register_link(label, path, icon:, visible:, position: 10, permission: nil)
|
|
17
|
+
@links.reject! { |l| l[:label] == label || l[:path] == path }
|
|
18
|
+
@links << { label: label, path: path, icon: icon, visible: visible, position: position, permission: permission }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def menu_items
|
|
22
|
+
raw_items = []
|
|
23
|
+
|
|
24
|
+
# Collect resources
|
|
25
|
+
all.each do |r|
|
|
26
|
+
next unless r.respond_to?(:menu_options) && r.menu_options[:visible]
|
|
27
|
+
raw_items << {
|
|
28
|
+
type: :resource,
|
|
29
|
+
object: r,
|
|
30
|
+
position: r.menu_options[:position] || 10,
|
|
31
|
+
group: r.menu_options[:group]
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Collect links
|
|
36
|
+
@links.each do |l|
|
|
37
|
+
next unless l[:visible]
|
|
38
|
+
raw_items << {
|
|
39
|
+
type: :link,
|
|
40
|
+
object: l,
|
|
41
|
+
position: l[:position] || 10,
|
|
42
|
+
group: nil # Links currently don't support grouping in this iteration, or we can add it later
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Grouping logic
|
|
47
|
+
grouped_items = {}
|
|
48
|
+
ungrouped_items = []
|
|
49
|
+
|
|
50
|
+
raw_items.each do |item|
|
|
51
|
+
if item[:group].present?
|
|
52
|
+
group_name = item[:group]
|
|
53
|
+
grouped_items[group_name] ||= { type: :group, label: group_name, items: [], position: nil }
|
|
54
|
+
grouped_items[group_name][:items] << item
|
|
55
|
+
else
|
|
56
|
+
ungrouped_items << item
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Calculate position for groups (min position of children)
|
|
61
|
+
grouped_items.each do |_, group|
|
|
62
|
+
group[:items].sort_by! { |i| i[:position] }
|
|
63
|
+
group[:position] = group[:items].first[:position]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Merge and sort
|
|
67
|
+
final_list = ungrouped_items + grouped_items.values
|
|
68
|
+
final_list.sort_by { |i| i[:position] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def [](model_or_name)
|
|
72
|
+
@resources[model_or_name.to_s]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def all
|
|
76
|
+
@resources.values
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def links
|
|
80
|
+
@links
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ZenAdmin
|
|
2
|
+
module Schema
|
|
3
|
+
class Serializer
|
|
4
|
+
def self.serialize(resource)
|
|
5
|
+
{
|
|
6
|
+
model: resource.model.name,
|
|
7
|
+
list: resource.list_fields.map(&:to_h),
|
|
8
|
+
form: resource.form_fields.map(&:to_h),
|
|
9
|
+
filters: resource.filters.map(&:to_h),
|
|
10
|
+
actions: resource.actions.map(&:to_h)
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "zen_admin/schema/serializer"
|
data/lib/zen_admin.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "zen_admin/version"
|
|
2
|
+
require "zen_admin/engine"
|
|
3
|
+
require "zen_admin/configuration"
|
|
4
|
+
require "zen_admin/core"
|
|
5
|
+
require "zen_admin/registry"
|
|
6
|
+
require "zen_admin/schema"
|
|
7
|
+
require "zen_admin/registerable"
|
|
8
|
+
require "zen_admin/link_registerable"
|
|
9
|
+
require "zen_admin/authenticatable"
|
|
10
|
+
require "kaminari"
|
|
11
|
+
require "pundit"
|
|
12
|
+
require "ransack"
|
|
13
|
+
|
|
14
|
+
module ZenAdmin
|
|
15
|
+
class << self
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def register(model, &block)
|
|
25
|
+
resource = Core::Resource.new(model)
|
|
26
|
+
block.call(resource) if block_given?
|
|
27
|
+
Registry.instance.register(resource)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def register_link(label, path, icon: "fas fa-link", visible: true, position: 10, permission: nil)
|
|
31
|
+
Registry.instance.register_link(label, path, icon: icon, visible: visible, position: position, permission: permission)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Auto-include in ActiveRecord::Base
|
|
37
|
+
ActiveSupport.on_load(:active_record) do
|
|
38
|
+
include ZenAdmin::Registerable
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Auto-include in ActionController::Base
|
|
42
|
+
|
|
43
|
+
ActiveSupport.on_load(:action_controller) do
|
|
44
|
+
|
|
45
|
+
include ZenAdmin::LinkRegisterable
|
|
46
|
+
|
|
47
|
+
include ZenAdmin::Authenticatable
|
|
48
|
+
|
|
49
|
+
helper ZenAdmin::ApplicationHelper
|
|
50
|
+
|
|
51
|
+
end
|