Rails ActiveAdmin 定制化实战

ThxFly · 2020年07月12日 · 最后由 ThxFly 回复于 2020年07月16日 · 2859 次阅读

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

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

lazybios 回复

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

需要 登录 后方可回复, 如果你还没有账号请 注册新账号