Merge pull request 'Worktree user avatar system' (#323) from worktree-user-avatar-system into main

Reviewed-on: CYL/wordpress-theme-aksrt#323
This commit is contained in:
kate522 committed 2026-07-29 13:50:00 +08:00
commit cf62eee95d
20 files changed
+341 -186

No files matched your search

-25
View File
@@ -85,22 +85,6 @@
</n-form> </n-form>
</n-card> </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. 备案 --> <!-- 5. 备案 -->
<n-card class="aksrt-section-card"> <n-card class="aksrt-section-card">
<div class="aksrt-section-header"> <div class="aksrt-section-header">
@@ -140,7 +124,6 @@ const saving = ref(false)
const basic = reactive({ const basic = reactive({
comment_enabled: true, comment_enabled: true,
comment_requires_registration: false, comment_requires_registration: false,
avatar_source: 'cn.cravatar.com',
icp_number: '', icp_number: '',
police_number: '', police_number: '',
lazyload_enabled: true, lazyload_enabled: true,
@@ -149,12 +132,6 @@ const basic = reactive({
comment_pagination_mode: 'numbered', 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() { async function loadAll() {
loading.value = true loading.value = true
try { try {
@@ -163,7 +140,6 @@ async function loadAll() {
const d = basicRes.data const d = basicRes.data
basic.comment_enabled = d.comment_enabled !== false basic.comment_enabled = d.comment_enabled !== false
basic.comment_requires_registration = !!d.comment_requires_registration basic.comment_requires_registration = !!d.comment_requires_registration
basic.avatar_source = d.avatar_source || 'cn.cravatar.com'
basic.icp_number = d.icp_number || '' basic.icp_number = d.icp_number || ''
basic.police_number = d.police_number || '' basic.police_number = d.police_number || ''
basic.lazyload_enabled = d.lazyload_enabled !== false basic.lazyload_enabled = d.lazyload_enabled !== false
@@ -184,7 +160,6 @@ async function handleSave() {
const basicPayload = { const basicPayload = {
comment_enabled: basic.comment_enabled, comment_enabled: basic.comment_enabled,
comment_requires_registration: basic.comment_requires_registration, comment_requires_registration: basic.comment_requires_registration,
avatar_source: basic.avatar_source,
icp_number: basic.icp_number, icp_number: basic.icp_number,
police_number: basic.police_number, police_number: basic.police_number,
lazyload_enabled: basic.lazyload_enabled, lazyload_enabled: basic.lazyload_enabled,
@@ -40,7 +40,7 @@
<n-form-item label="允许用户上传头像"> <n-form-item label="允许用户上传头像">
<n-switch v-model:value="form.allow_avatar_upload" /> <n-switch v-model:value="form.allow_avatar_upload" />
<template #feedback> <template #feedback>
<span class="aksrt-hint-text">关闭后用户只能使用 Gravatar/Cravatar 自动头像。</span> <span class="aksrt-hint-text">关闭后用户使用项目内置默认头像,且不能自行更换。</span>
</template> </template>
</n-form-item> </n-form-item>
@@ -65,13 +65,6 @@
<h3>公开资料编辑位</h3> <h3>公开资料编辑位</h3>
</div> </div>
<n-form :model="form" label-placement="left" label-width="170" class="aksrt-form"> <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-form-item label="背景图编辑位">
<n-switch v-model:value="form.user_cover_upload" /> <n-switch v-model:value="form.user_cover_upload" />
<template #feedback> <template #feedback>
@@ -121,7 +114,6 @@ const homeSummary = reactive({
const form = reactive({ const form = reactive({
dynamicTabs: ['posts', 'comments', 'favorites'], dynamicTabs: ['posts', 'comments', 'favorites'],
user_avatar_upload: true,
user_cover_upload: true, user_cover_upload: true,
user_social_links: true, user_social_links: true,
}) })
@@ -160,7 +152,6 @@ async function load() {
const permissionData = extractData(permissionRes) const permissionData = extractData(permissionRes)
form.dynamicTabs = parseTabs(featureData.user_center_tabs || 'profile,posts,comments,favorites') 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_cover_upload = featureData.user_cover_upload !== false
form.user_social_links = featureData.user_social_links !== false form.user_social_links = featureData.user_social_links !== false
@@ -179,7 +170,6 @@ async function handleSave() {
try { try {
await api.saveSchemaPage('users', 'features', { await api.saveSchemaPage('users', 'features', {
user_center_tabs: ['profile', ...form.dynamicTabs].join(','), user_center_tabs: ['profile', ...form.dynamicTabs].join(','),
user_avatar_upload: form.user_avatar_upload,
user_cover_upload: form.user_cover_upload, user_cover_upload: form.user_cover_upload,
user_social_links: form.user_social_links, user_social_links: form.user_social_links,
}) })
@@ -2,7 +2,7 @@ export const commonSchema = {
key: 'global.common', key: 'global.common',
title: '常用功能', title: '常用功能',
icon: 'fas fa-cog', icon: 'fas fa-cog',
description: '管理站点常用开关、懒加载、头像源与备案信息等基础配置。', description: '管理站点常用开关、懒加载与备案信息等基础配置。',
defaults: { defaults: {
comment_enabled: true, comment_enabled: true,
lazyload_enabled: true, lazyload_enabled: true,
@@ -17,7 +17,6 @@ export const commonSchema = {
lazyload_cover: true, lazyload_cover: true,
lazyload_other: true, lazyload_other: true,
lazyload_widget: true, lazyload_widget: true,
avatar_source: 'cn.cravatar.com',
icp_filing: '', icp_filing: '',
footer_text: '', footer_text: '',
}, },
@@ -67,25 +66,6 @@ export const commonSchema = {
{ key: 'lazyload_widget', label: '小工具图片懒加载', type: 'switch', default: true, dependsOn: { key: 'lazyload_enabled', value: true } }, { 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', key: 'filing',
title: '备案信息', title: '备案信息',
@@ -5,14 +5,12 @@ export const featuresSchema = {
description: '配置用户中心和个人主页的功能模块。', description: '配置用户中心和个人主页的功能模块。',
defaults: { defaults: {
user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth', user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth',
user_avatar_upload: true,
user_cover_upload: true, user_cover_upload: true,
user_social_links: true, user_social_links: true,
user_desc_std: '这家伙很懒…', user_desc_std: '这家伙很懒…',
user_city_s: true, user_city_s: true,
user_city_type: 'province', user_city_type: 'province',
user_join_day_my_name: '本站', user_join_day_my_name: '本站',
user_avatar_default: '',
user_cover_img: '', user_cover_img: '',
user_rewrite_slug: 'user', user_rewrite_slug: 'user',
user_follow_s: true, user_follow_s: true,
@@ -27,7 +25,6 @@ export const featuresSchema = {
icon: 'fas fa-user-shield', icon: 'fas fa-user-shield',
fields: [ fields: [
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入' }, { 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_cover_upload', label: '封面上传', type: 'switch', default: true },
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true }, { key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' }, { key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
@@ -45,7 +42,6 @@ export const featuresSchema = {
dependsOn: { key: 'user_city_s', value: true }, dependsOn: { key: 'user_city_s', value: true },
}, },
{ key: 'user_join_day_my_name', label: '站名', type: 'input', default: '本站', help: '用于「已加入 X 天」显示中的站点名称' }, { 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_cover_img', label: '默认封面', type: 'media', default: '' },
{ key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' }, { key: 'user_rewrite_slug', label: '用户页 slug', type: 'input', default: 'user' },
{ key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true }, { key: 'user_follow_s', label: '启用关注功能', type: 'switch', default: true },
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1254" height="1254">
<path d="M0 0 C413.82 0 827.64 0 1254 0 C1254 413.82 1254 827.64 1254 1254 C840.18 1254 426.36 1254 0 1254 C0 840.18 0 426.36 0 0 Z " fill="#F1F2F5" transform="translate(0,0)"/>
<path d="M0 0 C1.5863324 -0.00009064 1.5863324 -0.00009064 3.20471191 -0.00018311 C23.04532582 0.01582279 42.77737599 0.73576134 62.5625 2.1875 C63.61695313 2.26226563 64.67140625 2.33703125 65.7578125 2.4140625 C90.68303424 4.24779816 115.79754853 8.34376739 139.5625 16.1875 C141.13908315 16.6856553 142.71592688 17.18298631 144.29296875 17.6796875 C180.31346866 29.17718115 213.73844591 46.91584897 243.75927734 69.81591797 C245.3600076 71.03347857 246.97034998 72.23837896 248.58203125 73.44140625 C249.40058594 74.05886719 250.21914063 74.67632813 251.0625 75.3125 C251.7534375 75.83070313 252.444375 76.34890625 253.15625 76.8828125 C253.6203125 77.31335938 254.084375 77.74390625 254.5625 78.1875 C254.5625 78.8475 254.5625 79.5075 254.5625 80.1875 C255.3978125 80.55875 255.3978125 80.55875 256.25 80.9375 C258.98660913 82.41674818 261.23900869 84.12217439 263.5625 86.1875 C263.5625 86.8475 263.5625 87.5075 263.5625 88.1875 C264.09359375 88.39375 264.6246875 88.6 265.171875 88.8125 C268.57023436 90.76711192 271.07305928 93.40873717 273.8125 96.1875 C274.64394531 97.00734375 274.64394531 97.00734375 275.4921875 97.84375 C279.5625 101.9341133 279.5625 101.9341133 279.5625 104.1875 C280.2225 104.1875 280.8825 104.1875 281.5625 104.1875 C282.95849396 105.71587269 284.27837649 107.31400099 285.5625 108.9375 C286.36429688 109.94167969 287.16609375 110.94585938 287.9921875 111.98046875 C288.41338867 112.50946777 288.83458984 113.0384668 289.26855469 113.58349609 C291.36163499 116.17812583 293.5311623 118.70789546 295.6875 121.25 C304.21589206 131.50499904 311.19009217 142.70367214 318.21435547 154.01660156 C319.30486106 155.77262758 320.40318352 157.52379774 321.50390625 159.2734375 C350.92350748 207.62083036 361.71909927 263.84375211 362.41796875 319.68359375 C362.42656363 320.34984433 362.43515851 321.01609492 362.44401383 321.70253491 C362.52466518 328.01719121 362.59503956 334.33187626 362.63788891 340.64691067 C362.6925728 348.62094495 362.80218785 356.59015191 362.98412818 364.5623278 C363.10909298 370.23515007 363.1721137 375.90667708 363.18992692 381.58083588 C363.20210325 384.94723337 363.24843203 388.30402849 363.34587669 391.66967583 C363.90069452 411.91868005 363.90069452 411.91868005 360.00995827 417.35796928 C356.32093492 420.91106856 352.1894697 423.22808583 347.53113174 425.28785324 C344.6550164 426.60221173 342.0681414 428.315513 339.42578125 430.04296875 C337.57843854 431.15849199 335.72820132 432.26923689 333.875 433.375 C331.89695592 434.56610821 329.91909002 435.75751238 327.94140625 436.94921875 C326.54946045 437.78525635 326.54946045 437.78525635 325.12939453 438.63818359 C321.83797921 440.6248035 318.57500184 442.65378312 315.3125 444.6875 C309.8284471 448.06175535 304.25087169 451.17279942 298.5625 454.1875 C297.16388441 454.95019434 295.76546378 455.71324627 294.3671875 456.4765625 C270.24049193 469.51501339 245.05467412 480.36694684 219.43017578 490.08349609 C216.79884508 491.09651137 214.19495177 492.1601069 211.58984375 493.23828125 C205.70030526 495.60961726 199.63937338 497.3661427 193.5625 499.1875 C191.52363154 499.82720829 189.48584577 500.47037866 187.44921875 501.1171875 C139.89590439 516.09937369 91.22210278 524.56609571 41.5625 528.1875 C40.32918945 528.28321289 39.09587891 528.37892578 37.82519531 528.47753906 C-51.85911966 534.8786452 -145.13035892 519.12469667 -343.6343689 424.9197998 C-346.38711149 423.08873389 -346.38711149 423.08873389 -349.51960754 422.05914307 C-353.32989128 420.37780483 -356.22681239 418.74305847 -358.4375 415.1875 C-360.80658314 406.5483052 -359.90005362 397.05717227 -359.71484375 388.1953125 C-359.69365063 385.48893329 -359.68971858 382.78259264 -359.68495178 380.07614136 C-359.66562461 374.39442005 -359.60205535 368.71463899 -359.50756836 363.03369141 C-359.3878181 355.82605893 -359.33463758 348.61970225 -359.30564022 341.41119576 C-359.28033746 335.13345451 -359.22872829 328.85596388 -359.1705277 322.57845068 C-359.1526524 320.60439827 -359.13704295 318.63032576 -359.12147331 316.65625381 C-358.49962605 251.1443523 -340.47494106 186.44848395 -301.4375 133.1875 C-300.99647949 132.58405762 -300.55545898 131.98061523 -300.10107422 131.35888672 C-294.63759961 123.90541674 -289.1745564 116.55767643 -282.86328125 109.7890625 C-281.34951936 108.2287919 -281.34951936 108.2287919 -280.4375 106.1875 C-279.7775 106.1875 -279.1175 106.1875 -278.4375 106.1875 C-277.9425 104.7025 -277.9425 104.7025 -277.4375 103.1875 C-276.7775 103.1875 -276.1175 103.1875 -275.4375 103.1875 C-275.210625 102.63578125 -274.98375 102.0840625 -274.75 101.515625 C-273.05024074 98.50057583 -270.88945184 96.37519631 -268.4375 93.9375 C-267.550625 93.04546875 -266.66375 92.1534375 -265.75 91.234375 C-263.4375 89.1875 -263.4375 89.1875 -261.4375 89.1875 C-260.9425 87.7025 -260.9425 87.7025 -260.4375 86.1875 C-258.546875 84.5390625 -258.546875 84.5390625 -256.1875 82.8125 C-253.59396869 80.90651558 -251.28210383 79.03185439 -249.00390625 76.75 C-235.30509263 63.07203693 -217.22880218 52.583Line truncated
<path d="M0 0 C1.16943146 -0.00151062 1.16943146 -0.00151062 2.36248779 -0.00305176 C14.54798574 0.00758541 26.24285039 0.67827742 38.1875 3.25 C38.90421875 3.39453613 39.6209375 3.53907227 40.359375 3.68798828 C71.75731628 10.06216566 101.92323942 23.85882167 126.71875 44.2265625 C128.47015891 45.66205271 130.24347916 47.07099479 132.03125 48.4609375 C132.9490625 49.17507813 133.866875 49.88921875 134.8125 50.625 C135.65039062 51.27210938 136.48828125 51.91921875 137.3515625 52.5859375 C137.95742188 53.13507813 138.56328125 53.68421875 139.1875 54.25 C139.1875 54.91 139.1875 55.57 139.1875 56.25 C140.1775 56.58 141.1675 56.91 142.1875 57.25 C142.1875 57.91 142.1875 58.57 142.1875 59.25 C143.5075 59.91 144.8275 60.57 146.1875 61.25 C146.1875 61.91 146.1875 62.57 146.1875 63.25 C146.8475 63.25 147.5075 63.25 148.1875 63.25 C158.59503469 73.03663064 166.77109336 86.12584673 174.1875 98.25 C175.18910156 99.86261719 175.18910156 99.86261719 176.2109375 101.5078125 C184.52517668 115.40869461 190.99302056 130.6077055 195.1875 146.25 C195.59918875 147.67478926 196.01203356 149.09924478 196.42578125 150.5234375 C204.88000403 180.21166752 206.93623558 213.95901176 200.1875 244.25 C200.01057617 245.09191895 199.83365234 245.93383789 199.65136719 246.80126953 C194.02380326 273.38883624 184.45476599 297.671564 169.1875 320.25 C168.79095215 320.84329102 168.3944043 321.43658203 167.98583984 322.04785156 C163.13166249 329.26056699 157.84615417 336.02493093 152.1875 342.625 C151.62313232 343.28636963 151.05876465 343.94773926 150.47729492 344.62915039 C149.09356424 346.21106892 147.64582106 347.73656797 146.1875 349.25 C145.5275 349.25 144.8675 349.25 144.1875 349.25 C143.97867187 349.79269531 143.76984375 350.33539062 143.5546875 350.89453125 C141.67097659 354.13989605 139.06760334 356.44543271 136.3125 358.9375 C135.44129517 359.73087769 135.44129517 359.73087769 134.55249023 360.5402832 C91.69786263 399.04371473 36.77258113 413.96412945 -19.8125 411.25 C-67.09468754 407.54733138 -110.97922379 384.67306671 -144.8125 352.25 C-145.56402344 351.53972656 -146.31554687 350.82945312 -147.08984375 350.09765625 C-156.58944474 340.89903272 -164.42810042 330.17836235 -171.8125 319.25 C-172.25883789 318.59177246 -172.70517578 317.93354492 -173.16503906 317.25537109 C-181.57317595 304.67304543 -187.44327758 291.36639837 -192.8125 277.25 C-193.22604736 276.17322998 -193.22604736 276.17322998 -193.64794922 275.07470703 C-212.82738675 224.29646627 -208.34937592 166.71144538 -186.58105469 117.57397461 C-179.51308563 102.37975657 -170.12784216 88.41246419 -159.8125 75.25 C-159.27753906 74.48558594 -158.74257813 73.72117187 -158.19140625 72.93359375 C-157.73636719 72.37800781 -157.28132813 71.82242188 -156.8125 71.25 C-156.1525 71.25 -155.4925 71.25 -154.8125 71.25 C-154.585625 70.71375 -154.35875 70.1775 -154.125 69.625 C-151.09249042 64.13760172 -146.80618168 60.22029226 -142.08203125 56.21484375 C-140.50429566 54.8489212 -138.96188054 53.44156002 -137.44921875 52.00390625 C-115.84824505 31.51739856 -88.26744615 16.652174 -59.8125 8.25 C-58.62785156 7.89292969 -57.44320312 7.53585937 -56.22265625 7.16796875 C-46.21725668 4.31691039 -36.11134494 2.63618664 -25.8125 1.25 C-25.15902588 1.15283691 -24.50555176 1.05567383 -23.83227539 0.95556641 C-15.93477775 -0.0976854 -7.9531192 0.00712138 0 0 Z " fill="#C0C4CF" transform="translate(627.8125,289.75)"/>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

+36
View File
@@ -206,6 +206,7 @@
var button = document.getElementById(upload.buttonId || 'aksrt-upload-avatar-btn'); var button = document.getElementById(upload.buttonId || 'aksrt-upload-avatar-btn');
var input = document.getElementById(upload.inputId || 'aksrt-avatar-file-input'); var input = document.getElementById(upload.inputId || 'aksrt-avatar-file-input');
var image = document.getElementById(upload.imageId || 'aksrt-user-avatar-image'); 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; if (!button || !input || button.dataset.aksrtAvatarBound === '1') return;
button.dataset.aksrtAvatarBound = '1'; button.dataset.aksrtAvatarBound = '1';
@@ -246,6 +247,7 @@
var url = payload.url || (payload.data && payload.data.url) || ''; var url = payload.url || (payload.data && payload.data.url) || '';
if (!url) throw payload; if (!url) throw payload;
if (image) image.src = url; if (image) image.src = url;
if (removeButton) removeButton.hidden = false;
notify(payload.message || upload.successText || '头像已更新', 'success'); notify(payload.message || upload.successText || '头像已更新', 'success');
}) })
.catch(function (error) { .catch(function (error) {
@@ -256,6 +258,40 @@
input.value = ''; 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() { function init() {
-1
View File
@@ -3,7 +3,6 @@
* AKSRT Theme Core Hooks * AKSRT Theme Core Hooks
* *
* 从 functions.php 中抽离的核心功能钩子: * 从 functions.php 中抽离的核心功能钩子:
* - Gravatar 头像源替换(4层拦截)
* - SMTP 邮件配置注入 * - SMTP 邮件配置注入
* - 评论开关控制 * - 评论开关控制
* - Banner CPT 注册 * - Banner CPT 注册
-50
View File
@@ -20,56 +20,6 @@ add_filter('show_admin_bar', '__return_false');
add_filter('pre_option_link_manager_enabled', '__return_true'); 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 邮件配置注入 // SMTP 邮件配置注入
// ============================================================ // ============================================================
+185 -13
View File
@@ -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')) { 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 { function aksrt_get_custom_avatar_url(int $user_id, int $size = 96): string {
if ($user_id <= 0) { 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); $attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
if ($attachment_id > 0) { if ($attachment_id <= 0) {
$image = wp_get_attachment_image_url($attachment_id, [$size, $size]); return '';
if (is_string($image) && $image !== '') {
return $image;
}
} }
$oauth_avatar = esc_url_raw((string) get_user_meta($user_id, 'aksrt_oauth_avatar', true)); $attachment = get_post($attachment_id);
return $oauth_avatar; 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 !== '') { if ($custom !== '') {
return $custom; return $custom;
} }
return (string) get_avatar_url($subject, array_merge($args, ['size' => $size])); return aksrt_get_default_avatar_url();
} }
} }
@@ -119,9 +133,167 @@ if (!function_exists('aksrt_get_avatar_html')) {
add_filter('get_avatar_data', static function (array $args, $subject): array { add_filter('get_avatar_data', static function (array $args, $subject): array {
$user_id = aksrt_resolve_avatar_user_id($subject); $user_id = aksrt_resolve_avatar_user_id($subject);
$custom = aksrt_get_custom_avatar_url($user_id, (int) ($args['size'] ?? 96)); $custom = aksrt_get_custom_avatar_url($user_id, (int) ($args['size'] ?? 96));
if ($custom !== '') { $args['url'] = $custom !== '' ? $custom : aksrt_get_default_avatar_url();
$args['url'] = $custom; $args['found_avatar'] = true;
$args['found_avatar'] = true;
}
return $args; return $args;
}, 20, 2); }, 20, 2);
add_action('show_user_profile', 'aksrt_render_admin_avatar_field');
add_action('edit_user_profile', 'aksrt_render_admin_avatar_field');
add_action('user_edit_form_tag', static function (): void {
echo ' enctype="multipart/form-data"';
});
if (!function_exists('aksrt_is_avatar_upload_enabled')) {
/**
* Whether local avatar uploads are enabled by attachment policy.
*/
function aksrt_is_avatar_upload_enabled(): bool {
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']);
}
}
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) || !aksrt_is_avatar_upload_enabled()) {
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_filter('user_profile_update_errors', static function (WP_Error $errors, bool $update, WP_User $user): void {
if (empty($_FILES['aksrt_avatar_file']['name']) || !aksrt_is_avatar_upload_enabled()) {
return;
}
$file = $_FILES['aksrt_avatar_file'];
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2)));
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $actual_size <= 0 || $actual_size > $max_size_mb * MB_IN_BYTES) {
$errors->add('aksrt_avatar_size', sprintf(__('头像文件无效或超过 %dMB', 'aksrt'), $max_size_mb));
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$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',
]);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
$errors->add('aksrt_avatar_type', __('头像必须是有效的 JPG、PNG、GIF 或 WebP 图片,且总像素不能超过 4000 万。', 'aksrt'));
}
}, 10, 3);
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) || !aksrt_is_avatar_upload_enabled()) {
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'];
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || $actual_size <= 0 || $actual_size > $max_size) {
return;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$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',
]);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
return;
}
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);
}
}
+20 -13
View File
@@ -373,20 +373,27 @@ if (!function_exists('aksrt_user_center_avatar_upload_config')) {
if (!$enabled) { if (!$enabled) {
return []; 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 [ return [
'enabled' => true, 'enabled' => true,
'restUrl' => rest_url('aksrt/v1/user/avatar'), 'restUrl' => rest_url('aksrt/v1/user/avatar'),
'restNonce' => wp_create_nonce('wp_rest'), 'restNonce' => wp_create_nonce('wp_rest'),
'buttonId' => 'aksrt-upload-avatar-btn', 'buttonId' => 'aksrt-upload-avatar-btn',
'inputId' => 'aksrt-avatar-file-input', 'removeButtonId' => 'aksrt-remove-avatar-btn',
'imageId' => 'aksrt-user-avatar-image', 'inputId' => 'aksrt-avatar-file-input',
'fieldName' => 'avatar', 'imageId' => 'aksrt-user-avatar-image',
'maxSize' => 2 * MB_IN_BYTES, 'fieldName' => 'avatar',
'idleText' => __('更换头像', 'aksrt'), 'maxSize' => $max_size,
'loadingText' => __('上传中…', 'aksrt'), 'idleText' => __('更换头像', 'aksrt'),
'tooLargeText' => __('图片大小不能超过 2MB', 'aksrt'), 'loadingText' => __('上传中…', 'aksrt'),
'successText' => __('头像已更新', 'aksrt'), 'removeText' => __('恢复默认头像', 'aksrt'),
'errorText' => __('头像上传失败', '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"> <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> <i class="fas fa-camera" aria-hidden="true"></i> <span><?php esc_html_e('更换头像', 'aksrt'); ?></span>
</button> </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; ?> <?php endif; ?>
<a href="<?php echo esc_url(aksrt_get_user_home_url()); ?>" class="btn-ghost btn-sm"> <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'); ?> <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', '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
$option_count = (int) $wpdb->get_var( $option_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'" "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
-14
View File
@@ -26,13 +26,6 @@ class GlobalSchema {
['key' => 'performance', 'title' => '性能优化', 'fields' => [ ['key' => 'performance', 'title' => '性能优化', 'fields' => [
['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true], ['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' => 'filing', 'title' => '备案信息', 'fields' => [
['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''], ['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''],
['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''], ['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''],
@@ -203,13 +196,6 @@ class GlobalSchema {
['label' => '样式 10', 'value' => 'no10'], ['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' => [ 'code-highlight' => [
-2
View File
@@ -516,7 +516,6 @@ class SchemaUsers {
'groups' => [ 'groups' => [
['key' => 'center', 'title' => '用户中心', 'fields' => [ ['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_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_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true], ['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'], ['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
@@ -527,7 +526,6 @@ class SchemaUsers {
['label' => '国家+省份', 'value' => 'country_province'], ['label' => '国家+省份', 'value' => 'country_province'],
]], ]],
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'], ['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_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'], ['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true], ['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
+10 -3
View File
@@ -52,9 +52,16 @@ class UserController extends ControllerBase {
'permission_callback' => function () { return is_user_logged_in(); }, 'permission_callback' => function () { return is_user_logged_in(); },
]); ]);
register_rest_route(self::API_NAMESPACE, '/user/avatar', [ register_rest_route(self::API_NAMESPACE, '/user/avatar', [
'methods' => 'POST', [
'callback' => [$this, 'rest_upload_user_avatar'], 'methods' => 'POST',
'permission_callback' => function () { return is_user_logged_in(); }, '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) // 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 { public function rest_upload_user_background(\WP_REST_Request $request): \WP_REST_Response {
return $this->upload_profile_image( return $this->upload_profile_image(
['background', 'file'], ['background', 'file'],
@@ -58,15 +75,39 @@ trait UserControllerProfileMediaOperations {
return $this->error(__('请选择要上传的图片', 'aksrt'), 400); 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/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.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, [], [ $attachment_id = media_handle_upload($file_key, 0, [], [
'test_form' => false, 'test_form' => false,
'mimes' => [ 'mimes' => [
@@ -81,6 +122,14 @@ trait UserControllerProfileMediaOperations {
} }
$attachment_id = (int) $attachment_id; $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'); $url = wp_get_attachment_image_url($attachment_id, $is_avatar ? 'thumbnail' : 'full');
if (!$url) { if (!$url) {
wp_delete_attachment($attachment_id, true); wp_delete_attachment($attachment_id, true);
@@ -94,10 +143,11 @@ trait UserControllerProfileMediaOperations {
} }
if ($previous_id > 0 && $previous_id !== $attachment_id) { if ($previous_id > 0 && $previous_id !== $attachment_id) {
$previous = get_post($previous_id); $this->delete_owned_profile_attachment($previous_id, $user_id);
if ($previous instanceof \WP_Post && (int) $previous->post_author === $user_id) { }
wp_delete_attachment($previous_id, true);
} if ($is_avatar) {
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, $url);
} }
return $this->success([ 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);
}
} }
-1
View File
@@ -98,7 +98,6 @@ trait DisplayPageGroups {
'logo_text' => self::logo_text(), 'logo_text' => self::logo_text(),
'logo_width' => self::logo_width(), 'logo_width' => self::logo_width(),
'site_favicon' => self::favicon_url(), 'site_favicon' => self::favicon_url(),
'avatar_source' => self::avatar_source(),
'icp_filing' => self::icp_filing(), 'icp_filing' => self::icp_filing(),
'footer_text' => self::footer_text(), 'footer_text' => self::footer_text(),
'theme_mode' => self::theme_mode(), 'theme_mode' => self::theme_mode(),
-9
View File
@@ -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关键词 */ /** 获取SEO关键词 */
public static function seo_keywords(): string { public static function seo_keywords(): string {
$s = self::get_all(); $s = self::get_all();
+1 -1
View File
@@ -132,7 +132,7 @@ class UserRuntime {
return [ return [
'tabs' => $normalized_tabs, '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']), 'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']),
'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']), 'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']),
]; ];