前言
這段 PHP 程式碼主要用於 WordPress 的 MemberPress 外掛會員管理頁面,目的是在會員列表中新增電話、地址、贈品寄送狀態及一鍵複製欄位。適合需要擴充會員管理功能、提升後台操作效率的工程師與自學者。
新增會員列表欄位表頭
透過 mepr-admin-members-cols 過濾器,將電話、地址、贈品已寄送與複製按鈕四個欄位加入會員列表的表頭。這樣可以讓後台管理員一目了然看到更多會員資訊。
add_filter('mepr-admin-members-cols', function($cols){
$cols['mepr_phone'] = __('電話', 'memberpress');
$cols['mepr_address'] = __('地址', 'memberpress');
$cols['mepr_sent'] = __('贈品已寄送', 'memberpress');
$cols['mepr_copy'] = __('複製', 'memberpress');
return $cols;
});
渲染會員列表每列的欄位內容
使用 mepr_members_list_table_row 過濾器,依照欄位名稱輸出對應的會員資料:
- 電話與地址從使用者 meta 取得,地址欄位會截斷顯示並加上完整 tooltip。
- 贈品已寄送欄位為 checkbox,狀態可即時切換並透過 AJAX 更新。
- 複製欄位提供一鍵複製會員姓名、電話與地址的功能,方便快速取得會員聯絡資訊。
add_filter('mepr_members_list_table_row', function($attributes, $rec, $column_name, $column_display_name){
$user = get_user_by('login', $rec->username);
if(!$user){ return; }
$first = get_user_meta($user->ID, 'first_name', true);
$last = get_user_meta($user->ID, 'last_name', true);
$full = trim(implode(' ', array_filter([$first, $last])));
if($full === '') { $full = $user->display_name ?: $user->user_login; }
$phone = get_user_meta($user->ID, 'mepr_phone', true);
$addr = get_user_meta($user->ID, 'mepr_address', true);
if($column_name === 'mepr_phone'){
echo '<td '.$attributes.'>'.esc_html($phone ?: '—').'</td>';
}
if($column_name === 'mepr_address'){
$short = mb_strimwidth(wp_strip_all_tags((string)$addr), 0, 80, '…', 'UTF-8');
echo '<td '.$attributes.' title="'.esc_attr($addr).'">'.esc_html($short ?: '—').'</td>';
}
if($column_name === 'mepr_sent'){
$checked = get_user_meta($user->ID, 'mepr_sent', true) ? 'checked' : '';
$nonce = wp_create_nonce('mepr_sent_nonce');
echo '<td '.$attributes.' style="text-align:center">
<label style="display:inline-flex;align-items:center;gap:.4em;cursor:pointer;">
<input type="checkbox" class="mepr-sent-toggle"
data-user="'.esc_attr($user->ID).'"
data-nonce="'.esc_attr($nonce).'" '.$checked.' />
<span>'.($checked ? esc_html__('Yes','memberpress') : esc_html__('No','memberpress')).'</span>
</label>
</td>';
}
if($column_name === 'mepr_copy'){
$copy_text = "Full Name: {$full}\nPhone: " . ($phone ?: '') . "\nAddress: " . ($addr ? wp_strip_all_tags($addr) : '');
echo '<td '.$attributes.' style="text-align:center">
<button type="button" class="button button-small mepr-copy-btn"
data-copy="'.esc_attr($copy_text).'"
title="'.esc_attr__('Copy Full Name / Phone / Address','memberpress').'">
'.esc_html__('複製','memberpress').'
</button>
</td>';
}
}, 10, 4);
後台頁尾加入 JavaScript 強化互動
透過 admin_print_footer_scripts 動作,在會員頁面插入 JavaScript:
- 切換「贈品已寄送」checkbox 時,使用 AJAX 非同步更新後端資料,並即時更新 UI 狀態。
- 複製按鈕則利用 Clipboard API 或備援方案將會員資訊複製到剪貼簿,並顯示複製成功提示。
此設計提升管理者操作便利性,避免頻繁刷新頁面。
AJAX 後端處理贈品寄送狀態更新
使用 wp_ajax_mepr_toggle_sent 來驗證權限與 nonce,確保安全後更新使用者 meta。若驗證失敗或使用者不存在,會回傳錯誤訊息。
add_action('wp_ajax_mepr_toggle_sent', function(){
if( ! current_user_can('manage_options') ){
wp_send_json_error(['message' => '無權限'], 403);
}
if( ! check_ajax_referer('mepr_sent_nonce', '_wpnonce', false) ){
wp_send_json_error(['message' => 'Nonce 驗證失敗'], 400);
}
$user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
$value = isset($_POST['value']) ? sanitize_text_field($_POST['value']) : '0';
if( !$user_id || ! get_user_by('id', $user_id) ){
wp_send_json_error(['message' => '使用者不存在'], 404);
}
update_user_meta($user_id, 'mepr_sent', $value === '1' ? 1 : 0);
wp_send_json_success(['nonce' => wp_create_nonce('mepr_sent_nonce')]);
});
簡單的後台樣式調整
為了讓新增欄位版面更整齊,使用 admin_head 動作插入 CSS,調整欄寬與置中對齊。
實務應用與延伸
這套擴充方案適合需要在 MemberPress 後台快速查看會員詳細聯絡資訊並標記贈品寄送狀態的情境。未來可延伸加入更多欄位或改為前端 AJAX 分頁載入,提升大會員數的效能。
常見問題與注意事項
- AJAX 權限與 nonce 驗證不可少,避免安全漏洞。
- 複製功能在非 HTTPS 或不支援 Clipboard API 的環境會退回使用 textarea 方案。
- 地址欄位截斷避免版面過長,但完整資料仍可透過 tooltip 查看。
完整程式碼
<?php
/**
* MemberPress Members 列表欄位:電話/地址/贈品已寄送/複製(可即時切換)
* 放 WPCode(PHP Snippet,Run Everywhere)
*/
/* === 1) 新增欄位表頭(mepr-admin-members-cols) === */
add_filter('mepr-admin-members-cols', function($cols){
$cols['mepr_phone'] = __('電話', 'memberpress');
$cols['mepr_address'] = __('地址', 'memberpress');
$cols['mepr_sent'] = __('贈品已寄送', 'memberpress');
$cols['mepr_copy'] = __('複製', 'memberpress'); // 一鍵複製按鈕
return $cols;
});
/* === 2) 渲染每一列內容(mepr_members_list_table_row) === */
add_filter('mepr_members_list_table_row', function($attributes, $rec, $column_name, $column_display_name){
$user = get_user_by('login', $rec->username);
if(!$user){ return; }
// 共用:取 Full Name / Phone / Address
$first = get_user_meta($user->ID, 'first_name', true);
$last = get_user_meta($user->ID, 'last_name', true);
$full = trim(implode(' ', array_filter([$first, $last])));
if($full === '') { $full = $user->display_name ?: $user->user_login; }
$phone = get_user_meta($user->ID, 'mepr_phone', true);
$addr = get_user_meta($user->ID, 'mepr_address', true);
if($column_name === 'mepr_phone'){
echo '<td '.$attributes.'>'.esc_html($phone ?: '—').'</td>';
}
if($column_name === 'mepr_address'){
$short = mb_strimwidth(wp_strip_all_tags((string)$addr), 0, 80, '…', 'UTF-8');
echo '<td '.$attributes.' title="'.esc_attr($addr).'">'.esc_html($short ?: '—').'</td>';
}
if($column_name === 'mepr_sent'){
$checked = get_user_meta($user->ID, 'mepr_sent', true) ? 'checked' : '';
$nonce = wp_create_nonce('mepr_sent_nonce');
echo '<td '.$attributes.' style="text-align:center">
<label style="display:inline-flex;align-items:center;gap:.4em;cursor:pointer;">
<input type="checkbox" class="mepr-sent-toggle"
data-user="'.esc_attr($user->ID).'"
data-nonce="'.esc_attr($nonce).'" '.$checked.' />
<span>'.($checked ? esc_html__('Yes','memberpress') : esc_html__('No','memberpress')).'</span>
</label>
</td>';
}
if($column_name === 'mepr_copy'){
$copy_text = "Full Name: {$full}\nPhone: " . ($phone ?: '') . "\nAddress: " . ($addr ? wp_strip_all_tags($addr) : '');
echo '<td '.$attributes.' style="text-align:center">
<button type="button" class="button button-small mepr-copy-btn"
data-copy="'.esc_attr($copy_text).'"
title="'.esc_attr__('Copy Full Name / Phone / Address','memberpress').'">
'.esc_html__('複製','memberpress').'
</button>
</td>';
}
}, 10, 4);
/* === 3) 後台頁尾插入 JS(僅 Members 頁) === */
add_action('admin_print_footer_scripts', function(){
if( ! is_admin() ) return;
if( ! (isset($_GET['page']) && $_GET['page'] === 'memberpress-members') ) return;
$ajax_url = admin_url('admin-ajax.php');
?>
<script>
(function(){
// 已寄送:切換寫入
function setSentLabel(td, checked){
var s = td.querySelector('span');
if(s){ s.textContent = checked ? 'Yes' : 'No'; }
}
document.addEventListener('change', function(e){
var el = e.target;
if(!el.classList.contains('mepr-sent-toggle')) return;
var td = el.closest('td');
var uid = el.getAttribute('data-user');
var nonce = el.getAttribute('data-nonce');
var val = el.checked ? '1' : '0';
setSentLabel(td, el.checked); // 先行更新 UI
var xhr = new XMLHttpRequest();
var fd = new FormData();
fd.append('action', 'mepr_toggle_sent');
fd.append('user_id', uid);
fd.append('value', val);
fd.append('_wpnonce', nonce);
xhr.open('POST', '<?php echo esc_js($ajax_url); ?>', true);
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
try{
var res = JSON.parse(xhr.responseText||'{}');
if(!res.success){
el.checked = !el.checked; setSentLabel(td, el.checked);
alert(res.data && res.data.message ? res.data.message : '更新失敗');
}else if(res.data && res.data.nonce){
el.setAttribute('data-nonce', res.data.nonce);
}
}catch(err){
el.checked = !el.checked; setSentLabel(td, el.checked);
alert('伺服器回應格式錯誤');
}
}
};
xhr.send(fd);
}, false);
// 一鍵複製
document.addEventListener('click', async function(e){
var btn = e.target.closest('.mepr-copy-btn');
if(!btn) return;
var text = btn.getAttribute('data-copy') || '';
try{
if(navigator.clipboard && window.isSecureContext){
await navigator.clipboard.writeText(text);
}else{
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
btn.classList.add('mepr-copied');
btn.textContent = '已複製';
setTimeout(function(){ btn.classList.remove('mepr-copied'); btn.textContent = '複製'; }, 1200);
}catch(err){
alert('複製失敗,請手動複製。\n\n' + text);
}
}, false);
})();
</script>
<?php
});
/* === 4) AJAX 後端:儲存 mepr_sent === */
add_action('wp_ajax_mepr_toggle_sent', function(){
if( ! current_user_can('manage_options') ){
wp_send_json_error(['message' => '無權限'], 403);
}
if( ! check_ajax_referer('mepr_sent_nonce', '_wpnonce', false) ){
wp_send_json_error(['message' => 'Nonce 驗證失敗'], 400);
}
$user_id = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0;
$value = isset($_POST['value']) ? sanitize_text_field($_POST['value']) : '0';
if( !$user_id || ! get_user_by('id', $user_id) ){
wp_send_json_error(['message' => '使用者不存在'], 404);
}
update_user_meta($user_id, 'mepr_sent', $value === '1' ? 1 : 0);
wp_send_json_success(['nonce' => wp_create_nonce('mepr_sent_nonce')]);
});
/* === 5) 簡單樣式 === */
add_action('admin_head', function(){
if( ! (isset($_GET['page']) && $_GET['page'] === 'memberpress-members') ) return;
echo '
<style>
th#mepr_phone{width:140px}
th#mepr_address{width:260px}
th#mepr_sent{width:110px;text-align:center}
th#mepr_copy{width:90px;text-align:center}
td.column-mepr_sent, td.column-mepr_copy{text-align:center}
.mepr-copy-btn.button-small{line-height:22px;height:24px;padding:0 8px}
.mepr-copy-btn.mepr-copied{opacity:.8}
</style>';
});