261 lines
8.2 KiB
JavaScript
261 lines
8.2 KiB
JavaScript
/**
|
||
* AKSRT Admin AI SEO — SEO generate + summary → excerpt
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.aksrtAiSeo) return;
|
||
|
||
var CFG = window.aksrtAiSeo;
|
||
var REST = (CFG.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
|
||
var NONCE = CFG.nonce || '';
|
||
var I18N = CFG.i18n || {};
|
||
|
||
function notify($btn, msg, type) {
|
||
var wrap = $btn.closest('.aksrt-seo-meta-box, td, .form-field, p, .aksrt-ai-summary-row') || $btn.parentNode;
|
||
if (!wrap) return;
|
||
var old = wrap.querySelector('.aksrt-ai-seo-msg');
|
||
if (old) old.remove();
|
||
var el = document.createElement('span');
|
||
el.className = 'aksrt-ai-seo-msg ' + (type === 'error' ? 'is-error' : 'is-ok');
|
||
el.textContent = msg;
|
||
wrap.appendChild(el);
|
||
setTimeout(function () {
|
||
if (el.parentNode) el.parentNode.removeChild(el);
|
||
}, 8000);
|
||
}
|
||
|
||
function fillPost(data) {
|
||
var map = {
|
||
title: 'aksrt_seo_title',
|
||
keywords: 'aksrt_seo_keywords',
|
||
description: 'aksrt_seo_description',
|
||
};
|
||
Object.keys(map).forEach(function (key) {
|
||
var field = data[key] || {};
|
||
if (!field.content) return;
|
||
var input = document.getElementById(map[key]) || document.querySelector('[name="' + map[key] + '"]');
|
||
if (input) input.value = field.content;
|
||
});
|
||
}
|
||
|
||
function fillTerm(data) {
|
||
var map = {
|
||
title: '.aksrt-term-seo-title, #aksrt_seo_title',
|
||
keywords: '.aksrt-term-seo-keywords, #aksrt_seo_keywords',
|
||
description: '.aksrt-term-seo-description, #aksrt_seo_description',
|
||
};
|
||
Object.keys(map).forEach(function (key) {
|
||
var field = data[key] || {};
|
||
if (!field.content) return;
|
||
var input = document.querySelector(map[key]);
|
||
if (input) input.value = field.content;
|
||
});
|
||
}
|
||
|
||
function collectErrors(data) {
|
||
var errors = [];
|
||
['title', 'keywords', 'description'].forEach(function (key) {
|
||
if (data[key] && data[key].error) errors.push(data[key].error);
|
||
});
|
||
return errors;
|
||
}
|
||
|
||
function getEditorTitle() {
|
||
var titleInput = document.getElementById('title');
|
||
if (titleInput && titleInput.value) return titleInput.value.trim();
|
||
// Block editor
|
||
try {
|
||
if (window.wp && wp.data && wp.data.select) {
|
||
var ed = wp.data.select('core/editor');
|
||
if (ed && ed.getEditedPostAttribute) {
|
||
return String(ed.getEditedPostAttribute('title') || '').trim();
|
||
}
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
return '';
|
||
}
|
||
|
||
function getEditorContent() {
|
||
var content = '';
|
||
var ta = document.getElementById('content');
|
||
if (ta && ta.value) content = ta.value;
|
||
try {
|
||
if (window.wp && wp.data && wp.data.select) {
|
||
var ed = wp.data.select('core/editor');
|
||
if (ed && ed.getEditedPostContent) {
|
||
var blockContent = ed.getEditedPostContent();
|
||
if (blockContent) content = blockContent;
|
||
}
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
// Strip tags for length check / API
|
||
var div = document.createElement('div');
|
||
div.innerHTML = content || '';
|
||
return (div.textContent || div.innerText || content || '').trim();
|
||
}
|
||
|
||
function setExcerptValue(summary) {
|
||
if (!summary) return false;
|
||
|
||
// Classic: #excerpt
|
||
var classic = document.getElementById('excerpt');
|
||
if (classic) {
|
||
classic.value = summary;
|
||
classic.dispatchEvent(new Event('input', { bubbles: true }));
|
||
classic.dispatchEvent(new Event('change', { bubbles: true }));
|
||
return true;
|
||
}
|
||
|
||
// Block editor excerpt attribute
|
||
try {
|
||
if (window.wp && wp.data && wp.data.dispatch) {
|
||
var d = wp.data.dispatch('core/editor');
|
||
if (d && d.editPost) {
|
||
d.editPost({ excerpt: summary });
|
||
return true;
|
||
}
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
|
||
// Fallback name selector
|
||
var named = document.querySelector('textarea[name="excerpt"]');
|
||
if (named) {
|
||
named.value = summary;
|
||
named.dispatchEvent(new Event('input', { bubbles: true }));
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function setLoading(btn, on, original) {
|
||
if (on) {
|
||
btn.classList.add('is-loading');
|
||
btn.textContent = I18N.generating || '生成中…';
|
||
} else {
|
||
btn.classList.remove('is-loading');
|
||
btn.textContent = original;
|
||
}
|
||
}
|
||
|
||
document.addEventListener('click', function (e) {
|
||
// SEO generate
|
||
var seoBtn = e.target.closest('.aksrt-ai-seo-generate');
|
||
if (seoBtn) {
|
||
e.preventDefault();
|
||
if (seoBtn.classList.contains('is-loading') || seoBtn.disabled) return;
|
||
|
||
var type = seoBtn.getAttribute('data-type') || 'post';
|
||
var id = parseInt(seoBtn.getAttribute('data-id') || '0', 10);
|
||
if (!id) {
|
||
notify(seoBtn, I18N.needSave || '请先保存后再生成 SEO', 'error');
|
||
return;
|
||
}
|
||
if (CFG.ai && CFG.ai.available === false) {
|
||
notify(seoBtn, I18N.notAvailable || 'AI 未配置或未启用', 'error');
|
||
return;
|
||
}
|
||
|
||
var seoOriginal = seoBtn.textContent;
|
||
setLoading(seoBtn, true, seoOriginal);
|
||
|
||
fetch(REST + '/ai/seo-generate', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
},
|
||
body: JSON.stringify({ type: type, id: id }),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (json) {
|
||
return { ok: r.ok, json: json };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
setLoading(seoBtn, false, seoOriginal);
|
||
if (!res.ok || !res.json || res.json.success === false) {
|
||
var msg =
|
||
(res.json && (res.json.message || (res.json.data && res.json.data.message))) ||
|
||
I18N.failed ||
|
||
'生成失败';
|
||
notify(seoBtn, msg, 'error');
|
||
return;
|
||
}
|
||
var data = res.json.data || {};
|
||
if (type === 'term') fillTerm(data);
|
||
else fillPost(data);
|
||
var errors = collectErrors(data);
|
||
if (errors.length) notify(seoBtn, errors.join(';'), 'error');
|
||
else notify(seoBtn, I18N.success || '已填入 SEO 字段,请检查后更新保存', 'ok');
|
||
})
|
||
.catch(function () {
|
||
setLoading(seoBtn, false, seoOriginal);
|
||
notify(seoBtn, I18N.failed || '生成失败', 'error');
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Summary → excerpt
|
||
var sumBtn = e.target.closest('.aksrt-ai-summary-generate');
|
||
if (!sumBtn) return;
|
||
e.preventDefault();
|
||
if (sumBtn.classList.contains('is-loading') || sumBtn.disabled) return;
|
||
|
||
if (CFG.ai && CFG.ai.available === false) {
|
||
notify(sumBtn, I18N.notAvailable || 'AI 未配置或未启用', 'error');
|
||
return;
|
||
}
|
||
|
||
var content = getEditorContent();
|
||
if (!content) {
|
||
notify(sumBtn, I18N.needContent || '请先填写正文内容', 'error');
|
||
return;
|
||
}
|
||
|
||
var sumOriginal = sumBtn.textContent;
|
||
setLoading(sumBtn, true, sumOriginal);
|
||
|
||
fetch(REST + '/ai/summary-suggest', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
},
|
||
body: JSON.stringify({
|
||
title: getEditorTitle(),
|
||
content: content,
|
||
max_length: 200,
|
||
}),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (json) {
|
||
return { ok: r.ok, json: json };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
setLoading(sumBtn, false, sumOriginal);
|
||
if (!res.ok || !res.json || res.json.success === false) {
|
||
var msg =
|
||
(res.json && (res.json.message || (res.json.data && res.json.data.message))) ||
|
||
I18N.failed ||
|
||
'生成失败';
|
||
notify(sumBtn, msg, 'error');
|
||
return;
|
||
}
|
||
var summary = (res.json.data && res.json.data.summary) || '';
|
||
if (!setExcerptValue(summary)) {
|
||
notify(sumBtn, I18N.noExcerptField || '未找到摘要字段,请展开「摘要」面板', 'error');
|
||
return;
|
||
}
|
||
notify(sumBtn, I18N.summarySuccess || '已填入摘要字段,请检查后更新保存', 'ok');
|
||
})
|
||
.catch(function () {
|
||
setLoading(sumBtn, false, sumOriginal);
|
||
notify(sumBtn, I18N.failed || '生成失败', 'error');
|
||
});
|
||
});
|
||
})();
|