feat(links): move friend links to native WordPress links with review flow

- enable the native Links manager and drop the custom friend-link option/REST path
- add per-page link settings for the friends template
- render link categories from get_bookmarks() with search and page-content options
- add a validated public link submission endpoint with logo upload, captcha, rate limiting and duplicate checks
- create pending links for review and notify administrators by email and in-app message
- add native Links admin pending count, columns and edit notes
- cover the new submission and cleanup contracts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-05 19:54:33 +08:00
1 parent ef7b5939ba
commit 4c562428b4
24 files changed
+792 -273

No files matched your search

-5
View File
@@ -12,11 +12,6 @@ export const globalApiMethods = {
getFloatingButtons() { return this.request('/settings/floating-buttons') },
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
getFriendLinks() { return this.request('/friend-links') },
addFriendLink(data) { return this.request('/friend-links', { method: 'POST', body: JSON.stringify(data) }) },
deleteFriendLink(id) { return this.request(`/friend-links/${id}`, { method: 'DELETE' }) },
reorderFriendLinks(ids) { return this.request('/friend-links/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
getCmsFeatures() { return this.request('/settings/cms-features') },
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
}
@@ -87,6 +87,7 @@ export const permissionsSchemas = {
{ label: '打赏用户', value: 'tip_user' },
{ label: '提交工单', value: 'ticket_create' },
{ label: '回复工单', value: 'ticket_reply' },
{ label: '友情链接申请', value: 'friend_link' },
],
},
],
-74
View File
@@ -1,74 +0,0 @@
/**
* Jio Friends Page — 友情链接页样式
*
* @package Jio
*/
.friends-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-4);
margin-top: var(--space-6);
}
.friend-card {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--card-padding);
background: var(--surface-1);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
text-decoration: none;
color: var(--color-ink-2);
transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.friend-card:hover {
box-shadow: var(--shadow);
border-color: var(--color-primary-soft);
transform: none;
}
.friend-card__avatar {
width: 48px;
height: 48px;
border-radius: var(--radius-avatar, 50%);
object-fit: cover;
flex-shrink: 0;
background: var(--surface-2);
}
.friend-card__info {
flex: 1;
min-width: 0;
}
.friend-card__name {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-ink);
margin: 0 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.friend-card__desc {
font-size: var(--text-sm);
color: var(--color-ink-3);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: var(--leading-snug);
}
/* Responsive */
@media (max-width: 640px) {
.friends-grid {
grid-template-columns: 1fr;
}
}
+11
View File
@@ -148,6 +148,17 @@
margin-bottom: 6px;
}
.jio-links-submit-status {
margin: 10px 0;
font-size: 0.92em;
}
.jio-links-logo-preview {
width: 88px;
height: 88px;
object-fit: cover;
}
/* ---- search cover ---- */
.link-page-search-cover.page-cover {
position: relative;
+87
View File
@@ -0,0 +1,87 @@
/**
* Native link submission form.
*
* @package Jio
*/
(function () {
'use strict';
var cfg = window.jioLinksSubmit || {};
var i18n = cfg.i18n || {};
var form = document.querySelector('.jio-links-submit-form');
if (!form) return;
var button = form.querySelector('.jio-links-submit-btn');
var status = form.querySelector('.jio-links-submit-status');
var logoInput = form.querySelector('.jio-links-logo-input');
var preview = form.querySelector('.jio-links-logo-preview');
function setStatus(message, type) {
if (!status) return;
status.textContent = message || '';
status.className = 'jio-links-submit-status' + (type ? ' text-' + type : '');
}
if (logoInput && preview) {
logoInput.addEventListener('change', function () {
var file = logoInput.files && logoInput.files[0];
if (!file) return;
preview.src = URL.createObjectURL(file);
});
}
form.addEventListener('submit', function (event) {
event.preventDefault();
if (button && button.disabled) return;
var payload = new FormData(form);
var original = button ? button.innerHTML : '';
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fa fas fa-spinner fa-spin"></i> ' + (i18n.submitting || '正在提交…');
}
setStatus(i18n.submitting || '正在提交…', 'muted');
var captcha = window.JioCaptcha;
var captchaPromise = captcha ? captcha.run('friend_link', form) : Promise.resolve({});
captchaPromise.then(function (tokens) {
if (tokens === null) throw new Error(i18n.captcha || '请完成人机验证');
Object.keys(tokens || {}).forEach(function (key) {
payload.set(key, tokens[key]);
});
return fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', {
method: 'POST',
credentials: 'same-origin',
body: payload,
});
}).then(function (response) {
return response.json().then(function (body) {
return { ok: response.ok, body: body };
});
}).then(function (result) {
var body = result.body || {};
var data = body.data || {};
if (!result.ok || !body.success) {
throw new Error(data.message || i18n.error || '提交失败,请重试');
}
setStatus(data.message || i18n.success || '提交成功', 'success');
form.reset();
if (preview) preview.src = '';
if (button) button.disabled = false;
if (button) button.innerHTML = original;
if (captcha) captcha.reset('friend_link', form);
window.setTimeout(function () {
var modal = document.getElementById('submit-links-modal');
if (modal) modal.classList.remove('show', 'is-open');
setStatus('', '');
}, 1800);
}).catch(function (error) {
setStatus(error.message || i18n.error || '提交失败,请重试', 'danger');
if (button) {
button.disabled = false;
button.innerHTML = original;
}
if (captcha) captcha.reset('friend_link', form);
});
});
})();