各位观众老爷,晚上好!今天咱们聊聊 WordPress 后台列表页面的那些事儿,主要就是扒一扒 WP_List_Table
类的底裤,看看它是怎么把一个看似简单的列表页面,变得既强大又灵活的。
想象一下,你得开发一个插件,专门管理用户提交的反馈意见。这些反馈意见得在后台展示,能排序,能搜索,最好还能批量删除。如果让你从零开始写,那得掉多少头发啊?幸好 WordPress 提供了 WP_List_Table
这个救星,让我们能站在巨人的肩膀上。
WP_List_Table
是个啥?
简单来说,WP_List_Table
是一个抽象类,它定义了一个标准的 WordPress 后台列表页面的结构和行为。你可以继承它,然后根据自己的需求进行定制,比如定义列、添加排序、实现搜索等等。
第一步:继承 WP_List_Table
首先,我们需要创建一个类,并继承 WP_List_Table
。这个类将会负责处理我们自定义列表页面的所有逻辑。
<?php
if( ! class_exists( 'WP_List_Table' ) ) {
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}
class Feedback_List_Table extends WP_List_Table {
/** ************************************************************************
* REQUIRED. Set up a constructor that references the parent constructor.
* We use the parent reference to set some default configs.
***************************************************************************/
function __construct(){
global $status, $page;
//Set parent defaults
parent::__construct( array(
'singular' => 'feedback', //singular name of the listed records
'plural' => 'feedbacks', //plural name of the listed records
'ajax' => false //does this table support ajax?
) );
}
//...后面还有一大堆代码
}
?>
这段代码做了几件事:
- 包含了
class-wp-list-table.php
文件,确保WP_List_Table
类已经加载。 - 创建了一个名为
Feedback_List_Table
的类,继承了WP_List_Table
。 - 在构造函数中,调用了父类的构造函数,并设置了三个参数:
singular
:单数形式的名称,这里是 ‘feedback’。plural
:复数形式的名称,这里是 ‘feedbacks’。ajax
:是否支持 AJAX,这里设置为false
,简化例子。
第二步:定义列(get_columns()
)
接下来,我们要定义列表页面中要显示的列。这需要重写 get_columns()
方法。
/** ************************************************************************
* REQUIRED. This method dictates the table's columns and titles.
* This should return an array where the key is the column slug (and class)
* and the value is the column's title text. If you need a checkbox
* column, use the special slug 'cb'.
*
* The 'cb' column will automatically handle the checkboxes and register
* the columns headers.
*
* @return array An associative array containing column information: 'slugs'=>'Visible Titles'
**************************************************************************/
function get_columns(){
$columns = array(
'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
'title' => 'Title',
'author' => 'Author',
'message' => 'Message',
'date' => 'Date'
);
return $columns;
}
这个方法返回一个数组,数组的键是列的 slug(用于 CSS 类),值是列的标题。
cb
:这是一个特殊的 slug,用于显示复选框。WordPress 会自动处理复选框的渲染。title
:反馈的标题。author
:反馈的作者。message
:反馈的内容。date
:反馈的日期。
第三步:定义可排序的列(get_sortable_columns()
)
如果想让某些列可以排序,需要重写 get_sortable_columns()
方法。
/** ************************************************************************
* Optional. If you want one or more columns to be sortable (ASC/DESC toggle),
* you will need to register it here. This should return an array where the
* key is the column that needs to be sortable, and the value is db column to
* sort by. Often, the key and value will be the same - as is the case here.
*
* @return array An associative array containing all the columns that should be sortable: 'slugs'=>'data_values'
**************************************************************************/
function get_sortable_columns() {
$sortable_columns = array(
'title' => array('title',true), //true means it's already sorted
'author' => array('author',false),
'date' => array('date',false)
);
return $sortable_columns;
}
这个方法返回一个数组,数组的键是列的 slug,值是一个包含两个元素的数组:
- 第一个元素是用于排序的数据库字段名。
- 第二个元素是一个布尔值,表示是否默认升序排序(
true
)或降序排序(false
)。
第四步:定义批量操作(get_bulk_actions()
)
如果想要添加批量操作,比如批量删除,需要重写 get_bulk_actions()
方法。
/** ************************************************************************
* Optional. If you need to include bulk actions in your list table, this is
* the place to define them. Bulk actions are an associative array where
* the key is the internal name and the value is the display name.
*
* @return array An associative array containing all the bulk actions: 'slugs'=>'Visible Titles'
**************************************************************************/
function get_bulk_actions() {
$actions = array(
'delete' => 'Delete'
);
return $actions;
}
这个方法返回一个数组,数组的键是操作的 slug,值是操作的显示名称。
第五步:处理批量操作(process_bulk_action()
)
定义了批量操作之后,还需要处理这些操作。
/** ************************************************************************
* Optional. It is possible to place message above the table, such as an
* admin notice. Be careful about doing this with anything complicated -
* the point is that the user is querying a database, and good UI/UX
* dictates that they should see the data first.
*
* @return void
*/
function process_bulk_action() {
//Detect when a bulk action is being triggered...
if( 'delete'===$this->current_action() ) {
$ids = $_GET['feedback']; //注意安全,需要过滤和验证
if (is_array($ids)) {
foreach ($ids as $id) {
// 这里执行删除操作,比如调用 wp_delete_post() 或自定义的删除函数
// 假设我们有一个 delete_feedback 函数
delete_feedback($id);
}
wp_redirect(admin_url('admin.php?page=feedback_list&deleted=true')); // 重定向到列表页,并添加一个参数
exit;
} else {
// 处理单个删除的情况
delete_feedback($ids);
wp_redirect(admin_url('admin.php?page=feedback_list&deleted=true'));
exit;
}
}
}
这个方法首先检查当前的操作是否是 ‘delete’,如果是,就获取选中的 ID,然后执行删除操作。注意,这里需要进行严格的安全检查,防止 SQL 注入等安全问题。
第六步:准备数据(prepare_items()
)
这是最关键的一步,我们需要从数据库中获取数据,并将其格式化成 WP_List_Table
可以接受的格式。
/** ************************************************************************
* REQUIRED! This is where you prepare your data for display. This method will
* usually be used to query the database, sort and filter the data, and generally
* get it ready to be displayed. At a minimum, we should set $this->items
* and $this->set_pagination_args().
**************************************************************************/
function prepare_items() {
global $wpdb; //This is used only if making any database queries
/**
* First, lets decide how many records per page to show
*/
$per_page = 5;
/**
* REQUIRED. Now we need to define our column headers. This includes defining
* a CSS class and column title. This information is used to label the table
* headers.
*/
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
/**
* REQUIRED. Finally, we build an array to be used by the table.
*/
$this->_column_headers = array($columns, $hidden, $sortable);
/**
* Process bulk action
*/
$this->process_bulk_action();
/**
* Instead of querying a database, we're going to fetch the example
* data and process it.
*/
// $data = $this->example_data;
// 获取总数
$total_items = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}feedback");
// 排序参数
$orderby = !empty($_GET["orderby"]) ? sanitize_sql_orderby($_GET["orderby"]) : 'date';
$order = !empty($_GET["order"]) ? sanitize_sql_orderby($_GET["order"]) : 'DESC';
// 当前页码
$current_page = $this->get_pagenum();
// 计算偏移量
$offset = ($current_page - 1) * $per_page;
// 查询数据
$data = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}feedback ORDER BY %s %s LIMIT %d, %d", $orderby, $order, $offset, $per_page), ARRAY_A);
/**
* REQUIRED for pagination. Let's figure out what page the user is currently
* looking at. We'll need this later when we add pagination links.
*/
$current_page = $this->get_pagenum();
/**
* REQUIRED for pagination. Let's find out the total number of records that
* match the search query.
*/
$total_items = count($data); //This should be the total number of records
/**
* The WP_List_Table class does not handle pagination for you, so we need
* to handle it ourselves. We will use this helper function to calculate
* the pagination arguments.
*/
$this->set_pagination_args( array(
'total_items' => $total_items, //WE have to calculate the total number of items
'per_page' => $per_page, //WE have to determine how many items to show on a page
'total_pages' => ceil($total_items/$per_page) //WE have to calculate the total number of pages
) );
/**
* REQUIRED. Now we can add our *sorted* data to the items property, where
* it will be used by the rest of the WP_List_Table class.
*/
$this->items = $data;
}
这个方法做了很多事情:
- 定义了每页显示的记录数 (
$per_page
)。 - 调用
get_columns()
、get_hidden_columns()
和get_sortable_columns()
获取列的定义。 - 调用
process_bulk_action()
处理批量操作。 - 从数据库中获取数据,并进行排序和分页。
- 调用
set_pagination_args()
设置分页参数。 - 将数据赋值给
$this->items
。
第七步:显示列数据(column_default()
和 column_xxx()
)
现在,我们需要定义如何显示每一列的数据。WP_List_Table
提供了两种方法:
column_default($item, $column_name)
:用于显示没有特殊处理的列。column_xxx($item)
:用于显示名为 xxx 的列,比如column_title($item)
用于显示标题列。
/** ************************************************************************
* REQUIRED! This is how you display the contents of each column. Because we previously
* declared our columns with slugs, we can easily target each.
*
* If you want to make a column sortable, you need to register it with the sortable
* columns array in the get_sortable_columns() method.
*
* @param array $item A singular item (one full row's worth of data)
* @param string $column_name The name/slug of the column to be processed
* @return string Text or HTML to be placed inside the column
**************************************************************************/
function column_default( $item, $column_name ) {
switch( $column_name ) {
case 'message':
return substr($item[ $column_name ], 0, 50) . '...'; // 截取部分内容
default:
return $item[ $column_name ]; //Show the whole array for troubleshooting purposes
}
}
/** ************************************************************************
* Recommended. This is a custom column method and is responsible for what
* is rendered in any column with a name of 'title'. When editing a column with
* a name that is a reserved word (like 'title') always prefix it with
* your plugin's name. For example, if the plugin is called "Example Plugin"
* and you need to render the title column, the method name should be
* "example_plugin_column_title".
*
* Remember, when defining custom column handlers, the format is "column_[column_name]"
* and you will need to register it in the get_columns() method.
*
* @param array $item A singular item (one full row's worth of data)
* @return string Text to be placed inside the column
**************************************************************************/
function column_title($item) {
//Build row actions
$actions = array(
'edit' => sprintf('<a href="?page=%s&action=%s&feedback=%s">Edit</a>',$_REQUEST['page'],'edit',$item['id']),
'delete' => sprintf('<a href="?page=%s&action=%s&feedback=%s">Delete</a>',$_REQUEST['page'],'delete',$item['id']),
);
//Return the title contents
return sprintf('%1$s <span style="color:silver">(id:%2$s)</span>%3$s',
/*$1%s*/ $item['title'],
/*$2%s*/ $item['id'],
/*$3%s*/ $this->row_actions($actions)
);
}
/** ************************************************************************
* REQUIRED if displaying checkboxes or using bulk actions! The 'cb' column
* is given special treatment when columns are processed. It ALWAYS needs to have
* it's own method.
*
* @param array $item A singular item (one full row's worth of data)
* @return string Text to be placed inside the column
**************************************************************************/
function column_cb($item) {
return sprintf(
'<input type="checkbox" name="%1$s[]" value="%2$s" />',
/*$1%s*/ $this->_args['singular'], //Let's simply repurpose the table's singular label ("movie")
/*$2%s*/ $item['id'] //The value of the checkbox should be the record's id
);
}
column_default()
方法用于显示 ‘message’ 列,它截取了部分内容,避免内容过长影响页面布局。其他列则直接显示原始数据。column_title()
方法用于显示 ‘title’ 列,它添加了编辑和删除链接。column_cb()
方法用于显示复选框,这是批量操作的关键。
第八步:显示列表页面(display()
)
最后,我们需要在 WordPress 后台页面中显示这个列表。
/** ************************************************************************
* REQUIRED! This is how you display the table
**************************************************************************/
function display() {
$this->prepare_items(); // 准备数据
parent::display(); // 调用父类的 display() 方法
}
这个方法首先调用 prepare_items()
方法准备数据,然后调用父类的 display()
方法显示列表页面。
第九步:在 WordPress 后台添加菜单项
为了让用户能够访问这个列表页面,需要在 WordPress 后台添加一个菜单项。
add_action('admin_menu', 'feedback_list_menu');
function feedback_list_menu() {
add_menu_page(
'Feedbacks', // 页面标题
'Feedbacks', // 菜单标题
'manage_options', // 权限
'feedback_list', // 菜单 slug
'feedback_list_page' // 显示页面的函数
);
}
function feedback_list_page() {
$feedback_list_table = new Feedback_List_Table();
echo '<div class="wrap">';
echo '<h2>Feedbacks</h2>';
// 显示成功删除的消息
if (isset($_GET['deleted']) && $_GET['deleted'] == 'true') {
echo '<div class="updated"><p>Feedbacks deleted successfully.</p></div>';
}
$feedback_list_table->display();
echo '</div>';
}
这段代码做了两件事:
- 使用
add_menu_page()
函数添加了一个菜单项。 - 定义了一个
feedback_list_page()
函数,用于显示列表页面。在这个函数中,创建了一个Feedback_List_Table
实例,然后调用display()
方法显示列表。
完整代码示例
<?php
if( ! class_exists( 'WP_List_Table' ) ) {
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}
class Feedback_List_Table extends WP_List_Table {
function __construct(){
global $status, $page;
parent::__construct( array(
'singular' => 'feedback',
'plural' => 'feedbacks',
'ajax' => false
) );
}
function get_columns(){
$columns = array(
'cb' => '<input type="checkbox" />',
'title' => 'Title',
'author' => 'Author',
'message' => 'Message',
'date' => 'Date'
);
return $columns;
}
function get_sortable_columns() {
$sortable_columns = array(
'title' => array('title',true),
'author' => array('author',false),
'date' => array('date',false)
);
return $sortable_columns;
}
function get_bulk_actions() {
$actions = array(
'delete' => 'Delete'
);
return $actions;
}
function process_bulk_action() {
if( 'delete'===$this->current_action() ) {
$ids = $_GET['feedback']; //注意安全,需要过滤和验证
if (is_array($ids)) {
foreach ($ids as $id) {
// 这里执行删除操作,比如调用 wp_delete_post() 或自定义的删除函数
// 假设我们有一个 delete_feedback 函数
delete_feedback($id);
}
wp_redirect(admin_url('admin.php?page=feedback_list&deleted=true')); // 重定向到列表页,并添加一个参数
exit;
} else {
// 处理单个删除的情况
delete_feedback($ids);
wp_redirect(admin_url('admin.php?page=feedback_list&deleted=true'));
exit;
}
}
}
function prepare_items() {
global $wpdb;
$per_page = 5;
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array($columns, $hidden, $sortable);
$this->process_bulk_action();
// 获取总数
$total_items = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}feedback");
// 排序参数
$orderby = !empty($_GET["orderby"]) ? sanitize_sql_orderby($_GET["orderby"]) : 'date';
$order = !empty($_GET["order"]) ? sanitize_sql_orderby($_GET["order"]) : 'DESC';
// 当前页码
$current_page = $this->get_pagenum();
// 计算偏移量
$offset = ($current_page - 1) * $per_page;
// 查询数据
$data = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}feedback ORDER BY %s %s LIMIT %d, %d", $orderby, $order, $offset, $per_page), ARRAY_A);
$this->set_pagination_args( array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil($total_items/$per_page)
) );
$this->items = $data;
}
function column_default( $item, $column_name ) {
switch( $column_name ) {
case 'message':
return substr($item[ $column_name ], 0, 50) . '...'; // 截取部分内容
default:
return $item[ $column_name ]; //Show the whole array for troubleshooting purposes
}
}
function column_title($item) {
$actions = array(
'edit' => sprintf('<a href="?page=%s&action=%s&feedback=%s">Edit</a>',$_REQUEST['page'],'edit',$item['id']),
'delete' => sprintf('<a href="?page=%s&action=%s&feedback=%s">Delete</a>',$_REQUEST['page'],'delete',$item['id']),
);
return sprintf('%1$s <span style="color:silver">(id:%2$s)</span>%3$s',
$item['title'],
$item['id'],
$this->row_actions($actions)
);
}
function column_cb($item) {
return sprintf(
'<input type="checkbox" name="%1$s[]" value="%2$s" />',
$this->_args['singular'],
$item['id']
);
}
function display() {
$this->prepare_items();
parent::display();
}
}
add_action('admin_menu', 'feedback_list_menu');
function feedback_list_menu() {
add_menu_page(
'Feedbacks',
'Feedbacks',
'manage_options',
'feedback_list',
'feedback_list_page'
);
}
function feedback_list_page() {
$feedback_list_table = new Feedback_List_Table();
echo '<div class="wrap">';
echo '<h2>Feedbacks</h2>';
// 显示成功删除的消息
if (isset($_GET['deleted']) && $_GET['deleted'] == 'true') {
echo '<div class="updated"><p>Feedbacks deleted successfully.</p></div>';
}
$feedback_list_table->display();
echo '</div>';
}
// 模拟删除函数
function delete_feedback($id) {
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'feedback', array( 'id' => $id ) );
}
// 模拟数据表创建和插入
register_activation_hook( __FILE__, 'create_feedback_table' );
function create_feedback_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'feedback';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
title varchar(255) NOT NULL,
author varchar(255) NOT NULL,
message text NOT NULL,
date datetime NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
// 插入一些模拟数据
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 1',
'author' => 'John Doe',
'message' => 'This is the first feedback message.',
'date' => current_time( 'mysql' )
)
);
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 2',
'author' => 'Jane Smith',
'message' => 'This is the second feedback message.',
'date' => current_time( 'mysql' )
)
);
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 3',
'author' => 'Peter Jones',
'message' => 'This is the third feedback message.',
'date' => current_time( 'mysql' )
)
);
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 4',
'author' => 'Alice Brown',
'message' => 'This is the fourth feedback message.',
'date' => current_time( 'mysql' )
)
);
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 5',
'author' => 'Bob White',
'message' => 'This is the fifth feedback message.',
'date' => current_time( 'mysql' )
)
);
$wpdb->insert(
$table_name,
array(
'time' => current_time( 'mysql' ),
'title' => 'Feedback 6',
'author' => 'Charlie Green',
'message' => 'This is the sixth feedback message.',
'date' => current_time( 'mysql' )
)
);
}
?>
重要提示:
- 安全第一: 在处理用户输入时,一定要进行过滤和验证,防止安全漏洞。
- 性能优化: 如果数据量很大,需要考虑性能优化,比如使用缓存。
- 可扩展性:
WP_List_Table
提供了很多扩展点,可以根据自己的需求进行定制。
总结
WP_List_Table
类是一个强大的工具,可以帮助我们快速构建可扩展的 WordPress 后台列表页面。通过继承它,并重写相应的方法,我们可以轻松地实现各种功能,比如排序、搜索、批量操作等等。记住,安全和性能是永远需要考虑的因素。
好了,今天的讲座就到这里。希望大家有所收获,早日摆脱掉头发危机!