Rails ActiveAdmin 定制化实战

ThxFly · July 12, 2020 · Last by ThxFly replied at July 16, 2020 · 2853 hits

ActiveAdmin 是一款基于 Rails 的声明式后台管理框架,能够让大家写后台管理系统时会心一笑,下面是我司在生产中一些定制。

一、换用多个主题

ActiveAdmin 默认主题是灰色的,巨丑。为此,使用他人开源的三个主题包,让客服姐姐和程序员哥哥按照个人喜好选用不同主题。管理员表需加个主题字段

# Gemfile
gem 'active_admin_theme'  # 上下布局主题,比较实用,根据README说明安装,下同
gem 'active_material', '1.4.2' # 上下布局主题,支持移动端,Material Design风格;由于列表页的动作会隐藏,使得这个主题在我司用的人最少
gem 'arctic_admin', '3.0.0'  # 左右布局主题,简约风,支持移动端
// app/assets/stylesheets/active_admin_theme.scss    active_admin_theme这个主题不定制还是会有点丑
$skinMainSecondColor: #606ef0!default;
@import "active_admin/mixins";
@import "active_admin/base";

.site_title {
  color: white!important;
}

select {
  height: 29px;
  margin: 0;
  background-color: white;
  border: 1px solid #e4eaec;
}

a  {
  border-radius: initial!important;
}

table.index_table tr.even td {
  background-color: white;
}
# config/initializers/active_admin.rb
  config.register_stylesheet 'active_admin_theme.css'
  config.register_stylesheet 'active_material_theme.css'
  config.register_stylesheet 'arctic_admin_theme.css'

  config.register_javascript 'active_admin_theme.js'
  config.register_javascript 'active_material_theme.js'
  config.register_javascript 'arctic_admin_theme.js'

class ActiveAdmin::Views::Pages::Base < Arbre::HTML::Document
  def build_active_admin_head
    within head do
      html_title [title, helpers.active_admin_namespace.site_title(self)].compact.join(' | ')

      active_admin_namespace.meta_tags.each do |name, content|
        text_node(tag(:meta, name: name, content: content))
      end

      case current_admin_user.theme
      when 'active_material'
        text_node stylesheet_link_tag('active_material_theme.css')
        text_node javascript_include_tag('active_material_theme.js')
      when 'arctic_admin'
        text_node stylesheet_link_tag('arctic_admin_theme.css')
        text_node javascript_include_tag('arctic_admin_theme.js')
      else
        text_node stylesheet_link_tag('active_admin_theme.css')
        text_node javascript_include_tag('active_admin_theme.js')
      end

      if active_admin_namespace.favicon
        text_node(favicon_link_tag(active_admin_namespace.favicon))
      end

      text_node csrf_meta_tag
    end
  end
end
# app/controllers/admin/home_controller.rb, config/routes.rb
get '/admin/check_theme', to: 'admin/home#check_theme', as: :check_theme

# 切换主题
def check_theme
  theme = case current_admin_user.theme
          when 'active_admin_theme' then 'arctic_admin'
          when 'arctic_admin'       then 'active_material'
          when 'active_material'    then 'active_admin_theme'
          else
            'active_admin_theme'
          end
  current_admin_user.update_columns(theme: theme)
  redirect_back fallback_location: root_path
end

二、更改默认匹配顺序

ActiveAdmin 字符串类型的筛选框默认是模糊匹配,即用 like‘%?%’,这不符合索引优化原则,故改为完全匹配

# config/initializers/active_admin.rb 末尾添加
ActiveAdmin::Inputs::Filters::StringInput.filters.clear
ActiveAdmin::Inputs::Filters::StringInput.filter(:equals, :contains, :starts_with, :ends_with)

三、关闭自动加载关联关系

ActiveAdmin 进入列表页时外键对应的 select 条件框会加载所有关联的 ID,数据表如果有上百万行就会卡死,改为默认关闭

# config/initializers/active_admin.rb
config.include_default_association_filters = false

四、关闭默认列排序

