Compare commits
21
Commits
a3b708f9d4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5810693e01 | ||
|
|
0722c4f00e | ||
|
|
4f2edebab6 | ||
|
|
ce6fbb4e39 | ||
|
|
cf39e72126 | ||
|
|
740bd48f42 | ||
|
|
18e579a8c6 | ||
|
|
cdab7a5e5f | ||
|
|
bd01199930 | ||
|
|
7ed739615e | ||
|
|
60e4d67772 | ||
|
|
7287cfeaec | ||
|
|
f03d72c880 | ||
|
|
6e90a96a15 | ||
|
|
b368678662 | ||
|
|
40b9a3f36b | ||
|
|
6fd80be243 | ||
|
|
855e85e7db | ||
|
|
7e7bf6db23 | ||
|
|
6f815678ec | ||
|
|
8ff74f56ee |
@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
|
|||||||
|
|
||||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||||
|
|
||||||
/* AI 辅助 */
|
|
||||||
getAiStatus() { return this.request('/ai/status') },
|
|
||||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
|
||||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
|
||||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
|
||||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
import { renderFaIcon, schemaPage } from './helpers'
|
||||||
|
|
||||||
export const extensionsSection = {
|
export const extensionsSection = {
|
||||||
key: 'extensions',
|
key: 'extensions',
|
||||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
|||||||
label: '百度推送',
|
label: '百度推送',
|
||||||
route: '/extensions/push',
|
route: '/extensions/push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
@@ -56,15 +56,5 @@ export const extensionsSection = {
|
|||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'security',
|
section: 'security',
|
||||||
}),
|
}),
|
||||||
customPage({
|
|
||||||
key: 'extensions-ai',
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
label: 'AI 连接配置',
|
|
||||||
route: '/extensions/ai',
|
|
||||||
component: 'ExtensionAI',
|
|
||||||
status: 'available',
|
|
||||||
title: 'AI 连接配置',
|
|
||||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'sms',
|
section: 'sms',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'users-wechat',
|
|
||||||
name: 'UsersWechat',
|
|
||||||
label: '微信通知',
|
|
||||||
route: '/users/wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'wechat',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-wechat-template',
|
key: 'users-wechat-template',
|
||||||
name: 'UsersWechatTemplate',
|
name: 'UsersWechatTemplate',
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
|
||||||
|
|
||||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
|
||||||
<strong>加载失败:</strong>{{ loadError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__status">
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
|
||||||
<i class="fa fab fa-android"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">可用状态</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-plug"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-key"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
|
||||||
<i class="fa fas fa-road"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">当前后端</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>主题 Provider 配置(回退链路)</h3>
|
|
||||||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>AI Provider</label>
|
|
||||||
<select v-model="form.provider" class="aksrt-select">
|
|
||||||
<option value="none">不使用</option>
|
|
||||||
<option value="openai">OpenAI 兼容接口</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>API Key</label>
|
|
||||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
|
||||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
|
||||||
<label>API Base URL</label>
|
|
||||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
|
|
||||||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>模型</label>
|
|
||||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>最大 Token 数</label>
|
|
||||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>每分钟请求限制</label>
|
|
||||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
|
||||||
{{ saving ? '保存中...' : '保存设置' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>已接通能力</h3>
|
|
||||||
<table class="aksrt-table aksrt-table--sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>能力</th>
|
|
||||||
<th>入口</th>
|
|
||||||
<th>状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="feat in features" :key="feat.key">
|
|
||||||
<td>{{ feat.name }}</td>
|
|
||||||
<td>{{ feat.scene }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
|
||||||
{{ feat.ready ? '可用' : '未就绪' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
|
||||||
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
|
|
||||||
<ul>
|
|
||||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
|
||||||
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
|
||||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
|
||||||
import PageHeader from '@/components/PageHeader.vue';
|
|
||||||
import api from '@/api';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
components: { PageHeader },
|
|
||||||
setup() {
|
|
||||||
const status = ref({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
configured: false,
|
|
||||||
model: '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: 10,
|
|
||||||
available: false,
|
|
||||||
backend: 'none',
|
|
||||||
wp_ai_available: false,
|
|
||||||
theme_available: false,
|
|
||||||
seo_ai_enabled: false,
|
|
||||||
write_assist: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
api_key: '',
|
|
||||||
api_base: 'https://api.openai.com/v1',
|
|
||||||
model: 'gpt-3.5-turbo',
|
|
||||||
max_tokens: 1000,
|
|
||||||
rate_limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const saving = ref(false);
|
|
||||||
const loadError = ref('');
|
|
||||||
|
|
||||||
const backendLabel = computed(() => {
|
|
||||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
|
||||||
return map[status.value.backend] || status.value.backend || '无';
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = computed(() => {
|
|
||||||
const avail = !!status.value.available;
|
|
||||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
|
||||||
return [
|
|
||||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
|
||||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
|
||||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function hydrateFromStatus(data) {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
status.value = {
|
|
||||||
enabled: !!data.enabled,
|
|
||||||
provider: data.provider || 'none',
|
|
||||||
configured: !!data.configured,
|
|
||||||
model: data.model || '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: data.rate_limit || 10,
|
|
||||||
available: !!data.available,
|
|
||||||
backend: data.backend || 'none',
|
|
||||||
wp_ai_available: !!data.wp_ai_available,
|
|
||||||
theme_available: !!data.theme_available,
|
|
||||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
|
||||||
write_assist: !!data.write_assist,
|
|
||||||
};
|
|
||||||
form.enabled = !!data.enabled;
|
|
||||||
form.provider = data.provider || 'none';
|
|
||||||
form.model = data.model || 'gpt-3.5-turbo';
|
|
||||||
form.rate_limit = data.rate_limit || 10;
|
|
||||||
if (data.api_base) form.api_base = data.api_base;
|
|
||||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
|
||||||
form.max_tokens = data.max_tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatus() {
|
|
||||||
loadError.value = '';
|
|
||||||
try {
|
|
||||||
const json = await api.getAiStatus();
|
|
||||||
hydrateFromStatus(json.data || {});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Load AI status failed:', e);
|
|
||||||
loadError.value = e?.message || '加载 AI 状态失败';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettings() {
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
enabled: form.enabled,
|
|
||||||
provider: form.provider,
|
|
||||||
api_base: form.api_base,
|
|
||||||
model: form.model,
|
|
||||||
max_tokens: form.max_tokens,
|
|
||||||
rate_limit: form.rate_limit,
|
|
||||||
};
|
|
||||||
// Only send api_key when the user entered a new one.
|
|
||||||
if (form.api_key && form.api_key !== '***') {
|
|
||||||
payload.api_key = form.api_key;
|
|
||||||
}
|
|
||||||
const json = await api.updateAiSettings(payload);
|
|
||||||
alert(json.message || '设置已保存');
|
|
||||||
await loadStatus();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Save failed:', e);
|
|
||||||
alert(e?.message || '保存失败');
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadStatus());
|
|
||||||
|
|
||||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-ai__status {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
|
||||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
|
|
||||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
|
||||||
.aksrt-ai__form-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
|
||||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
||||||
.aksrt-ai__load-error {
|
|
||||||
background: #fdecea;
|
|
||||||
border-color: #f5c2c0;
|
|
||||||
color: #842029;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
|
||||||
.aksrt-ai__notice h3 { color: #e65100; }
|
|
||||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
|
||||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
|
||||||
.aksrt-ai__notice code {
|
|
||||||
background: rgba(0,0,0,0.06);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
|
||||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
||||||
.aksrt-form-group { margin-bottom: 14px; }
|
|
||||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
|
||||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
|
||||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
|
||||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
|
||||||
</style>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const extensionsPageLoaders = {
|
|
||||||
ExtensionAI: () => import('@/pages/extensions/ExtensionAI.vue'),
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { corePageLoaders } from './core'
|
import { corePageLoaders } from './core'
|
||||||
import { usersPageLoaders } from './users'
|
import { usersPageLoaders } from './users'
|
||||||
import { toolsPageLoaders } from './tools'
|
import { toolsPageLoaders } from './tools'
|
||||||
import { extensionsPageLoaders } from './extensions'
|
|
||||||
|
|
||||||
export const componentLoaders = {
|
export const componentLoaders = {
|
||||||
...corePageLoaders,
|
...corePageLoaders,
|
||||||
...usersPageLoaders,
|
...usersPageLoaders,
|
||||||
...toolsPageLoaders,
|
...toolsPageLoaders,
|
||||||
...extensionsPageLoaders,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
|||||||
key: 'extensions.push',
|
key: 'extensions.push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
icon: 'fa fas fa-paper-plane',
|
icon: 'fa fas fa-paper-plane',
|
||||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
description: '配置百度主动推送、收录通道和推送状态。',
|
||||||
defaults: {
|
defaults: {
|
||||||
baidu_push_enabled: false,
|
baidu_push_enabled: false,
|
||||||
baidu_push_token: '',
|
baidu_push_token: '',
|
||||||
baidu_push_auto: true,
|
baidu_push_auto: true,
|
||||||
baidu_push_on_publish: true,
|
baidu_push_on_publish: true,
|
||||||
baidu_push_on_update: false,
|
baidu_push_on_update: false,
|
||||||
baidu_push_batch_size: 50,
|
baidu_push_normal: true,
|
||||||
|
baidu_push_fast: false,
|
||||||
baidu_push_log_enabled: true,
|
baidu_push_log_enabled: true,
|
||||||
xzh_on: false,
|
|
||||||
xzh_appid: '',
|
|
||||||
xzh_render_tail: true,
|
|
||||||
xzh_jsonld_single: true,
|
|
||||||
xzh_jsonld_page: false,
|
|
||||||
xzh_jsonld_img: false,
|
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-rocket',
|
icon: 'fa fas fa-rocket',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
{ key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' },
|
||||||
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' },
|
{ key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,21 +29,17 @@ export const pushSchema = {
|
|||||||
icon: 'fa fas fa-bolt',
|
icon: 'fa fas fa-bolt',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
{ key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' },
|
||||||
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' },
|
||||||
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'xzh',
|
key: 'channel',
|
||||||
title: '熊掌号 / JSON-LD',
|
title: '收录通道',
|
||||||
icon: 'fa fas fa-paw',
|
icon: 'fa fas fa-inbox',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||||
{ key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
{ key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
|||||||
title: '高级设置',
|
title: '高级设置',
|
||||||
icon: 'fa fas fa-cog',
|
icon: 'fa fas fa-cog',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const seoSchema = {
|
|||||||
key: 'global.seo',
|
key: 'global.seo',
|
||||||
title: 'SEO 优化',
|
title: 'SEO 优化',
|
||||||
icon: 'fa fas fa-search',
|
icon: 'fa fas fa-search',
|
||||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
|
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
|
||||||
defaults: {
|
defaults: {
|
||||||
seo_enabled: true,
|
seo_enabled: true,
|
||||||
seo_ai_enabled: false,
|
seo_ai_enabled: false,
|
||||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
|||||||
ai_seo_title_prompt: '',
|
ai_seo_title_prompt: '',
|
||||||
ai_seo_keywords_prompt: '',
|
ai_seo_keywords_prompt: '',
|
||||||
ai_seo_description_prompt: '',
|
ai_seo_description_prompt: '',
|
||||||
seo_baidu_push: false,
|
|
||||||
seo_baidu_normal: false,
|
|
||||||
seo_baidu_fast: false,
|
|
||||||
seo_baidu_token: '',
|
|
||||||
seo_image_optimize: true,
|
seo_image_optimize: true,
|
||||||
seo_image_default: '',
|
seo_image_default: '',
|
||||||
seo_image_always_default: false,
|
seo_image_always_default: false,
|
||||||
@@ -48,7 +44,7 @@ export const seoSchema = {
|
|||||||
label: '启用 AI SEO 生成',
|
label: '启用 AI SEO 生成',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
default: false,
|
default: false,
|
||||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
|
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
|
||||||
dependsOn: { key: 'seo_enabled', value: true },
|
dependsOn: { key: 'seo_enabled', value: true },
|
||||||
},
|
},
|
||||||
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
{ key: 'seo_connector', label: '标题连接符', type: 'input', default: '-', placeholder: '例如:- 或 |', help: 'AI 生成标题时会用此连接符追加站点名。' },
|
||||||
@@ -95,17 +91,6 @@ export const seoSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'baidu',
|
|
||||||
title: '百度推送',
|
|
||||||
icon: 'fa fas fa-paper-plane',
|
|
||||||
fields: [
|
|
||||||
{ key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false },
|
|
||||||
{ key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'image',
|
key: 'image',
|
||||||
title: '图片 SEO',
|
title: '图片 SEO',
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { featuresSchema } from './features.js'
|
|||||||
import { verifySchema } from './verify.js'
|
import { verifySchema } from './verify.js'
|
||||||
import { levelSchema } from './level.js'
|
import { levelSchema } from './level.js'
|
||||||
import { badgeSchema } from './badge.js'
|
import { badgeSchema } from './badge.js'
|
||||||
import { wechatSchema } from './wechat.js'
|
|
||||||
import { reportSchema } from './report.js'
|
import { reportSchema } from './report.js'
|
||||||
import { smsSchema } from './sms.js'
|
import { smsSchema } from './sms.js'
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export const usersSchemas = {
|
|||||||
verify: verifySchema,
|
verify: verifySchema,
|
||||||
level: levelSchema,
|
level: levelSchema,
|
||||||
badge: badgeSchema,
|
badge: badgeSchema,
|
||||||
wechat: wechatSchema,
|
|
||||||
report: reportSchema,
|
report: reportSchema,
|
||||||
sms: smsSchema,
|
sms: smsSchema,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
export const wechatSchema = {
|
|
||||||
key: 'users.wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
description: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
defaults: {
|
|
||||||
wechat_notify_enabled: false,
|
|
||||||
wechat_app_id: '',
|
|
||||||
wechat_app_secret: '',
|
|
||||||
wechat_template_id: '',
|
|
||||||
wechat_token: '',
|
|
||||||
wechat_encoding_aes_key: '',
|
|
||||||
wechat_notify_comment: true,
|
|
||||||
wechat_notify_reply: true,
|
|
||||||
wechat_notify_system: true,
|
|
||||||
wechat_bind_entry: true,
|
|
||||||
},
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
key: 'account',
|
|
||||||
title: '公众号配置',
|
|
||||||
icon: 'fa fab fa-weixin',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_enabled', label: '启用微信通知', type: 'switch', default: false, help: '开启后用户可绑定微信接收通知' },
|
|
||||||
{ key: 'wechat_app_id', label: 'AppID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_app_secret', label: 'AppSecret', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_template_id', label: '模板消息 ID', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在微信公众平台申请的模板消息 ID' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'verify',
|
|
||||||
title: '服务器验证',
|
|
||||||
icon: 'fa fas fa-check-circle',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_token', label: 'Token', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '微信公众平台服务器配置中的 Token' },
|
|
||||||
{ key: 'wechat_encoding_aes_key', label: 'EncodingAESKey', type: 'input', default: '', dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'triggers',
|
|
||||||
title: '通知触发器',
|
|
||||||
icon: 'fa fas fa-bell',
|
|
||||||
fields: [
|
|
||||||
{ key: 'wechat_notify_comment', label: '新评论通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_reply', label: '评论回复通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_notify_system', label: '系统通知', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true } },
|
|
||||||
{ key: 'wechat_bind_entry', label: '显示绑定入口', type: 'switch', default: true, dependsOn: { key: 'wechat_notify_enabled', value: true }, help: '在用户中心显示微信绑定/解绑按钮' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
+57
-347
@@ -916,6 +916,7 @@ strong, b {
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +962,8 @@ strong, b {
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
@@ -2705,31 +2708,28 @@ main.container,
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups, so
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* only the account row carries its own alignment context; the button groups
|
||||||
display: flex;
|
* inherit their geometry from .navbar-top .navbar-but .but and .navbar-form. */
|
||||||
align-items: center;
|
.navbar-top .aksrt-navbar-account {
|
||||||
gap: 10px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions > *,
|
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
min-height: 50px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -2742,39 +2742,6 @@ main.container,
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -2807,33 +2774,22 @@ main.container,
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
.navbar-top .navbar-form .toggle-radius {
|
||||||
flex: 0 0 29px;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-language {
|
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -2869,85 +2825,41 @@ main.container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -2967,222 +2879,20 @@ main.container,
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* source: assets/css/frontend/card-primitives.css */
|
/* source: assets/css/frontend/card-primitives.css */
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -137,6 +137,7 @@
|
|||||||
|
|
||||||
/* === Full width === */
|
/* === Full width === */
|
||||||
.btn-block {
|
.btn-block {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +183,8 @@
|
|||||||
.but > .fa:not(.fa-angle-right),
|
.but > .fa:not(.fa-angle-right),
|
||||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||||
|
|
||||||
|
.but.block { display: block; }
|
||||||
|
|
||||||
/* hover / active */
|
/* hover / active */
|
||||||
.but:focus,
|
.but:focus,
|
||||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* Header account popover.
|
* Header account popover.
|
||||||
*
|
*
|
||||||
* The shared legacy navigation styles provide the popover shell. This module
|
* The shared legacy navigation styles provide the popover shell, and every
|
||||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
* account row (avatar, VIP bar, check-in, wallet tiles, stats badges) renders
|
||||||
|
* through the reference legacy classes. This module only owns the navigation
|
||||||
|
* shell anchoring plus the account action-row geometry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* The desktop action cluster owns one horizontal alignment context. */
|
/* The reference keeps the right cluster as three floated .navbar-form groups, so
|
||||||
.navbar-top .aksrt-navbar-actions {
|
* only the account row carries its own alignment context; the button groups
|
||||||
display: flex;
|
* inherit their geometry from .navbar-top .navbar-but .but and .navbar-form. */
|
||||||
align-items: center;
|
.navbar-top .aksrt-navbar-account {
|
||||||
gap: 10px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions > *,
|
|
||||||
.navbar-top .aksrt-navbar-account,
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
gap: 4px;
|
||||||
|
min-height: 50px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account > li {
|
.navbar-top .aksrt-navbar-account > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -38,39 +35,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account {
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-account.navbar-text {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-height: 29px;
|
|
||||||
margin: 0;
|
|
||||||
padding: .3em 1em !important;
|
|
||||||
border-radius: var(--radius-btn, 4px);
|
|
||||||
font-size: .8em;
|
|
||||||
line-height: 1.44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-nav-icon,
|
.navbar-top .aksrt-nav-icon,
|
||||||
.navbar-top .aksrt-nav-avatar {
|
.navbar-top .aksrt-nav-avatar {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
@@ -103,33 +67,22 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
.navbar-top .navbar-form .toggle-radius {
|
||||||
flex: 0 0 29px;
|
flex: 0 0 29px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-language {
|
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||||
* the absolute panel's static position beside the compact CTA. */
|
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||||
position: relative;
|
* would move that static position to the flex container's cross-start. */
|
||||||
align-self: stretch;
|
.navbar-top .navbar-but > .hover-show {
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
width: max-content;
|
|
||||||
max-width: calc(100vw - 24px);
|
|
||||||
margin: 0;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||||
@@ -165,85 +118,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox {
|
.aksrt-header-userbox {
|
||||||
width: 260px;
|
|
||||||
color: var(--color-ink-2);
|
color: var(--color-ink-2);
|
||||||
|
line-height: 1.42857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info {
|
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||||
min-height: 44px;
|
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||||
padding: 2px;
|
.aksrt-header-userbox .muted-box.padding-6 {
|
||||||
|
background-color: var(--this-bg, var(--color-line-soft));
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .avatar {
|
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||||
flex: 0 0 44px;
|
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||||
width: 44px;
|
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||||
height: 44px;
|
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||||
object-fit: cover;
|
* declarations are the last word in the cascade. Re-assert them here, which
|
||||||
|
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
|
||||||
|
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
|
||||||
|
.payvip-icon {
|
||||||
|
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
|
||||||
|
color: #ece0e0;
|
||||||
|
--this-color: #ece0e0;
|
||||||
|
border: none;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right,
|
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||||
.aksrt-header-userbox .user-right > .flex1 {
|
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||||
min-width: 0;
|
display: block;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
.aksrt-header-userbox .user-right b {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .display-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-right .img-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-user-level-icon,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-header-user-medal,
|
|
||||||
.aksrt-header-userbox .user-right .aksrt-identity-certification-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .user-info > .abs-right {
|
|
||||||
top: 50%;
|
|
||||||
right: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .aksrt-notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4px;
|
|
||||||
right: -4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border: 2px solid var(--surface-1);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e74c3c;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||||
.aksrt-nav-notification .aksrt-notification-badge {
|
.aksrt-nav-notification .aksrt-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@@ -263,220 +172,18 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-header-wallet {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet:hover {
|
|
||||||
color: var(--color-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item {
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item i {
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-wallet__item b {
|
|
||||||
margin-left: 2px;
|
|
||||||
color: var(--color-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: #fff6db !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: #b78103 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-checkin:hover {
|
|
||||||
background: #fff0c2 !important;
|
|
||||||
color: #8a5a00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
|
|
||||||
* 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
|
|
||||||
.aksrt-header-checkin:disabled,
|
|
||||||
.aksrt-header-checkin:disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
opacity: .85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but {
|
|
||||||
display: inline-flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
min-height: 36px;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 8px 10px !important;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
background: linear-gradient(90deg, #ff6b6b, #f04494) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .payvip-icon > i,
|
|
||||||
.aksrt-header-payvip:not(.is-vip) .but > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__cta:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
opacity: .92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
padding: 7px 10px !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__level > i {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-payvip__exp {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
box-shadow: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: .85em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
opacity: .9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation links are globally capped at 50px. Account actions are taller
|
/* Navigation links are globally capped at 50px. Account actions are taller
|
||||||
* icon tiles and must opt out or their labels overlap the following row. */
|
* icon tiles and must opt out or their labels overlap the following row.
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
* The tile geometry itself stays with the shared legacy rules
|
||||||
align-items: flex-start;
|
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||||
gap: 4px;
|
* so the action row keeps the reference proportions. */
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
.navbar-top .aksrt-header-userbox .header-user-href > a,
|
||||||
.aksrt-header-userbox .header-user-href > a {
|
.aksrt-header-userbox .header-user-href > a {
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 10px 2px 2px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a:hover {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex: 0 0 36px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href > a > div:last-child {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-navbar .aksrt-header-userbox {
|
.mobile-navbar .aksrt-header-userbox {
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin-right: auto;
|
margin: auto;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin {
|
|
||||||
background: rgba(255, 193, 7, .18) !important;
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-checkin > i {
|
|
||||||
color: #f0c14b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet {
|
|
||||||
background: rgba(255, 255, 255, .06);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .aksrt-header-wallet__item,
|
|
||||||
body.dark-theme .aksrt-header-wallet__item b {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.aksrt-header-userbox .header-user-href {
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-header-userbox .header-user-href .toggle-radius {
|
|
||||||
flex-basis: 34px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,22 +101,3 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v2 {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(90deg, #ffb347, #ff6b6b);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vipbg-v2.level-2 {
|
|
||||||
background: linear-gradient(90deg, #9b59b6, #3f51b5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-expdate-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-11
@@ -5936,7 +5936,7 @@ body:has(.modal.in) .float-btn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.payvip-icon:before {
|
.payvip-icon:before {
|
||||||
content: '\f219';
|
content: '\f3a5';
|
||||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||||
font-size: 3.5em;
|
font-size: 3.5em;
|
||||||
right: -0.2em !important;
|
right: -0.2em !important;
|
||||||
@@ -5944,16 +5944,6 @@ body:has(.modal.in) .float-btn,
|
|||||||
opacity: 0.06;
|
opacity: 0.06;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vipbg-v1 {
|
|
||||||
background: linear-gradient(60deg, #f0bf81 0, #fdefd6 30%, #f2deb9 40%, #fdc277 80%, #f9e1b7 100%);
|
|
||||||
color: #8c5728;
|
|
||||||
--this-color: #8c5728;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vipbg-v1:before {
|
|
||||||
content: '\f006';
|
|
||||||
}
|
|
||||||
|
|
||||||
.vip-texticon {
|
.vip-texticon {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+14
-4
@@ -38,6 +38,14 @@
|
|||||||
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
return (BigInt(match[1]) * 100n) + BigInt((match[2] || '').padEnd(2, '0') || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCutCount(value) {
|
||||||
|
var number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return String(value);
|
||||||
|
if (number > 999999) return String(Math.round(number / 10000)) + 'W+';
|
||||||
|
if (number > 9999) return String(Math.round(number / 1000) / 10) + 'W+';
|
||||||
|
return String(number);
|
||||||
|
}
|
||||||
|
|
||||||
function api(path, options) {
|
function api(path, options) {
|
||||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||||
return {
|
return {
|
||||||
@@ -64,16 +72,18 @@
|
|||||||
var walletNumber = Number(wallet);
|
var walletNumber = Number(wallet);
|
||||||
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
||||||
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
||||||
element.textContent = walletText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
|
||||||
element.title = walletText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (points !== undefined && points !== null && points !== '') {
|
if (points !== undefined && points !== null && points !== '') {
|
||||||
var pointsNumber = Number(points);
|
var pointsNumber = Number(points);
|
||||||
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
||||||
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
||||||
element.textContent = pointsText;
|
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
|
||||||
element.title = pointsText;
|
element.textContent = text;
|
||||||
|
element.title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
this.bindFeatured();
|
this.bindFeatured();
|
||||||
this.bindPay();
|
this.bindPay();
|
||||||
this.bindLeaveWarning();
|
this.bindLeaveWarning();
|
||||||
this.bindAi();
|
|
||||||
this.bindTagActive();
|
this.bindTagActive();
|
||||||
this.loadFeaturedFromDom();
|
this.loadFeaturedFromDom();
|
||||||
},
|
},
|
||||||
@@ -635,67 +634,6 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
bindAi: function () {
|
|
||||||
var ai = cfg.ai || {};
|
|
||||||
if (!ai.available) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qsa('.aksrt-ai-assist-btn').forEach(function (btn) {
|
|
||||||
btn.hidden = false;
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
var action = btn.getAttribute('data-ai-action');
|
|
||||||
var content = getEditorContent();
|
|
||||||
if (!content || content.replace(/<[^>]+>/g, '').trim().length < 10) {
|
|
||||||
notyf(i18n.needContent || '请先填写正文内容', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var toastId = action === 'tags'
|
|
||||||
? 'aksrt-front-submit-ai-tags-toast'
|
|
||||||
: 'aksrt-front-submit-ai-summary-toast';
|
|
||||||
var old = btn.textContent;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = i18n.aiGenerating || '...';
|
|
||||||
notyf(i18n.aiGenerating || '正在生成…', 'load', '', toastId);
|
|
||||||
fetch((cfg.restUrl || '').replace(/\/$/, '') + '/ai/' + action, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-WP-Nonce': cfg.restNonce || '',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ content: content }),
|
|
||||||
})
|
|
||||||
.then(function (r) {
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
var text = (res && (res.data || res).text) || (res && res.result) || '';
|
|
||||||
if (!text) {
|
|
||||||
throw new Error(i18n.aiFailed || 'AI failed');
|
|
||||||
}
|
|
||||||
if (action === 'summary') {
|
|
||||||
var ex = qs('[name="excerpt"]');
|
|
||||||
if (ex) {
|
|
||||||
ex.value = text;
|
|
||||||
}
|
|
||||||
} else if (action === 'tags') {
|
|
||||||
var tags = qs('[name="tags"]');
|
|
||||||
if (tags) {
|
|
||||||
tags.value = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notyf(i18n.aiFilled || '已填入,可继续编辑', 'success', 5, toastId);
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
notyf(err.message || i18n.aiFailed || 'AI 生成失败', 'danger', 5, toastId);
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = old;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
jQuery(document).off('auto_fun.aksrt-front-submit').on('auto_fun.aksrt-front-submit', function () { App.init(); });
|
||||||
|
|||||||
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
|||||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||||||
|
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||||||
|
}
|
||||||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||||||
\AKSRT\Core\SEOHead::get_instance()->init();
|
\AKSRT\Core\SEOHead::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
|||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_normalize_header_cta_label')) :
|
|
||||||
/**
|
|
||||||
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
|
|
||||||
*/
|
|
||||||
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
|
|
||||||
$label = trim($label);
|
|
||||||
if ($label === '') {
|
|
||||||
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stripos($label, '<i') === false) {
|
|
||||||
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
|
|
||||||
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
|
|
||||||
} else {
|
|
||||||
$label = preg_replace(
|
|
||||||
'/(<\/i>)\s*([^<].*)$/u',
|
|
||||||
'$1<span class="navbar-cta-text">$2</span>',
|
|
||||||
$label
|
|
||||||
) ?? $label;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $label;
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
if (!function_exists('aksrt_render_new_add_btns')) :
|
if (!function_exists('aksrt_render_new_add_btns')) :
|
||||||
/**
|
/**
|
||||||
* Multi-type publish button (legacy newadd-btns).
|
* Publish chip (legacy newadd-btns, single action).
|
||||||
|
*
|
||||||
|
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
|
||||||
|
* the paint classes ride on the anchor itself and the label is emitted
|
||||||
|
* verbatim. Only the post action exists, so there is no dropdown branch.
|
||||||
*
|
*
|
||||||
* @param string[] $types
|
* @param string[] $types
|
||||||
*/
|
*/
|
||||||
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$actions = aksrt_get_new_add_actions($types);
|
$actions = aksrt_get_new_add_actions($types);
|
||||||
if (!$actions) {
|
if (!$actions) {
|
||||||
// Last resort: keep a write-post entry so the top-nav chip is never blank.
|
return '';
|
||||||
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
|
|
||||||
if ($fallback === '') {
|
|
||||||
$fallback = admin_url('post-new.php');
|
|
||||||
}
|
|
||||||
$actions = [[
|
|
||||||
'type' => 'post',
|
|
||||||
'url' => $fallback,
|
|
||||||
'label' => __('写文章', 'aksrt'),
|
|
||||||
'icon' => 'fa fas fa-file-alt',
|
|
||||||
]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$a = $actions[0];
|
||||||
if ($label === '') {
|
if ($label === '') {
|
||||||
$label = __('发布', 'aksrt');
|
$label = __('发布', 'aksrt');
|
||||||
}
|
}
|
||||||
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
|
|
||||||
|
|
||||||
// Split paint classes vs wrapper tokens.
|
return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
|
||||||
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
. $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
|
||||||
$cta_tokens = [];
|
|
||||||
$wrap_tokens = ['newadd-btns'];
|
|
||||||
foreach ($class_tokens as $token) {
|
|
||||||
if ($token === 'newadd-btns') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
|
|
||||||
$cta_tokens[] = $token;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$wrap_tokens[] = $token;
|
|
||||||
}
|
|
||||||
if (!$cta_tokens) {
|
|
||||||
$cta_tokens[] = 'jb-blue';
|
|
||||||
}
|
|
||||||
if (!in_array('but', $cta_tokens, true)) {
|
|
||||||
$cta_tokens[] = 'but';
|
|
||||||
}
|
|
||||||
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
|
|
||||||
|
|
||||||
if (count($actions) === 1) {
|
|
||||||
$a = $actions[0];
|
|
||||||
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
|
|
||||||
. $label_html
|
|
||||||
. '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
|
|
||||||
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
|
|
||||||
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
|
|
||||||
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
|
|
||||||
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
|
|
||||||
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
|
|
||||||
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
|
|
||||||
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
|
|
||||||
foreach ($actions as $a) {
|
|
||||||
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
|
|
||||||
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
|
|
||||||
. '<text>' . esc_html($a['label']) . '</text>';
|
|
||||||
$html .= '</a>';
|
|
||||||
}
|
|
||||||
$html .= '</div></span>';
|
|
||||||
return $html;
|
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
*/
|
*/
|
||||||
function aksrt_render_header_menu_buttons(): string {
|
function aksrt_render_header_menu_buttons(): string {
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
|
||||||
$cfg = [
|
$enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
|
||||||
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']),
|
$text = trim((string) ($settings['nav_newposts_text'] ?? ''));
|
||||||
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))),
|
$pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
|
||||||
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']),
|
$skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
|
||||||
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
|
|
||||||
];
|
|
||||||
$html = '';
|
$html = '';
|
||||||
if (!empty($cfg['enabled'])) {
|
if ($enabled) {
|
||||||
// legacy default: but nowave jb-blue (+ radius).
|
// legacy default: but nowave jb-blue (+ radius).
|
||||||
$class = 'but nowave navbar-newposts';
|
$class = 'but nowave ' . $skin;
|
||||||
$skin = trim((string) ($cfg['class'] ?? ''));
|
|
||||||
if ($skin === '') {
|
|
||||||
$skin = 'jb-blue';
|
|
||||||
}
|
|
||||||
$class .= ' ' . $skin;
|
|
||||||
$pill = !empty($cfg['radius_pill']);
|
|
||||||
if ($pill && !str_contains($class, 'radius')) {
|
if ($pill && !str_contains($class, 'radius')) {
|
||||||
$class .= ' radius';
|
$class .= ' radius';
|
||||||
}
|
}
|
||||||
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
|
$label = $text !== '' ? $text : __('发布', 'aksrt');
|
||||||
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
||||||
if ($btn !== '') {
|
if ($btn !== '') {
|
||||||
$html .= $btn;
|
$html .= $btn;
|
||||||
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
$show_vip = false;
|
$show_vip = false;
|
||||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
||||||
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']);
|
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
|
||||||
|
&& !\AKSRT\Settings\Settings::close_sign();
|
||||||
}
|
}
|
||||||
if ($show_vip) {
|
if ($show_vip) {
|
||||||
$is_vip = false;
|
$is_vip = false;
|
||||||
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
|||||||
if (method_exists($svc, 'render_vip_card')) {
|
if (method_exists($svc, 'render_vip_card')) {
|
||||||
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
||||||
if ($cards !== '') {
|
if ($cards !== '') {
|
||||||
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>';
|
$hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card
|
// legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
|
||||||
$html .= '<span class="hover-show inline-block navbar-vip-hover">';
|
$html .= '<span class="hover-show inline-block">';
|
||||||
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"'
|
$html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
|
||||||
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
. '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
. esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
|
||||||
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
|
|
||||||
$html .= '</a>' . $hover . '</span>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
* Check-in CTA for header dropdown and compact card placements.
|
* Check-in CTA for header dropdown and compact card placements.
|
||||||
*
|
*
|
||||||
* @param int $user_id Current user ID.
|
* @param int $user_id Current user ID.
|
||||||
* @param bool $compact Render a short card-friendly label.
|
|
||||||
* @param string|null $class Override the classes and render the reference-parity
|
* @param string|null $class Override the classes and render the reference-parity
|
||||||
* <a> badge (user-card 签到书签); null keeps the header
|
* <a> badge (user-card 签到书签); null keeps the header
|
||||||
* dropdown <button> runtime.
|
* dropdown <a> runtime.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_checkin_btn(int $user_id = 0, bool $compact = false, ?string $class = null): string {
|
function aksrt_header_user_checkin_btn(int $user_id = 0, ?string $class = null): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
||||||
return '';
|
return '';
|
||||||
@@ -56,38 +55,39 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
|||||||
|
|
||||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||||
if ($signed) {
|
if ($signed) {
|
||||||
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
|
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
$class . ' is-done',
|
$class . ' is-done',
|
||||||
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
|
. '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return aksrt_get_user_checkin_details_link(
|
return aksrt_get_user_checkin_details_link(
|
||||||
'but block c-yellow aksrt-header-checkin is-done',
|
'but block c-yellow is-done',
|
||||||
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
|
'<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. esc_html__('今日已签到', 'aksrt')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : ''));
|
|
||||||
|
|
||||||
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
// Card badge variant (explicit $class, e.g. the user-card 签到 badge):
|
||||||
// reference parity renders an <a> so the badge inherits the card font
|
// reference parity renders an <a> so the badge inherits the card font
|
||||||
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
|
||||||
if ($class !== null) {
|
if ($class !== null) {
|
||||||
return '<a class="' . esc_attr($classes) . '" href="javascript:;" data-signin-btn'
|
return '<a class="' . esc_attr($class) . '" href="javascript:;" data-signin-btn'
|
||||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
||||||
. '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
. '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||||
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
|
||||||
. '</a>';
|
. '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
|
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
|
||||||
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
|
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||||
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
|
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
||||||
. '</button>';
|
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||||
|
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
|
||||||
|
. '</a>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -118,42 +118,64 @@ add_action('wp_footer', 'aksrt_render_signin_captcha');
|
|||||||
|
|
||||||
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
if (!function_exists('aksrt_header_user_wallet_mini')) :
|
||||||
/**
|
/**
|
||||||
* Mini wallet strip: credits + cash balance.
|
* Mini wallet tiles: cash balance + credits (reference parity).
|
||||||
|
*
|
||||||
|
* Each tile is gated by its payment switch; an all-off configuration
|
||||||
|
* renders nothing at all.
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
function aksrt_header_user_wallet_mini(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0) {
|
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$credits = aksrt_user_points($user_id);
|
|
||||||
$currency = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_currency_name()
|
|
||||||
: __('积分', 'aksrt');
|
|
||||||
$wallet = aksrt_user_balance_decimal($user_id);
|
|
||||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
|
||||||
|
|
||||||
$html = '<a class="aksrt-header-wallet flex ac jsb" href="' . esc_url($center) . '">';
|
$balance_on = !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-database" aria-hidden="true"></i> '
|
$points_on = \AKSRT\Settings\Settings::credits_enabled();
|
||||||
. esc_html($currency) . ' <b data-aksrt-points-value data-credits-value>' . esc_html(number_format_i18n($credits)) . '</b></span>';
|
if (!$balance_on && !$points_on) {
|
||||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-money-bill" aria-hidden="true"></i> '
|
return '';
|
||||||
. esc_html__('余额', 'aksrt') . ' <b data-aksrt-wallet-value>' . esc_html(number_format_i18n($wallet, 2)) . '</b></span>';
|
}
|
||||||
$html .= '</a>';
|
|
||||||
return $html;
|
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||||
|
$box = '';
|
||||||
|
|
||||||
|
if ($balance_on) {
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html__('余额', 'aksrt')
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-blue-2" data-aksrt-wallet-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count((float) aksrt_user_balance_decimal($user_id)))
|
||||||
|
. '</span><i class="fa fas fa-money-bill-wave em14 c-blue-2" aria-hidden="true"></i></div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($points_on) {
|
||||||
|
$currency = \AKSRT\Settings\Settings::get_currency_name();
|
||||||
|
$box .= '<a rel="nofollow" style="flex: 1;" class="muted-box padding-6 flex1 radius4" href="' . esc_url($center) . '">'
|
||||||
|
. '<div class="muted-2-color px12">' . esc_html($currency)
|
||||||
|
. '<i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></div>'
|
||||||
|
. '<div class="flex jsb"><span class="font-bold c-yellow" data-aksrt-points-value data-aksrt-count-format="cut-count">'
|
||||||
|
. esc_html(aksrt_cut_count(aksrt_user_points($user_id)))
|
||||||
|
. '</span><i class="fa fas fa-coins em14 c-yellow" aria-hidden="true"></i></div></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_header_user_badges_row')) :
|
if (!function_exists('aksrt_header_user_badges_row')) :
|
||||||
/**
|
/**
|
||||||
* Identity / medal chips under profile.
|
* Interaction stats row under the wallet (reference parity).
|
||||||
*/
|
*/
|
||||||
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
function aksrt_header_user_badges_row(int $user_id = 0): string {
|
||||||
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) {
|
if ($user_id <= 0 || !function_exists('aksrt_get_user_badges')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Certification is displayed beside the name and level icon in the profile row.
|
$items = aksrt_get_user_badges($user_id);
|
||||||
|
if ($items === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
return '<div class="em09 author-tag mb10 mt6 flex jc">' . $items . '</div>';
|
||||||
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
if (!function_exists('aksrt_header_payvip_bar')) :
|
if (!function_exists('aksrt_header_payvip_bar')) :
|
||||||
@@ -173,30 +195,33 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
|
||||||
if ($level > 0) {
|
if ($level > 0) {
|
||||||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||||||
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||||
|
// 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到
|
||||||
|
// 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久
|
||||||
|
// 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。
|
||||||
$exp_label = '';
|
$exp_label = '';
|
||||||
if (function_exists('aksrt_get_vip_info')) {
|
if (function_exists('aksrt_get_user_vip_state')) {
|
||||||
$info = aksrt_get_vip_info($user_id);
|
$state = aksrt_get_user_vip_state($user_id);
|
||||||
$expires = max(0, (int) ($info['expires'] ?? 0));
|
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||||||
$exp_label = $expires > 0
|
if ($expires > 0) {
|
||||||
? sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))
|
$exp_label = $expires <= time()
|
||||||
: (string) ($info['expires_label'] ?? '');
|
? __('会员已过期', 'aksrt')
|
||||||
|
: wp_date('Y年m月d日', $expires);
|
||||||
|
} elseif (!empty($state['is_permanent'])) {
|
||||||
|
$exp_label = __('永久会员', 'aksrt');
|
||||||
}
|
}
|
||||||
$html = '<div class="mt10 em09 aksrt-header-payvip is-vip">';
|
}
|
||||||
$html .= '<span class="radius4 payvip-icon btn-block text-center vipbg-v2 level-' . (int) $level . ' aksrt-header-payvip__active">';
|
$html = '<div class="mt10 em09" style="padding:2px;">';
|
||||||
$html .= '<span class="aksrt-header-payvip__level">';
|
$html .= '<span class="radius4 payvip-icon btn-block text-center">';
|
||||||
$html .= '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
$html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : '';
|
||||||
$html .= '<span>' . esc_html($name) . '</span>';
|
$html .= '<span>' . esc_html($name) . '</span>';
|
||||||
$html .= '</span>';
|
|
||||||
if ($exp_label !== '') {
|
if ($exp_label !== '') {
|
||||||
$html .= '<span class="aksrt-header-payvip__exp">' . esc_html($exp_label) . '</span>';
|
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag">'
|
||||||
|
. '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>'
|
||||||
|
. esc_html($exp_label) . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '</span></div>';
|
$html .= '</span></div>';
|
||||||
return $html;
|
return $html;
|
||||||
@@ -205,11 +230,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match check-in CTA geometry (full width, 6px radius) — not top-nav navbar-cta chips.
|
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||||
return '<div class="mt10 em09 aksrt-header-payvip">'
|
return '<div class="mt10 em09" style="padding:2px;">'
|
||||||
. '<a class="pay-vip but block payvip-icon aksrt-header-payvip__cta" href="javascript:;" vip-level="1">'
|
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||||
. '<i class="fa fas fa-gem" aria-hidden="true"></i>'
|
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||||
. '</a></div>';
|
. '</a></div>';
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|||||||
@@ -351,14 +351,14 @@ if (!function_exists('aksrt_get_user_badges')) {
|
|||||||
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
$view_n = aksrt_cut_count(aksrt_get_user_posts_meta_sum($user_id, 'aksrt_post_views'));
|
||||||
|
|
||||||
$html = '';
|
$html = '';
|
||||||
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('post') . $post_n . '</a>';
|
$html .= '<a class="but c-blue tag-posts" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s篇文章', 'aksrt'), $post_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-file-alt" aria-hidden="true"></i>' . $post_n . '</a>';
|
||||||
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '">' . aksrt_renderer_svg_icon('comment') . $com_n . '</a>';
|
$html .= '<a class="but c-green tag-comment" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s条评论', 'aksrt'), $com_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-comment-dots" aria-hidden="true"></i>' . $com_n . '</a>';
|
||||||
if ($followed_n) {
|
if ($followed_n) {
|
||||||
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
$html .= '<a class="but c-yellow tag-follow" data-toggle="tooltip" title="' . esc_attr(sprintf(__('共%s个粉丝', 'aksrt'), $followed_n)) . '" href="' . esc_url($home) . '"><i class="fa fas fa-heart em09" aria-hidden="true"></i>' . $followed_n . '</a>';
|
||||||
} elseif ($like_n) {
|
} elseif ($like_n) {
|
||||||
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '">' . aksrt_renderer_svg_icon('like') . $like_n . '</span>';
|
$html .= '<span class="badg c-yellow tag-like" data-toggle="tooltip" title="' . esc_attr(sprintf(__('获得%s个点赞', 'aksrt'), $like_n)) . '"><i class="fa fas fa-thumbs-up" aria-hidden="true"></i>' . $like_n . '</span>';
|
||||||
}
|
}
|
||||||
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '">' . aksrt_renderer_svg_icon('hot') . $view_n . '</span>';
|
$html .= '<span class="badg c-red tag-view" data-toggle="tooltip" title="' . esc_attr(sprintf(__('人气值 %s', 'aksrt'), $view_n)) . '"><i class="fa fas fa-fire" aria-hidden="true"></i>' . $view_n . '</span>';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the author-card count badges row.
|
* Filter the author-card count badges row.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
if ($user_id > 0) {
|
if ($user_id > 0) {
|
||||||
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
$user = $ctx['current_user'] instanceof WP_User ? $ctx['current_user'] : wp_get_current_user();
|
||||||
$display_name = $user->display_name ?: $user->user_login;
|
$display_name = $user->display_name ?: $user->user_login;
|
||||||
$avatar = aksrt_get_avatar_html($user_id, 44, ['alt' => $display_name]);
|
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img', true, false);
|
||||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||||
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
$vip_badge = function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id) : '';
|
||||||
// Level and certification icons ride inline to the right of the display name.
|
// Level and certification icons ride inline to the right of the display name.
|
||||||
@@ -174,10 +174,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
|
|
||||||
$msg_icon = '';
|
$msg_icon = '';
|
||||||
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
if ($msg_url !== '' && in_array('avatar_dropdown', $positions, true)) {
|
||||||
$msg_icon = '<a class="abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
$msg_icon = '<a class="msg-news-icon abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||||
$msg_icon .= '<i class="fa fas fa-bell" aria-hidden="true"></i>';
|
$msg_icon .= '<span class="toggle-radius msg-icon"><i class="fa fas fa-bell" aria-hidden="true"></i></span>';
|
||||||
if ($unread > 0) {
|
if ($unread > 0) {
|
||||||
$msg_icon .= '<span class="aksrt-notification-badge">' . esc_html((string) min(99, $unread)) . '</span>';
|
$msg_icon .= '<badge class="top">' . esc_html((string) min(99, $unread)) . '</badge>';
|
||||||
}
|
}
|
||||||
$msg_icon .= '</a>';
|
$msg_icon .= '</a>';
|
||||||
}
|
}
|
||||||
@@ -224,6 +224,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-chart-pie" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
||||||
|
// legacy: 商城中心 → 支付中心(AKSRT 支付中心后台菜单)。
|
||||||
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('admin.php?page=aksrt_pay')) . '">';
|
||||||
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-credit-card" aria-hidden="true"></i></div>';
|
||||||
|
$href .= '<div class="c-yellow">' . esc_html__('支付中心', 'aksrt') . '</div></a>';
|
||||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
||||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fa fas fa-cog" aria-hidden="true"></i></div>';
|
||||||
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
||||||
@@ -234,7 +238,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$con .= $avatar;
|
$con .= $avatar;
|
||||||
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
||||||
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
|
||||||
. '<span class="display-name text-ellipsis">' . esc_html($display_name) . '</span>' . $vip_badge . $medal_badge . $certification_badge . $level_badge
|
. '<name class="flex ac flex1">'
|
||||||
|
. '<a class="display-name text-ellipsis" href="' . esc_url(function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user_id) : get_author_posts_url($user_id)) . '">' . esc_html($display_name) . '</a>'
|
||||||
|
. $vip_badge . $medal_badge . $certification_badge . $level_badge
|
||||||
|
. '</name>'
|
||||||
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
|
||||||
$con .= '</div>';
|
$con .= '</div>';
|
||||||
$con .= $msg_icon;
|
$con .= $msg_icon;
|
||||||
@@ -264,7 +271,16 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
|||||||
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
$href .= '<a href="javascript:;" class="signup-loader"><div class="badg mb6 toggle-radius c-green"><i class="fa fas fa-user-plus" aria-hidden="true"></i></div><div class="c-green">'
|
||||||
. esc_html__('注册', 'aksrt') . '</div></a>';
|
. esc_html__('注册', 'aksrt') . '</div></a>';
|
||||||
}
|
}
|
||||||
$href .= '<a rel="nofollow" href="' . esc_url(wp_lostpassword_url(home_url('/'))) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
// legacy: 找回密码 → 统一登录页 resetpassword 标签(带回跳)。
|
||||||
|
$reset_url = function_exists('aksrt_get_sign_page_url')
|
||||||
|
? aksrt_get_sign_page_url('resetpassword', [
|
||||||
|
'redirect_to' => remove_query_arg(
|
||||||
|
'redirect_to',
|
||||||
|
home_url(isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: wp_lostpassword_url(home_url('/'));
|
||||||
|
$href .= '<a target="_blank" rel="nofollow" href="' . esc_url($reset_url) . '"><div class="badg mb6 toggle-radius c-purple"><i class="fa fas fa-key" aria-hidden="true"></i></div><div class="c-purple">'
|
||||||
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
. esc_html__('找回密码', 'aksrt') . '</div></a>';
|
||||||
$href .= '</div>';
|
$href .= '</div>';
|
||||||
|
|
||||||
@@ -287,6 +303,12 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
/**
|
/**
|
||||||
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
* Right cluster: fold, publish/VIP, theme, user, search (legacy).
|
||||||
*
|
*
|
||||||
|
* The reference splits the cluster into three floated .navbar-form
|
||||||
|
* groups (publish/VIP, theme + language, account row). Each group
|
||||||
|
* keeps its own .navbar-form padding, which is what produces the
|
||||||
|
* reference inter-cluster spacing, so the groups are emitted
|
||||||
|
* separately instead of being merged into one flex container.
|
||||||
|
*
|
||||||
* @param array<string,mixed> $ctx
|
* @param array<string,mixed> $ctx
|
||||||
*/
|
*/
|
||||||
function aksrt_menu_button(array $ctx): void {
|
function aksrt_menu_button(array $ctx): void {
|
||||||
@@ -355,14 +377,24 @@ if (!function_exists('aksrt_menu_button')) :
|
|||||||
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
||||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
. $notification_but . $sign_but . $search_but . '</ul>';
|
. $notification_but . $sign_but . $search_but . '</ul>';
|
||||||
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
|
|
||||||
. $actions . $utility_buttons . $account . '</div>';
|
// Reference cluster split: publish/VIP, theme + language, account row.
|
||||||
|
$button = '';
|
||||||
|
if ($actions !== '') {
|
||||||
|
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
|
||||||
|
}
|
||||||
|
if ($utility_buttons !== '') {
|
||||||
|
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
|
||||||
|
}
|
||||||
|
$right_but = '<div class="navbar-form navbar-right'
|
||||||
|
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||||
|
. $account . '</div>';
|
||||||
|
|
||||||
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
|
||||||
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
|
||||||
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
|
||||||
|
|
||||||
$html = $fold . $button;
|
$html = $fold . $button . $right_but;
|
||||||
|
|
||||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-105
@@ -19,30 +19,12 @@ class AIController extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => [$this, 'get_status'],
|
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'update_settings'],
|
|
||||||
'permission_callback' => [$this, 'check_permission'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'seo_generate'],
|
'callback' => [$this, 'seo_generate'],
|
||||||
'permission_callback' => [$this, 'check_logged_in'],
|
'permission_callback' => [$this, 'check_logged_in'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => [$this, 'seo_suggest'],
|
|
||||||
'permission_callback' => [$this, 'check_write_assist'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'summary_suggest'],
|
'callback' => [$this, 'summary_suggest'],
|
||||||
@@ -76,66 +58,6 @@ class AIController extends ControllerBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_status(): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->success(['data' => [
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => 'none',
|
|
||||||
'configured' => false,
|
|
||||||
'available' => false,
|
|
||||||
'backend' => 'none',
|
|
||||||
]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$allowed = [
|
|
||||||
'enabled' => 'bool',
|
|
||||||
'provider' => 'string',
|
|
||||||
'api_key' => 'string',
|
|
||||||
'api_base' => 'url',
|
|
||||||
'model' => 'string',
|
|
||||||
'max_tokens' => 'int',
|
|
||||||
'rate_limit' => 'int',
|
|
||||||
];
|
|
||||||
|
|
||||||
$config = [];
|
|
||||||
foreach ($allowed as $key => $type) {
|
|
||||||
if (!isset($params[$key])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch ($type) {
|
|
||||||
case 'bool':
|
|
||||||
$config[$key] = !empty($params[$key]);
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$config[$key] = (int) $params[$key];
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
$config[$key] = esc_url_raw((string) $params[$key]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$config[$key] = sanitize_text_field((string) $params[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$success = \AKSRT\Ai\AI::get_instance()->update_config($config);
|
|
||||||
if ($success) {
|
|
||||||
return $this->success([
|
|
||||||
'message' => __('AI 设置已保存', 'aksrt'),
|
|
||||||
'data' => \AKSRT\Ai\AI::get_instance()->get_status(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $this->error(__('保存失败', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
||||||
*/
|
*/
|
||||||
@@ -170,32 +92,6 @@ class AIController extends ControllerBase {
|
|||||||
return $this->success(['data' => $result['data']]);
|
return $this->success(['data' => $result['data']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
|
||||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
|
||||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = $this->get_json_params($request);
|
|
||||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
|
||||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
|
||||||
$post_id = absint($params['post_id'] ?? 0);
|
|
||||||
|
|
||||||
if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
|
|
||||||
return $this->error(__('权限不足', 'aksrt'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($title === '') {
|
|
||||||
return $this->error(__('标题不能为空', 'aksrt'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content);
|
|
||||||
if (!empty($result['success'])) {
|
|
||||||
return $this->success(['data' => $result['data']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||||
@@ -224,7 +120,7 @@ class AIController extends ControllerBase {
|
|||||||
$ai = \AKSRT\Ai\AI::get_instance();
|
$ai = \AKSRT\Ai\AI::get_instance();
|
||||||
if (!$ai->is_available()) {
|
if (!$ai->is_available()) {
|
||||||
return $this->error(
|
return $this->error(
|
||||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
__('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||||
503
|
503
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ class PostListEnhance {
|
|||||||
|
|
||||||
$order = $this->current_order();
|
$order = $this->current_order();
|
||||||
$order_by = $this->current_orderby();
|
$order_by = $this->current_orderby();
|
||||||
$o_icon = '<i class="opacity5 ml3 fa fa-long-arrow-' . ($order === 'asc' ? 'down' : 'up') . '"></i>';
|
$o_icon = $order === 'asc'
|
||||||
|
? '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-down"></i>'
|
||||||
|
: '<i class="opacity5 ml3 fa fas fa-long-arrow-alt-up"></i>';
|
||||||
|
|
||||||
$new_columns = [];
|
$new_columns = [];
|
||||||
foreach ($columns as $key => $label) {
|
foreach ($columns as $key => $label) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class GlobalSchema {
|
|||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||||
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
||||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需先在 WordPress 配置 AI Connectors。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
||||||
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
||||||
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
||||||
@@ -79,12 +79,6 @@ class GlobalSchema {
|
|||||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
|
||||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
|
||||||
]],
|
|
||||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ class SchemaExtensionsPush {
|
|||||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||||
]],
|
]],
|
||||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
['key' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
|
||||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
|
||||||
]],
|
]],
|
||||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
|
||||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
|||||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||||
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
|
||||||
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
|
||||||
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -611,36 +611,6 @@ class SchemaUsers {
|
|||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'wechat' => [
|
|
||||||
'key' => 'wechat',
|
|
||||||
'title' => '微信通知',
|
|
||||||
'version' => '1.0.0',
|
|
||||||
'option_key' => 'aksrt_settings',
|
|
||||||
'groups' => [
|
|
||||||
['key' => 'account', 'title' => '公众号配置', 'fields' => [
|
|
||||||
['key' => 'wechat_notify_enabled', 'label' => '启用微信通知', 'type' => 'switch', 'default' => false],
|
|
||||||
['key' => 'wechat_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
]],
|
|
||||||
['key' => 'verify', 'title' => '服务器验证', 'fields' => [
|
|
||||||
['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
]],
|
|
||||||
['key' => 'templates', 'title' => '模板消息', 'fields' => [
|
|
||||||
['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
]],
|
|
||||||
['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
|
|
||||||
['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
|
||||||
]],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,6 +325,54 @@ class UserController extends ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /wechat/status — 微信模板消息配置状态。
|
||||||
|
*
|
||||||
|
* 状态形状与 WeChatNotify::get_status() 对齐(该方法的既定用途即 REST 与 AJAX 复用)。
|
||||||
|
*/
|
||||||
|
public function get_wechat_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
|
if (!class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||||
|
return $this->error(__('微信通知组件未加载', 'aksrt'), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(['data' => \AKSRT\Payment\WeChatNotify::get_instance()->get_status()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /wechat/settings — 更新微信模板消息配置。
|
||||||
|
*
|
||||||
|
* 前端在密钥留空且已配置时发送掩码 ***,此时保留已存密钥;
|
||||||
|
* 未提供的字段沿用当前值。键白名单由 Integration::update_wechat_settings() 收敛。
|
||||||
|
*/
|
||||||
|
public function update_wechat_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
|
$params = $this->get_json_params($request);
|
||||||
|
$current = \AKSRT\Settings\Settings::get_wechat_settings();
|
||||||
|
|
||||||
|
$enabled = array_key_exists('enabled', $params) ? !empty($params['enabled']) : !empty($current['enabled']);
|
||||||
|
$app_id = array_key_exists('app_id', $params)
|
||||||
|
? sanitize_text_field((string) $params['app_id'])
|
||||||
|
: (string) ($current['app_id'] ?? '');
|
||||||
|
$template_id = array_key_exists('template_id', $params)
|
||||||
|
? sanitize_text_field((string) $params['template_id'])
|
||||||
|
: (string) ($current['template_id'] ?? '');
|
||||||
|
|
||||||
|
// 掩码 *** 表示「不修改密钥」;仅在显式传入其他值时才覆盖。
|
||||||
|
if (array_key_exists('app_secret', $params) && (string) $params['app_secret'] !== '***') {
|
||||||
|
$app_secret = sanitize_text_field((string) $params['app_secret']);
|
||||||
|
} else {
|
||||||
|
$app_secret = (string) ($current['app_secret'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
\AKSRT\Settings\Settings::update_wechat_settings([
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'app_id' => $app_id,
|
||||||
|
'app_secret' => $app_secret,
|
||||||
|
'template_id' => $template_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success(['message' => __('微信通知配置已保存', 'aksrt')]);
|
||||||
|
}
|
||||||
|
|
||||||
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
|
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if ($request->get_method() === 'GET') {
|
if ($request->get_method() === 'GET') {
|
||||||
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
||||||
|
|||||||
+9
-314
@@ -11,9 +11,6 @@ class AI {
|
|||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
const PROVIDER_NONE = 'none';
|
|
||||||
const PROVIDER_OPENAI = 'openai';
|
|
||||||
|
|
||||||
public static function get_instance(): self {
|
public static function get_instance(): self {
|
||||||
if (null === self::$instance) {
|
if (null === self::$instance) {
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
@@ -21,95 +18,10 @@ class AI {
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 AI 配置(主题侧 provider)
|
|
||||||
*/
|
|
||||||
public function get_config(): array {
|
|
||||||
$defaults = [
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => self::PROVIDER_NONE,
|
|
||||||
'api_key' => '',
|
|
||||||
'api_base' => 'https://api.openai.com/v1',
|
|
||||||
'model' => 'gpt-3.5-turbo',
|
|
||||||
'max_tokens' => 1000,
|
|
||||||
'rate_limit' => 10,
|
|
||||||
];
|
|
||||||
|
|
||||||
$saved = get_option('aksrt_ai_settings', []);
|
|
||||||
if (!is_array($saved)) {
|
|
||||||
$saved = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge($defaults, $saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 AI 配置
|
|
||||||
*/
|
|
||||||
public function update_config(array $config): bool {
|
|
||||||
$current = $this->get_config();
|
|
||||||
$merged = array_merge($current, $config);
|
|
||||||
|
|
||||||
// Preserve existing key when client sends the masked placeholder.
|
|
||||||
if (isset($config['api_key']) && $config['api_key'] === '***') {
|
|
||||||
$merged['api_key'] = $current['api_key'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$merged['enabled'] = !empty($merged['enabled']);
|
|
||||||
$merged['provider'] = sanitize_key((string) ($merged['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$merged['api_key'] = (string) ($merged['api_key'] ?? '');
|
|
||||||
$merged['api_base'] = $this->normalize_api_base((string) ($merged['api_base'] ?? 'https://api.openai.com/v1'));
|
|
||||||
$merged['model'] = sanitize_text_field((string) ($merged['model'] ?? 'gpt-3.5-turbo'));
|
|
||||||
$merged['max_tokens'] = max(100, min(4000, (int) ($merged['max_tokens'] ?? 1000)));
|
|
||||||
$merged['rate_limit'] = max(1, min(100, (int) ($merged['rate_limit'] ?? 10)));
|
|
||||||
|
|
||||||
// Persist only the known keys so shape stays stable.
|
|
||||||
$to_store = [
|
|
||||||
'enabled' => $merged['enabled'],
|
|
||||||
'provider' => $merged['provider'],
|
|
||||||
'api_key' => $merged['api_key'],
|
|
||||||
'api_base' => $merged['api_base'],
|
|
||||||
'model' => $merged['model'],
|
|
||||||
'max_tokens' => $merged['max_tokens'],
|
|
||||||
'rate_limit' => $merged['rate_limit'],
|
|
||||||
];
|
|
||||||
|
|
||||||
$before = get_option('aksrt_ai_settings', []);
|
|
||||||
$updated = update_option('aksrt_ai_settings', $to_store);
|
|
||||||
if ($updated) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update_option returns false when the value is unchanged — treat as success.
|
|
||||||
if (is_array($before)) {
|
|
||||||
$before_normalized = array_merge([
|
|
||||||
'enabled' => false,
|
|
||||||
'provider' => self::PROVIDER_NONE,
|
|
||||||
'api_key' => '',
|
|
||||||
'api_base' => 'https://api.openai.com/v1',
|
|
||||||
'model' => 'gpt-3.5-turbo',
|
|
||||||
'max_tokens' => 1000,
|
|
||||||
'rate_limit' => 10,
|
|
||||||
], $before);
|
|
||||||
$before_normalized['enabled'] = !empty($before_normalized['enabled']);
|
|
||||||
$before_normalized['provider'] = sanitize_key((string) ($before_normalized['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$before_normalized['api_key'] = (string) ($before_normalized['api_key'] ?? '');
|
|
||||||
$before_normalized['api_base'] = esc_url_raw((string) ($before_normalized['api_base'] ?? 'https://api.openai.com/v1'));
|
|
||||||
$before_normalized['model'] = sanitize_text_field((string) ($before_normalized['model'] ?? 'gpt-3.5-turbo'));
|
|
||||||
$before_normalized['max_tokens'] = max(100, min(4000, (int) ($before_normalized['max_tokens'] ?? 1000)));
|
|
||||||
$before_normalized['rate_limit'] = max(1, min(100, (int) ($before_normalized['rate_limit'] ?? 10)));
|
|
||||||
|
|
||||||
return maybe_serialize($before_normalized) === maybe_serialize($to_store)
|
|
||||||
|| $before_normalized == $to_store;
|
|
||||||
}
|
|
||||||
|
|
||||||
return maybe_serialize($before) === maybe_serialize($to_store);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WordPress 核心 AI Client 是否真正可用。
|
* WordPress 核心 AI Client 是否真正可用。
|
||||||
*
|
*
|
||||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应抢占主题 provider。
|
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应声称可用。
|
||||||
*/
|
*/
|
||||||
public function is_wp_ai_available(): bool {
|
public function is_wp_ai_available(): bool {
|
||||||
if (!function_exists('wp_ai_client_prompt')) {
|
if (!function_exists('wp_ai_client_prompt')) {
|
||||||
@@ -124,89 +36,24 @@ class AI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题 OpenAI 兼容 provider 是否可用。
|
* AI 生成是否可用(连接由 WordPress AI Connectors 提供)。
|
||||||
*/
|
|
||||||
public function is_theme_provider_available(): bool {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$provider = sanitize_key((string) ($config['provider'] ?? self::PROVIDER_NONE));
|
|
||||||
$api_key = trim((string) ($config['api_key'] ?? ''));
|
|
||||||
$api_base = trim((string) ($config['api_base'] ?? ''));
|
|
||||||
|
|
||||||
return !empty($config['enabled'])
|
|
||||||
&& $provider !== self::PROVIDER_NONE
|
|
||||||
&& $provider !== ''
|
|
||||||
&& $api_key !== ''
|
|
||||||
&& $api_base !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任一后端可用即可生成。
|
|
||||||
*/
|
*/
|
||||||
public function is_available(): bool {
|
public function is_available(): bool {
|
||||||
return $this->is_theme_provider_available() || $this->is_wp_ai_available();
|
return $this->is_wp_ai_available();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前优先后端标识。
|
|
||||||
*
|
|
||||||
* Theme provider wins when explicitly configured — WP AI stubs often exist
|
|
||||||
* without a working connector and previously produced opaque 400 failures.
|
|
||||||
*/
|
|
||||||
public function resolve_backend(): string {
|
|
||||||
if ($this->is_theme_provider_available()) {
|
|
||||||
return 'theme';
|
|
||||||
}
|
|
||||||
if ($this->is_wp_ai_available()) {
|
|
||||||
return 'wp_ai';
|
|
||||||
}
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 AI 状态(不回显完整密钥)。
|
|
||||||
*/
|
|
||||||
public function get_status(): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'enabled' => !empty($config['enabled']),
|
|
||||||
'provider' => (string) ($config['provider'] ?? self::PROVIDER_NONE),
|
|
||||||
'configured' => !empty($config['api_key']) || $this->is_wp_ai_available(),
|
|
||||||
'model' => (string) ($config['model'] ?? ''),
|
|
||||||
'api_base' => (string) ($config['api_base'] ?? ''),
|
|
||||||
'api_key' => !empty($config['api_key']) ? '***' : '',
|
|
||||||
'max_tokens' => (int) ($config['max_tokens'] ?? 1000),
|
|
||||||
'rate_limit' => (int) ($config['rate_limit'] ?? 10),
|
|
||||||
'available' => $this->is_available(),
|
|
||||||
'backend' => $this->resolve_backend(),
|
|
||||||
'wp_ai_available' => $this->is_wp_ai_available(),
|
|
||||||
'theme_available' => $this->is_theme_provider_available(),
|
|
||||||
'seo_enabled' => !empty($seo['seo_enabled']),
|
|
||||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']),
|
|
||||||
'write_assist' => $this->is_available(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一文本生成出口。
|
* 统一文本生成出口。
|
||||||
*
|
*
|
||||||
* @param string $prompt 用户提示 / 上下文
|
* @param string $prompt 用户提示 / 上下文
|
||||||
* @param array $options system_instruction, max_tokens, temperature
|
* @param array $options system_instruction(其余选项由 WP AI Client 忽略)
|
||||||
* @return array{success:bool,content?:string,message?:string,backend?:string}
|
* @return array{success:bool,content?:string,message?:string,backend?:string}
|
||||||
*/
|
*/
|
||||||
public function generate_text(string $prompt, array $options = []): array {
|
public function generate_text(string $prompt, array $options = []): array {
|
||||||
if (!$this->is_available()) {
|
if (!$this->is_available()) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
'message' => __('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->check_rate_limit()) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('请求过于频繁,请稍后再试', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,42 +61,12 @@ class AI {
|
|||||||
? (string) $options['system_instruction']
|
? (string) $options['system_instruction']
|
||||||
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
||||||
|
|
||||||
$last_error = [
|
$result = $this->generate_via_wp_ai($prompt, $system, $options);
|
||||||
'success' => false,
|
if (!empty($result['success'])) {
|
||||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
$result['backend'] = 'wp_ai';
|
||||||
];
|
|
||||||
|
|
||||||
// 1) Prefer explicitly configured theme OpenAI-compatible provider.
|
|
||||||
if ($this->is_theme_provider_available()) {
|
|
||||||
$theme_result = $this->generate_via_theme($prompt, $system, $options);
|
|
||||||
$this->record_call();
|
|
||||||
if (!empty($theme_result['success'])) {
|
|
||||||
$theme_result['backend'] = 'theme';
|
|
||||||
return $theme_result;
|
|
||||||
}
|
|
||||||
$last_error = $theme_result;
|
|
||||||
// Fall through to WP AI if available.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) WordPress AI Client (Connectors).
|
return $result;
|
||||||
if ($this->is_wp_ai_available()) {
|
|
||||||
$wp_result = $this->generate_via_wp_ai($prompt, $system, $options);
|
|
||||||
$this->record_call();
|
|
||||||
if (!empty($wp_result['success'])) {
|
|
||||||
$wp_result['backend'] = 'wp_ai';
|
|
||||||
return $wp_result;
|
|
||||||
}
|
|
||||||
$last_error = $wp_result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $last_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容旧调用。
|
|
||||||
*/
|
|
||||||
public function generate(string $prompt, array $options = []): array {
|
|
||||||
return $this->generate_text($prompt, $options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generate_via_wp_ai(string $prompt, string $system, array $options): array {
|
private function generate_via_wp_ai(string $prompt, string $system, array $options): array {
|
||||||
@@ -289,126 +106,4 @@ class AI {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize OpenAI-compatible base URL.
|
|
||||||
*
|
|
||||||
* Accepts gateway root (https://host), /v1, trailing slash, or full
|
|
||||||
* .../chat/completions and returns a base that can append /chat/completions.
|
|
||||||
*/
|
|
||||||
public function normalize_api_base(string $api_base): string {
|
|
||||||
$api_base = trim($api_base);
|
|
||||||
if ($api_base === '') {
|
|
||||||
return 'https://api.openai.com/v1';
|
|
||||||
}
|
|
||||||
|
|
||||||
$api_base = esc_url_raw($api_base);
|
|
||||||
$api_base = rtrim($api_base, '/');
|
|
||||||
|
|
||||||
// If user pasted the full chat endpoint, strip the leaf.
|
|
||||||
if (preg_match('#/chat/completions$#i', $api_base)) {
|
|
||||||
$api_base = (string) preg_replace('#/chat/completions$#i', '', $api_base);
|
|
||||||
$api_base = rtrim($api_base, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common gateways (NewAPI etc.): root host needs /v1.
|
|
||||||
if (!preg_match('#/(v1|openai/v1)$#i', $api_base)) {
|
|
||||||
$api_base .= '/v1';
|
|
||||||
}
|
|
||||||
|
|
||||||
return rtrim($api_base, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generate_via_theme(string $prompt, string $system, array $options): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']);
|
|
||||||
$temperature = (float) ($options['temperature'] ?? 0.7);
|
|
||||||
$api_base = $this->normalize_api_base((string) $config['api_base']);
|
|
||||||
|
|
||||||
$response = wp_remote_post($api_base . '/chat/completions', [
|
|
||||||
'headers' => [
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
'Authorization' => 'Bearer ' . $config['api_key'],
|
|
||||||
],
|
|
||||||
'body' => wp_json_encode([
|
|
||||||
'model' => $config['model'],
|
|
||||||
'messages' => [
|
|
||||||
['role' => 'system', 'content' => $system],
|
|
||||||
['role' => 'user', 'content' => $prompt],
|
|
||||||
],
|
|
||||||
'max_tokens' => $max_tokens,
|
|
||||||
'temperature' => $temperature,
|
|
||||||
]),
|
|
||||||
'timeout' => 45,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => sprintf(
|
|
||||||
/* translators: %s: error message */
|
|
||||||
__('API 请求失败: %s', 'aksrt'),
|
|
||||||
$response->get_error_message()
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
|
||||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
|
||||||
|
|
||||||
if ($status_code !== 200) {
|
|
||||||
$error_msg = is_array($body) ? ($body['error']['message'] ?? __('未知错误', 'aksrt')) : __('未知错误', 'aksrt');
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => sprintf(
|
|
||||||
/* translators: 1: HTTP status, 2: error message */
|
|
||||||
__('API 错误 (%1$d): %2$s', 'aksrt'),
|
|
||||||
$status_code,
|
|
||||||
$error_msg
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = is_array($body) ? ($body['choices'][0]['message'] ?? []) : [];
|
|
||||||
$content = is_array($message) ? (string) ($message['content'] ?? '') : '';
|
|
||||||
|
|
||||||
if ($content === '' && is_array($message) && !empty($message['reasoning_content'])) {
|
|
||||||
$content = (string) $message['reasoning_content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = trim($content);
|
|
||||||
if ($content === '') {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('AI 返回内容为空(可能 max_tokens 不足)', 'aksrt'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'content' => $content,
|
|
||||||
'reasoning' => is_array($message) ? (string) ($message['reasoning_content'] ?? '') : '',
|
|
||||||
'usage' => is_array($body) ? ($body['usage'] ?? []) : [],
|
|
||||||
'finish_reason' => is_array($body) ? (string) ($body['choices'][0]['finish_reason'] ?? '') : '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function check_rate_limit(): bool {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$limit = (int) $config['rate_limit'];
|
|
||||||
if ($limit <= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
|
||||||
$count = (int) get_transient($key);
|
|
||||||
|
|
||||||
return $count < $limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function record_call(): void {
|
|
||||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
|
||||||
$count = (int) get_transient($key);
|
|
||||||
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,31 +77,6 @@ class AIPrompt {
|
|||||||
6. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
6. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容旧单 prompt 构建(草稿/无 ID 场景)。
|
|
||||||
*/
|
|
||||||
public function build_seo_prompt(string $title, string $content): string {
|
|
||||||
$title_instruction = $this->get_seo_title_system_instruction();
|
|
||||||
$kw_instruction = $this->get_seo_keywords_system_instruction();
|
|
||||||
$desc_instruction = $this->get_seo_description_system_instruction();
|
|
||||||
|
|
||||||
return <<<PROMPT
|
|
||||||
请根据以下文章内容,生成 SEO 优化建议。
|
|
||||||
|
|
||||||
文章标题:{$title}
|
|
||||||
|
|
||||||
文章内容:
|
|
||||||
{$content}
|
|
||||||
|
|
||||||
请以 JSON 格式返回,包含以下字段:
|
|
||||||
- suggested_title: {$title_instruction}
|
|
||||||
- suggested_description: {$desc_instruction}
|
|
||||||
- suggested_keywords: {$kw_instruction}(以 JSON 数组形式返回)
|
|
||||||
|
|
||||||
只返回 JSON,不要其他内容。
|
|
||||||
PROMPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
||||||
$content = mb_substr($content, 0, 3000);
|
$content = mb_substr($content, 0, 3000);
|
||||||
return <<<PROMPT
|
return <<<PROMPT
|
||||||
|
|||||||
@@ -107,67 +107,6 @@ class AISEO {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 草稿场景:用传入 title/content 生成(兼容旧 /ai/seo-suggest)。
|
|
||||||
*/
|
|
||||||
public function suggest(string $title, string $content = ''): array {
|
|
||||||
if (!$this->is_enabled()) {
|
|
||||||
return ['success' => false, 'message' => __('AI SEO 功能未启用', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists('AKSRT\\Ai\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
|
||||||
return ['success' => false, 'message' => __('AI 模块未加载', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ai = AI::get_instance();
|
|
||||||
if (!$ai->is_available()) {
|
|
||||||
return ['success' => false, 'message' => __('AI 服务未启用', 'aksrt')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = trim($title);
|
|
||||||
$content = trim(wp_strip_all_tags($content));
|
|
||||||
if ($title === '' && $content === '') {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'message' => __('无可用内容,请先填写标题或正文', 'aksrt'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
if ($title !== '') {
|
|
||||||
$parts[] = '标题: ' . $title;
|
|
||||||
}
|
|
||||||
if ($content !== '') {
|
|
||||||
$content = wp_trim_words($content, 1500, '');
|
|
||||||
$parts[] = "正文:\n" . $content;
|
|
||||||
}
|
|
||||||
$context = implode("\n\n", $parts);
|
|
||||||
$prompt = AIPrompt::get_instance();
|
|
||||||
|
|
||||||
$title_out = $this->run_field($ai, $context, $prompt->get_seo_title_system_instruction(), 'title');
|
|
||||||
$kw_out = $this->run_field($ai, $context, $prompt->get_seo_keywords_system_instruction(), 'keywords');
|
|
||||||
$desc_out = $this->run_field($ai, $context, $prompt->get_seo_description_system_instruction(), 'description');
|
|
||||||
|
|
||||||
$keywords = [];
|
|
||||||
if (!empty($kw_out['content'])) {
|
|
||||||
$keywords = array_values(array_filter(array_map('trim', explode(',', $kw_out['content']))));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'suggested_title' => (string) ($title_out['content'] ?? ''),
|
|
||||||
'suggested_description' => (string) ($desc_out['content'] ?? ''),
|
|
||||||
'suggested_keywords' => $keywords,
|
|
||||||
'fields' => [
|
|
||||||
'title' => $title_out,
|
|
||||||
'keywords' => $kw_out,
|
|
||||||
'description' => $desc_out,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string|\WP_Error
|
* @return string|\WP_Error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -210,16 +210,16 @@ class SEOMetaBox {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
$status = class_exists('\AKSRT\Ai\AI') ? \AKSRT\Ai\AI::get_instance()->get_status() : [];
|
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||||
|
|
||||||
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
||||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||||
'nonce' => wp_create_nonce('wp_rest'),
|
'nonce' => wp_create_nonce('wp_rest'),
|
||||||
'ai' => [
|
'ai' => [
|
||||||
'available' => !empty($status['available']),
|
'available' => $ai_available,
|
||||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
||||||
'write_assist' => !empty($status['available']),
|
'write_assist' => $ai_available,
|
||||||
],
|
],
|
||||||
'i18n' => [
|
'i18n' => [
|
||||||
'generating' => __('生成中…', 'aksrt'),
|
'generating' => __('生成中…', 'aksrt'),
|
||||||
@@ -262,7 +262,7 @@ class SEOMetaBox {
|
|||||||
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||||
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
||||||
$title = !$available
|
$title = !$available
|
||||||
? esc_attr__('请先配置 AI 连接(扩展 → AI 连接配置 或 WP Connectors)', 'aksrt')
|
? esc_attr__('请先配置 WordPress AI Connectors', 'aksrt')
|
||||||
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
|
|||||||
+275
-59
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
class SearchPush {
|
class SearchPush {
|
||||||
|
|
||||||
|
/** 推送结果 meta 键(编辑页面板与去重逻辑共用)。 */
|
||||||
|
public const META_KEY = '_aksrt_baidu_push_back';
|
||||||
|
|
||||||
|
/** 「重新提交」nonce 与复选框字段名(与 SearchPushMetaBox 共用)。 */
|
||||||
|
public const NONCE_ACTION = 'aksrt_push_meta_save';
|
||||||
|
public const NONCE_FIELD = 'aksrt_push_meta_nonce';
|
||||||
|
public const RESUBMIT_FIELD = 'aksrt_push_resubmit';
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
public static function get_instance(): self {
|
public static function get_instance(): self {
|
||||||
@@ -17,16 +25,20 @@ class SearchPush {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。
|
* 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。
|
||||||
*/
|
*/
|
||||||
public function init(): void {
|
public function init(): void {
|
||||||
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
|
add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3);
|
||||||
|
add_action('saved_term', [$this, 'on_saved_term'], 10, 3);
|
||||||
add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
|
add_action('wp_footer', [$this, 'output_auto_push_script'], 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文章发布/更新时主动推送链接到百度。
|
* 文章发布/更新时主动推送链接到百度。
|
||||||
*
|
*
|
||||||
|
* 与参考主题一致:推送结果写入 meta,成功后不再重复提交;
|
||||||
|
* 编辑页勾选「重新提交」可清空记录并强制再次推送。
|
||||||
|
*
|
||||||
* @param string $new_status 新状态。
|
* @param string $new_status 新状态。
|
||||||
* @param string $old_status 旧状态。
|
* @param string $old_status 旧状态。
|
||||||
* @param \WP_Post $post 文章对象。
|
* @param \WP_Post $post 文章对象。
|
||||||
@@ -42,18 +54,18 @@ class SearchPush {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 仅推送公开可索引的内容类型。
|
// 仅推送公开可索引的内容类型。
|
||||||
$allowed = function_exists('aksrt_searchable_post_types')
|
if (!in_array($post->post_type, self::pushable_post_types(), true)) {
|
||||||
? aksrt_searchable_post_types()
|
|
||||||
: ['post'];
|
|
||||||
if (!in_array($post->post_type, $allowed, true)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = $this->get_config();
|
$config = $this->get_config();
|
||||||
if (empty($config['baidu_push_enabled'])) {
|
if (!$this->should_push($config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->resubmit_requested()) {
|
||||||
|
self::clear_push_record($post->ID);
|
||||||
|
} else {
|
||||||
$is_new = $old_status !== 'publish';
|
$is_new = $old_status !== 'publish';
|
||||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
||||||
return;
|
return;
|
||||||
@@ -61,8 +73,211 @@ class SearchPush {
|
|||||||
if (!$is_new && empty($config['baidu_push_on_update'])) {
|
if (!$is_new && empty($config['baidu_push_on_update'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 已成功提交过的链接不再重复推送。
|
||||||
|
if ($this->record_is_complete(self::get_push_record($post->ID), $config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->push_url((string) get_permalink($post));
|
$this->push_and_record((string) get_permalink($post), $post->ID, false, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类法术语保存时主动推送链接到百度。
|
||||||
|
*
|
||||||
|
* 术语无发布/更新状态,沿用参考主题的 saved_term 触发,
|
||||||
|
* 与文章推送共用 meta 记录、去重与「重新提交」。
|
||||||
|
*
|
||||||
|
* @param int $term_id 术语 ID。
|
||||||
|
* @param int $tt_id 术语分类法 ID。
|
||||||
|
* @param string $taxonomy 分类法名称。
|
||||||
|
*/
|
||||||
|
public function on_saved_term(int $term_id, int $tt_id, string $taxonomy): void {
|
||||||
|
if (!in_array($taxonomy, self::pushable_taxonomies(), true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $this->get_config();
|
||||||
|
if (!$this->should_push($config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->resubmit_requested()) {
|
||||||
|
self::clear_push_record($term_id, true);
|
||||||
|
} elseif ($this->record_is_complete(self::get_push_record($term_id, true), $config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = get_term_link($term_id, $taxonomy);
|
||||||
|
if (is_wp_error($url) || $url === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->push_and_record((string) $url, $term_id, true, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可推送的文章类型(与站内搜索 / MeiliSearch 索引共用同一策略)。
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function pushable_post_types(): array {
|
||||||
|
$types = function_exists('aksrt_searchable_post_types') ? aksrt_searchable_post_types() : ['post'];
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter((array) $types)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可推送的分类法:可推送文章类型上的公开分类法(排除文章形式等)。
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function pushable_taxonomies(): array {
|
||||||
|
$taxonomies = get_object_taxonomies(self::pushable_post_types(), 'names');
|
||||||
|
$taxonomies = array_diff((array) $taxonomies, ['post_format', 'nav_menu']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤百度推送覆盖的分类法。
|
||||||
|
*
|
||||||
|
* @param string[] $taxonomies 分类法 slug 列表。
|
||||||
|
*/
|
||||||
|
return array_values(array_unique(apply_filters('aksrt_baidu_push_taxonomies', $taxonomies)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送功能是否已启用且配置完整(决定编辑页面板是否展示)。
|
||||||
|
*/
|
||||||
|
public function is_ready(): bool {
|
||||||
|
return $this->should_push($this->get_config());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文章或术语的推送记录。
|
||||||
|
*
|
||||||
|
* @param int $object_id 文章 / 术语 ID。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
* @return array 记录为空时返回 []。
|
||||||
|
*/
|
||||||
|
public static function get_push_record(int $object_id, bool $is_term = false): array {
|
||||||
|
$record = $is_term ? get_term_meta($object_id, self::META_KEY, true) : get_post_meta($object_id, self::META_KEY, true);
|
||||||
|
|
||||||
|
return is_array($record) ? $record : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入推送记录。
|
||||||
|
*
|
||||||
|
* @param int $object_id 文章 / 术语 ID。
|
||||||
|
* @param array $record 推送记录。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
*/
|
||||||
|
public static function save_push_record(int $object_id, array $record, bool $is_term = false): void {
|
||||||
|
if ($is_term) {
|
||||||
|
update_term_meta($object_id, self::META_KEY, $record);
|
||||||
|
} else {
|
||||||
|
update_post_meta($object_id, self::META_KEY, $record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除推送记录(「重新提交」时调用)。
|
||||||
|
*
|
||||||
|
* @param int $object_id 文章 / 术语 ID。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
*/
|
||||||
|
public static function clear_push_record(int $object_id, bool $is_term = false): void {
|
||||||
|
if ($is_term) {
|
||||||
|
delete_term_meta($object_id, self::META_KEY);
|
||||||
|
} else {
|
||||||
|
delete_post_meta($object_id, self::META_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送是否可用:总开关开启、Token 已配置且至少启用一个收录通道。
|
||||||
|
*
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
*/
|
||||||
|
private function should_push(array $config): bool {
|
||||||
|
if (empty($config['baidu_push_enabled'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((string) ($config['baidu_push_token'] ?? '') === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($config['baidu_push_normal']) || !empty($config['baidu_push_fast']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送 URL 并把结果写入 meta(成功记录用于去重)。
|
||||||
|
*
|
||||||
|
* @param string $url 待推送链接。
|
||||||
|
* @param int $object_id 文章 / 术语 ID。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
*/
|
||||||
|
private function push_and_record(string $url, int $object_id, bool $is_term, array $config): void {
|
||||||
|
if ($url === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outcome = $this->push_url($url);
|
||||||
|
self::save_push_record($object_id, $this->build_record($outcome, $config), $is_term);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装推送记录:通道结果 + 更新时间(结构与参考主题 meta 对齐)。
|
||||||
|
*
|
||||||
|
* @param array $outcome push_url() 返回值。
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
*/
|
||||||
|
private function build_record(array $outcome, array $config): array {
|
||||||
|
$record = ['update_time' => current_time('Y-m-d H:i:s')];
|
||||||
|
$result = isset($outcome['result']) && is_array($outcome['result']) ? $outcome['result'] : [];
|
||||||
|
|
||||||
|
if (!empty($config['baidu_push_normal'])) {
|
||||||
|
$channel = $result['normal'] ?? null;
|
||||||
|
$record['normal_push'] = is_array($channel) && !empty($channel['success']);
|
||||||
|
$record['normal_result'] = $channel;
|
||||||
|
}
|
||||||
|
if (!empty($config['baidu_push_fast'])) {
|
||||||
|
$channel = $result['daily'] ?? null;
|
||||||
|
$record['daily_push'] = is_array($channel) && !empty($channel['success']);
|
||||||
|
$record['daily_result'] = $channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送记录是否已覆盖全部启用通道(去重判断)。
|
||||||
|
*
|
||||||
|
* @param array $record 推送记录。
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
*/
|
||||||
|
private function record_is_complete(array $record, array $config): bool {
|
||||||
|
if (!empty($config['baidu_push_normal']) && empty($record['normal_push'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!empty($config['baidu_push_fast']) && empty($record['daily_push'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑页是否勾选了「重新提交」(需通过 metabox nonce 校验)。
|
||||||
|
*/
|
||||||
|
private function resubmit_requested(): bool {
|
||||||
|
if (empty($_POST[self::RESUBMIT_FIELD])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonce = isset($_POST[self::NONCE_FIELD]) ? sanitize_text_field(wp_unslash($_POST[self::NONCE_FIELD])) : '';
|
||||||
|
|
||||||
|
return (bool) wp_verify_nonce($nonce, self::NONCE_ACTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,7 +311,50 @@ class SearchPush {
|
|||||||
return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []];
|
return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
// 收录通道与百度接口 type 参数对应:普通收录不带参数,快速收录为 daily。
|
||||||
|
$channels = [];
|
||||||
|
if (!empty($config['baidu_push_normal'])) {
|
||||||
|
$channels['normal'] = '';
|
||||||
|
}
|
||||||
|
if (!empty($config['baidu_push_fast'])) {
|
||||||
|
$channels['daily'] = '&type=daily';
|
||||||
|
}
|
||||||
|
if (!$channels) {
|
||||||
|
return ['success' => false, 'message' => __('未启用普通收录或快速收录通道', 'aksrt'), 'result' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = false;
|
||||||
|
$results = [];
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
foreach ($channels as $channel => $suffix) {
|
||||||
|
$outcome = $this->submit($url, $token, $suffix, $config);
|
||||||
|
$results[$channel] = $outcome['result'];
|
||||||
|
if ($outcome['success']) {
|
||||||
|
$success = true;
|
||||||
|
} else {
|
||||||
|
$messages[] = $outcome['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $results];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => implode('; ', $messages), 'result' => $results];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向百度链接提交接口发送单条 URL。
|
||||||
|
*
|
||||||
|
* @param string $url 待推送的完整 URL。
|
||||||
|
* @param string $token 百度准入密钥。
|
||||||
|
* @param string $suffix 接口附加参数(快速收录为 &type=daily)。
|
||||||
|
* @param array $config 推送配置。
|
||||||
|
* @return array{success: bool, message: string, result: array}
|
||||||
|
*/
|
||||||
|
private function submit(string $url, string $token, string $suffix, array $config): array {
|
||||||
|
$api_url = 'http://data.zz.baidu.com/urls?site=' . rawurlencode(home_url('/')) . '&token=' . $token . $suffix;
|
||||||
|
|
||||||
$response = wp_remote_post($api_url, [
|
$response = wp_remote_post($api_url, [
|
||||||
'timeout' => 10,
|
'timeout' => 10,
|
||||||
@@ -105,7 +363,7 @@ class SearchPush {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
if (is_wp_error($response)) {
|
||||||
$this->log('error', $url, $response->get_error_message());
|
$this->log($config, 'error', $url, $response->get_error_message());
|
||||||
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
|
return ['success' => false, 'message' => $response->get_error_message(), 'result' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,59 +371,15 @@ class SearchPush {
|
|||||||
$status = wp_remote_retrieve_response_code($response);
|
$status = wp_remote_retrieve_response_code($response);
|
||||||
|
|
||||||
if ($status === 200 && isset($body['success'])) {
|
if ($status === 200 && isset($body['success'])) {
|
||||||
$this->log('success', $url, wp_json_encode($body));
|
$this->log($config, 'success', $url, wp_json_encode($body));
|
||||||
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
|
return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body];
|
||||||
}
|
}
|
||||||
|
|
||||||
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
|
$err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status));
|
||||||
$this->log('error', $url, $err_msg);
|
$this->log($config, 'error', $url, $err_msg);
|
||||||
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
|
return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量推送 URL
|
|
||||||
*
|
|
||||||
* @param string[] $urls
|
|
||||||
* @return array{success: int, failed: int, results: array}
|
|
||||||
*/
|
|
||||||
public function push_batch(array $urls): array {
|
|
||||||
$config = $this->get_config();
|
|
||||||
$batch_size = max(1, min(500, (int) ($config['baidu_push_batch_size'] ?? 50)));
|
|
||||||
$success = 0;
|
|
||||||
$failed = 0;
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
$chunks = array_chunk($urls, $batch_size);
|
|
||||||
foreach ($chunks as $chunk) {
|
|
||||||
$token = (string) ($config['baidu_push_token'] ?? '');
|
|
||||||
$api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}";
|
|
||||||
$body_str = implode("\n", $chunk);
|
|
||||||
|
|
||||||
$response = wp_remote_post($api_url, [
|
|
||||||
'timeout' => 15,
|
|
||||||
'headers' => ['Content-Type' => 'text/plain'],
|
|
||||||
'body' => $body_str,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
$failed += count($chunk);
|
|
||||||
$results[] = ['error' => $response->get_error_message()];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
|
||||||
if (isset($body['success'])) {
|
|
||||||
$success += (int) ($body['success'] ?? 0);
|
|
||||||
$failed += (int) ($body['not_same_site'] ?? 0) + (int) ($body['not_valid'] ?? 0);
|
|
||||||
} else {
|
|
||||||
$failed += count($chunk);
|
|
||||||
}
|
|
||||||
$results[] = $body ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => $success, 'failed' => $failed, 'results' => $results];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取自动推送 JS 代码
|
* 获取自动推送 JS 代码
|
||||||
*/
|
*/
|
||||||
@@ -175,7 +389,6 @@ class SearchPush {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$site_url = home_url('/');
|
|
||||||
return <<<SCRIPT
|
return <<<SCRIPT
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
@@ -211,13 +424,15 @@ SCRIPT;
|
|||||||
'provider' => 'baidu',
|
'provider' => 'baidu',
|
||||||
'configured' => $enabled && $token !== '',
|
'configured' => $enabled && $token !== '',
|
||||||
'available' => $enabled && $token !== '',
|
'available' => $enabled && $token !== '',
|
||||||
'verified' => !empty($config['baidu_push_verified']),
|
// 百度链接提交接口无单独校验握手,Token 配置完成即可推送。
|
||||||
|
'verified' => false,
|
||||||
'transport' => 'http_api',
|
'transport' => 'http_api',
|
||||||
'missing_fields' => $missing,
|
'missing_fields' => $missing,
|
||||||
'auto_push' => !empty($config['baidu_push_auto']),
|
'auto_push' => !empty($config['baidu_push_auto']),
|
||||||
|
'normal_push' => !empty($config['baidu_push_normal']),
|
||||||
|
'fast_push' => !empty($config['baidu_push_fast']),
|
||||||
'on_publish' => !empty($config['baidu_push_on_publish']),
|
'on_publish' => !empty($config['baidu_push_on_publish']),
|
||||||
'on_update' => !empty($config['baidu_push_on_update']),
|
'on_update' => !empty($config['baidu_push_on_update']),
|
||||||
'batch_size' => max(1, (int) ($config['baidu_push_batch_size'] ?? 50)),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +443,8 @@ SCRIPT;
|
|||||||
return ['baidu_push_enabled' => false];
|
return ['baidu_push_enabled' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function log(string $level, string $url, string $message): void {
|
private function log(array $config, string $level, string $url, string $message): void {
|
||||||
|
if (empty($config['baidu_push_log_enabled'])) return;
|
||||||
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
if (!defined('WP_DEBUG') || !WP_DEBUG) return;
|
||||||
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
|
error_log(sprintf('[AKSRT Search Push] [%s] URL: %s — %s', $level, $url, $message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Extensions;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度推送编辑页面板(对齐参考主题「百度资源提交」Metabox)。
|
||||||
|
*
|
||||||
|
* 文章:侧栏 Metabox;分类法:编辑 / 新增表单行。
|
||||||
|
* 展示推送结果与更新时间,并提供「重新提交」勾选——
|
||||||
|
* 勾选后保存会清空去重记录并强制再次推送。
|
||||||
|
*/
|
||||||
|
class SearchPushMetaBox {
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_action('add_meta_boxes', [$this, 'register_post_meta_box']);
|
||||||
|
// 分类法可能由插件延后注册,面板在 admin_init 统一挂载。
|
||||||
|
add_action('admin_init', [$this, 'register_term_panels']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为可推送文章类型注册侧栏 Metabox(推送功能就绪后才展示)。
|
||||||
|
*/
|
||||||
|
public function register_post_meta_box(): void {
|
||||||
|
if (!$this->is_ready()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SearchPush::pushable_post_types() as $post_type) {
|
||||||
|
add_meta_box(
|
||||||
|
'aksrt_baidu_push',
|
||||||
|
__('百度资源提交', 'aksrt'),
|
||||||
|
[$this, 'render_post_panel'],
|
||||||
|
$post_type,
|
||||||
|
'side',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为可推送分类法注册编辑 / 新增表单面板。
|
||||||
|
*/
|
||||||
|
public function register_term_panels(): void {
|
||||||
|
if (!$this->is_ready()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SearchPush::pushable_taxonomies() as $taxonomy) {
|
||||||
|
add_action("{$taxonomy}_edit_form_fields", [$this, 'render_term_panel'], 20);
|
||||||
|
add_action("{$taxonomy}_add_form_fields", [$this, 'render_term_panel'], 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染文章 Metabox。
|
||||||
|
*
|
||||||
|
* @param mixed $post WP_Post 或 null。
|
||||||
|
*/
|
||||||
|
public function render_post_panel($post): void {
|
||||||
|
if (!$post instanceof \WP_Post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render_panel((int) $post->ID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染分类法表单行(新增表单无术语记录,仅展示提示)。
|
||||||
|
*
|
||||||
|
* @param mixed $term WP_Term、分类名字符串或 null。
|
||||||
|
*/
|
||||||
|
public function render_term_panel($term = null): void {
|
||||||
|
$term_id = 0;
|
||||||
|
if ($term instanceof \WP_Term) {
|
||||||
|
$term_id = (int) $term->term_id;
|
||||||
|
} elseif (isset($_GET['tag_ID'])) {
|
||||||
|
$term_id = (int) $_GET['tag_ID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增表单无表格结构,仅编辑表单以表单行包裹。
|
||||||
|
$this->render_panel($term_id, true, $term_id > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function is_ready(): bool {
|
||||||
|
return class_exists(SearchPush::class) && SearchPush::get_instance()->is_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染推送结果面板。
|
||||||
|
*
|
||||||
|
* @param int $object_id 文章 / 术语 ID(0 表示新增表单)。
|
||||||
|
* @param bool $is_term 是否为术语。
|
||||||
|
* @param bool $as_row 是否以分类法表单行包裹。
|
||||||
|
*/
|
||||||
|
private function render_panel(int $object_id, bool $is_term, bool $as_row = false): void {
|
||||||
|
ob_start();
|
||||||
|
wp_nonce_field(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD);
|
||||||
|
|
||||||
|
$record = $object_id > 0 ? SearchPush::get_push_record($object_id, $is_term) : [];
|
||||||
|
$lines = $this->result_lines($record);
|
||||||
|
?>
|
||||||
|
<div class="aksrt-push-meta-box">
|
||||||
|
<?php if ($lines) : ?>
|
||||||
|
<p class="aksrt-push-meta-box__label"><strong><?php esc_html_e('提交结果:', 'aksrt'); ?></strong></p>
|
||||||
|
<ul class="aksrt-push-meta-box__results">
|
||||||
|
<?php foreach ($lines as $line) : ?>
|
||||||
|
<li><strong><?php echo esc_html($line[0]); ?></strong> <span><?php echo esc_html($line[1]); ?></span></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else : ?>
|
||||||
|
<p class="description"><?php esc_html_e('发布、更新后刷新页面后可查看提交结果', 'aksrt'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($record['update_time'])) : ?>
|
||||||
|
<p class="aksrt-push-meta-box__resubmit">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="<?php echo esc_attr(SearchPush::RESUBMIT_FIELD); ?>" value="1" />
|
||||||
|
<?php esc_html_e('重新提交', 'aksrt'); ?>
|
||||||
|
</label>
|
||||||
|
<span class="description"><?php esc_html_e('勾选后保存将清空提交记录并再次推送', 'aksrt'); ?></span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$body = (string) ob_get_clean();
|
||||||
|
|
||||||
|
if ($as_row) {
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||||
|
echo '<tr class="form-field"><th scope="row">' . esc_html(__('百度资源提交', 'aksrt')) . '</th><td>' . $body . '</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 面板 HTML 已在输出前逐项转义。
|
||||||
|
echo $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总推送结果行(展示项与参考主题对齐)。
|
||||||
|
*
|
||||||
|
* @param array $record 推送记录。
|
||||||
|
* @return array<int,array{0:string,1:string}>
|
||||||
|
*/
|
||||||
|
private function result_lines(array $record): array {
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
if (!empty($record['normal_push'])) {
|
||||||
|
$lines[] = [__('普通收录:成功', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||||
|
} elseif (array_key_exists('normal_push', $record)) {
|
||||||
|
$lines[] = [__('普通收录:失败', 'aksrt'), $this->format_result($record['normal_result'] ?? null)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($record['daily_push'])) {
|
||||||
|
$lines[] = [__('快速收录:成功', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||||
|
} elseif (array_key_exists('daily_push', $record)) {
|
||||||
|
$lines[] = [__('快速收录:失败', 'aksrt'), $this->format_result($record['daily_result'] ?? null)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($record['update_time'])) {
|
||||||
|
$lines[] = [__('更新时间:', 'aksrt'), (string) $record['update_time']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化接口返回结果。
|
||||||
|
*
|
||||||
|
* @param mixed $result 百度接口返回的解码数据。
|
||||||
|
*/
|
||||||
|
private function format_result($result): string {
|
||||||
|
if (is_array($result) || is_object($result)) {
|
||||||
|
return (string) wp_json_encode($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,6 @@ trait DisplayPageGroups {
|
|||||||
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
|
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
|
||||||
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')),
|
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_prompt'] ?? '')),
|
||||||
'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')),
|
'ai_seo_description_prompt' => trim((string) ($s['ai_seo_description_prompt'] ?? '')),
|
||||||
'seo_baidu_push' => !empty($s['seo_baidu_push']) || !empty($s['baidu_push_enabled']),
|
|
||||||
'seo_baidu_normal' => !empty($s['seo_baidu_normal']),
|
|
||||||
'seo_baidu_fast' => !empty($s['seo_baidu_fast']),
|
|
||||||
'seo_baidu_token' => (string) (($s['seo_baidu_token'] ?? '') ?: ($s['baidu_push_token'] ?? '')),
|
|
||||||
'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']),
|
'seo_image_optimize' => !isset($s['seo_image_optimize']) || !empty($s['seo_image_optimize']),
|
||||||
'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')),
|
'seo_image_default' => esc_url_raw((string) ($s['seo_image_default'] ?? '')),
|
||||||
'seo_image_always_default'=> !empty($s['seo_image_always_default']),
|
'seo_image_always_default'=> !empty($s['seo_image_always_default']),
|
||||||
|
|||||||
@@ -128,29 +128,15 @@ class ExtensionRuntime {
|
|||||||
public static function get_search_push_settings(): array {
|
public static function get_search_push_settings(): array {
|
||||||
$settings = Settings::get_all();
|
$settings = Settings::get_all();
|
||||||
|
|
||||||
$enabled = !empty($settings['baidu_push_enabled']) || !empty($settings['seo_baidu_push']);
|
|
||||||
$token = (string) ($settings['baidu_push_token'] ?? '');
|
|
||||||
if ($token === '' && !empty($settings['seo_baidu_token'])) {
|
|
||||||
$token = (string) $settings['seo_baidu_token'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'baidu_push_enabled' => $enabled,
|
'baidu_push_enabled' => !empty($settings['baidu_push_enabled']),
|
||||||
'baidu_push_token' => $token,
|
'baidu_push_token' => (string) ($settings['baidu_push_token'] ?? ''),
|
||||||
'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']),
|
'baidu_push_auto' => !isset($settings['baidu_push_auto']) || !empty($settings['baidu_push_auto']),
|
||||||
|
'baidu_push_normal' => !isset($settings['baidu_push_normal']) || !empty($settings['baidu_push_normal']),
|
||||||
|
'baidu_push_fast' => !empty($settings['baidu_push_fast']),
|
||||||
'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']),
|
'baidu_push_on_publish' => !isset($settings['baidu_push_on_publish']) || !empty($settings['baidu_push_on_publish']),
|
||||||
'baidu_push_on_update' => !empty($settings['baidu_push_on_update']),
|
'baidu_push_on_update' => !empty($settings['baidu_push_on_update']),
|
||||||
'baidu_push_batch_size' => max(1, min(500, (int) ($settings['baidu_push_batch_size'] ?? 50))),
|
|
||||||
'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']),
|
'baidu_push_log_enabled' => !isset($settings['baidu_push_log_enabled']) || !empty($settings['baidu_push_log_enabled']),
|
||||||
'baidu_push_verified' => !empty($settings['baidu_push_verified']),
|
|
||||||
'xzh_on' => !empty($settings['xzh_on']),
|
|
||||||
'xzh_appid' => (string) ($settings['xzh_appid'] ?? ''),
|
|
||||||
'xzh_render_tail' => !isset($settings['xzh_render_tail']) || !empty($settings['xzh_render_tail']),
|
|
||||||
'xzh_jsonld_single' => !isset($settings['xzh_jsonld_single']) || !empty($settings['xzh_jsonld_single']),
|
|
||||||
'xzh_jsonld_page' => !empty($settings['xzh_jsonld_page']),
|
|
||||||
'xzh_jsonld_img' => !empty($settings['xzh_jsonld_img']),
|
|
||||||
'seo_baidu_normal' => !empty($settings['seo_baidu_normal']),
|
|
||||||
'seo_baidu_fast' => !empty($settings['seo_baidu_fast']),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ final class UserRenderer {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if ($args['show_checkin'] && $cuid === $user_id) {
|
if ($args['show_checkin'] && $cuid === $user_id) {
|
||||||
$checkin = aksrt_header_user_checkin_btn($user_id, true, 'img-badge jb-yellow');
|
$checkin = aksrt_header_user_checkin_btn($user_id, 'img-badge jb-yellow');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($args['show_button']) {
|
if ($args['show_button']) {
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ if (file_exists($js)) {
|
|||||||
],
|
],
|
||||||
|
|
||||||
'imageUploadMultiple' => $image_upload_multiple,
|
'imageUploadMultiple' => $image_upload_multiple,
|
||||||
'ai' => [
|
|
||||||
'available' => !empty($ctx['ai_available']),
|
|
||||||
'summary' => !empty($ctx['ai_available']),
|
|
||||||
'tags' => !empty($ctx['ai_available']),
|
|
||||||
],
|
|
||||||
'editor' => [
|
'editor' => [
|
||||||
'canUploadImage' => !empty($ctx['can_upload_img']),
|
'canUploadImage' => !empty($ctx['can_upload_img']),
|
||||||
'canUploadVideo' => !empty($ctx['can_upload_video']),
|
'canUploadVideo' => !empty($ctx['can_upload_video']),
|
||||||
@@ -142,10 +137,6 @@ if (file_exists($js)) {
|
|||||||
'submitted' => __('投稿已提交', 'aksrt'),
|
'submitted' => __('投稿已提交', 'aksrt'),
|
||||||
'error' => __('操作失败,请重试', 'aksrt'),
|
'error' => __('操作失败,请重试', 'aksrt'),
|
||||||
'confirmDelete' => __('确定要删除这个草稿吗?', 'aksrt'),
|
'confirmDelete' => __('确定要删除这个草稿吗?', 'aksrt'),
|
||||||
'aiGenerating' => __('生成中…', 'aksrt'),
|
|
||||||
'aiFailed' => __('AI 生成失败', 'aksrt'),
|
|
||||||
'needContent' => __('请先填写正文内容', 'aksrt'),
|
|
||||||
'aiFilled' => __('已填入,可继续编辑', 'aksrt'),
|
|
||||||
'deleting' => __('正在删除草稿…', 'aksrt'),
|
'deleting' => __('正在删除草稿…', 'aksrt'),
|
||||||
'deleted' => __('草稿已删除', 'aksrt'),
|
'deleted' => __('草稿已删除', 'aksrt'),
|
||||||
'uploading' => __('正在上传…', 'aksrt'),
|
'uploading' => __('正在上传…', 'aksrt'),
|
||||||
|
|||||||
@@ -177,8 +177,6 @@ if ($is_guest) {
|
|||||||
$submit_label = __('保存', 'aksrt');
|
$submit_label = __('保存', 'aksrt');
|
||||||
}
|
}
|
||||||
|
|
||||||
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'front' => $front,
|
'front' => $front,
|
||||||
'user_id' => $user_id,
|
'user_id' => $user_id,
|
||||||
@@ -207,7 +205,6 @@ return [
|
|||||||
'vip' => $vip,
|
'vip' => $vip,
|
||||||
'points_enabled' => $points_enabled,
|
'points_enabled' => $points_enabled,
|
||||||
'submit_label' => $submit_label,
|
'submit_label' => $submit_label,
|
||||||
'ai_available' => $ai_available,
|
|
||||||
'can_pay' => $cap('new_post_pay'),
|
'can_pay' => $cap('new_post_pay'),
|
||||||
'can_pay_download' => $cap('new_post_pay_download'),
|
'can_pay_download' => $cap('new_post_pay_download'),
|
||||||
'can_cover_image' => $cap('new_post_image_cover'),
|
'can_cover_image' => $cap('new_post_image_cover'),
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** AI 连接配置页已移除:主题只走 WordPress AI Connectors,生成能力与入口保留。 */
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
|
||||||
|
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||||||
|
|
||||||
|
const ai = read('src/Ai/AI.php');
|
||||||
|
const controller = read('src/Admin/AIController.php');
|
||||||
|
const seoSchema = read('assets/admin/src/schema/sections/global/seo.js');
|
||||||
|
const extensionsMenu = read('assets/admin/src/constants/sections/extensions.js');
|
||||||
|
const apiModule = read('assets/admin/src/api/modules/extensions.js');
|
||||||
|
const frontSubmit = read('assets/js/front-submit.js');
|
||||||
|
const submitContext = read('templates/parts/submit/context.php');
|
||||||
|
const submitPage = read('templates/pages/page-submit.php');
|
||||||
|
|
||||||
|
const removed = [
|
||||||
|
'aksrt_ai_settings',
|
||||||
|
'AI 连接配置',
|
||||||
|
'AI连接配置',
|
||||||
|
'ExtensionAI',
|
||||||
|
'extensions-ai',
|
||||||
|
'/ai/settings',
|
||||||
|
'/ai/status',
|
||||||
|
'ai-assist-btn',
|
||||||
|
'扩展 → AI',
|
||||||
|
];
|
||||||
|
|
||||||
|
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return ['node_modules', 'build', 'dist'].includes(entry.name) ? [] : walk(full);
|
||||||
|
}
|
||||||
|
return [full];
|
||||||
|
});
|
||||||
|
|
||||||
|
const sources = ['src', 'assets/admin/src', 'templates', 'assets/js', 'inc']
|
||||||
|
.flatMap((dir) => walk(path.join(root, dir)));
|
||||||
|
|
||||||
|
test('后台菜单、路由与 API 中不再有 AI 连接配置页', () => {
|
||||||
|
assert.doesNotMatch(extensionsMenu, /extensions-ai|ExtensionAI|AI 连接配置/);
|
||||||
|
assert.doesNotMatch(apiModule, /getAiStatus|updateAiSettings|generateSeo|suggestSummary|suggestTags/);
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/extensions/ExtensionAI.vue')));
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/router/component-loaders/extensions.js')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AI.php 只保留 WP AI Client 单路出口', () => {
|
||||||
|
assert.doesNotMatch(ai, /PROVIDER_OPENAI|PROVIDER_NONE/);
|
||||||
|
assert.doesNotMatch(ai, /get_config|update_config/);
|
||||||
|
assert.doesNotMatch(ai, /generate_via_theme|normalize_api_base/);
|
||||||
|
assert.doesNotMatch(ai, /is_theme_provider_available|resolve_backend/);
|
||||||
|
assert.doesNotMatch(ai, /check_rate_limit|record_call/);
|
||||||
|
assert.match(ai, /wp_ai_client_prompt/);
|
||||||
|
assert.match(ai, /function is_wp_ai_available/);
|
||||||
|
assert.match(ai, /function is_available/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('生成路由保留,连接配置与旧草稿兼容路由移除', () => {
|
||||||
|
assert.match(controller, /\/ai\/seo-generate/);
|
||||||
|
assert.match(controller, /\/ai\/summary-suggest/);
|
||||||
|
assert.match(controller, /\/ai\/tags-suggest/);
|
||||||
|
assert.doesNotMatch(controller, /\/ai\/seo-suggest/);
|
||||||
|
assert.doesNotMatch(controller, /function get_status|function update_settings|function seo_suggest/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('残留文案不再指向已删除的配置页', () => {
|
||||||
|
for (const [name, source] of [['AI.php', ai], ['AIController.php', controller], ['seo.js', seoSchema]]) {
|
||||||
|
assert.doesNotMatch(source, /AI 连接配置/, `${name} 仍引用 AI 连接配置页`);
|
||||||
|
assert.doesNotMatch(source, /扩展 → AI/, `${name} 仍引导到扩展菜单`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('前台投稿 AI 辅助死代码已清理', () => {
|
||||||
|
assert.doesNotMatch(frontSubmit, /bindAi|ai-assist-btn|cfg\.ai/);
|
||||||
|
assert.doesNotMatch(submitContext, /ai_available/);
|
||||||
|
assert.doesNotMatch(submitPage, /'ai'\s*=>|aiGenerating|aiFailed|aiFilled|needContent/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('全仓源码不再引用已删除的 AI 连接配置', () => {
|
||||||
|
const offenders = sources.filter((file) => {
|
||||||
|
const text = fs.readFileSync(file, 'utf8');
|
||||||
|
return removed.some((pattern) => text.includes(pattern));
|
||||||
|
});
|
||||||
|
assert.deepEqual(offenders.map((file) => path.relative(root, file)), []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Baidu push harness.
|
||||||
|
*
|
||||||
|
* Drives the REAL SearchPush / SearchPushMetaBox against WP stubs so the
|
||||||
|
* reference-parity contract is covered end to end:
|
||||||
|
* - normal / daily channels hit data.zz.baidu.com (daily appends type=daily);
|
||||||
|
* - successful pushes are recorded in meta and never re-submitted;
|
||||||
|
* - failed pushes retry on update when baidu_push_on_update is on;
|
||||||
|
* - the 「重新提交」checkbox forces a push only with a valid metabox nonce;
|
||||||
|
* - terms push via saved_term (post_format and other taxonomies excluded);
|
||||||
|
* - the edit panel renders status lines, update time and the resubmit checkbox.
|
||||||
|
*
|
||||||
|
* Usage: php baidu-push-harness.php -> prints "Baidu push harness passed".
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
define('ABSPATH', __DIR__ . '/');
|
||||||
|
define('WP_DEBUG', false);
|
||||||
|
|
||||||
|
final class WP_Error {
|
||||||
|
public function __construct(private string $code, private string $message) {}
|
||||||
|
public function get_error_message(): string { return $this->message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_Post {
|
||||||
|
public int $ID = 0;
|
||||||
|
public string $post_type = 'post';
|
||||||
|
public string $post_status = 'publish';
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_Term {
|
||||||
|
public int $term_id = 0;
|
||||||
|
public string $taxonomy = 'category';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- WP function stubs ----------
|
||||||
|
function add_action(...$args) {}
|
||||||
|
function apply_filters($tag, $value) { return $value; }
|
||||||
|
function __($s, $d = null) { return (string) $s; }
|
||||||
|
function esc_html($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
|
||||||
|
function esc_attr($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }
|
||||||
|
function esc_html_e($s, $d = null) { echo esc_html($s); }
|
||||||
|
function wp_json_encode($v, $f = 0) { return (string) json_encode($v, $f); }
|
||||||
|
function wp_nonce_field($action, $name) { echo '<input type="hidden" name="' . $name . '" value="nonce" />'; }
|
||||||
|
function wp_unslash($s) { return $s; }
|
||||||
|
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
|
||||||
|
function wp_verify_nonce($nonce, $action) { return $nonce === 'valid-nonce' && $action === 'aksrt_push_meta_save'; }
|
||||||
|
function is_wp_error($t) { return $t instanceof WP_Error; }
|
||||||
|
function home_url($path = '/') { return 'https://example.test' . $path; }
|
||||||
|
function current_time($type = 'mysql') { return '2026-10-13 12:00:00'; }
|
||||||
|
function get_permalink($post) { return 'https://example.test/?p=' . (int) $post->ID; }
|
||||||
|
function get_term_link($term, $taxonomy = '') {
|
||||||
|
$id = is_object($term) ? (int) $term->term_id : (int) $term;
|
||||||
|
return 'https://example.test/?cat=' . $id;
|
||||||
|
}
|
||||||
|
function wp_is_post_revision($id) { return false; }
|
||||||
|
function wp_is_post_autosave($id) { return false; }
|
||||||
|
function aksrt_searchable_post_types(): array { return ['post']; }
|
||||||
|
function get_object_taxonomies($types, $output = 'names') { return ['post_tag', 'category', 'post_format']; }
|
||||||
|
|
||||||
|
// Outgoing HTTP: every call is captured so channel/URL/body can be asserted.
|
||||||
|
$GLOBALS['push_requests'] = [];
|
||||||
|
function wp_remote_post($url, $args = []) {
|
||||||
|
$GLOBALS['push_requests'][] = ['url' => (string) $url, 'body' => (string) ($args['body'] ?? '')];
|
||||||
|
return ['response' => ['code' => 200], 'body' => (string) json_encode(['success' => 1, 'remain' => 99])];
|
||||||
|
}
|
||||||
|
function wp_remote_retrieve_body($r) { return is_array($r) ? (string) ($r['body'] ?? '') : ''; }
|
||||||
|
function wp_remote_retrieve_response_code($r) { return is_array($r) ? (int) ($r['response']['code'] ?? 0) : 0; }
|
||||||
|
|
||||||
|
// In-memory meta stores.
|
||||||
|
$GLOBALS['push_post_meta'] = [];
|
||||||
|
$GLOBALS['push_term_meta'] = [];
|
||||||
|
function get_post_meta($id, $key, $single = false) { return $GLOBALS['push_post_meta'][$id][$key] ?? ''; }
|
||||||
|
function update_post_meta($id, $key, $value) { $GLOBALS['push_post_meta'][$id][$key] = $value; return true; }
|
||||||
|
function delete_post_meta($id, $key) { unset($GLOBALS['push_post_meta'][$id][$key]); return true; }
|
||||||
|
function get_term_meta($id, $key, $single = false) { return $GLOBALS['push_term_meta'][$id][$key] ?? ''; }
|
||||||
|
function update_term_meta($id, $key, $value) { $GLOBALS['push_term_meta'][$id][$key] = $value; return true; }
|
||||||
|
function delete_term_meta($id, $key) { unset($GLOBALS['push_term_meta'][$id][$key]); return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AKSRT\Settings {
|
||||||
|
class Settings {
|
||||||
|
public static array $config = [];
|
||||||
|
public static function get_search_push_settings(): array { return self::$config; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPush.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Extensions/SearchPushMetaBox.php';
|
||||||
|
|
||||||
|
use AKSRT\Extensions\SearchPush;
|
||||||
|
use AKSRT\Extensions\SearchPushMetaBox;
|
||||||
|
|
||||||
|
$failures = [];
|
||||||
|
$check = static function (string $label, bool $ok) use (&$failures): void {
|
||||||
|
if (!$ok) {
|
||||||
|
$failures[] = $label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$reset = static function (array $config): void {
|
||||||
|
\AKSRT\Settings\Settings::$config = $config;
|
||||||
|
$GLOBALS['push_requests'] = [];
|
||||||
|
$GLOBALS['push_post_meta'] = [];
|
||||||
|
$GLOBALS['push_term_meta'] = [];
|
||||||
|
$_POST = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
$post = static function (int $id): WP_Post {
|
||||||
|
$p = new WP_Post();
|
||||||
|
$p->ID = $id;
|
||||||
|
return $p;
|
||||||
|
};
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'baidu_push_enabled' => true,
|
||||||
|
'baidu_push_token' => 'TOKEN',
|
||||||
|
'baidu_push_auto' => true,
|
||||||
|
'baidu_push_on_publish' => true,
|
||||||
|
'baidu_push_on_update' => false,
|
||||||
|
'baidu_push_normal' => true,
|
||||||
|
'baidu_push_fast' => false,
|
||||||
|
'baidu_push_log_enabled' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$push = SearchPush::get_instance();
|
||||||
|
$api = 'http://data.zz.baidu.com/urls?site=' . rawurlencode('https://example.test/') . '&token=TOKEN';
|
||||||
|
|
||||||
|
// 1) First publish pushes once through the normal-collection endpoint.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(11));
|
||||||
|
$check('首次发布推送一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('普通收录请求地址正确', ($GLOBALS['push_requests'][0]['url'] ?? '') === $api);
|
||||||
|
$check('普通收录请求体为文章链接', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?p=11');
|
||||||
|
$record = SearchPush::get_push_record(11);
|
||||||
|
$check('推送记录写入 meta', !empty($record['normal_push']));
|
||||||
|
$check('推送记录带更新时间', !empty($record['update_time']));
|
||||||
|
$check('推送记录不含快速收录', !array_key_exists('daily_push', $record));
|
||||||
|
|
||||||
|
// 2) A completed push is never re-submitted on save.
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(11));
|
||||||
|
$check('成功后更新不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 3) Failed push retries on update when the update switch is on.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_on_update' => true]));
|
||||||
|
SearchPush::save_push_record(12, ['normal_push' => false, 'normal_result' => null, 'update_time' => '2026-10-13 09:00:00']);
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(12));
|
||||||
|
$check('失败后更新重试一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('重试后记录为成功', !empty(SearchPush::get_push_record(12)['normal_push']));
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(12));
|
||||||
|
$check('重试成功后不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 4) 「重新提交」with a valid nonce forces a push even after success.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(13));
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(13));
|
||||||
|
$check('重新提交强制再次推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 5) A forged nonce is ignored, dedup still applies.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(14));
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'forged-nonce';
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(14));
|
||||||
|
$check('无效 nonce 的重新提交被忽略', count($GLOBALS['push_requests']) === 1);
|
||||||
|
|
||||||
|
// 6) Fast collection appends type=daily and records both channels.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_fast' => true]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(15));
|
||||||
|
$check('双通道推送两次', count($GLOBALS['push_requests']) === 2);
|
||||||
|
$check('快速收录走 type=daily', str_contains($GLOBALS['push_requests'][1]['url'] ?? '', '&type=daily'));
|
||||||
|
$record15 = SearchPush::get_push_record(15);
|
||||||
|
$check('双通道记录完整', !empty($record15['normal_push']) && !empty($record15['daily_push']));
|
||||||
|
$push->on_transition_post_status('publish', 'publish', $post(15));
|
||||||
|
$check('双通道成功后不重复推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 7) No channel enabled: nothing pushed, nothing recorded.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_normal' => false]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(16));
|
||||||
|
$check('通道全关不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$check('通道全关不写记录', SearchPush::get_push_record(16) === []);
|
||||||
|
|
||||||
|
// 8) Feature off / token missing: nothing pushed, nothing recorded.
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_enabled' => false]));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(17));
|
||||||
|
$check('未启用不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$reset(array_merge($defaults, ['baidu_push_token' => '']));
|
||||||
|
$push->on_transition_post_status('publish', 'draft', $post(18));
|
||||||
|
$check('缺少 Token 不推送', count($GLOBALS['push_requests']) === 0);
|
||||||
|
$check('缺少 Token 不写记录', SearchPush::get_push_record(18) === []);
|
||||||
|
|
||||||
|
// 9) Terms push via saved_term with the same dedup / resubmit rules.
|
||||||
|
$reset($defaults);
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语推送一次', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$check('术语推送链接正确', ($GLOBALS['push_requests'][0]['body'] ?? '') === 'https://example.test/?cat=21');
|
||||||
|
$check('术语记录写入', !empty(SearchPush::get_push_record(21, true)['normal_push']));
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语成功后不重复推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$push->on_saved_term(22, 22, 'post_format');
|
||||||
|
$check('post_format 术语不推送', count($GLOBALS['push_requests']) === 1);
|
||||||
|
$_POST[SearchPush::RESUBMIT_FIELD] = '1';
|
||||||
|
$_POST[SearchPush::NONCE_FIELD] = 'valid-nonce';
|
||||||
|
$push->on_saved_term(21, 21, 'category');
|
||||||
|
$check('术语重新提交强制推送', count($GLOBALS['push_requests']) === 2);
|
||||||
|
|
||||||
|
// 10) Shared type / taxonomy policy.
|
||||||
|
$check('可推送文章类型', SearchPush::pushable_post_types() === ['post']);
|
||||||
|
$check('可推送分类法排除 post_format', SearchPush::pushable_taxonomies() === ['post_tag', 'category']);
|
||||||
|
|
||||||
|
// 11) Edit panel rendering.
|
||||||
|
$reset($defaults);
|
||||||
|
SearchPush::save_push_record(31, [
|
||||||
|
'normal_push' => true,
|
||||||
|
'normal_result' => ['success' => 1],
|
||||||
|
'daily_push' => false,
|
||||||
|
'daily_result' => null,
|
||||||
|
'update_time' => '2026-10-13 09:00:00',
|
||||||
|
]);
|
||||||
|
SearchPush::save_push_record(31, [
|
||||||
|
'normal_push' => true,
|
||||||
|
'normal_result' => ['success' => 1],
|
||||||
|
'daily_push' => false,
|
||||||
|
'daily_result' => null,
|
||||||
|
'update_time' => '2026-10-13 09:00:00',
|
||||||
|
], true);
|
||||||
|
$box = SearchPushMetaBox::get_instance();
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$box->render_post_panel($post(31));
|
||||||
|
$html = (string) ob_get_clean();
|
||||||
|
$check('面板含普通收录成功', str_contains($html, '普通收录:成功'));
|
||||||
|
$check('面板含快速收录失败', str_contains($html, '快速收录:失败'));
|
||||||
|
$check('面板含更新时间', str_contains($html, '2026-10-13 09:00:00'));
|
||||||
|
$check('面板含重新提交勾选', str_contains($html, 'name="aksrt_push_resubmit"'));
|
||||||
|
$check('面板含 nonce 字段', str_contains($html, 'name="aksrt_push_meta_nonce"'));
|
||||||
|
$check('面板转义接口 JSON', str_contains($html, '{"success":1}'));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$box->render_term_panel(null);
|
||||||
|
$addHtml = (string) ob_get_clean();
|
||||||
|
$check('新增术语表单仅提示', str_contains($addHtml, '发布、更新后刷新页面后可查看提交结果') && !str_contains($addHtml, '<tr'));
|
||||||
|
|
||||||
|
$term = new WP_Term();
|
||||||
|
$term->term_id = 31;
|
||||||
|
ob_start();
|
||||||
|
$box->render_term_panel($term);
|
||||||
|
$termHtml = (string) ob_get_clean();
|
||||||
|
$check('术语编辑表单为表单行', str_contains($termHtml, '<tr class="form-field">'));
|
||||||
|
$check('术语编辑表单含重新提交', str_contains($termHtml, 'name="aksrt_push_resubmit"'));
|
||||||
|
|
||||||
|
if ($failures) {
|
||||||
|
echo "Baidu push harness FAILED\n";
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
echo ' - ' . $failure . "\n";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Baidu push harness passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「百度推送」后台设置页契约(对齐参考主题 zibll):
|
||||||
|
* 1) PHP schema(SchemaExtensionsPush.php)与 JS schema(push.js)字段键、顺序、默认值一致;
|
||||||
|
* 2) 参考主题字段全集存在:自动推送 JS、普通收录、快速收录、准入 Token、发布/更新触发;
|
||||||
|
* 3) 双路/迁移字段(SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送 batch_*)已彻底移除,无双路兼容;
|
||||||
|
* 4) 运行时(SearchPush + ExtensionRuntime)只读取 canonical 键,快速收录走 type=daily;
|
||||||
|
* 5) 分类法 saved_term 推送、meta 去重、「重新提交」nonce 与编辑页面板均已接入,
|
||||||
|
* 并由 PHP harness 驱动真实逻辑验证。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaExtensionsPush.php'), 'utf8');
|
||||||
|
const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/extensions/push.js'), 'utf8');
|
||||||
|
const runtime = fs.readFileSync(path.join(root, 'src/Extensions/SearchPush.php'), 'utf8');
|
||||||
|
const extensionRuntime = fs.readFileSync(path.join(root, 'src/Settings/ExtensionRuntime.php'), 'utf8');
|
||||||
|
const globalPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
|
||||||
|
const seoJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/seo.js'), 'utf8');
|
||||||
|
const displayGroups = fs.readFileSync(path.join(root, 'src/Settings/DisplayPageGroups.php'), 'utf8');
|
||||||
|
const metaBox = fs.readFileSync(path.join(root, 'src/Extensions/SearchPushMetaBox.php'), 'utf8');
|
||||||
|
const functions = fs.readFileSync(path.join(root, 'functions.php'), 'utf8');
|
||||||
|
|
||||||
|
// 参考主题(zibll)百度推送字段全集,AKSRT 单一 canonical 配置面(扩展&增强 → 百度推送)。
|
||||||
|
const REFERENCE_KEYS = [
|
||||||
|
'baidu_push_enabled',
|
||||||
|
'baidu_push_token',
|
||||||
|
'baidu_push_auto',
|
||||||
|
'baidu_push_on_publish',
|
||||||
|
'baidu_push_on_update',
|
||||||
|
'baidu_push_normal',
|
||||||
|
'baidu_push_fast',
|
||||||
|
'baidu_push_log_enabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 双路 / 迁移 / 死代码字段:SEO 页 seo_baidu_*、熊掌号 xzh_*、批量推送与校验占位。
|
||||||
|
const DUAL_KEYS = [
|
||||||
|
'seo_baidu_push',
|
||||||
|
'seo_baidu_normal',
|
||||||
|
'seo_baidu_fast',
|
||||||
|
'seo_baidu_token',
|
||||||
|
'xzh_on',
|
||||||
|
'xzh_appid',
|
||||||
|
'xzh_render_tail',
|
||||||
|
'xzh_jsonld_single',
|
||||||
|
'xzh_jsonld_page',
|
||||||
|
'xzh_jsonld_img',
|
||||||
|
'baidu_push_batch_size',
|
||||||
|
'baidu_push_verified',
|
||||||
|
'push_batch',
|
||||||
|
'batch_size',
|
||||||
|
];
|
||||||
|
|
||||||
|
function phpFieldKeys(src) {
|
||||||
|
const found = [];
|
||||||
|
const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(src)) !== null) found.push(m[1]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsFieldKeys(src) {
|
||||||
|
const found = [];
|
||||||
|
const re = /key:\s*'([a-z0-9_]+)',\s*label:/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(src)) !== null) found.push(m[1]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('PHP push schema 字段键与顺序对齐参考主题', () => {
|
||||||
|
assert.deepEqual(phpFieldKeys(php), REFERENCE_KEYS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JS push schema 字段键与顺序对齐参考主题', () => {
|
||||||
|
assert.deepEqual(jsFieldKeys(js), REFERENCE_KEYS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PHP schema 默认值对齐', () => {
|
||||||
|
assert.match(php, /'baidu_push_enabled',\s*'label'\s*=>\s*'启用百度推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_token',\s*'label'\s*=>\s*'推送 Token',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*''/);
|
||||||
|
assert.match(php, /'baidu_push_auto',\s*'label'\s*=>\s*'自动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_on_publish',\s*'label'\s*=>\s*'发布时主动推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_on_update',\s*'label'\s*=>\s*'更新时推送',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_normal',\s*'label'\s*=>\s*'普通收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
assert.match(php, /'baidu_push_fast',\s*'label'\s*=>\s*'快速收录',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*false/);
|
||||||
|
assert.match(php, /'baidu_push_log_enabled',\s*'label'\s*=>\s*'记录推送日志',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JS schema 默认值对齐', () => {
|
||||||
|
assert.match(js, /baidu_push_enabled:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_token:\s*''/);
|
||||||
|
assert.match(js, /baidu_push_auto:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_on_publish:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_on_update:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_normal:\s*true/);
|
||||||
|
assert.match(js, /baidu_push_fast:\s*false/);
|
||||||
|
assert.match(js, /baidu_push_log_enabled:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('双路/迁移/死代码字段在 schema、运行时与展示层均已移除(无双路)', () => {
|
||||||
|
for (const legacy of DUAL_KEYS) {
|
||||||
|
assert.ok(!php.includes(legacy), `SchemaExtensionsPush 残留: ${legacy}`);
|
||||||
|
assert.ok(!js.includes(legacy), `push.js 残留: ${legacy}`);
|
||||||
|
assert.ok(!runtime.includes(legacy), `SearchPush 残留: ${legacy}`);
|
||||||
|
assert.ok(!extensionRuntime.includes(legacy), `ExtensionRuntime 残留: ${legacy}`);
|
||||||
|
assert.ok(!globalPhp.includes(legacy), `GlobalSchema 残留: ${legacy}`);
|
||||||
|
assert.ok(!seoJs.includes(legacy), `seo.js 残留: ${legacy}`);
|
||||||
|
assert.ok(!displayGroups.includes(legacy), `DisplayPageGroups 残留: ${legacy}`);
|
||||||
|
assert.ok(!metaBox.includes(legacy), `SearchPushMetaBox 残留: ${legacy}`);
|
||||||
|
assert.ok(!functions.includes(legacy), `functions.php 残留: ${legacy}`);
|
||||||
|
}
|
||||||
|
assert.ok(!globalPhp.includes("'baidu'"), 'GlobalSchema SEO 分组残留百度重复配置面');
|
||||||
|
assert.ok(!seoJs.includes("key: 'baidu'"), 'seo.js 残留百度重复配置面');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('普通/快速收录通道接入百度链接提交接口', () => {
|
||||||
|
assert.match(runtime, /data\.zz\.baidu\.com\/urls\?site=/);
|
||||||
|
assert.match(runtime, /'baidu_push_normal'/);
|
||||||
|
assert.match(runtime, /'baidu_push_fast'/);
|
||||||
|
assert.match(runtime, /&type=daily/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_normal'\s*=>/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_fast'\s*=>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('前台自动推送 JS 与参考主题一致', () => {
|
||||||
|
assert.match(runtime, /zz\.bdstatic\.com\/linksubmit\/push\.js/);
|
||||||
|
assert.match(runtime, /push\.zhanzhang\.baidu\.com\/push\.js/);
|
||||||
|
assert.match(runtime, /add_action\('wp_footer'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('发布/更新触发沿用 transition_post_status 且仅限可检索内容类型', () => {
|
||||||
|
assert.match(runtime, /transition_post_status/);
|
||||||
|
assert.match(runtime, /aksrt_searchable_post_types/);
|
||||||
|
assert.match(runtime, /wp_is_post_revision/);
|
||||||
|
assert.match(runtime, /wp_is_post_autosave/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('推送日志开关接入运行时', () => {
|
||||||
|
assert.match(runtime, /baidu_push_log_enabled/);
|
||||||
|
assert.match(extensionRuntime, /'baidu_push_log_enabled'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('服务状态契约保留扩展总览消费的键', () => {
|
||||||
|
for (const key of ['enabled', 'provider', 'configured', 'available', 'verified', 'transport', 'missing_fields']) {
|
||||||
|
assert.ok(runtime.includes(`'${key}'`), `get_status 缺少 ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('分类法推送接入 saved_term 且排除文章形式', () => {
|
||||||
|
assert.match(runtime, /add_action\('saved_term'/);
|
||||||
|
assert.match(runtime, /public static function pushable_taxonomies/);
|
||||||
|
assert.match(runtime, /array_diff\(\(array\) \$taxonomies, \['post_format', 'nav_menu'\]\)/);
|
||||||
|
assert.match(runtime, /aksrt_baidu_push_taxonomies/);
|
||||||
|
assert.match(runtime, /get_term_link\(\$term_id, \$taxonomy\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('推送结果写入 meta 并按记录去重', () => {
|
||||||
|
assert.match(runtime, /META_KEY\s*=\s*'_aksrt_baidu_push_back'/);
|
||||||
|
assert.match(runtime, /private function record_is_complete/);
|
||||||
|
assert.match(runtime, /private function build_record/);
|
||||||
|
assert.match(runtime, /'normal_push'/);
|
||||||
|
assert.match(runtime, /'daily_push'/);
|
||||||
|
assert.match(runtime, /'update_time'/);
|
||||||
|
assert.match(runtime, /update_term_meta\(/);
|
||||||
|
assert.match(runtime, /delete_term_meta\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('「重新提交」勾选需通过 metabox nonce 校验', () => {
|
||||||
|
assert.match(runtime, /NONCE_ACTION\s*=\s*'aksrt_push_meta_save'/);
|
||||||
|
assert.match(runtime, /NONCE_FIELD\s*=\s*'aksrt_push_meta_nonce'/);
|
||||||
|
assert.match(runtime, /RESUBMIT_FIELD\s*=\s*'aksrt_push_resubmit'/);
|
||||||
|
assert.match(runtime, /wp_verify_nonce\(\$nonce, self::NONCE_ACTION\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('编辑页面板与参考主题「百度资源提交」Metabox 对齐', () => {
|
||||||
|
assert.match(metaBox, /add_meta_box\(\s*'aksrt_baidu_push',\s*__\('百度资源提交', 'aksrt'\)/);
|
||||||
|
assert.match(metaBox, /'side',\s*'high'/);
|
||||||
|
assert.match(metaBox, /\{\$taxonomy\}_edit_form_fields/);
|
||||||
|
assert.match(metaBox, /\{\$taxonomy\}_add_form_fields/);
|
||||||
|
assert.match(metaBox, /普通收录:成功/);
|
||||||
|
assert.match(metaBox, /快速收录:失败/);
|
||||||
|
assert.match(metaBox, /更新时间:/);
|
||||||
|
assert.match(metaBox, /重新提交/);
|
||||||
|
assert.match(metaBox, /发布、更新后刷新页面后可查看提交结果/);
|
||||||
|
assert.match(metaBox, /wp_nonce_field\(SearchPush::NONCE_ACTION, SearchPush::NONCE_FIELD\)/);
|
||||||
|
assert.match(metaBox, /esc_html\(\$line\[1\]\)/, '接口结果需转义后输出');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('functions.php 接线推送面板', () => {
|
||||||
|
assert.match(functions, /SearchPushMetaBox::get_instance\(\)->init\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('百度推送行为 harness 通过', () => {
|
||||||
|
const harness = path.join(root, 'tests', 'admin', 'baidu-push-harness.php');
|
||||||
|
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||||
|
assert.match(output, /Baidu push harness passed/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WeChat template-message settings harness.
|
||||||
|
*
|
||||||
|
* Drives the REAL UserController REST callbacks (get_wechat_status /
|
||||||
|
* update_wechat_settings) against WP stubs so the previously-broken routes are
|
||||||
|
* covered end to end:
|
||||||
|
* - GET /wechat/status returns the WeChatNotify status shape (masked secret);
|
||||||
|
* - POST /wechat/settings persists through Integration's key whitelist;
|
||||||
|
* - the *** mask keeps the stored secret instead of overwriting it;
|
||||||
|
* - omitted fields fall back to current values; unknown keys are dropped;
|
||||||
|
* - inputs are sanitized before storage.
|
||||||
|
*
|
||||||
|
* Usage: php wechat-settings-harness.php -> prints "WeChat settings harness passed".
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
define('ABSPATH', __DIR__ . '/');
|
||||||
|
|
||||||
|
final class WP_REST_Response {
|
||||||
|
public $data;
|
||||||
|
public $status;
|
||||||
|
public function __construct($data = null, int $status = 200) {
|
||||||
|
$this->data = $data;
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WP_REST_Request {
|
||||||
|
private array $json;
|
||||||
|
public function __construct(array $json = []) { $this->json = $json; }
|
||||||
|
public function get_json_params(): array { return $this->json; }
|
||||||
|
public function get_method(): string { return 'POST'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- WP function stubs ----------
|
||||||
|
function __($s, $d = null) { return (string) $s; }
|
||||||
|
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
|
||||||
|
function current_user_can($c) { return true; }
|
||||||
|
function wp_send_json_success($d) { echo (string) json_encode($d); }
|
||||||
|
function get_transient($k) { return $GLOBALS['wechat_transient'] ?? false; }
|
||||||
|
function get_option($k, $default = false) { return $GLOBALS['wechat_options'][$k] ?? $default; }
|
||||||
|
function update_option($k, $v) { $GLOBALS['wechat_options'][$k] = $v; return true; }
|
||||||
|
|
||||||
|
$GLOBALS['wechat_options'] = ['aksrt_wechat_settings' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxAPPID',
|
||||||
|
'app_secret' => 'SECRET123',
|
||||||
|
'template_id' => 'TPL1',
|
||||||
|
]];
|
||||||
|
$GLOBALS['wechat_transient'] = 'ACCESS_TOKEN';
|
||||||
|
|
||||||
|
$GLOBALS['wpdb'] = new class {
|
||||||
|
public $usermeta = 'wp_usermeta';
|
||||||
|
public function get_var($sql) { return 7; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace AKSRT\Settings {
|
||||||
|
// Settings 层仅为薄委托,harness 直接对接真实 Integration。
|
||||||
|
class Settings {
|
||||||
|
public static function get_wechat_settings(): array { return Integration::get_wechat_settings(); }
|
||||||
|
public static function update_wechat_settings(array $settings): void { Integration::update_wechat_settings($settings); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/ControllerBase.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerReportOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerAuthPhoneOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerRestrictionOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerFollowOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerProfileMediaOperations.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Admin/UserController.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Settings/Integration.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Settings/DomainGroups.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/Payment/WeChatNotify.php';
|
||||||
|
|
||||||
|
$failures = [];
|
||||||
|
$check = static function (string $label, bool $ok) use (&$failures): void {
|
||||||
|
if (!$ok) {
|
||||||
|
$failures[] = $label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$stored = static function (): array {
|
||||||
|
$v = get_option('aksrt_wechat_settings', []);
|
||||||
|
return is_array($v) ? $v : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
$controller = \AKSRT\Admin\UserController::get_instance();
|
||||||
|
|
||||||
|
// 1) GET /wechat/status 返回完整状态形状(密钥掩码)。
|
||||||
|
$response = $controller->get_wechat_status(new WP_REST_Request());
|
||||||
|
$data = is_array($response->data) ? ($response->data['data'] ?? []) : [];
|
||||||
|
$check('status 含 enabled', array_key_exists('enabled', $data) && !empty($data['enabled']));
|
||||||
|
$check('status 含 appid', ($data['appid'] ?? '') === 'wxAPPID');
|
||||||
|
$check('status configured', !empty($data['configured']));
|
||||||
|
$check('status has_token', !empty($data['has_token']));
|
||||||
|
$check('status has_template', !empty($data['has_template']));
|
||||||
|
$check('status bound_users', ($data['bound_users'] ?? 0) === 7);
|
||||||
|
$check('status 掩码 app_secret', ($data['app_secret'] ?? '') === '***');
|
||||||
|
$check('status allowed_types 非空', !empty($data['allowed_types']));
|
||||||
|
|
||||||
|
// 2) 完整更新写入全部字段。
|
||||||
|
$response = $controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxNEW',
|
||||||
|
'app_secret' => 'NEWSECRET',
|
||||||
|
'template_id' => 'TPL2',
|
||||||
|
]));
|
||||||
|
$check('更新写入 app_id', ($stored()['app_id'] ?? '') === 'wxNEW');
|
||||||
|
$check('更新写入 app_secret', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||||
|
$check('更新写入 template_id', ($stored()['template_id'] ?? '') === 'TPL2');
|
||||||
|
$check('更新返回成功消息', str_contains((string) ($response->data['message'] ?? ''), '已保存'));
|
||||||
|
|
||||||
|
// 3) 掩码 *** 保留已存密钥。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'enabled' => true,
|
||||||
|
'app_id' => 'wxNEW',
|
||||||
|
'app_secret' => '***',
|
||||||
|
]));
|
||||||
|
$check('掩码保留原密钥', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||||
|
|
||||||
|
// 4) 缺省字段沿用当前值,开关可关闭。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request(['enabled' => false]));
|
||||||
|
$check('缺省字段沿用当前值', ($stored()['app_id'] ?? '') === 'wxNEW' && ($stored()['template_id'] ?? '') === 'TPL2');
|
||||||
|
$check('enabled 可关闭', empty($stored()['enabled']));
|
||||||
|
|
||||||
|
// 5) 输入消毒 + 未知键被白名单丢弃。
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request([
|
||||||
|
'app_id' => '<b>wxXSS</b>',
|
||||||
|
'evil' => 'x',
|
||||||
|
]));
|
||||||
|
$check('app_id 经消毒', ($stored()['app_id'] ?? '') === 'wxXSS');
|
||||||
|
$check('未知键被忽略', !array_key_exists('evil', $stored()));
|
||||||
|
|
||||||
|
// 6) 未配置时空密钥可显式清空。
|
||||||
|
update_option('aksrt_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
|
||||||
|
$controller->update_wechat_settings(new WP_REST_Request(['app_secret' => '']));
|
||||||
|
$check('空密钥可清存', ($stored()['app_secret'] ?? '') === '');
|
||||||
|
|
||||||
|
if ($failures) {
|
||||||
|
echo "WeChat settings harness FAILED\n";
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
echo ' - ' . $failure . "\n";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "WeChat settings harness passed\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息 REST 契约(修复存量审计错误):
|
||||||
|
* 1) /wechat/status 与 /wechat/settings 的路由回调方法在 UserController 中真实存在;
|
||||||
|
* 2) 状态读取委托 WeChatNotify::get_status(),写入委托 Settings::update_wechat_settings();
|
||||||
|
* 3) app_secret 掩码 *** 不覆盖已存密钥,输入经 sanitize_text_field 消毒;
|
||||||
|
* 4) PHP harness 驱动真实控制器验证上述行为;
|
||||||
|
* 5) aksrt_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
|
||||||
|
* 活配置面仅剩 aksrt_wechat_settings(/users/wechat-template 自定义页)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const controller = fs.readFileSync(path.join(root, 'src/Admin/UserController.php'), 'utf8');
|
||||||
|
const schemaUsers = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
|
||||||
|
const usersSchemaIndex = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/index.js'), 'utf8');
|
||||||
|
const usersSections = fs.readFileSync(path.join(root, 'assets/admin/src/constants/sections/users.js'), 'utf8');
|
||||||
|
|
||||||
|
// 死双路面:写进 aksrt_settings、全仓无消费方的微信通知键。
|
||||||
|
const DEAD_WECHAT_KEYS = [
|
||||||
|
'wechat_notify_enabled',
|
||||||
|
'wechat_app_id',
|
||||||
|
'wechat_app_secret',
|
||||||
|
'wechat_template_id',
|
||||||
|
'wechat_token',
|
||||||
|
'wechat_encoding_aes_key',
|
||||||
|
'wechat_template_comment',
|
||||||
|
'wechat_template_reply',
|
||||||
|
'wechat_template_system',
|
||||||
|
'wechat_template_order',
|
||||||
|
'wechat_notify_comment',
|
||||||
|
'wechat_notify_reply',
|
||||||
|
'wechat_notify_system',
|
||||||
|
'wechat_notify_order',
|
||||||
|
'wechat_bind_entry',
|
||||||
|
];
|
||||||
|
|
||||||
|
test('路由与回调方法配对存在', () => {
|
||||||
|
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/status'[\s\S]*?'callback'\s*=>\s*\[\$this, 'get_wechat_status'\]/);
|
||||||
|
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/settings'[\s\S]*?'callback'\s*=>\s*\[\$this, 'update_wechat_settings'\]/);
|
||||||
|
assert.match(controller, /public function get_wechat_status\(\\WP_REST_Request \$request\)/);
|
||||||
|
assert.match(controller, /public function update_wechat_settings\(\\WP_REST_Request \$request\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('状态与写入委托既有服务层', () => {
|
||||||
|
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||||
|
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('密钥掩码与输入消毒', () => {
|
||||||
|
assert.match(controller, /\$params\['app_secret'\] !== '\*\*\*'/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_id'\]\)/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_secret'\]\)/);
|
||||||
|
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['template_id'\]\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('微信设置行为 harness 通过', () => {
|
||||||
|
const harness = path.join(root, 'tests', 'admin', 'wechat-settings-harness.php');
|
||||||
|
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||||
|
assert.match(output, /WeChat settings harness passed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('死双路面(aksrt_settings 微信通知键)已彻底移除', () => {
|
||||||
|
for (const key of DEAD_WECHAT_KEYS) {
|
||||||
|
assert.ok(!schemaUsers.includes(key), `SchemaUsers 残留: ${key}`);
|
||||||
|
assert.ok(!usersSections.includes(key), `users 菜单常量残留: ${key}`);
|
||||||
|
}
|
||||||
|
assert.ok(!usersSchemaIndex.includes('wechatSchema'), 'users schema index 残留 wechatSchema 注册');
|
||||||
|
assert.ok(!usersSections.includes("route: '/users/wechat',"), '用户菜单残留微信通知 schema 页');
|
||||||
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/schema/sections/users/wechat.js')), 'wechat.js 未删除');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('活配置面保持唯一(aksrt_wechat_settings + 自定义页)', () => {
|
||||||
|
assert.match(usersSections, /key: 'users-wechat-template'/);
|
||||||
|
assert.match(usersSections, /route: '\/users\/wechat-template'/);
|
||||||
|
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||||
|
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||||
|
});
|
||||||
@@ -167,14 +167,88 @@ test('guest account member VIP and publish contracts share reference header DOM'
|
|||||||
assert.match(header, /navbar-form navbar-right navbar-but/);
|
assert.match(header, /navbar-form navbar-right navbar-but/);
|
||||||
assert.match(header, /pay-vip/);
|
assert.match(header, /pay-vip/);
|
||||||
assert.match(scripts, /\$enqueue_module\('header-userbox'\)/);
|
assert.match(scripts, /\$enqueue_module\('header-userbox'\)/);
|
||||||
assert.match(userboxCss, /\.aksrt-header-userbox \.user-info > \.avatar/);
|
|
||||||
assert.match(userboxCss, /\.aksrt-header-wallet/);
|
|
||||||
assert.match(userboxCss, /\.aksrt-header-checkin/);
|
|
||||||
assert.match(userboxCss, /\.aksrt-header-payvip__cta/);
|
|
||||||
assert.match(userboxCss, /\.header-user-href > a[\s\S]*?max-height: none/);
|
assert.match(userboxCss, /\.header-user-href > a[\s\S]*?max-height: none/);
|
||||||
assert.match(userboxCss, /body\.dark-theme \.aksrt-header-checkin/);
|
|
||||||
assert.doesNotMatch(userboxCss, /user-dropdown|nav-user|html\.dark|theme-dark/);
|
assert.doesNotMatch(userboxCss, /user-dropdown|nav-user|html\.dark|theme-dark/);
|
||||||
assert.doesNotMatch(userboxCss, /--(?:main|muted|key)-|@import/);
|
assert.doesNotMatch(userboxCss, /--(?:main|muted|key)-|@import/);
|
||||||
|
// Account rows render through the shared legacy classes; the module keeps only
|
||||||
|
// the shell anchoring, the account href geometry and the top-nav bell badge.
|
||||||
|
assert.match(userboxCss, /\.aksrt-nav-notification \.aksrt-notification-badge/);
|
||||||
|
assert.doesNotMatch(userboxCss, /aksrt-header-wallet|aksrt-header-checkin|aksrt-header-payvip|\.aksrt-header-userbox \.aksrt-notification-badge/);
|
||||||
|
assert.doesNotMatch(userboxCss, /width:\s*260px/);
|
||||||
|
assert.match(userboxCss, /\.aksrt-header-userbox\s*\{[^}]*line-height:\s*1\.42857143/s);
|
||||||
|
assert.match(userboxCss, /\.aksrt-header-userbox\s+\.muted-box\.padding-6\s*\{[^}]*background-color:\s*var\(--this-bg,\s*var\(--color-line-soft\)\)[^}]*border-radius:\s*4px[^}]*padding:\s*6px/s);
|
||||||
|
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the account popover rows match the reference user box structure', () => {
|
||||||
|
const identity = read('inc/helpers/aksrt-identity.php');
|
||||||
|
const legacyMainCss = read('assets/css/main.css');
|
||||||
|
const buttonsCss = read('assets/css/core/buttons.css');
|
||||||
|
const userboxCss = read('assets/css/frontend/header-userbox.css');
|
||||||
|
|
||||||
|
// Stats row is wired with the reference wrapper and carries the posts chip.
|
||||||
|
assert.match(header, /aksrt_header_user_badges_row\(\$user_id\)/);
|
||||||
|
assert.match(header, /class="em09 author-tag mb10 mt6 flex jc"/);
|
||||||
|
assert.match(identity, /tag-posts/);
|
||||||
|
assert.match(identity, /fa fas fa-file-alt/);
|
||||||
|
assert.match(identity, /fa fas fa-comment-dots/);
|
||||||
|
assert.match(identity, /fa fas fa-thumbs-up/);
|
||||||
|
assert.match(identity, /fa fas fa-fire/);
|
||||||
|
assert.doesNotMatch(identity, /aksrt_renderer_svg_icon/);
|
||||||
|
|
||||||
|
// Admin row exposes the AKSRT payment center at the reference mall slot.
|
||||||
|
assert.match(header, /admin\.php\?page=aksrt_pay/);
|
||||||
|
assert.match(header, /支付中心/);
|
||||||
|
|
||||||
|
// Wallet tiles: balance first, muted-box geometry, FA5 icons.
|
||||||
|
assert.match(header, /class="flex ab jsb col-ml6"/);
|
||||||
|
assert.match(header, /muted-box padding-6 flex1 radius4/);
|
||||||
|
assert.match(header, /font-bold c-blue-2/);
|
||||||
|
assert.match(header, /font-bold c-yellow/);
|
||||||
|
assert.match(header, /fa fas fa-money-bill-wave em14 c-blue-2/);
|
||||||
|
assert.match(header, /fa fas fa-coins em14 c-yellow/);
|
||||||
|
|
||||||
|
// Message icon uses the reference markup.
|
||||||
|
assert.match(header, /msg-news-icon abs-right/);
|
||||||
|
assert.match(header, /toggle-radius msg-icon/);
|
||||||
|
assert.match(header, /<badge class="top">/);
|
||||||
|
|
||||||
|
// VIP bar and CTA use the reference payvip geometry.
|
||||||
|
assert.match(header, /radius4 payvip-icon btn-block text-center/);
|
||||||
|
assert.match(header, /vip-expdate-tag/);
|
||||||
|
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
|
||||||
|
// core/buttons.css restates .but after main.css, which would otherwise replace
|
||||||
|
// the legacy .payvip-icon surface with the .jb-* skin. The restoration must
|
||||||
|
// cover background, color, border and text-shadow, and must not be scoped to
|
||||||
|
// one call site or the top-nav chip keeps the wrong gradient.
|
||||||
|
assert.match(userboxCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;[\s\S]*?border:\s*none;[\s\S]*?text-shadow:\s*none;/s);
|
||||||
|
assert.doesNotMatch(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon/)
|
||||||
|
assert.doesNotMatch(header, /vipbg-v2/);
|
||||||
|
|
||||||
|
// Check-in entry is an anchor with the reference button classes.
|
||||||
|
assert.match(header, /but block c-yellow/);
|
||||||
|
assert.match(header, /but block c-yellow is-done/);
|
||||||
|
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
|
||||||
|
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
|
||||||
|
// The reference selector is a descendant, so the VIP chip inside its hover-show
|
||||||
|
// wrapper receives the same .but geometry (font-size, padding, margin) as the
|
||||||
|
// publish chip instead of falling back to the bare .but defaults.
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
|
||||||
|
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{/);
|
||||||
|
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
|
||||||
|
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
|
||||||
|
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
|
||||||
|
// The reference renders fa-calendar-check-o, an outline glyph; FA5 keeps that
|
||||||
|
// look through the regular weight rather than the solid one.
|
||||||
|
assert.match(header, /fa far fa-calendar-check/);
|
||||||
|
assert.doesNotMatch(header, /fas fa-calendar-check/);
|
||||||
|
// .but already gives its icon a 0.25em margin, so the popover entry carries no
|
||||||
|
// margin class; the user-card badge keeps its own because it is not a .but.
|
||||||
|
assert.match(header, /<i class="fa far fa-calendar-check" aria-hidden="true"><\/i> '/);
|
||||||
|
assert.match(header, /fa far fa-calendar-check ml3 mr6/);
|
||||||
|
|
||||||
|
// Card geometry falls back to the reference .sub-user-box width.
|
||||||
|
assert.match(legacyMainCss, /\.sub-user-box\s*\{\s*width:\s*242px;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the only menu metadata and Walker contract is the legacy runtime', () => {
|
test('the only menu metadata and Walker contract is the legacy runtime', () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const test = require('node:test')
|
|||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
|
const vm = require('node:vm')
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '..', '..')
|
const root = path.resolve(__dirname, '..', '..')
|
||||||
|
|
||||||
@@ -421,6 +422,23 @@ test('asset purchases share the order center and refresh every wallet surface',
|
|||||||
assert.match(sidebar, /data-aksrt-points-value/)
|
assert.match(sidebar, /data-aksrt-points-value/)
|
||||||
assert.match(header, /data-aksrt-wallet-value/)
|
assert.match(header, /data-aksrt-wallet-value/)
|
||||||
assert.match(header, /data-aksrt-points-value/)
|
assert.match(header, /data-aksrt-points-value/)
|
||||||
|
assert.match(header, /data-aksrt-wallet-value data-aksrt-count-format="cut-count"/)
|
||||||
|
assert.match(header, /data-aksrt-points-value data-aksrt-count-format="cut-count"/)
|
||||||
|
assert.match(creditsJs, /element\.dataset\.aksrtCountFormat === 'cut-count' \? formatCutCount\(/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('header wallet count formatting matches the server cut-count thresholds', () => {
|
||||||
|
const creditsJs = fs.readFileSync(path.join(root, 'assets', 'js', 'credits.js'), 'utf8')
|
||||||
|
const formatterSource = creditsJs.match(/function formatCutCount\(value\) \{[\s\S]*?\n\t\}/)
|
||||||
|
|
||||||
|
assert.ok(formatterSource, 'credits runtime must define the header cut-count formatter')
|
||||||
|
const formatCutCount = vm.runInNewContext('(' + formatterSource[0] + ')')
|
||||||
|
|
||||||
|
assert.equal(formatCutCount('0.00'), '0')
|
||||||
|
assert.equal(formatCutCount('9999'), '9999')
|
||||||
|
assert.equal(formatCutCount('10000'), '1W+')
|
||||||
|
assert.equal(formatCutCount('12345'), '1.2W+')
|
||||||
|
assert.equal(formatCutCount('1000000'), '100W+')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('recharge completion always credits the ledger and expiry SQL is strict-mode safe', () => {
|
test('recharge completion always credits the ledger and expiry SQL is strict-mode safe', () => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ test('notification icon positions map to their four render locations', () => {
|
|||||||
const integration = read('src/Settings/Integration.php');
|
const integration = read('src/Settings/Integration.php');
|
||||||
const header = read('inc/helpers/header/aksrt-header-navbar.php');
|
const header = read('inc/helpers/header/aksrt-header-navbar.php');
|
||||||
const headerStyles = read('assets/css/frontend/header-userbox.css');
|
const headerStyles = read('assets/css/frontend/header-userbox.css');
|
||||||
|
const legacyMainCss = read('assets/css/main.css');
|
||||||
const userCenter = read('inc/helpers/aksrt-user-center.php');
|
const userCenter = read('inc/helpers/aksrt-user-center.php');
|
||||||
const userHome = read('templates/parts/user/author-hero.php');
|
const userHome = read('templates/parts/user/author-hero.php');
|
||||||
|
|
||||||
@@ -40,7 +41,13 @@ test('notification icon positions map to their four render locations', () => {
|
|||||||
assert.match(integration, /\['nav_menu', 'avatar_dropdown', 'user_center_header', 'user_home_header'\]/);
|
assert.match(integration, /\['nav_menu', 'avatar_dropdown', 'user_center_header', 'user_home_header'\]/);
|
||||||
assert.match(header, /in_array\('nav_menu', \$positions, true\)/);
|
assert.match(header, /in_array\('nav_menu', \$positions, true\)/);
|
||||||
assert.match(header, /in_array\('avatar_dropdown', \$positions, true\)/);
|
assert.match(header, /in_array\('avatar_dropdown', \$positions, true\)/);
|
||||||
assert.match(headerStyles, /\.aksrt-header-userbox \.user-info > \.abs-right \{[\s\S]*?top: 50%;[\s\S]*?transform: translateY\(-50%\);/);
|
// The card message icon uses the reference msg-news-icon markup, positioned by
|
||||||
|
// the shared legacy stylesheet instead of a bespoke popover rule.
|
||||||
|
assert.match(header, /msg-news-icon abs-right/);
|
||||||
|
assert.match(header, /toggle-radius msg-icon/);
|
||||||
|
assert.match(header, /<badge class="top">/);
|
||||||
|
assert.match(legacyMainCss, /\.sub-user-box \.msg-news-icon \{[\s\S]*?top: 0\.4em;[\s\S]*?right: 0;/);
|
||||||
|
assert.doesNotMatch(headerStyles, /\.aksrt-header-userbox \.user-info > \.abs-right/);
|
||||||
assert.match(header, /aksrt-nav-icon aksrt-nav-notification/);
|
assert.match(header, /aksrt-nav-icon aksrt-nav-notification/);
|
||||||
assert.match(header, /\$notification_but \. \$sign_but \. \$search_but/);
|
assert.match(header, /\$notification_but \. \$sign_but \. \$search_but/);
|
||||||
assert.match(userCenter, /in_array\('user_center_header'/);
|
assert.match(userCenter, /in_array\('user_center_header'/);
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ test('defaults, nested structures, dependencies, and excluded product families a
|
|||||||
}
|
}
|
||||||
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
|
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
|
||||||
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
|
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
|
||||||
|
// FA5 spelling of the reference '<i class="fa fa-fw fa-pencil"></i>发布'
|
||||||
|
// default: fa-pencil does not exist in the bundled FA 5.15.4 free set.
|
||||||
|
assert.match(schema, /'nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"><\/i>发布'/)
|
||||||
|
assert.doesNotMatch(schema, /fa fas fa-fw fas /)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VIP navigation purchase switches are saved and consumed end to end', () => {
|
test('VIP navigation purchase switches are saved and consumed end to end', () => {
|
||||||
@@ -136,8 +140,18 @@ test('VIP navigation purchase switches are saved and consumed end to end', () =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('header VIP hover cards match the legacy markup and style contract', () => {
|
test('header VIP hover cards match the legacy markup and style contract', () => {
|
||||||
assert.match(headerMenuButtons, /<span class="hover-show inline-block navbar-vip-hover">/)
|
assert.match(headerMenuButtons, /<span class="hover-show inline-block">/)
|
||||||
assert.match(headerMenuButtons, /<div class="hover-show-con sub-menu sub-vip-card">/)
|
assert.match(headerMenuButtons, /<div class="sub-menu hover-show-con sub-vip-card">/)
|
||||||
|
assert.match(headerMenuButtons, /'<a class="' \. esc_attr\(\$cls\) \. ' but jb-red radius payvip-icon ml10" href="javascript:;">'/)
|
||||||
|
// The payvip surface must survive the .but compatibility layer for the top-nav
|
||||||
|
// chip too, not just the user-card CTA.
|
||||||
|
assert.match(headerCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?--this-color:\s*#ece0e0;/s)
|
||||||
|
assert.match(headerMenuButtons, /<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
|
||||||
|
assert.match(headerMenuButtons, /'<a rel="nofollow" class="newadd-btns ' \. esc_attr\(\$class\) \. ' btn-newadd"/)
|
||||||
|
assert.equal((headerMenuButtons.match(/Settings::close_sign\(\)/g) || []).length, 2)
|
||||||
|
for (const dead of ['navbar-cta', 'navbar-cta-text', 'navbar-newposts', 'navbar-vip-hover', 'newadd-btns__', 'vip-level', 'fa-gem']) {
|
||||||
|
assert.equal(headerMenuButtons.includes(dead), false, `AKSRT-only header CTA token remains: ${dead}`)
|
||||||
|
}
|
||||||
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
|
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
|
||||||
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
|
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
|
||||||
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
|
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
|
||||||
@@ -154,8 +168,21 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
|
|||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
|
||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
|
||||||
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
|
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover\s*\{[^}]*align-self:\s*stretch;[^}]*overflow:\s*visible;/s)
|
// The hover wrapper must stay a plain block container so the legacy static
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover > \.sub-vip-card\s*\{[^}]*top:\s*100%;[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*max-width:\s*calc\(100vw - 24px\);/s)
|
// position of .sub-vip-card matches the reference theme.
|
||||||
|
assert.match(headerCss, /\.navbar-top \.navbar-but > \.hover-show\s*\{[^}]*display:\s*inline-block;/s)
|
||||||
|
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
|
||||||
|
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
|
||||||
|
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
|
||||||
|
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'aksrt'\)/)
|
||||||
|
for (const dead of ['fa-gem', 'vip-level']) {
|
||||||
|
assert.equal(headerUserbox.includes(dead), false, `AKSRT-only user card CTA token remains: ${dead}`)
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(headerUserbox, /<span>' \. esc_html__\('开通会员 尊享会员权益'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
|
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
|
||||||
@@ -290,7 +317,7 @@ test('user profile and author widgets render the reference user-card contract',
|
|||||||
// 用户个人信息小工具不显示文章模块,显示签到徽章。
|
// 用户个人信息小工具不显示文章模块,显示签到徽章。
|
||||||
assert.match(userRenderer, /'show_posts' => false,/)
|
assert.match(userRenderer, /'show_posts' => false,/)
|
||||||
assert.match(userRenderer, /'show_checkin' => true,/)
|
assert.match(userRenderer, /'show_checkin' => true,/)
|
||||||
assert.match(userRenderer, /aksrt_header_user_checkin_btn\(\$user_id, true, 'img-badge jb-yellow'\)/)
|
assert.match(userRenderer, /aksrt_header_user_checkin_btn\(\$user_id, 'img-badge jb-yellow'\)/)
|
||||||
// 文章作者信息小工具显示文章模块,并走同一 widget 变体。
|
// 文章作者信息小工具显示文章模块,并走同一 widget 变体。
|
||||||
assert.match(userRenderer, /'show_posts' => !isset\(\$i\['show_posts'\]\) \|\| !empty\(\$i\['show_posts'\]\),/)
|
assert.match(userRenderer, /'show_posts' => !isset\(\$i\['show_posts'\]\) \|\| !empty\(\$i\['show_posts'\]\),/)
|
||||||
assert.match(userRenderer, /'post_type' => 'post',/)
|
assert.match(userRenderer, /'post_type' => 'post',/)
|
||||||
@@ -322,13 +349,21 @@ test('user profile and author widgets render the reference user-card contract',
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('desktop header actions share one row without generic button masks', () => {
|
test('desktop header actions share one row without generic button masks', () => {
|
||||||
assert.match(headerNavbar, /aksrt-navbar-actions navbar-form navbar-right/)
|
assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
|
||||||
|
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
|
||||||
|
assert.match(headerNavbar, /\$fold \. \$button \. \$right_but/)
|
||||||
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
|
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
|
||||||
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
|
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*display:\s*inline-flex;[^}]*align-items:\s*center;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
|
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta\.radius\s*\{[^}]*border-radius:\s*50px;/s)
|
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta > \.icon\s*\{[^}]*width:\s*1\.2em;[^}]*height:\s*1\.2em;[^}]*flex:\s*0 0 auto;/s)
|
// the header module may restate their padding, margin, radius or icon size.
|
||||||
|
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
|
||||||
|
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s)
|
||||||
|
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
|
||||||
|
// No merged flex container: the three groups keep their own .navbar-form
|
||||||
|
// padding, which is what produces the reference inter-cluster spacing.
|
||||||
|
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions/)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
|
||||||
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
|
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
|
||||||
})
|
})
|
||||||
@@ -372,8 +407,24 @@ test('header user-card publish controls are removed from configuration and runti
|
|||||||
test('header user-card membership stays on one row with a date-only expiry', () => {
|
test('header user-card membership stays on one row with a date-only expiry', () => {
|
||||||
assert.match(headerUserbox, /wp_date\('Y年m月d日', \$expires\)/)
|
assert.match(headerUserbox, /wp_date\('Y年m月d日', \$expires\)/)
|
||||||
assert.doesNotMatch(headerUserbox, /wp_date\('Y-m-d H:i', \$expires\)/)
|
assert.doesNotMatch(headerUserbox, /wp_date\('Y-m-d H:i', \$expires\)/)
|
||||||
assert.match(headerCss, /\.aksrt-header-payvip__active\s*\{[^}]*flex-direction:\s*row;[^}]*gap:\s*8px !important;[^}]*white-space:\s*nowrap;/s)
|
// Reference VIP bar: legacy payvip-icon geometry, the untouched level-icon
|
||||||
assert.match(headerCss, /\.aksrt-header-payvip__exp\s*\{[^}]*display:\s*inline;[^}]*white-space:\s*nowrap;/s)
|
// contract, and the yellow expiry pill.
|
||||||
|
assert.match(headerUserbox, /radius4 payvip-icon btn-block text-center/)
|
||||||
|
assert.match(headerUserbox, /aksrt_get_vip_icon\(\$user_id, 'em12 mr6', false\)/)
|
||||||
|
assert.match(headerUserbox, /class="ml10 badg jb-yellow vip-expdate-tag"/)
|
||||||
|
assert.match(legacyMainCss, /\.sub-user-box \.payvip-icon\s*\{[^}]*padding:\s*8px;/s)
|
||||||
|
assert.match(legacyMainCss, /\.vip-expdate-tag\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.1em 6px;[^}]*vertical-align:\s*0\.1em;/s)
|
||||||
|
assert.doesNotMatch(vipModalCss, /\.vip-expdate-tag\s*\{/)
|
||||||
|
assert.doesNotMatch(headerCss, /aksrt-header-payvip/)
|
||||||
|
// The CTA wrapper is the reference padding wrapper, not a bespoke hook.
|
||||||
|
assert.match(headerUserbox, /'<div class="mt10 em09" style="padding:2px;">'/)
|
||||||
|
assert.doesNotMatch(headerUserbox, /aksrt-header-payvip/)
|
||||||
|
// Expiry badge follows zib_get_user_vip_exp_date_text(): an expiry that has
|
||||||
|
// passed reads 会员已过期, a live one reads the bare date, 永久会员 is reserved
|
||||||
|
// for an explicit permanent membership, and no expiry data renders no badge.
|
||||||
|
assert.match(headerUserbox, /\$expires <= time\(\)/)
|
||||||
|
assert.match(headerUserbox, /empty\(\$state\['is_permanent'\]\)/)
|
||||||
|
assert.doesNotMatch(headerUserbox, /aksrt_get_vip_info/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('save contract rejects invalid input and round-trips canonical nested values', () => {
|
test('save contract rejects invalid input and round-trips canonical nested values', () => {
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ test('after-box author card mirrors legacy user-card structure', () => {
|
|||||||
assert.match(render, /<div class="user-desc mt10 muted-2-color em09">/);
|
assert.match(render, /<div class="user-desc mt10 muted-2-color em09">/);
|
||||||
assert.doesNotMatch(render, /user-btns/);
|
assert.doesNotMatch(render, /user-btns/);
|
||||||
assert.doesNotMatch(render, /发布文章.*用户中心/s);
|
assert.doesNotMatch(render, /发布文章.*用户中心/s);
|
||||||
|
// The stats row carries the posts chip.
|
||||||
assert.match(identity, /tag-posts/);
|
assert.match(identity, /tag-posts/);
|
||||||
assert.match(identity, /tag-comment/);
|
assert.match(identity, /tag-comment/);
|
||||||
assert.match(identity, /tag-follow/);
|
assert.match(identity, /tag-follow/);
|
||||||
|
|||||||
@@ -379,8 +379,6 @@ test('front submit actions use direct fixed-id notyf flows', () => {
|
|||||||
'aksrt-front-submit-delete-draft-toast',
|
'aksrt-front-submit-delete-draft-toast',
|
||||||
'aksrt-front-submit-resource-upload-toast',
|
'aksrt-front-submit-resource-upload-toast',
|
||||||
'aksrt-front-submit-editor-media-toast',
|
'aksrt-front-submit-editor-media-toast',
|
||||||
'aksrt-front-submit-ai-tags-toast',
|
|
||||||
'aksrt-front-submit-ai-summary-toast',
|
|
||||||
].forEach((id) => assert.match(source, new RegExp("['\"]" + id + "['\"]")));
|
].forEach((id) => assert.match(source, new RegExp("['\"]" + id + "['\"]")));
|
||||||
|
|
||||||
assert.match(submit, /submit: function[\s\S]*?notyf\([^;]+?'load'[^;]+?toastId[\s\S]*?notyf\([^;]+?'success'[^;]+?toastId[\s\S]*?notyf\([^;]+?'danger'[^;]+?toastId/);
|
assert.match(submit, /submit: function[\s\S]*?notyf\([^;]+?'load'[^;]+?toastId[\s\S]*?notyf\([^;]+?'success'[^;]+?toastId[\s\S]*?notyf\([^;]+?'danger'[^;]+?toastId/);
|
||||||
@@ -398,8 +396,6 @@ test('front submit actions use direct fixed-id notyf flows', () => {
|
|||||||
// 付费下载附件选择后仅补一条成功确认,上传过程通知属于选择器。
|
// 付费下载附件选择后仅补一条成功确认,上传过程通知属于选择器。
|
||||||
assert.match(downloads, /AksrtSubmitMedia\.open\([\s\S]*?'success', 5, 'aksrt-front-submit-resource-upload-toast'/);
|
assert.match(downloads, /AksrtSubmitMedia\.open\([\s\S]*?'success', 5, 'aksrt-front-submit-resource-upload-toast'/);
|
||||||
assert.match(editor, /aksrt-front-submit-editor-media-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
|
assert.match(editor, /aksrt-front-submit-editor-media-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
|
||||||
assert.match(submit, /aksrt-front-submit-ai-tags-toast[\s\S]*?aksrt-front-submit-ai-summary-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
|
|
||||||
assert.match(submit, /needContent[^;]+?'warning'/);
|
|
||||||
assert.doesNotMatch(submit, /\b(?:window\.)?alert\s*\(/);
|
assert.doesNotMatch(submit, /\b(?:window\.)?alert\s*\(/);
|
||||||
|
|
||||||
assert.match(page, /'aksrt-front-submit',[\s\S]*?'\/assets\/js\/front-submit\.js',[\s\S]*?\[[^\]]*'loader_js'[^\]]*\]/);
|
assert.match(page, /'aksrt-front-submit',[\s\S]*?'\/assets\/js\/front-submit\.js',[\s\S]*?\[[^\]]*'loader_js'[^\]]*\]/);
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ namespace {
|
|||||||
}
|
}
|
||||||
function aksrt_get_user_badges(int $user_id): string { return '<span class="badg">' . $user_id . '</span>'; }
|
function aksrt_get_user_badges(int $user_id): string { return '<span class="badg">' . $user_id . '</span>'; }
|
||||||
function aksrt_get_user_description(int $user_id): string { return 'Description ' . $user_id; }
|
function aksrt_get_user_description(int $user_id): string { return 'Description ' . $user_id; }
|
||||||
function aksrt_header_user_checkin_btn(int $user_id, bool $large, string $class): string {
|
function aksrt_header_user_checkin_btn(int $user_id, string $class): string {
|
||||||
unset($user_id, $large);
|
unset($user_id);
|
||||||
return '<button class="' . esc_attr($class) . '">签到</button>';
|
return '<a class="' . esc_attr($class) . '" href="javascript:;">签到</a>';
|
||||||
}
|
}
|
||||||
function aksrt_render_user_card_posts(array $args): string {
|
function aksrt_render_user_card_posts(array $args): string {
|
||||||
return '<div data-post-type="' . esc_attr((string) ($args['post_type'] ?? '')) . '" data-post-style="' . esc_attr((string) ($args['post_style'] ?? '')) . '"></div>';
|
return '<div data-post-type="' . esc_attr((string) ($args['post_type'] ?? '')) . '" data-post-style="' . esc_attr((string) ($args['post_style'] ?? '')) . '"></div>';
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ test('retired admin pages, schemas, APIs, and metadata chains are absent', () =>
|
|||||||
'assets/admin/src/constants/sections/extensions.js',
|
'assets/admin/src/constants/sections/extensions.js',
|
||||||
'assets/admin/src/router/component-loaders/users.js',
|
'assets/admin/src/router/component-loaders/users.js',
|
||||||
'assets/admin/src/router/component-loaders/tools.js',
|
'assets/admin/src/router/component-loaders/tools.js',
|
||||||
'assets/admin/src/router/component-loaders/extensions.js',
|
|
||||||
'assets/admin/src/api/modules/tools.js',
|
'assets/admin/src/api/modules/tools.js',
|
||||||
'assets/admin/src/schema/sections/tools/index.js',
|
'assets/admin/src/schema/sections/tools/index.js',
|
||||||
'src/Admin/Schema/SchemaTools.php',
|
'src/Admin/Schema/SchemaTools.php',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-10-03T20:06:15+00:00",
|
"generated_at": "2026-10-13T12:53:18+00:00",
|
||||||
"errors": [],
|
"errors": [],
|
||||||
"warnings": [
|
"warnings": [
|
||||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
"enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]",
|
"enum incomplete parse for 'search_type_in' (php subset of js): php=[post] js=[post,user]",
|
||||||
"enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]",
|
"enum incomplete parse for 'sign_card_position' (php subset of js): php=[left] js=[left,center,right]",
|
||||||
"enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]",
|
"enum incomplete parse for 'sign_theme_mode' (php subset of js): php=[no] js=[no,white-theme,dark-theme]",
|
||||||
|
"enum incomplete parse for 'sms_ali_api_type' (php subset of js): php=[sms] js=[sms,verify]",
|
||||||
|
"enum incomplete parse for 'sms_sdk' (php subset of js): php=[null] js=[null,ali,tencent,smsbao]",
|
||||||
"enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]",
|
"enum incomplete parse for 'style' (php subset of js): php=[null] js=[null,style2]",
|
||||||
"enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
"enum incomplete parse for 'system_notice_btn1_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
||||||
"enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
"enum incomplete parse for 'system_notice_btn2_color' (php subset of js): php=[primary] js=[primary,success,warning,danger,info,purple,dark]",
|
||||||
@@ -57,12 +59,12 @@
|
|||||||
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
||||||
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
||||||
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
|
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
|
||||||
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]",
|
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]"
|
||||||
"assets/admin/build/ missing — run: cd assets/admin && npm run build"
|
|
||||||
],
|
],
|
||||||
"info": [
|
"info": [
|
||||||
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ToolsBackup, ToolsHealth, ToolsImportExport",
|
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ExtensionsSecurity, ExtensionsSystem, ToolsBackup, ToolsHealth, ToolsImportExport",
|
||||||
"Menu pages discovered: 60"
|
"Menu pages discovered: 61",
|
||||||
|
"admin build ok; chunks=30"
|
||||||
],
|
],
|
||||||
"domains": {
|
"domains": {
|
||||||
"global": {
|
"global": {
|
||||||
@@ -136,8 +138,7 @@
|
|||||||
"oauth",
|
"oauth",
|
||||||
"report",
|
"report",
|
||||||
"sms",
|
"sms",
|
||||||
"verify",
|
"verify"
|
||||||
"wechat"
|
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src/Admin/Schema/SchemaUsers.php"
|
"src/Admin/Schema/SchemaUsers.php"
|
||||||
@@ -146,6 +147,7 @@
|
|||||||
"payment": {
|
"payment": {
|
||||||
"sections": [
|
"sections": [
|
||||||
"affiliate",
|
"affiliate",
|
||||||
|
"after_sale",
|
||||||
"gateway",
|
"gateway",
|
||||||
"knowledge",
|
"knowledge",
|
||||||
"recharge",
|
"recharge",
|
||||||
@@ -161,7 +163,9 @@
|
|||||||
"sections": [
|
"sections": [
|
||||||
"audit",
|
"audit",
|
||||||
"ip",
|
"ip",
|
||||||
"push"
|
"push",
|
||||||
|
"security",
|
||||||
|
"system"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src/Admin/Schema/SchemaExtensions.php"
|
"src/Admin/Schema/SchemaExtensions.php"
|
||||||
@@ -201,6 +205,22 @@
|
|||||||
"src/Admin/Schema/SchemaExtensionsPush.php"
|
"src/Admin/Schema/SchemaExtensionsPush.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ExtensionsSecurity": {
|
||||||
|
"sections": [
|
||||||
|
"security"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src/Admin/Schema/SchemaExtensionsSecurity.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ExtensionsSystem": {
|
||||||
|
"sections": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src/Admin/Schema/SchemaExtensionsSystem.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ToolsBackup": {
|
"ToolsBackup": {
|
||||||
"sections": [
|
"sections": [
|
||||||
"backup"
|
"backup"
|
||||||
@@ -226,11 +246,42 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page_count": 60,
|
"page_count": 61,
|
||||||
"build": {
|
"build": {
|
||||||
"present": false,
|
"present": true,
|
||||||
"entry": false,
|
"entry": true,
|
||||||
"missing_chunks": [],
|
"missing_chunks": [],
|
||||||
"chunks": []
|
"chunks": [
|
||||||
|
"SchemaPage.BrFz57tJ.js",
|
||||||
|
"vendor-vue.BqIbVHit.js",
|
||||||
|
"FaIcon.BMA0TNWc.js",
|
||||||
|
"AksMediaField.C9RRDn2V.js",
|
||||||
|
"FaIconPicker.CA3w9r30.js",
|
||||||
|
"shared-api.Cak1GAri.js",
|
||||||
|
"vendor-misc.Dr7TL_YE.js",
|
||||||
|
"vendor-draggable.CjYjXbZD.js",
|
||||||
|
"level.E3ANSDAX.js",
|
||||||
|
"badge.CKTsmH1z.js",
|
||||||
|
"vendor-naive.Dzr6v856.js",
|
||||||
|
"DeferredPage.Bo6-Z82H.js",
|
||||||
|
"NativeOpsRedirect.wMMcdVjx.js",
|
||||||
|
"FloatingButtonsSettings.DM1NXEcr.js",
|
||||||
|
"PageHeader.UhySXCpq.js",
|
||||||
|
"FooterTabbarSettings.8D3P8zNv.js",
|
||||||
|
"PostPageSettings.B5Sm-lcm.js",
|
||||||
|
"CommentSettings.DiQrTo4p.js",
|
||||||
|
"PermissionsCapabilities.CnQfjtxC.js",
|
||||||
|
"CmsCredits.Dh2b9uHb.js",
|
||||||
|
"ThemeInfoPage.zYktV7qz.js",
|
||||||
|
"ThemeAuthorizationPage.e-ksjN6X.js",
|
||||||
|
"DocsPage.Dtd_-3fP.js",
|
||||||
|
"UsersVerify.CAJUfACd.js",
|
||||||
|
"UsersLevel.CBoqr3ux.js",
|
||||||
|
"UsersBadge.B_lwRfdl.js",
|
||||||
|
"UsersWechatTemplate.F0aCC5C-.js",
|
||||||
|
"ToolsImportExport.CN30A6Ge.js",
|
||||||
|
"ToolsHealth.B0GkAdJR.js",
|
||||||
|
"ToolsUpdate.DHtpNLfB.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user