feat(user): add local avatar management flow
This commit is contained in:
@@ -85,22 +85,6 @@
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 4. 头像 -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<span class="section-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span>
|
||||
<h3>Gravatar 头像源</h3>
|
||||
</div>
|
||||
<n-form :model="basic" label-placement="left" label-width="130" class="aksrt-form">
|
||||
<n-form-item label="头像服务源">
|
||||
<n-select v-model:value="basic.avatar_source" :options="avatarSourceOptions" placeholder="选择头像镜像源" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">国内站点建议优先选 Cravatar。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 5. 备案 -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
@@ -140,7 +124,6 @@ const saving = ref(false)
|
||||
const basic = reactive({
|
||||
comment_enabled: true,
|
||||
comment_requires_registration: false,
|
||||
avatar_source: 'cn.cravatar.com',
|
||||
icp_number: '',
|
||||
police_number: '',
|
||||
lazyload_enabled: true,
|
||||
@@ -149,12 +132,6 @@ const basic = reactive({
|
||||
comment_pagination_mode: 'numbered',
|
||||
})
|
||||
|
||||
const avatarSourceOptions = [
|
||||
{ label: 'Cravatar (国内推荐)', value: 'cn.cravatar.com' },
|
||||
{ label: 'Gravatar 国内镜像', value: 'cn.gravatar.com' },
|
||||
{ label: 'Gravatar 官方', value: 'gravatar.com' },
|
||||
]
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -163,7 +140,6 @@ async function loadAll() {
|
||||
const d = basicRes.data
|
||||
basic.comment_enabled = d.comment_enabled !== false
|
||||
basic.comment_requires_registration = !!d.comment_requires_registration
|
||||
basic.avatar_source = d.avatar_source || 'cn.cravatar.com'
|
||||
basic.icp_number = d.icp_number || ''
|
||||
basic.police_number = d.police_number || ''
|
||||
basic.lazyload_enabled = d.lazyload_enabled !== false
|
||||
@@ -184,7 +160,6 @@ async function handleSave() {
|
||||
const basicPayload = {
|
||||
comment_enabled: basic.comment_enabled,
|
||||
comment_requires_registration: basic.comment_requires_registration,
|
||||
avatar_source: basic.avatar_source,
|
||||
icp_number: basic.icp_number,
|
||||
police_number: basic.police_number,
|
||||
lazyload_enabled: basic.lazyload_enabled,
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<n-form-item label="允许用户上传头像">
|
||||
<n-switch v-model:value="form.allow_avatar_upload" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">关闭后用户只能使用 Gravatar/Cravatar 自动头像。</span>
|
||||
<span class="aksrt-hint-text">关闭后用户使用项目内置默认头像,且不能自行更换。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
|
||||
@@ -65,13 +65,6 @@
|
||||
<h3>公开资料编辑位</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="170" class="aksrt-form">
|
||||
<n-form-item label="头像编辑入口">
|
||||
<n-switch v-model:value="form.user_avatar_upload" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">开启后,用户中心会显示跳转到 WordPress 原生资料页的头像修改入口。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="背景图编辑位">
|
||||
<n-switch v-model:value="form.user_cover_upload" />
|
||||
<template #feedback>
|
||||
@@ -121,7 +114,6 @@ const homeSummary = reactive({
|
||||
|
||||
const form = reactive({
|
||||
dynamicTabs: ['posts', 'comments', 'favorites'],
|
||||
user_avatar_upload: true,
|
||||
user_cover_upload: true,
|
||||
user_social_links: true,
|
||||
})
|
||||
@@ -160,7 +152,6 @@ async function load() {
|
||||
const permissionData = extractData(permissionRes)
|
||||
|
||||
form.dynamicTabs = parseTabs(featureData.user_center_tabs || 'profile,posts,comments,favorites')
|
||||
form.user_avatar_upload = featureData.user_avatar_upload !== false
|
||||
form.user_cover_upload = featureData.user_cover_upload !== false
|
||||
form.user_social_links = featureData.user_social_links !== false
|
||||
|
||||
@@ -179,7 +170,6 @@ async function handleSave() {
|
||||
try {
|
||||
await api.saveSchemaPage('users', 'features', {
|
||||
user_center_tabs: ['profile', ...form.dynamicTabs].join(','),
|
||||
user_avatar_upload: form.user_avatar_upload,
|
||||
user_cover_upload: form.user_cover_upload,
|
||||
user_social_links: form.user_social_links,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ export const commonSchema = {
|
||||
key: 'global.common',
|
||||
title: '常用功能',
|
||||
icon: 'fas fa-cog',
|
||||
description: '管理站点常用开关、懒加载、头像源与备案信息等基础配置。',
|
||||
description: '管理站点常用开关、懒加载与备案信息等基础配置。',
|
||||
defaults: {
|
||||
comment_enabled: true,
|
||||
lazyload_enabled: true,
|
||||
@@ -17,7 +17,6 @@ export const commonSchema = {
|
||||
lazyload_cover: true,
|
||||
lazyload_other: true,
|
||||
lazyload_widget: true,
|
||||
avatar_source: 'cn.cravatar.com',
|
||||
icp_filing: '',
|
||||
footer_text: '',
|
||||
},
|
||||
@@ -67,25 +66,6 @@ export const commonSchema = {
|
||||
{ key: 'lazyload_widget', label: '小工具图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'avatar',
|
||||
title: '头像服务',
|
||||
icon: 'fas fa-user-circle',
|
||||
fields: [
|
||||
{
|
||||
key: 'avatar_source',
|
||||
label: '头像源',
|
||||
type: 'select',
|
||||
default: 'cn.cravatar.com',
|
||||
options: [
|
||||
{ label: 'Cravatar (推荐)', value: 'cn.cravatar.com' },
|
||||
{ label: 'Gravatar 中国', value: 'cn.gravatar.com' },
|
||||
{ label: 'Gravatar 国际', value: 'gravatar.com' },
|
||||
],
|
||||
help: 'Cravatar 是国内最快的头像服务,推荐使用',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'filing',
|
||||
title: '备案信息',
|
||||
|
||||
@@ -5,14 +5,12 @@ export const featuresSchema = {
|
||||
description: '配置用户中心和个人主页的功能模块。',
|
||||
defaults: {
|
||||
user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth',
|
||||
user_avatar_upload: true,
|
||||
user_cover_upload: true,
|
||||
user_social_links: true,
|
||||
user_desc_std: '这家伙很懒…',
|
||||
user_city_s: true,
|
||||
user_city_type: 'province',
|
||||
user_join_day_my_name: '本站',
|
||||
user_avatar_default: '',
|
||||
user_cover_img: '',
|
||||
user_rewrite_slug: 'user',
|
||||
user_follow_s: true,
|
||||
@@ -27,7 +25,6 @@ export const featuresSchema = {
|
||||
icon: 'fas fa-user-shield',
|
||||
fields: [
|
||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入' },
|
||||
{ key: 'user_avatar_upload', label: '头像上传', type: 'switch', default: true },
|
||||
{ key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
|
||||
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
|
||||
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
|
||||
@@ -45,7 +42,6 @@ export const featuresSchema = {
|
||||
dependsOn: { key: 'user_city_s', value: true },
|
||||
},
|
||||
{ key: 'user_join_day_my_name', label: '站名', type: 'input', default: '本站', help: '用于「已加入 X 天」显示中的站点名称' },
|
||||
{ key: 'user_avatar_default', label: '默认头像', type: 'media', default: '' },
|
||||
{ key: 'user_cover_img', label: '默认封面', type: 'media', default: '' },
|
||||
{ key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' },
|
||||
{ key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true },
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.8 KiB |
@@ -206,6 +206,7 @@
|
||||
var button = document.getElementById(upload.buttonId || 'aksrt-upload-avatar-btn');
|
||||
var input = document.getElementById(upload.inputId || 'aksrt-avatar-file-input');
|
||||
var image = document.getElementById(upload.imageId || 'aksrt-user-avatar-image');
|
||||
var removeButton = document.getElementById(upload.removeButtonId || 'aksrt-remove-avatar-btn');
|
||||
if (!button || !input || button.dataset.aksrtAvatarBound === '1') return;
|
||||
button.dataset.aksrtAvatarBound = '1';
|
||||
|
||||
@@ -246,6 +247,7 @@
|
||||
var url = payload.url || (payload.data && payload.data.url) || '';
|
||||
if (!url) throw payload;
|
||||
if (image) image.src = url;
|
||||
if (removeButton) removeButton.hidden = false;
|
||||
notify(payload.message || upload.successText || '头像已更新', 'success');
|
||||
})
|
||||
.catch(function (error) {
|
||||
@@ -256,6 +258,40 @@
|
||||
input.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
if (removeButton) {
|
||||
var removeLabel = removeButton.querySelector('span');
|
||||
var removeIdleText = upload.removeText || (removeLabel ? removeLabel.textContent : '恢复默认头像');
|
||||
removeButton.addEventListener('click', function () {
|
||||
if (!window.confirm(upload.confirmRemoveText || '确定恢复为默认头像?')) return;
|
||||
removeButton.disabled = true;
|
||||
if (removeLabel) removeLabel.textContent = upload.removingText || '恢复中…';
|
||||
|
||||
fetch(upload.restUrl, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-WP-Nonce': upload.restNonce || '' }
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().then(function (payload) {
|
||||
if (!response.ok || !payload || !payload.success) throw payload || {};
|
||||
return payload;
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (image && payload.url) image.src = payload.url;
|
||||
removeButton.hidden = true;
|
||||
notify(payload.message || upload.removeSuccessText || '已恢复默认头像', 'success');
|
||||
})
|
||||
.catch(function (error) {
|
||||
notify((error && error.message) || upload.errorText || '头像操作失败', 'error');
|
||||
})
|
||||
.finally(function () {
|
||||
removeButton.disabled = false;
|
||||
if (removeLabel) removeLabel.textContent = removeIdleText;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* AKSRT Theme Core Hooks
|
||||
*
|
||||
* 从 functions.php 中抽离的核心功能钩子:
|
||||
* - Gravatar 头像源替换(4层拦截)
|
||||
* - SMTP 邮件配置注入
|
||||
* - 评论开关控制
|
||||
* - Banner CPT 注册
|
||||
|
||||
@@ -20,56 +20,6 @@ add_filter('show_admin_bar', '__return_false');
|
||||
|
||||
add_filter('pre_option_link_manager_enabled', '__return_true');
|
||||
|
||||
// ============================================================
|
||||
// Gravatar 头像源替换(4层拦截:get_avatar_url / pre_get_avatar_data / avatar_defaults / ob_start 兜底)
|
||||
// ============================================================
|
||||
|
||||
function aksrt_get_gravatar_source() {
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
return \AKSRT\Settings\Settings::avatar_source();
|
||||
}
|
||||
|
||||
$source = aksrt_get_setting('avatar_source', 'cn.cravatar.com');
|
||||
$allowed = array('cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com');
|
||||
return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com';
|
||||
}
|
||||
|
||||
function aksrt_replace_gravatar_domain($url) {
|
||||
$source = aksrt_get_gravatar_source();
|
||||
if ($source === 'gravatar.com') return $url;
|
||||
return preg_replace('#^https?://([0-9]+\.)?(secure\.)?gravatar\.com#i', 'https://' . $source, $url);
|
||||
}
|
||||
|
||||
add_filter('get_avatar_url', function ($url) {
|
||||
return aksrt_replace_gravatar_domain($url);
|
||||
});
|
||||
|
||||
add_filter('pre_get_avatar_data', function ($args) {
|
||||
if (!empty($args['url'])) {
|
||||
$args['url'] = aksrt_replace_gravatar_domain($args['url']);
|
||||
}
|
||||
return $args;
|
||||
}, 10, 1);
|
||||
|
||||
add_filter('avatar_defaults', function ($defaults) {
|
||||
foreach ($defaults as &$url) {
|
||||
$url = aksrt_replace_gravatar_domain($url);
|
||||
}
|
||||
unset($url);
|
||||
return $defaults;
|
||||
});
|
||||
|
||||
// 全局输出缓冲兜底(覆盖 JS/API 直接请求的 URL)
|
||||
add_action('init', function () {
|
||||
if (is_admin()) ob_start(function ($html) {
|
||||
return preg_replace_callback(
|
||||
'#https?://([0-9]+\.)?(secure\.)?gravatar\.com#i',
|
||||
function () { return 'https://' . aksrt_get_gravatar_source(); },
|
||||
$html
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SMTP 邮件配置注入
|
||||
// ============================================================
|
||||
|
||||
+133
-13
@@ -37,9 +37,21 @@ if (!function_exists('aksrt_resolve_avatar_user_id')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_get_default_avatar_url')) {
|
||||
/**
|
||||
* Return the bundled avatar used when a user has no custom image.
|
||||
*/
|
||||
function aksrt_get_default_avatar_url(): string {
|
||||
return (string) apply_filters(
|
||||
'aksrt_default_avatar_url',
|
||||
get_theme_file_uri('assets/img/user/default avatar.svg')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_get_custom_avatar_url')) {
|
||||
/**
|
||||
* Return the locally uploaded or OAuth avatar URL.
|
||||
* Return the locally uploaded avatar URL.
|
||||
*/
|
||||
function aksrt_get_custom_avatar_url(int $user_id, int $size = 96): string {
|
||||
if ($user_id <= 0) {
|
||||
@@ -47,15 +59,17 @@ if (!function_exists('aksrt_get_custom_avatar_url')) {
|
||||
}
|
||||
|
||||
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
|
||||
if ($attachment_id > 0) {
|
||||
$image = wp_get_attachment_image_url($attachment_id, [$size, $size]);
|
||||
if (is_string($image) && $image !== '') {
|
||||
return $image;
|
||||
}
|
||||
if ($attachment_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$oauth_avatar = esc_url_raw((string) get_user_meta($user_id, 'aksrt_oauth_avatar', true));
|
||||
return $oauth_avatar;
|
||||
$attachment = get_post($attachment_id);
|
||||
if (!$attachment instanceof WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id || !wp_attachment_is_image($attachment_id)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url($attachment_id, [$size, $size]);
|
||||
return is_string($image) ? $image : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +91,7 @@ if (!function_exists('aksrt_get_avatar_url')) {
|
||||
if ($custom !== '') {
|
||||
return $custom;
|
||||
}
|
||||
return (string) get_avatar_url($subject, array_merge($args, ['size' => $size]));
|
||||
return aksrt_get_default_avatar_url();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +133,115 @@ if (!function_exists('aksrt_get_avatar_html')) {
|
||||
add_filter('get_avatar_data', static function (array $args, $subject): array {
|
||||
$user_id = aksrt_resolve_avatar_user_id($subject);
|
||||
$custom = aksrt_get_custom_avatar_url($user_id, (int) ($args['size'] ?? 96));
|
||||
if ($custom !== '') {
|
||||
$args['url'] = $custom;
|
||||
$args['found_avatar'] = true;
|
||||
}
|
||||
$args['url'] = $custom !== '' ? $custom : aksrt_get_default_avatar_url();
|
||||
$args['found_avatar'] = true;
|
||||
return $args;
|
||||
}, 20, 2);
|
||||
|
||||
add_action('show_user_profile', 'aksrt_render_admin_avatar_field');
|
||||
add_action('edit_user_profile', 'aksrt_render_admin_avatar_field');
|
||||
|
||||
if (!function_exists('aksrt_render_admin_avatar_field')) {
|
||||
/**
|
||||
* Render local avatar controls on the native WordPress user profile screen.
|
||||
*/
|
||||
function aksrt_render_admin_avatar_field(WP_User $user): void {
|
||||
if (!current_user_can('edit_user', $user->ID)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<h2><?php esc_html_e('AKSRT 头像', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('当前头像', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<?php echo aksrt_get_avatar_html($user->ID, 96, ['loading' => 'eager']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<p><input type="file" name="aksrt_avatar_file" accept="image/jpeg,image/png,image/webp,image/gif"></p>
|
||||
<?php if (aksrt_get_custom_avatar_url((int) $user->ID, 96) !== '') : ?>
|
||||
<label><input type="checkbox" name="aksrt_avatar_reset" value="1"> <?php esc_html_e('恢复项目默认头像并删除当前头像文件', 'aksrt'); ?></label>
|
||||
<?php endif; ?>
|
||||
<p class="description"><?php esc_html_e('支持 JPG、PNG、GIF、WebP;大小上限由“权限 → 附件权限”控制。', 'aksrt'); ?></p>
|
||||
<?php wp_nonce_field('aksrt_save_admin_avatar_' . $user->ID, 'aksrt_admin_avatar_nonce'); ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
add_action('personal_options_update', 'aksrt_save_admin_avatar_field');
|
||||
add_action('edit_user_profile_update', 'aksrt_save_admin_avatar_field');
|
||||
|
||||
if (!function_exists('aksrt_save_admin_avatar_field')) {
|
||||
/**
|
||||
* Save local avatar changes from the native WordPress profile screen.
|
||||
*/
|
||||
function aksrt_save_admin_avatar_field(int $user_id): void {
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return;
|
||||
}
|
||||
$nonce = isset($_POST['aksrt_admin_avatar_nonce']) ? sanitize_text_field(wp_unslash($_POST['aksrt_admin_avatar_nonce'])) : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_save_admin_avatar_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$previous_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
|
||||
if (!empty($_POST['aksrt_avatar_reset'])) {
|
||||
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
|
||||
aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
|
||||
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($_FILES['aksrt_avatar_file']['name'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
|
||||
$file = $_FILES['aksrt_avatar_file'];
|
||||
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || (int) ($file['size'] ?? 0) <= 0 || (int) $file['size'] > $max_size) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
$attachment_id = media_handle_upload('aksrt_avatar_file', 0, [], [
|
||||
'test_form' => false,
|
||||
'mimes' => [
|
||||
'jpg|jpeg|jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
],
|
||||
]);
|
||||
if (is_wp_error($attachment_id) || !wp_attachment_is_image((int) $attachment_id)) {
|
||||
if (!is_wp_error($attachment_id)) {
|
||||
wp_delete_attachment((int) $attachment_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$attachment_id = (int) $attachment_id;
|
||||
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_type', 'avatar');
|
||||
update_user_meta($user_id, 'aksrt_avatar_attachment_id', $attachment_id);
|
||||
aksrt_delete_owned_avatar_attachment($previous_id, $user_id);
|
||||
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, (string) wp_get_attachment_image_url($attachment_id, 'thumbnail'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_delete_owned_avatar_attachment')) {
|
||||
/**
|
||||
* Delete an avatar attachment only when it belongs to the target user.
|
||||
*/
|
||||
function aksrt_delete_owned_avatar_attachment(int $attachment_id, int $user_id): bool {
|
||||
$attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
|
||||
if (!$attachment instanceof WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
return (bool) wp_delete_attachment($attachment_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,20 +373,27 @@ if (!function_exists('aksrt_user_center_avatar_upload_config')) {
|
||||
if (!$enabled) {
|
||||
return [];
|
||||
}
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
|
||||
return [
|
||||
'enabled' => true,
|
||||
'restUrl' => rest_url('aksrt/v1/user/avatar'),
|
||||
'restNonce' => wp_create_nonce('wp_rest'),
|
||||
'buttonId' => 'aksrt-upload-avatar-btn',
|
||||
'inputId' => 'aksrt-avatar-file-input',
|
||||
'imageId' => 'aksrt-user-avatar-image',
|
||||
'fieldName' => 'avatar',
|
||||
'maxSize' => 2 * MB_IN_BYTES,
|
||||
'idleText' => __('更换头像', 'aksrt'),
|
||||
'loadingText' => __('上传中…', 'aksrt'),
|
||||
'tooLargeText' => __('图片大小不能超过 2MB', 'aksrt'),
|
||||
'successText' => __('头像已更新', 'aksrt'),
|
||||
'errorText' => __('头像上传失败', 'aksrt'),
|
||||
'enabled' => true,
|
||||
'restUrl' => rest_url('aksrt/v1/user/avatar'),
|
||||
'restNonce' => wp_create_nonce('wp_rest'),
|
||||
'buttonId' => 'aksrt-upload-avatar-btn',
|
||||
'removeButtonId' => 'aksrt-remove-avatar-btn',
|
||||
'inputId' => 'aksrt-avatar-file-input',
|
||||
'imageId' => 'aksrt-user-avatar-image',
|
||||
'fieldName' => 'avatar',
|
||||
'maxSize' => $max_size,
|
||||
'idleText' => __('更换头像', 'aksrt'),
|
||||
'loadingText' => __('上传中…', 'aksrt'),
|
||||
'removeText' => __('恢复默认头像', 'aksrt'),
|
||||
'removingText' => __('恢复中…', 'aksrt'),
|
||||
'confirmRemoveText' => __('确定恢复为默认头像?当前上传的头像文件将被删除。', 'aksrt'),
|
||||
'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
|
||||
'successText' => __('头像已更新', 'aksrt'),
|
||||
'removeSuccessText' => __('已恢复默认头像', 'aksrt'),
|
||||
'errorText' => __('头像操作失败', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,10 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
||||
<button type="button" id="aksrt-upload-avatar-btn" class="btn-ghost btn-sm">
|
||||
<i class="fas fa-camera" aria-hidden="true"></i> <span><?php esc_html_e('更换头像', 'aksrt'); ?></span>
|
||||
</button>
|
||||
<input type="file" id="aksrt-avatar-file-input" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
||||
<input type="file" id="aksrt-avatar-file-input" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
||||
<button type="button" id="aksrt-remove-avatar-btn" class="btn-ghost btn-sm"<?php echo get_user_meta($user_id, 'aksrt_avatar_attachment_id', true) ? '' : ' hidden'; ?>>
|
||||
<i class="fas fa-undo" aria-hidden="true"></i> <span><?php esc_html_e('恢复默认头像', 'aksrt'); ?></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<a href="<?php echo esc_url(aksrt_get_user_home_url()); ?>" class="btn-ghost btn-sm">
|
||||
<i class="fas fa-map-marker-alt" aria-hidden="true"></i> <?php esc_html_e('我的主页', 'aksrt'); ?>
|
||||
|
||||
@@ -371,14 +371,6 @@ class ImportExportController extends ControllerBase {
|
||||
'status' => $backfill_done ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Avatar source
|
||||
$avatar_source = \AKSRT\Settings\Settings::get_all()['avatar_source'] ?? 'cn.cravatar.com';
|
||||
$checks['avatar_source'] = [
|
||||
'label' => '头像服务',
|
||||
'value' => $avatar_source,
|
||||
'status' => $avatar_source === 'cn.cravatar.com' ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Option count
|
||||
$option_count = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
|
||||
|
||||
@@ -26,13 +26,6 @@ class GlobalSchema {
|
||||
['key' => 'performance', 'title' => '性能优化', 'fields' => [
|
||||
['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'avatar', 'title' => '头像服务', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'filing', 'title' => '备案信息', 'fields' => [
|
||||
['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''],
|
||||
@@ -203,13 +196,6 @@ class GlobalSchema {
|
||||
['label' => '样式 10', 'value' => 'no10'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'avatar', 'title' => '头像设置', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
], 'help' => '国内站点建议优先选 Cravatar'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'code-highlight' => [
|
||||
|
||||
@@ -516,7 +516,6 @@ class SchemaUsers {
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入'],
|
||||
['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
@@ -527,7 +526,6 @@ class SchemaUsers {
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
||||
['key' => 'user_avatar_default', 'label' => '默认头像', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
|
||||
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
|
||||
|
||||
@@ -52,9 +52,16 @@ class UserController extends ControllerBase {
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/avatar', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
[
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
]);
|
||||
|
||||
// Comment inline image upload (logged-in users)
|
||||
|
||||
@@ -19,6 +19,23 @@ trait UserControllerProfileMediaOperations {
|
||||
);
|
||||
}
|
||||
|
||||
public function rest_delete_user_avatar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
|
||||
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
|
||||
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
|
||||
$this->delete_owned_profile_attachment($attachment_id, $user_id);
|
||||
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
|
||||
|
||||
return $this->success([
|
||||
'message' => __('已恢复默认头像', 'aksrt'),
|
||||
'url' => function_exists('aksrt_get_default_avatar_url') ? aksrt_get_default_avatar_url() : '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_upload_user_background(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->upload_profile_image(
|
||||
['background', 'file'],
|
||||
@@ -58,15 +75,39 @@ trait UserControllerProfileMediaOperations {
|
||||
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$file = $_FILES[$file_key];
|
||||
if (!empty($file['size']) && (int) $file['size'] > 2 * MB_IN_BYTES) {
|
||||
return $this->error(__('图片大小不能超过 2MB', 'aksrt'), 413);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$file = $_FILES[$file_key];
|
||||
$upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
|
||||
if ($upload_error !== UPLOAD_ERR_OK) {
|
||||
return $this->error(__('头像文件上传失败,请重新选择', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2)));
|
||||
$max_size = $max_size_mb * MB_IN_BYTES;
|
||||
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
|
||||
if ($actual_size <= 0 || $actual_size > $max_size) {
|
||||
return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413);
|
||||
}
|
||||
|
||||
$checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], [
|
||||
'jpg|jpeg|jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
]);
|
||||
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type'])) {
|
||||
return $this->error(__('仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$image_size = wp_getimagesize($file['tmp_name']);
|
||||
if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
|
||||
return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$attachment_id = media_handle_upload($file_key, 0, [], [
|
||||
'test_form' => false,
|
||||
'mimes' => [
|
||||
@@ -81,6 +122,14 @@ trait UserControllerProfileMediaOperations {
|
||||
}
|
||||
|
||||
$attachment_id = (int) $attachment_id;
|
||||
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
|
||||
if (!wp_attachment_is_image($attachment_id)) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
return $this->error(__('上传文件不是有效图片', 'aksrt'), 415);
|
||||
}
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_type', $is_avatar ? 'avatar' : 'background');
|
||||
|
||||
$url = wp_get_attachment_image_url($attachment_id, $is_avatar ? 'thumbnail' : 'full');
|
||||
if (!$url) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
@@ -94,10 +143,11 @@ trait UserControllerProfileMediaOperations {
|
||||
}
|
||||
|
||||
if ($previous_id > 0 && $previous_id !== $attachment_id) {
|
||||
$previous = get_post($previous_id);
|
||||
if ($previous instanceof \WP_Post && (int) $previous->post_author === $user_id) {
|
||||
wp_delete_attachment($previous_id, true);
|
||||
}
|
||||
$this->delete_owned_profile_attachment($previous_id, $user_id);
|
||||
}
|
||||
|
||||
if ($is_avatar) {
|
||||
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, $url);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
@@ -109,4 +159,22 @@ trait UserControllerProfileMediaOperations {
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function delete_owned_profile_attachment(int $attachment_id, int $user_id): bool {
|
||||
if ($attachment_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attachment = get_post($attachment_id);
|
||||
if (!$attachment instanceof \WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$managed_user_id = (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true);
|
||||
if ($managed_user_id > 0 && $managed_user_id !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) wp_delete_attachment($attachment_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ trait DisplayPageGroups {
|
||||
'logo_text' => self::logo_text(),
|
||||
'logo_width' => self::logo_width(),
|
||||
'site_favicon' => self::favicon_url(),
|
||||
'avatar_source' => self::avatar_source(),
|
||||
'icp_filing' => self::icp_filing(),
|
||||
'footer_text' => self::footer_text(),
|
||||
'theme_mode' => self::theme_mode(),
|
||||
|
||||
@@ -502,15 +502,6 @@ trait FacadeBulk {
|
||||
|
||||
|
||||
|
||||
/** 获取头像镜像源 */
|
||||
public static function avatar_source(): string {
|
||||
$source = sanitize_text_field((string) self::get_value('avatar_source', 'cn.cravatar.com'));
|
||||
$allowed = ['cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com'];
|
||||
return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 获取SEO关键词 */
|
||||
public static function seo_keywords(): string {
|
||||
$s = self::get_all();
|
||||
|
||||
@@ -132,7 +132,7 @@ class UserRuntime {
|
||||
|
||||
return [
|
||||
'tabs' => $normalized_tabs,
|
||||
'avatar_upload' => !isset($settings['user_avatar_upload']) || !empty($settings['user_avatar_upload']),
|
||||
'avatar_upload' => !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']),
|
||||
'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']),
|
||||
'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user