默认情况下,每一列都会生成排序按钮,如果某列没有索引,排序会很慢,改为默认关闭,要用时手动开启

# config/initializers/active_admin.rb
class ActiveAdmin::Views::Pages::TableFor
  class Column
    def sortable?
      return false
    end
  end
end

五、过滤搜索条件的空格

搜索条件如果输多了空格,客服姐姐就会来找你麻烦,默认帮她们去掉

class ApplicationController < ActionController::Base
  before_action :strip_params, only: :index

  def strip_params
    return if params[:q].blank?

    params[:q].each do |k, v|
      params[:q][k] = v.strip
    end
  end
end

六、关闭强参数验证

内部系统,别人要有管理员账号和密码才能黑进来,做不做强参数无所谓,关闭默认的强参数机制,省一些容易出错代码

class ApplicationController < ActionController::Base
  before_action do
    params.permit!
  end
end

七、新增额外列方法

提取对于通用的 column 操作,避免代码重复(具体定义根据自己需求和代码来,以下是示例)

# config/initializers/active_admin.rb
class ActiveAdmin::Views::Pages::TableFor
  # 昵称列,通过user_id在缓存中搜索昵称, 避免关联用户表
  def user_column(attribute)
    column('用户') do |model|
      user_id = model.send(attribute)
      link_to User.nick_name(user_id), admin_user_path(user_id)
    end
  end

  # 手机列,通过user_id在缓存中搜索手机, 避免关联用户表
  def phone_column(attribute)
    column('手机号') do |model|
      user_id = model.send(attribute)
      link_to User.phone(user_id), admin_user_path(user_id)
    end
  end

  # 数据库存储的数字,布尔列
  def bool_column(attribute)
    column(attribute) { |model| model.send(attribute).to_i == 1 ? status_tag('yes') : status_tag('no') }
  end

  # 图片列
  def image_column(attribute, options = {})
    column(attribute) do |model|
      url = model.send(attribute)
      image_tag(url, options) if url.present?
    end
  end

  # 枚举值
  def enum_column(attribute, alias_attribute = nil)
    column(attribute) do |model|
      enums = BaseValue.send(alias_attribute || attribute)
      enums[model.send(attribute)]
    end
  end

  # 时间戳转时间显示
  def time_column(attribute)
    column(attribute) do |model|
      Time.at(model.send(attribute).to_i)
    end
  end
end

八、新增额外行方法

提取通用的 row 操作,避免代码重复

# config/initializers/active_admin.rb
class ActiveAdmin::Views::Pages::AttributesTable
  def user_row(attribute)
    row('用户') do |model|
      user_id = model.send(attribute)
      link_to User.nick_name(user_id), admin_user_path(user_id)
    end
  end

  def phone_row(attribute)
    row('手机号') do |model|
      user_id = model.send(attribute)
      link_to User.phone(user_id), admin_user_path(user_id)
    end
  end

  def bool_row(attribute)
    row(attribute) { |model| model.send(attribute) == 1 ? status_tag('yes') : status_tag('no') }
  end

  # 调用示例       image_row :avatar_qnniu, size: '50', style: 'border-radius: 50%'
  def image_row(attribute, options = {})
    row(attribute) do |model|
      url = model.send(attribute)
      image_tag(url, options) if url.present?
    end
  end

  # 枚举值
  def enum_row(attribute, alias_attribute = nil)
    row(attribute) do |model|
      enums = BaseValue.send(alias_attribute || attribute)
      enums[model.send(attribute)]
    end
  end

  def time_row(attribute)
    row(attribute) do |model|
      Time.at(model.send(attribute).to_i)
    end
  end
end

九、新增通用视图帮助类

不同资源的列表页,当用 user_id 搜索时,都需要显示用户相关的一些按钮,可以提取成一个 helper 类,避免重复定义

# app/controllers/concerns/show_user_helper.rb
module ShowUserHelper
  def self.included(dsl)
    dsl.action_item(:show_user, only: [:index]) do
      user_id = params.dig(:q, :user_id_equals) || params[:user_id]
      resource = User.find_by(user_id: user_id) if user_id.present?
      if resource.present?
       # ......省略好些个按钮
        if authorized?(:view_operates, resource)
          a '日志', href: admin_operates_path(q: { user_id_equals: resource.user_id })
        end
        if authorized?(:generate_temp_password, resource)
          a '生成临时密码', href: generate_temp_password_admin_user_path(resource), 'data-method': :post
        end
      end
    end
  end
end

ActiveAdmin.register_page 'Operates' do
  menu label: '日志管理', parent: '系统管理', priority: 54
  include ShowUserHelper   # 包含helper, 这样上面那些按钮都有了
end

十、自定义页脚

# config/initializers/active_admin.rb
  config.footer = ->(_footer) { MyFooter.build(request) }
# config/initializers/my_footer.rb
class MyFooter < ActiveAdmin::Component
  def self.build(request)
    new.build(request)
  end

  def build(request)
    div do
      a('Change Language', href: '/admin/check_locale')
      span '|'
      a('Change Theme', href: '/admin/check_theme')
    end
  end
end

十一、定制详情页的标题

# 对应model代码里定义实例方法
def display_name
  phone || user_id
end

上面这些实战,主要是通过打开类,修改 ActiveAdmin 源代码的方式操作的。

感谢分享,都是一些很实用的技巧,已经接近两年没用 ActiveAdmin 了,翻了翻自己的陈年老代码,我也分享一些个人感觉有些作用的技巧吧😂

  • 指定某些 controller 下引入特定的 JS 文件。当时应该是为了只在需要用到富文本编辑器的页面去引用对应的 JS。
# config/initializers/active_admin.rb
module AdminPageLayoutOverride
  def build_active_admin_head
    # you can move the call to super at the end, if you wish
    # to insert things at the begining of the page
    super

    # this will be added at the end of <head>
    within @head do
      white_list = %w(admin/announcements admin/distributions admin/introductions admin/versions)
      if params['controller'].in?(white_list)
        text_node javascript_include_tag(params['controller'])
      end
    end
  end
end
ActiveAdmin::Views::Pages::Base.send :prepend, AdminPageLayoutOverride
  • 增加自定义的 attribute
# config/initializers/active_admin.rb
module ActiveAdmin
  module Views
    class RtfContent < ActiveAdmin::Component
      builder_method :rtf_content

      def build(content, attributes = {})
        super(attributes)
        textarea class: 'rtf-content-ckeditor', name: SecureRandom.uuid do
          content
        end
      end
    end
  end
end
  • 在多个 namespace 使用 ActiveAdmin + Devise
# config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
#...
  config.namespace :admin do |ns|
    ns.site_title = "Admin System"
    ns.authentication_method = :authenticate_admin_user!
    ns.authorization_adapter = ActiveAdmin::PunditAdapter
    ns.pundit_default_policy = 'AdminPolicy'
    ns.current_user_method = :current_admin_user
    ns.logout_link_path = :destroy_admin_user_session_path
  end

  config.namespace :public do |ns|
    ns.site_title = "Public NS"
    ns.site_title_link = '/public'
    ns.authentication_method = :authenticate_teacher!
    ns.current_user_method = :current_teacher
    ns.logout_link_path = :destroy_teacher_session_path
    ns.footer = '教师工作台'
  end
#...
end

# Devise 的处理
class ActiveAdmin::Devise::SessionsController
  def after_sign_out_path_for(resource_or_scope)
    case resource_or_scope
    when :teacher
      new_teacher_session_path
    when :admin_user
      new_admin_user_session_path
    end
  end

  def after_sign_in_path_for(resource)
    case resource
    when Teacher
      public_root_path
    when AdminUser
      admin_dashboard_path
    end
  end
end

想确认真的能会心一笑吗?

Reply to lazybios

代码量越少,人的幸福指数越高。 复杂的逻辑都在 API,对后台的要求只是增删改查,要求上不高。 如果是 ERP 那种重后台系统,倒是可能不会会心一笑。

You need to Sign in before reply, if you don't have an account, please Sign up first.