Compare commits
59
Commits
536ac67f3c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a120f61e13 | ||
|
|
0d51beeaad | ||
|
|
573040381d | ||
|
|
7a0445fc4a | ||
|
|
0bc72616e5 | ||
|
|
064f4e0955 | ||
|
|
4b5098a753 | ||
|
|
05f16d5fd7 | ||
|
|
af8dccdc45 | ||
|
|
dcb80cff6e | ||
|
|
2e2558e5ce | ||
|
|
817ac9c647 | ||
|
|
e94ecdd7b1 | ||
|
|
659b77719c | ||
|
|
7f556892ad | ||
|
|
cb76b95b01 | ||
|
|
319ec2f47f | ||
|
|
4b18f7ef2c | ||
|
|
1461c9fe27 | ||
|
|
5641c40958 | ||
|
|
0bee2c605e | ||
|
|
c472045ce4 | ||
|
|
6dc9265260 | ||
|
|
008e63524d | ||
|
|
cff3083a91 | ||
|
|
4d69aaee50 | ||
|
|
ee39024549 | ||
|
|
5810693e01 | ||
|
|
0722c4f00e | ||
|
|
4f2edebab6 | ||
|
|
ce6fbb4e39 | ||
|
|
cf39e72126 | ||
|
|
740bd48f42 | ||
|
|
18e579a8c6 | ||
|
|
cdab7a5e5f | ||
|
|
bd01199930 | ||
|
|
7ed739615e | ||
|
|
60e4d67772 | ||
|
|
7287cfeaec | ||
|
|
f03d72c880 | ||
|
|
6e90a96a15 | ||
|
|
b368678662 | ||
|
|
40b9a3f36b | ||
|
|
6fd80be243 | ||
|
|
855e85e7db | ||
|
|
7e7bf6db23 | ||
|
|
6f815678ec | ||
|
|
8ff74f56ee | ||
|
|
a3b708f9d4 | ||
|
|
9cb5e7bea3 | ||
|
|
16b57dcf1c | ||
|
|
1875470ac7 | ||
|
|
c955fd35f2 | ||
|
|
36b3ba6f45 | ||
|
|
039fa6d863 | ||
|
|
26ce9a5904 | ||
|
|
31e6ba00af | ||
|
|
89ae462610 | ||
|
|
afd08885fc |
No files matched your search
@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
|
||||
|
||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||
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 }) },
|
||||
}
|
||||
@@ -476,14 +476,43 @@
|
||||
|
||||
<!-- Fieldset (single nested object) -->
|
||||
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
|
||||
<AksField
|
||||
v-for="field in visibleNestedFields"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
<template v-if="fieldsetSections.length">
|
||||
<div
|
||||
v-for="(section, sIndex) in fieldsetSections"
|
||||
:key="section.title"
|
||||
class="aksrt-field__accordion-section"
|
||||
:class="{ 'is-collapsed': !isFieldsetSectionOpen(sIndex) }"
|
||||
>
|
||||
<div class="aksrt-field__accordion-section-head" @click="toggleFieldsetSection(sIndex)">
|
||||
<span class="aksrt-field__accordion-section-title">{{ section.title }}</span>
|
||||
<FaIcon
|
||||
:icon="isFieldsetSectionOpen(sIndex) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
|
||||
:size="12"
|
||||
class="aksrt-field__accordion-section-toggle"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="isFieldsetSectionOpen(sIndex)" class="aksrt-field__accordion-section-body">
|
||||
<AksField
|
||||
v-for="field in sectionFields(section)"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!fieldsetSections.length">
|
||||
<AksField
|
||||
v-for="field in visibleNestedFields"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
:form-data="formData"
|
||||
@update:model-value="updateFieldsetValue(field.key, $event)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
|
||||
@@ -517,23 +546,38 @@
|
||||
animation="180"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="aksrt-field__repeater-item">
|
||||
<div class="aksrt-field__repeater-header">
|
||||
<div class="aksrt-field__repeater-item" :class="{ 'is-collapsible': repeaterCollapsible, 'is-collapsed': repeaterCollapsible && !isRepeaterItemOpen(index) }">
|
||||
<div
|
||||
class="aksrt-field__repeater-header"
|
||||
:class="{ 'is-clickable': repeaterCollapsible }"
|
||||
@click="repeaterCollapsible ? toggleRepeaterItem(index) : undefined"
|
||||
>
|
||||
<div class="aksrt-field__repeater-title">
|
||||
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true"></i>
|
||||
<i class="fa fas fa-grip-vertical aksrt-field__repeater-handle" aria-hidden="true" @click.stop></i>
|
||||
<span class="aksrt-field__repeater-index">#{{ index + 1 }}</span>
|
||||
<span v-if="repeaterCollapsible && repeaterItemSummary(element)" class="aksrt-field__repeater-summary">
|
||||
{{ repeaterItemSummary(element) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="aksrt-field__repeater-header-actions" @click.stop>
|
||||
<FaIcon
|
||||
v-if="repeaterCollapsible"
|
||||
:icon="isRepeaterItemOpen(index) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
|
||||
:size="12"
|
||||
class="aksrt-field__repeater-toggle"
|
||||
/>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
text
|
||||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||||
@click="removeRepeaterItem(index)"
|
||||
>
|
||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
text
|
||||
:disabled="schema.disabled || reachedRepeaterMinimum"
|
||||
@click="removeRepeaterItem(index)"
|
||||
>
|
||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="aksrt-field__repeater-fields">
|
||||
<div v-show="!repeaterCollapsible || isRepeaterItemOpen(index)" class="aksrt-field__repeater-fields">
|
||||
<template v-for="field in (schema.fields || [])" :key="field.key">
|
||||
<AksField
|
||||
v-if="matchDependency(field.dependsOn, element)"
|
||||
@@ -697,6 +741,7 @@ import FaIconPicker from './FaIconPicker.vue'
|
||||
import AksMediaField from './AksMediaField.vue'
|
||||
import AksGalleryField from './AksGalleryField.vue'
|
||||
import AksCodeEditor from './AksCodeEditor.vue'
|
||||
import { buildFieldsetSections } from './fieldset-sections.js'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -730,6 +775,60 @@ const repeaterItems = computed(() => Array.isArray(props.modelValue) ? props.mod
|
||||
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
|
||||
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* 手风琴模式:schema.collapsible 开启后,每一项默认收起,仅显示标题行,
|
||||
* 点击标题行展开/收起。defaultCollapsed 为 false 时改为默认全部展开。
|
||||
* ------------------------------------------------------------------ */
|
||||
const repeaterCollapsible = computed(() => !!props.schema.collapsible)
|
||||
const repeaterDefaultOpen = computed(() => props.schema.defaultCollapsed === false)
|
||||
// 只记录「与默认状态相反」的下标,新增/删除项时其余项的开合状态不受影响。
|
||||
const toggledRepeaterItems = ref(new Set())
|
||||
|
||||
function isRepeaterItemOpen(index) {
|
||||
if (!repeaterCollapsible.value) return true
|
||||
return repeaterDefaultOpen.value !== toggledRepeaterItems.value.has(index)
|
||||
}
|
||||
|
||||
function toggleRepeaterItem(index) {
|
||||
const next = new Set(toggledRepeaterItems.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
toggledRepeaterItems.value = next
|
||||
}
|
||||
|
||||
/** 收起态标题行上的一行摘要,取自 listPreview 配置。 */
|
||||
function repeaterItemSummary(item) {
|
||||
if (!repeaterCollapsible.value || !item) return ''
|
||||
const cfg = getRepeaterPreviewConfig()
|
||||
const titleKey = cfg.titleKey || 'title'
|
||||
const title = String(item[titleKey] ?? '').trim()
|
||||
const prefix = String(cfg.prefix || '')
|
||||
if (title === '') return prefix
|
||||
const desc = repeaterItemDuration(item)
|
||||
return desc ? `${prefix}${title} · ${desc}` : `${prefix}${title}`
|
||||
}
|
||||
|
||||
/** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */
|
||||
function repeaterItemDuration(item) {
|
||||
const cfg = getRepeaterPreviewConfig()
|
||||
const descKey = cfg.descKey
|
||||
if (!descKey) return ''
|
||||
const raw = item[descKey]
|
||||
if (raw === '' || raw == null) return ''
|
||||
const amount = Number(raw)
|
||||
if (!Number.isFinite(amount)) return String(raw)
|
||||
if (amount <= 0) return '永久'
|
||||
const unitKey = cfg.descUnitKey
|
||||
if (!unitKey) return String(raw)
|
||||
const unit = String(item[unitKey] || '')
|
||||
if (unit === 'month') return `${amount} 个月`
|
||||
if (unit === 'day') return `${amount} 天`
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
/** 拖拽视图:vuedraggable 以 modelValue 模式工作(内部复制后回传 update:modelValue)。 */
|
||||
const repeaterDragList = computed({
|
||||
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
|
||||
@@ -762,6 +861,35 @@ function fieldsetValue(field) {
|
||||
return Object.prototype.hasOwnProperty.call(current, field.key) ? current[field.key] : field.default
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* accordion/fieldset 的分块手风琴:schema.sections 声明每块的标题与字段
|
||||
* key,默认全部收起,点击块标题展开。未声明 sections 时保持平铺渲染。
|
||||
* ------------------------------------------------------------------ */
|
||||
const fieldsetSections = computed(() => {
|
||||
return buildFieldsetSections(props.schema)
|
||||
})
|
||||
|
||||
const openFieldsetSections = ref(new Set())
|
||||
|
||||
function isFieldsetSectionOpen(index) {
|
||||
return openFieldsetSections.value.has(index)
|
||||
}
|
||||
|
||||
/** 分块内字段同样遵循 dependsOn,数据源是本 fieldset 的值对象。 */
|
||||
function sectionFields(section) {
|
||||
return (section.fields || []).filter((field) => matchDependency(field.dependsOn, props.modelValue))
|
||||
}
|
||||
|
||||
function toggleFieldsetSection(index) {
|
||||
const next = new Set(openFieldsetSections.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
next.add(index)
|
||||
}
|
||||
openFieldsetSections.value = next
|
||||
}
|
||||
|
||||
function updateFieldsetValue(key, value) {
|
||||
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||
? props.modelValue
|
||||
@@ -1053,6 +1181,8 @@ const repeaterDraft = ref({})
|
||||
function createRepeaterItem() {
|
||||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||||
for (const field of (props.schema.fields || [])) {
|
||||
// UI-only chrome rows (notice / alert) are presentation only.
|
||||
if (field.ui_only) continue
|
||||
if (field.default !== undefined) {
|
||||
newItem[field.key] = field.default
|
||||
} else if (field.type === 'switch') {
|
||||
@@ -1082,7 +1212,16 @@ function removeRepeaterItem(index) {
|
||||
|
||||
function updateRepeaterItem(index, fieldKey, value) {
|
||||
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
||||
current[index] = { ...current[index], [fieldKey]: value }
|
||||
const row = current[index]
|
||||
if (row && typeof row === 'object') {
|
||||
// 必须原地写,不能展开成新对象:drag key 存在以「对象身份」为键的 WeakMap
|
||||
// 里,而行数据是 reactive 的——换一个新对象后,渲染时读到的是它的一个新
|
||||
// reactive 代理,WeakMap 认不出它便生成新 key,整行重挂载、输入框失焦,
|
||||
// 表现为打一个字就退出编辑。经代理原地写既保持身份,也照样触发响应式。
|
||||
row[fieldKey] = value
|
||||
} else {
|
||||
current[index] = { [fieldKey]: value }
|
||||
}
|
||||
emit('update:modelValue', current)
|
||||
}
|
||||
|
||||
@@ -1184,6 +1323,15 @@ function confirmRepeaterModal() {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
|
||||
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
|
||||
.aksrt-field--group,
|
||||
.aksrt-field--repeater,
|
||||
.aksrt-field--accordion,
|
||||
.aksrt-field--fieldset {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.aksrt-field__input-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1449,6 +1597,48 @@ function confirmRepeaterModal() {
|
||||
background: var(--aksrt-color-canvas);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--aksrt-color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--aksrt-color-surface);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--aksrt-color-text);
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-head:hover {
|
||||
border-color: var(--aksrt-color-primary);
|
||||
color: var(--aksrt-color-primary);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-toggle {
|
||||
flex-shrink: 0;
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 2px 2px;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1477,6 +1667,47 @@ function confirmRepeaterModal() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header.is-clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
margin: -4px -4px 0;
|
||||
padding: 4px 4px 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header.is-clickable:hover .aksrt-field__repeater-index {
|
||||
color: var(--aksrt-color-primary-hover);
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-toggle {
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: var(--aksrt-color-text-secondary);
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-item.is-collapsible {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-item.is-collapsed .aksrt-field__repeater-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.aksrt-field__repeater-handle {
|
||||
cursor: grab;
|
||||
color: var(--aksrt-color-text-tertiary);
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in field.tabs"
|
||||
:key="tab.title || tab.key"
|
||||
:name="tab.title || tab.key"
|
||||
:tab="tab.title || tab.key"
|
||||
:key="tab.key || tab.title"
|
||||
:name="tabName(tab)"
|
||||
:tab="tabName(tab)"
|
||||
>
|
||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||
<AksField
|
||||
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
|
||||
return matchDependsOn(field.dependsOn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||
* while that value is still empty.
|
||||
*/
|
||||
function tabName(tab) {
|
||||
if (!tab) return ''
|
||||
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||
}
|
||||
|
||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||
function isGroupVisible(group) {
|
||||
return matchDependsOn(group.dependsOn)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
|
||||
*
|
||||
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
|
||||
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
|
||||
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
|
||||
*/
|
||||
export function buildFieldsetSections(schema) {
|
||||
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
|
||||
if (!declared.length) return []
|
||||
|
||||
const byKey = {}
|
||||
const nested = Array.isArray(schema.fields) ? schema.fields : []
|
||||
for (const field of nested) {
|
||||
if (field && field.key) byKey[field.key] = field
|
||||
}
|
||||
|
||||
return declared.map((section) => ({
|
||||
title: String((section && section.title) || ''),
|
||||
fields: (Array.isArray(section && section.keys) ? section.keys : [])
|
||||
.map((key) => byKey[key])
|
||||
.filter(Boolean),
|
||||
}))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
||||
import { renderFaIcon, schemaPage } from './helpers'
|
||||
|
||||
export const extensionsSection = {
|
||||
key: 'extensions',
|
||||
@@ -42,7 +42,7 @@ export const extensionsSection = {
|
||||
label: '百度推送',
|
||||
route: '/extensions/push',
|
||||
title: '百度推送',
|
||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
||||
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||
canonicalDomain: 'extensions',
|
||||
section: 'push',
|
||||
}),
|
||||
@@ -56,15 +56,5 @@ export const extensionsSection = {
|
||||
canonicalDomain: 'extensions',
|
||||
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',
|
||||
section: 'sms',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'users-wechat',
|
||||
name: 'UsersWechat',
|
||||
label: '微信通知',
|
||||
route: '/users/wechat',
|
||||
title: '微信通知',
|
||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
||||
canonicalDomain: 'users',
|
||||
section: 'wechat',
|
||||
}),
|
||||
customPage({
|
||||
key: 'users-wechat-template',
|
||||
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 { usersPageLoaders } from './users'
|
||||
import { toolsPageLoaders } from './tools'
|
||||
import { extensionsPageLoaders } from './extensions'
|
||||
|
||||
export const componentLoaders = {
|
||||
...corePageLoaders,
|
||||
...usersPageLoaders,
|
||||
...toolsPageLoaders,
|
||||
...extensionsPageLoaders,
|
||||
}
|
||||
@@ -2,21 +2,16 @@ export const pushSchema = {
|
||||
key: 'extensions.push',
|
||||
title: '百度推送',
|
||||
icon: 'fa fas fa-paper-plane',
|
||||
description: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
||||
description: '配置百度主动推送、收录通道和推送状态。',
|
||||
defaults: {
|
||||
baidu_push_enabled: false,
|
||||
baidu_push_token: '',
|
||||
baidu_push_auto: true,
|
||||
baidu_push_on_publish: true,
|
||||
baidu_push_on_update: false,
|
||||
baidu_push_batch_size: 50,
|
||||
baidu_push_normal: true,
|
||||
baidu_push_fast: false,
|
||||
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: [
|
||||
{
|
||||
@@ -25,7 +20,7 @@ export const pushSchema = {
|
||||
icon: 'fa fas fa-rocket',
|
||||
fields: [
|
||||
{ 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',
|
||||
fields: [
|
||||
{ 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_update', label: '更新时推送', type: 'switch', default: false, 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 }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'xzh',
|
||||
title: '熊掌号 / JSON-LD',
|
||||
icon: 'fa fas fa-paw',
|
||||
key: 'channel',
|
||||
title: '收录通道',
|
||||
icon: 'fa fas fa-inbox',
|
||||
fields: [
|
||||
{ key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false },
|
||||
{ key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } },
|
||||
{ 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 } },
|
||||
{ key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' },
|
||||
{ key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -56,8 +47,7 @@ export const pushSchema = {
|
||||
title: '高级设置',
|
||||
icon: 'fa fas fa-cog',
|
||||
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 } },
|
||||
{ key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@ export const seoSchema = {
|
||||
key: 'global.seo',
|
||||
title: 'SEO 优化',
|
||||
icon: 'fa fas fa-search',
|
||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框;连接配置在「扩展 → AI 连接配置」。优先使用 WordPress AI Connectors。',
|
||||
description: '站点标题、描述、关键词与 AI SEO。AI 生成入口在文章「独立 SEO」框,连接由 WordPress AI Connectors 提供。',
|
||||
defaults: {
|
||||
seo_enabled: true,
|
||||
seo_ai_enabled: false,
|
||||
@@ -13,10 +13,6 @@ export const seoSchema = {
|
||||
ai_seo_title_prompt: '',
|
||||
ai_seo_keywords_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_default: '',
|
||||
seo_image_always_default: false,
|
||||
@@ -48,7 +44,7 @@ export const seoSchema = {
|
||||
label: '启用 AI SEO 生成',
|
||||
type: 'switch',
|
||||
default: false,
|
||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。',
|
||||
help: '开启后文章/分类「独立 SEO」框显示「AI 生成 SEO」按钮。需先在 WordPress 配置 AI Connectors。',
|
||||
dependsOn: { key: 'seo_enabled', value: true },
|
||||
},
|
||||
{ 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',
|
||||
title: '图片 SEO',
|
||||
|
||||
@@ -111,7 +111,7 @@ export const rechargeSchema = {
|
||||
{ key: 'points_pass_exchange_desc', label: '卡密兑换积分说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'points_pass_exchange_only_password', label: '积分卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'points_pass_exchange_s', value: true }, help: '积分卡密仅需输入密码,不需要卡号' },
|
||||
{ key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false, help: '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员' },
|
||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' },
|
||||
{ key: 'pay_vip_pass_charge_only_password', label: '会员卡密单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,27 +1,127 @@
|
||||
/**
|
||||
* VIP会员 — 参考主题结构:会员等级、会员权益(普通/一级/二级 tab)、
|
||||
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
|
||||
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
|
||||
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
|
||||
* 积分兑换会员与界面文案。
|
||||
*
|
||||
* 会员商品 / 续费商品 / 升级价格 / 积分兑换明细均为结构化 group 或
|
||||
* accordion,不再使用 `|` 分隔的 textarea。等级 Tab 标题通过 titleKey
|
||||
* 绑定「会员显示名称」,改名即改 Tab 名。
|
||||
*/
|
||||
|
||||
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
|
||||
const vipTagPalette = [
|
||||
{ label: '#f74b3d', value: 'b-red' },
|
||||
{ label: '#f3920a', value: 'b-yellow' },
|
||||
{ label: '#08c4c1', value: 'b-cyan' },
|
||||
{ label: '#0a8cf3', value: 'b-blue' },
|
||||
{ label: '#1fd05a', value: 'b-green' },
|
||||
{ label: '#c133f5', value: 'b-purple' },
|
||||
{ label: '#121517', value: 'b-black' },
|
||||
{ label: 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)', value: 'jb-red' },
|
||||
{ label: 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)', value: 'jb-pink' },
|
||||
{ label: 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)', value: 'jb-yellow' },
|
||||
{ label: 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)', value: 'jb-cyan' },
|
||||
{ label: 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)', value: 'jb-blue' },
|
||||
{ label: 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)', value: 'jb-green' },
|
||||
{ label: 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)', value: 'jb-purple' },
|
||||
{ label: 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)', value: 'jb-vip1' },
|
||||
{ label: 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)', value: 'jb-vip2' },
|
||||
];
|
||||
|
||||
const vipUnitOptions = [
|
||||
{ label: '天', value: 'day' },
|
||||
{ label: '月', value: 'month' },
|
||||
];
|
||||
|
||||
// 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||
const vipProductGroupOptions = {
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
listPreview: { prefix: '价格:', titleKey: 'price', descKey: 'time', descUnitKey: 'unit' },
|
||||
};
|
||||
|
||||
// 积分兑换明细的手风琴配置:默认收起,标题行显示积分与时长摘要。
|
||||
const vipPointsGroupOptions = {
|
||||
collapsible: true,
|
||||
defaultCollapsed: true,
|
||||
listPreview: { prefix: '积分:', titleKey: 'points', descKey: 'time', descUnitKey: 'unit' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 会员商品字段集(参考主题 CFS_Module::vip_product)。
|
||||
*/
|
||||
function vipProductFields() {
|
||||
return [
|
||||
{ key: 'price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '699.00', min: 0, suffix: '元', help: '会员的实际购买价格' },
|
||||
{ key: 'show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '999.00', min: 0, suffix: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { key: 'tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
{ key: 'time', label: '会员有效时间', type: 'spinner', default: 3, min: 0, max: 3600, step: 1, help: '开通会员的时长。填0则为永久会员' },
|
||||
{ key: 'unit', label: '时长单位', type: 'radio', default: 'month', inline: true, dependsOn: { key: 'time', operator: 'truthy' }, options: vipUnitOptions },
|
||||
{ key: '_time_permanent_hint', label: '', type: 'alert', alertType: 'success', ui_only: true, dependsOn: { key: 'time', operator: 'eq', value: 0 }, content: '会员有效时间已设置为:永久会员' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 字段集(参考主题 CFS_Module::vip_tab,显示名称并入 Tab)。
|
||||
*/
|
||||
function vipLevelTabFields(level) {
|
||||
const dep = { key: `pay_user_vip_${level}_s`, value: true };
|
||||
return [
|
||||
{ key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' },
|
||||
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' },
|
||||
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用 <li> 列表项' },
|
||||
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
|
||||
{
|
||||
key: `vip_${level}_product`,
|
||||
label: '会员商品',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加会员商品',
|
||||
default: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
...vipProductGroupOptions,
|
||||
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
|
||||
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
|
||||
];
|
||||
}
|
||||
|
||||
export const vipSchema = {
|
||||
key: 'payment.vip',
|
||||
title: 'VIP会员',
|
||||
icon: 'fa fas fa-gem',
|
||||
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
|
||||
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
|
||||
defaults: {
|
||||
nav_pay_vip: true,
|
||||
nav_user_pay_vip: true,
|
||||
pay_user_vip_1_s: true,
|
||||
pay_user_vip_1_name: '一级会员',
|
||||
pay_user_vip_1_desc: '尊享一级会员特权',
|
||||
pay_user_vip_1_equity: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>',
|
||||
pay_user_vip_1_name: '黄金会员',
|
||||
pay_user_vip_1_desc: '尊享专属特权',
|
||||
pay_user_vip_1_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_1_product_s: true,
|
||||
vip_1_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_1_img_icon: '',
|
||||
pay_user_vip_2_s: true,
|
||||
pay_user_vip_2_name: '二级会员',
|
||||
pay_user_vip_2_desc: '尊享二级会员特权',
|
||||
pay_user_vip_2_equity: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>',
|
||||
pay_user_vip_2_name: '钻石会员',
|
||||
pay_user_vip_2_desc: '尊享专属特权',
|
||||
pay_user_vip_2_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_2_product_s: true,
|
||||
vip_2_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_img_icon: '',
|
||||
pay_user_vip_desc: '',
|
||||
pay_user_vip_more: '',
|
||||
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||
pay_download_limit: 0,
|
||||
pay_download_speed: 0,
|
||||
pay_download_limit_vip_1: 0,
|
||||
@@ -29,19 +129,45 @@ export const vipSchema = {
|
||||
pay_download_limit_vip_2: 0,
|
||||
pay_download_speed_vip_2: 0,
|
||||
vip_1_product_s: true,
|
||||
payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
vip_2_product_s: true,
|
||||
payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
vip_renew: true,
|
||||
vip_renew_desc: '',
|
||||
vip_renew_desc: '立即续费会员,畅享VIP权益',
|
||||
vip_renew_price_type: 'discount',
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_1_renew_product: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_renew_product: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_product_s: true,
|
||||
vip_renew: true,
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_upgrade: true,
|
||||
vip_upgrade_desc: '',
|
||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
|
||||
vip_upgrade_product: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
pay_vip_points_exchange_s: false,
|
||||
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
pay_vip_points_exchange_product: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
pay_vip_points_exchange_desc: '',
|
||||
},
|
||||
groups: [
|
||||
@@ -67,17 +193,16 @@ export const vipSchema = {
|
||||
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
||||
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
|
||||
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '一级会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享一级会员特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', default: '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用 <li> 列表项' },
|
||||
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
|
||||
{
|
||||
key: 'vip_level_order_notice',
|
||||
label: '',
|
||||
type: 'notice',
|
||||
ui_only: true,
|
||||
alertType: 'danger',
|
||||
dependsOn: { all: [{ key: 'pay_user_vip_2_s', value: true }, { key: 'pay_user_vip_1_s', operator: 'falsy' }] },
|
||||
content: '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>',
|
||||
},
|
||||
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '二级会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享二级会员特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', default: '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' },
|
||||
{ key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' },
|
||||
{ key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -101,6 +226,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -109,6 +236,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -131,53 +260,111 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_1_products',
|
||||
label: '一级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_1_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(1),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_2_products',
|
||||
label: '二级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_2_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(2),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'renew',
|
||||
title: '会员续费',
|
||||
fields: [
|
||||
{ key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' },
|
||||
{ key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } },
|
||||
{
|
||||
key: 'vip_renew',
|
||||
label: '允许续费',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_desc',
|
||||
label: '续费介绍',
|
||||
type: 'input',
|
||||
sanitize_type: 'html',
|
||||
default: '立即续费会员,畅享VIP权益',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
help: '续费会员的一句话简介,支持HTML',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_price_type',
|
||||
label: '续费价格类型',
|
||||
type: 'radio',
|
||||
type: 'select',
|
||||
default: 'discount',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
options: [
|
||||
{ label: '相同价格', value: 'same' },
|
||||
{ label: '折扣价格', value: 'discount' },
|
||||
{ label: '优惠立减', value: 'reduce' },
|
||||
{ label: '自定义商品', value: 'customize' },
|
||||
],
|
||||
help: '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_discount',
|
||||
label: '续费折扣',
|
||||
type: 'slider',
|
||||
sanitize_type: 'float',
|
||||
default: 8,
|
||||
min: 0.1,
|
||||
max: 9.9,
|
||||
step: 0.1,
|
||||
unit: '折',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'discount' },
|
||||
help: '在会员商品基础上打几折,如 8 = 八折',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_reduce',
|
||||
label: '续费减免金额',
|
||||
type: 'spinner',
|
||||
sanitize_type: 'money_decimal',
|
||||
default: '20.00',
|
||||
min: 0,
|
||||
max: 500,
|
||||
step: 5,
|
||||
unit: '元',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'reduce' },
|
||||
help: '在会员商品基础上优惠的金额,优惠后总金额不能为0元',
|
||||
},
|
||||
{
|
||||
key: 'vip_1_renew_product',
|
||||
label: '一级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '一级会员续费的商品选项',
|
||||
...vipProductGroupOptions,
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{
|
||||
key: 'vip_2_renew_product',
|
||||
label: '二级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '二级会员续费的商品选项',
|
||||
...vipProductGroupOptions,
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
|
||||
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -185,14 +372,50 @@ export const vipSchema = {
|
||||
title: '会员升级',
|
||||
fields: [
|
||||
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
||||
{ key: 'vip_upgrade_desc', label: '升级说明', type: 'textarea', default: '', dependsOn: { key: 'vip_upgrade', value: true } },
|
||||
{ key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' },
|
||||
{
|
||||
key: 'vip_upgrade_product',
|
||||
label: '升级产品',
|
||||
type: 'textarea',
|
||||
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
label: '升级价格',
|
||||
type: 'accordion',
|
||||
default: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
dependsOn: { key: 'vip_upgrade', value: true },
|
||||
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题',
|
||||
help: '会员升级的商品选项',
|
||||
sections: [
|
||||
{ title: '月费会员升级月费会员', keys: ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class'] },
|
||||
{ title: '月费会员升级永久会员', keys: ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class'] },
|
||||
{ title: '永久会员升级永久会员', keys: ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class'] },
|
||||
],
|
||||
fields: [
|
||||
{ key: '_upgrade_unit_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价' },
|
||||
{ key: 'month_to_month_s', label: '月费会员升级月费会员', type: 'switch', default: true },
|
||||
{ key: 'unit_price', label: '天单价', type: 'number', sanitize_type: 'money_decimal', default: '10.00', min: 0, unit: '元', dependsOn: { key: 'month_to_month_s', value: true } },
|
||||
{ key: 'unit_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', rows: 1, dependsOn: { key: 'month_to_month_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'unit_tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { all: [{ key: 'month_to_month_s', value: true }, { key: 'unit_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: '_upgrade_jump_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项' },
|
||||
{ key: 'jump_s', label: '月费会员升级永久会员', type: 'switch', default: false },
|
||||
{ key: 'jump_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '月费会员升级永久会员的价格' },
|
||||
{ key: 'jump_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'jump_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, dependsOn: { key: 'jump_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'jump_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { all: [{ key: 'jump_s', value: true }, { key: 'jump_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: 'permanent_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', help: '永久会员升级永久会员的价格' },
|
||||
{ key: 'permanent_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'permanent_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'permanent_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { key: 'permanent_tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -205,16 +428,72 @@ export const vipSchema = {
|
||||
title: '积分兑换会员',
|
||||
icon: 'fa fas fa-database',
|
||||
fields: [
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false, help: '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费' },
|
||||
{
|
||||
key: 'payment_vip_points_packages',
|
||||
label: '兑换套餐',
|
||||
type: 'textarea',
|
||||
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
key: 'pay_vip_points_exchange_product',
|
||||
label: '积分兑换明细',
|
||||
type: 'group',
|
||||
max: 16,
|
||||
button_title: '添加积分兑换商品',
|
||||
default: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
|
||||
help: '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)',
|
||||
help: '每一行为一个可兑换的会员套餐',
|
||||
...vipPointsGroupOptions,
|
||||
fields: [
|
||||
{ key: 'points', label: '使用积分', type: 'number', default: 100, min: 0, unit: '积分' },
|
||||
{
|
||||
key: 'level',
|
||||
label: '兑换会员',
|
||||
type: 'radio',
|
||||
default: 1,
|
||||
inline: true,
|
||||
options: [
|
||||
{ label: '一级会员', value: 1 },
|
||||
{ label: '二级会员', value: 2 },
|
||||
],
|
||||
},
|
||||
{ key: 'time', label: '兑换时长', type: 'input', default: '', help: '填 Permanent 为永久' },
|
||||
{
|
||||
key: 'unit',
|
||||
label: '时长单位',
|
||||
type: 'radio',
|
||||
default: 'day',
|
||||
inline: true,
|
||||
dependsOn: { key: 'time', operator: 'neq', value: 'Permanent' },
|
||||
options: vipUnitOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_points_exchange_s', value: true }, help: '显示在积分兑换弹窗底部,可以为提醒事项、兑换协议等,支持HTML代码' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'modal-copy',
|
||||
title: '界面文案',
|
||||
icon: 'fa fas fa-align-left',
|
||||
fields: [
|
||||
{
|
||||
key: 'pay_user_vip_desc',
|
||||
label: '会员介绍',
|
||||
type: 'input',
|
||||
sanitize_type: 'html',
|
||||
default: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||
help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML',
|
||||
},
|
||||
{
|
||||
key: 'pay_user_vip_more',
|
||||
label: '开通会员更多内容',
|
||||
type: 'textarea',
|
||||
sanitize_type: 'html',
|
||||
default: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||
rows: 4,
|
||||
dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] },
|
||||
help: '显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码',
|
||||
},
|
||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,7 +10,6 @@ import { featuresSchema } from './features.js'
|
||||
import { verifySchema } from './verify.js'
|
||||
import { levelSchema } from './level.js'
|
||||
import { badgeSchema } from './badge.js'
|
||||
import { wechatSchema } from './wechat.js'
|
||||
import { reportSchema } from './report.js'
|
||||
import { smsSchema } from './sms.js'
|
||||
|
||||
@@ -23,7 +22,6 @@ export const usersSchemas = {
|
||||
verify: verifySchema,
|
||||
level: levelSchema,
|
||||
badge: badgeSchema,
|
||||
wechat: wechatSchema,
|
||||
report: reportSchema,
|
||||
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: '在用户中心显示微信绑定/解绑按钮' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -709,8 +709,9 @@
|
||||
color: var(--color-primary, #f04494);
|
||||
}
|
||||
|
||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but {
|
||||
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but,
|
||||
.widget a.img-badge {
|
||||
color: var(--this-color, #fff);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
||||
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
|
||||
+117
-349
@@ -916,6 +916,7 @@ strong, b {
|
||||
|
||||
/* === Full width === */
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -961,6 +962,8 @@ strong, b {
|
||||
.but > .fa:not(.fa-angle-right),
|
||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||
|
||||
.but.block { display: block; }
|
||||
|
||||
/* hover / active */
|
||||
.but:focus,
|
||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||
@@ -1092,7 +1095,7 @@ strong, b {
|
||||
|
||||
/* gradient modifiers (legacy .jb-*) */
|
||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||
@@ -2062,6 +2065,22 @@ html.dark .alert--info { color: #93C5FD; }
|
||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||
|
||||
/* ============================================================
|
||||
* 8b. 头像形状统一 — 圆形基底
|
||||
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||
* ============================================================ */
|
||||
img.avatar,
|
||||
img.avatar-mini,
|
||||
img.avatar-md,
|
||||
img.avatar-lg,
|
||||
.avatar-img img {
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||
@@ -2638,7 +2657,8 @@ main.container,
|
||||
.user-card.author .user-name > name {
|
||||
overflow: visible;
|
||||
}
|
||||
.user-card.author .user-name > name > .display-name {
|
||||
.user-card.author .user-name > name > .display-name,
|
||||
.user-card.widget .user-name > name > .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2688,31 +2708,35 @@ main.container,
|
||||
/**
|
||||
* Header account popover.
|
||||
*
|
||||
* The shared legacy navigation styles provide the popover shell. This module
|
||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
||||
* The shared legacy navigation styles provide the popover shell, and every
|
||||
* 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. */
|
||||
.navbar-top .aksrt-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||
* override only zeroes the horizontal margins, so every right-cluster group
|
||||
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||
* min-height: it would double-count the 8px and push that group's content 8px
|
||||
* below the line while inflating the navbar from 50px to 58px. */
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -2725,39 +2749,6 @@ main.container,
|
||||
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-avatar {
|
||||
display: inline-flex !important;
|
||||
@@ -2790,33 +2781,34 @@ main.container,
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
||||
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||
* margins that space them apart. */
|
||||
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||
flex: 0 0 29px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-language {
|
||||
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||
* midline instead of sitting 2.5px above it. */
|
||||
.navbar-top .navbar-form.flex.ac {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
||||
* the absolute panel's static position beside the compact CTA. */
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
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;
|
||||
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||
* would move that static position to the flex container's cross-start. */
|
||||
.navbar-top .navbar-but > .hover-show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||
@@ -2852,85 +2844,41 @@ main.container,
|
||||
}
|
||||
|
||||
.aksrt-header-userbox {
|
||||
width: 260px;
|
||||
color: var(--color-ink-2);
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info {
|
||||
min-height: 44px;
|
||||
padding: 2px;
|
||||
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||
.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 {
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||
* 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,
|
||||
.aksrt-header-userbox .user-right > .flex1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||
.aksrt-nav-notification .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
@@ -2950,222 +2898,20 @@ main.container,
|
||||
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
|
||||
* icon tiles and must opt out or their labels overlap the following row. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
* icon tiles and must opt out or their labels overlap the following row.
|
||||
* The tile geometry itself stays with the shared legacy rules
|
||||
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||
* so the action row keeps the reference proportions. */
|
||||
.navbar-top .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;
|
||||
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 {
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
margin-right: 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;
|
||||
}
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* source: assets/css/frontend/card-primitives.css */
|
||||
@@ -6693,6 +6439,28 @@ body.aksrt-modal-runtime-open {
|
||||
.aksrt-after-sale-status.is-rejected,
|
||||
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
||||
|
||||
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
|
||||
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
|
||||
::before,末项不收口,避免出现悬空的线段。 */
|
||||
.aksrt-after-sale-log { display: grid; gap: 6px; }
|
||||
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
|
||||
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
|
||||
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
|
||||
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
|
||||
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
|
||||
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
|
||||
.aksrt-after-sale-timeline__item.is-rejected,
|
||||
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
|
||||
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
|
||||
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
|
||||
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
|
||||
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
|
||||
|
||||
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
|
||||
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||
@@ -137,6 +137,7 @@
|
||||
|
||||
/* === Full width === */
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -182,6 +183,8 @@
|
||||
.but > .fa:not(.fa-angle-right),
|
||||
.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }
|
||||
|
||||
.but.block { display: block; }
|
||||
|
||||
/* hover / active */
|
||||
.but:focus,
|
||||
.but:hover { opacity: 0.8; color: var(--this-color); }
|
||||
@@ -313,7 +316,7 @@
|
||||
|
||||
/* gradient modifiers (legacy .jb-*) */
|
||||
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); }
|
||||
.jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
|
||||
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
|
||||
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
|
||||
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
|
||||
|
||||
@@ -424,6 +424,22 @@
|
||||
.aksrt-avatar--lg { width: var(--aksrt-avatar-size-lg); height: var(--aksrt-avatar-size-lg); }
|
||||
.aksrt-avatar--xl { width: var(--aksrt-avatar-size-xl); height: var(--aksrt-avatar-size-xl); }
|
||||
|
||||
/* ============================================================
|
||||
* 8b. 头像形状统一 — 圆形基底
|
||||
* --radius-avatar 是前端用户头像形状的唯一来源。任何带头像类名的
|
||||
* <img>(含未包 .avatar-img 的裸 img)都必须是正圆,供后续头像
|
||||
* 挂件/装扮叠加。特异性 0,1,1 高于 main.css 的 .avatar{100px},
|
||||
* 同值为圆;路由 CSS 中显式声明圆角的头像框需各自对齐。
|
||||
* ============================================================ */
|
||||
img.avatar,
|
||||
img.avatar-mini,
|
||||
img.avatar-md,
|
||||
img.avatar-lg,
|
||||
.avatar-img img {
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 9. 涟漪点击效果 — .aksrt-ripple
|
||||
* 容器加此类,点击时 JS 注入 span.aksrt-ripple__wave
|
||||
|
||||
@@ -242,7 +242,8 @@ main.container,
|
||||
.user-card.author .user-name > name {
|
||||
overflow: visible;
|
||||
}
|
||||
.user-card.author .user-name > name > .display-name {
|
||||
.user-card.author .user-name > name > .display-name,
|
||||
.user-card.widget .user-name > name > .display-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
/**
|
||||
* Header account popover.
|
||||
*
|
||||
* The shared legacy navigation styles provide the popover shell. This module
|
||||
* owns AKSRT-specific account content such as check-in, wallet and VIP rows.
|
||||
* The shared legacy navigation styles provide the popover shell, and every
|
||||
* 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. */
|
||||
.navbar-top .aksrt-navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
/* The reference keeps the right cluster as three floated .navbar-form groups.
|
||||
* Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
|
||||
* override only zeroes the horizontal margins, so every right-cluster group
|
||||
* starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
|
||||
* put the navbar midline at 25px, so a 34px cross-size lands a group's midline
|
||||
* at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
|
||||
* own 0.4em margin stacked on the group's 8px; the account row reaches it from
|
||||
* its 34px icon tiles, and the theme group needs the same cross-size because
|
||||
* its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
|
||||
* min-height: it would double-count the 8px and push that group's content 8px
|
||||
* below the line while inflating the navbar from 50px to 58px. */
|
||||
.navbar-top .aksrt-navbar-account {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-account > li {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -38,39 +42,6 @@
|
||||
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-avatar {
|
||||
display: inline-flex !important;
|
||||
@@ -103,33 +74,34 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-actions .toggle-radius {
|
||||
/* The account-row chips, the theme and the language toggles all want the 29px
|
||||
* midline circle. The popover's quick-login buttons are .toggle-radius too and
|
||||
* also live inside .navbar-form, but they must keep the .social_loginbar
|
||||
* geometry from main.css: in a flex container flex-basis outranks width, so
|
||||
* without this exemption the buttons collapse to 29px and lose the 5px side
|
||||
* margins that space them apart. */
|
||||
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
|
||||
flex: 0 0 29px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navbar-top .aksrt-navbar-language {
|
||||
/* Match the account row's 34px cross-size so the 29px toggle shares the 25px
|
||||
* midline instead of sitting 2.5px above it. */
|
||||
.navbar-top .navbar-form.flex.ac {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.navbar-top .navbar-form .aksrt-navbar-language {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Anchor the VIP preview to the full navigation row instead of relying on
|
||||
* the absolute panel's static position beside the compact CTA. */
|
||||
.navbar-top .aksrt-navbar-actions .navbar-vip-hover {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
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;
|
||||
/* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
|
||||
* absolutely positioned .sub-vip-card takes its normal-flow static position.
|
||||
* The shared action cluster otherwise forces inline-flex on every child, which
|
||||
* would move that static position to the flex container's cross-start. */
|
||||
.navbar-top .navbar-but > .hover-show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Keep the drop-down search panel above a fixed header (z-index: 999). */
|
||||
@@ -165,85 +137,41 @@
|
||||
}
|
||||
|
||||
.aksrt-header-userbox {
|
||||
width: 260px;
|
||||
color: var(--color-ink-2);
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
.aksrt-header-userbox .user-info {
|
||||
min-height: 44px;
|
||||
padding: 2px;
|
||||
/* public.css redeclares .muted-box after the legacy utility rules, which
|
||||
* otherwise overrides the wallet tiles' padding-6 and radius4 utilities. */
|
||||
.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 {
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
/* core/buttons.css restates the legacy .but block after main.css, so its
|
||||
* background / color / border / text-shadow declarations outrank .payvip-icon
|
||||
* and replace the payvip surface with whatever .jb-* skin is on the element.
|
||||
* The reference theme has no such compatibility layer, so its .payvip-icon
|
||||
* 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,
|
||||
.aksrt-header-userbox .user-right > .flex1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* Keep the header check-in action flush with the VIP row's padded inner width. */
|
||||
.aksrt-header-userbox > .mt6.em09 > .but.block {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Top-nav bell badge (the card itself uses the legacy badge.top element). */
|
||||
.aksrt-nav-notification .aksrt-notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
@@ -263,220 +191,18 @@
|
||||
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
|
||||
* icon tiles and must opt out or their labels overlap the following row. */
|
||||
.navbar-top .aksrt-header-userbox .header-user-href {
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
* icon tiles and must opt out or their labels overlap the following row.
|
||||
* The tile geometry itself stays with the shared legacy rules
|
||||
* (.header-user-href > a and .header-user-href .toggle-radius in main.css)
|
||||
* so the action row keeps the reference proportions. */
|
||||
.navbar-top .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;
|
||||
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 {
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
margin-right: 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;
|
||||
}
|
||||
margin: auto;
|
||||
}
|
||||
@@ -816,6 +816,28 @@ body.aksrt-modal-runtime-open {
|
||||
.aksrt-after-sale-status.is-rejected,
|
||||
.aksrt-after-sale-status.is-cancelled { color: var(--color-error, #ef4444); }
|
||||
|
||||
/* 售后处理记录时间线:[节点][处理状态][时间] 一行,处理备注另起一行并对齐状态文字。
|
||||
售后详细弹窗与订单详情弹窗的「售后记录」共用同一套标记;竖线为条目的
|
||||
::before,末项不收口,避免出现悬空的线段。 */
|
||||
.aksrt-after-sale-log { display: grid; gap: 6px; }
|
||||
.aksrt-after-sale-log__empty { color: var(--modal-muted); font-size: 12px; }
|
||||
.aksrt-after-sale-timeline { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||
.aksrt-after-sale-timeline__item { --as-accent: var(--modal-muted); position: relative; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; padding: 0 0 14px; }
|
||||
.aksrt-after-sale-timeline__item:last-child { padding-bottom: 0; }
|
||||
.aksrt-after-sale-timeline__item.is-pending { --as-accent: var(--modal-warning); }
|
||||
.aksrt-after-sale-timeline__item.is-processing { --as-accent: var(--modal-primary); }
|
||||
.aksrt-after-sale-timeline__item.is-completed { --as-accent: var(--modal-success); }
|
||||
.aksrt-after-sale-timeline__item.is-rejected,
|
||||
.aksrt-after-sale-timeline__item.is-cancelled { --as-accent: var(--modal-danger, #ef4444); }
|
||||
.aksrt-after-sale-timeline__item::before { content: ""; position: absolute; top: 14px; bottom: -2px; left: 4px; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--as-accent) 30%, var(--modal-line)); }
|
||||
.aksrt-after-sale-timeline__item:last-child::before { display: none; }
|
||||
.aksrt-after-sale-timeline__knot { position: relative; z-index: 1; width: 10px; height: 10px; margin-top: 4px; border: 2px solid var(--as-accent); border-radius: 50%; background: var(--modal-surface, #fff); }
|
||||
.aksrt-after-sale-timeline__body { display: grid; gap: 3px; min-width: 0; }
|
||||
.aksrt-after-sale-timeline__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||
.aksrt-after-sale-timeline__status { color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-after-sale-timeline__time { color: var(--modal-muted); font-size: 11px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-after-sale-timeline__note { margin: 0; color: var(--modal-ink); font-size: 12.5px; line-height: 1.65; word-break: break-word; }
|
||||
|
||||
/* 状态行右侧单元格:状态徽章 + 「撤销售后」入口(仅待审核工单渲染)。 */
|
||||
.aksrt-after-sale-detail__statusCell { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.aksrt-after-sale-withdraw { display: inline-flex; align-items: center; gap: 3px; padding: 0; border: 0; background: transparent; color: var(--modal-danger, #ef4444); font-size: 12px; font-weight: 600; line-height: 1.4; cursor: pointer; transition: color .15s ease; }
|
||||
|
||||
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 社交登录分隔线 */
|
||||
.social-login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 24px 0 16px;
|
||||
color: var(--color-ink-3, #94a3b8);
|
||||
font-size: 13px;
|
||||
}
|
||||
.social-login-divider::before,
|
||||
.social-login-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--color-line, #e2e8f0);
|
||||
}
|
||||
.social-login-divider span {
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oauth-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-btn);
|
||||
border: 1px solid var(--color-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-ink-3);
|
||||
transition: border-color 0.2s, color 0.2s;
|
||||
}
|
||||
.oauth-btn i {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.oauth-btn:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.auth-footer-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
||||
@@ -50,8 +50,9 @@
|
||||
color: var(--color-primary, #f04494);
|
||||
}
|
||||
|
||||
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but {
|
||||
/* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
|
||||
.widget a.but,
|
||||
.widget a.img-badge {
|
||||
color: var(--this-color, #fff);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,27 @@
|
||||
z-index: 100055;
|
||||
}
|
||||
|
||||
/* public.css redeclares .muted-box after legacy utilities, overriding its
|
||||
background, radius, and padding utilities. Restore the reference box contract. */
|
||||
.modal[data-aksrt-vip-points-modal] .muted-box.padding-h10 {
|
||||
background-color: var(--this-bg, var(--muted-border-color));
|
||||
border-radius: var(--main-radius);
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.modal[data-aksrt-vip-points-modal] .muted-box.padding-10,
|
||||
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||
background-color: var(--this-bg, var(--muted-border-color));
|
||||
border-radius: var(--main-radius);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal[data-aksrt-vip-points-modal] .aksrt-vip-points-desc {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
color: var(--muted-2-color, #999);
|
||||
}
|
||||
|
||||
.pay-vip-form-row {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -102,21 +123,85 @@
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.vipbg-v2 {
|
||||
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
|
||||
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
|
||||
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
|
||||
aksrt-modal-skeleton(build/public.min.css 已全局入队);
|
||||
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
|
||||
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
|
||||
.aksrt-vip-modal-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__tab {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__pane {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__products {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-vip-modal-skeleton__button {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* 专属形状共用骨架底色与 shimmer(值与 modal-runtime.css 的线条/块一致)。 */
|
||||
.aksrt-vip-modal-skeleton__avatar,
|
||||
.aksrt-vip-modal-skeleton__tab,
|
||||
.aksrt-vip-modal-skeleton__button {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ffb347, #ff6b6b);
|
||||
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
|
||||
background-size: 200% 100%;
|
||||
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.vipbg-v2.level-2 {
|
||||
background: linear-gradient(90deg, #9b59b6, #3f51b5);
|
||||
}
|
||||
|
||||
.vip-expdate-tag {
|
||||
font-size: 12px;
|
||||
@media (min-width: 640px) {
|
||||
.aksrt-vip-modal-skeleton__pane {
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
}
|
||||
}
|
||||
+9
-14
@@ -5936,7 +5936,7 @@ body:has(.modal.in) .float-btn,
|
||||
}
|
||||
|
||||
.payvip-icon:before {
|
||||
content: '\f219';
|
||||
content: '\f3a5';
|
||||
font: normal normal 900 14px/1 "Font Awesome 5 Free";
|
||||
font-size: 3.5em;
|
||||
right: -0.2em !important;
|
||||
@@ -5944,16 +5944,6 @@ body:has(.modal.in) .float-btn,
|
||||
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 {
|
||||
font-size: 0.85em;
|
||||
background: transparent;
|
||||
@@ -9665,11 +9655,16 @@ img.alignleft {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap
|
||||
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高;
|
||||
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */
|
||||
/* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
|
||||
* 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
|
||||
* "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
|
||||
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
|
||||
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
|
||||
.user-card .card-content .img-badge {
|
||||
line-height: 1.42857143;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: auto;
|
||||
text-rendering: auto;
|
||||
}
|
||||
|
||||
.user-card name .display-name {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
||||
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
|
||||
@@ -34,7 +34,7 @@
|
||||
.friend-card__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-avatar, 50%);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface-2);
|
||||
|
||||
@@ -211,10 +211,5 @@ html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card { backgro
|
||||
.aksrt-order-detail__after-sale-badge { padding: 3px 11px; border-radius: 999px; background: color-mix(in srgb, var(--as-accent) 12%, transparent); color: var(--as-accent); font-size: 12px; font-weight: 700; line-height: 1.5; }
|
||||
.aksrt-order-detail__after-sale-head time { color: var(--detail-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||
.aksrt-order-detail__after-sale-reason { margin: 0; color: var(--detail-ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
||||
.aksrt-order-detail__after-sale-meta { display: grid; margin: 0; overflow: hidden; border: 1px solid var(--detail-line); border-radius: 10px; background: var(--detail-surface-3); }
|
||||
.aksrt-order-detail__after-sale-meta > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--detail-line); font-size: 12.5px; }
|
||||
.aksrt-order-detail__after-sale-meta > div:last-child { border-bottom: 0; }
|
||||
.aksrt-order-detail__after-sale-meta dt { color: var(--detail-muted); }
|
||||
.aksrt-order-detail__after-sale-meta dd { margin: 0; color: var(--detail-ink); font-size: 12.5px; text-align: right; word-break: break-word; }
|
||||
.aksrt-order-detail__after-sale-field { color: var(--detail-muted); }
|
||||
.aksrt-order-detail__after-sale-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
@@ -125,6 +125,8 @@
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
/* 用户中心头图卡保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
|
||||
@@ -478,18 +478,6 @@
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-softer);
|
||||
}
|
||||
.binding-item__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--color-ink);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.binding-item__icon i {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
.binding-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -304,12 +304,24 @@
|
||||
body.uc-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.but.jb-blue,
|
||||
.but.b-blue-2 {
|
||||
/* Scoped to the user-center content shell. These are the credits panel's own
|
||||
* button skins, and every element they target renders inside .uc-content (the
|
||||
* panels and their modals are all emitted through templates/parts/user-center/
|
||||
* panel.php, which templates/pages/page-profile.php wraps in .uc-content).
|
||||
*
|
||||
* Left unscoped they are (0,2,0) rules that load after public.css, so they
|
||||
* out-rank the legacy .but.radius pill and flatten every .but.jb-blue on the
|
||||
* page down to 4px. The top-nav publish chip is .but.jb-blue.radius, so on the
|
||||
* user-center route it lost its 50px pill and no longer matched the homepage.
|
||||
* Site-wide .but.jb-blue callers outside the panel (login, submit, badge and
|
||||
* cashier buttons) also picked up the flat #3b82f6 skin instead of the legacy
|
||||
* gradient.
|
||||
*
|
||||
* The .btn-block restatement that used to sit here was byte-identical to the
|
||||
* one in core/buttons.css, which loads earlier on every page, so it could never
|
||||
* win and is dropped rather than scoped. */
|
||||
.uc-content .but.jb-blue,
|
||||
.uc-content .but.b-blue-2 {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
@@ -317,18 +329,18 @@ body.uc-modal-open {
|
||||
padding: 0.45em 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.but.c-blue-2.hollow,
|
||||
.but.c-green.hollow {
|
||||
.uc-content .but.c-blue-2.hollow,
|
||||
.uc-content .but.c-green.hollow {
|
||||
background: transparent;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 4px;
|
||||
padding: 0.4em 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.but.c-blue-2 {
|
||||
.uc-content .but.c-blue-2 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.but.c-green {
|
||||
.uc-content .but.c-green {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +264,6 @@
|
||||
.aksrt-account-settings__section--social .binding-captcha { margin-bottom: 12px; }
|
||||
.aksrt-account-list--social .binding-item { border: 0; }
|
||||
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
|
||||
.aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); }
|
||||
.aksrt-account-list--social .binding-item__icon i { font-size: 22px; line-height: 1; }
|
||||
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
|
||||
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
|
||||
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
|
||||
|
||||
@@ -622,25 +622,6 @@ body.aksrt-user-center .uc-content,
|
||||
}
|
||||
}
|
||||
|
||||
.uc-service-grid__icon small {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -7px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border: 2px solid var(--surface-1);
|
||||
border-radius: 99px;
|
||||
background: var(--color-error, #ef4444);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.uc-sidebar__pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -684,7 +665,8 @@ body.aksrt-user-center .uc-content,
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.uc-summary-card__body > i {
|
||||
.uc-summary-card__body > i,
|
||||
.uc-summary-card__body > svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -716,14 +698,26 @@ body.aksrt-user-center .uc-content,
|
||||
|
||||
.uc-center-card strong { font-size: 13px; }
|
||||
.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; }
|
||||
.uc-center-card > i { color: var(--color-ink-4); font-size: 11px; }
|
||||
.uc-center-card > i,
|
||||
.uc-center-card > svg { color: var(--color-ink-4); font-size: 11px; }
|
||||
.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); }
|
||||
.uc-center-card--revenue .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); }
|
||||
|
||||
.uc-service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
/* Services / settings grids, ported from the reference theme sidebar:
|
||||
four per row, no tinted tile behind the icon. */
|
||||
.user-icon-but-box > a {
|
||||
width: calc(25% - 10px);
|
||||
margin: 5px;
|
||||
min-width: 50px;
|
||||
max-width: 100px;
|
||||
color: var(--color-ink-3);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-icon-but-box > a:hover,
|
||||
.user-icon-but-box > a.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uc-follow-management__list { display: grid; gap: 10px; margin-top: 16px; }
|
||||
@@ -735,44 +729,3 @@ body.aksrt-user-center .uc-content,
|
||||
.uc-follow-management__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
|
||||
@media (max-width: 560px) { .uc-follow-management__item { align-items: flex-start; flex-wrap: wrap; } .uc-follow-management__actions { width: 100%; margin-left: 60px; } }
|
||||
|
||||
.uc-service-grid__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
padding: 4px 2px;
|
||||
color: var(--color-ink-3);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.uc-service-grid__icon {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-2);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uc-service-grid__item--cart .uc-service-grid__icon { color: #dd6941; background: rgba(238, 109, 61, 0.12); }
|
||||
.uc-service-grid__item--level .uc-service-grid__icon { color: #9b6b0a; background: rgba(234, 179, 8, 0.13); }
|
||||
.uc-service-grid__item--revenue .uc-service-grid__icon { color: #b8377d; background: rgba(219, 39, 119, 0.12); }
|
||||
.uc-service-grid__item--auth .uc-service-grid__icon { color: #287bc4; background: rgba(14, 165, 233, 0.12); }
|
||||
.uc-service-grid__item--orders .uc-service-grid__icon { color: #c05621; background: rgba(234, 88, 12, 0.12); }
|
||||
.uc-service-grid__item--notice .uc-service-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); }
|
||||
.uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); }
|
||||
.uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); }
|
||||
|
||||
.uc-service-grid__item:hover,
|
||||
.uc-service-grid__item.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uc-service-grid__item.is-active .uc-service-grid__icon {
|
||||
box-shadow: 0 0 0 2px var(--color-primary-soft), 0 5px 14px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
@@ -88,6 +88,8 @@
|
||||
.aksrt-author-hero__avatar img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
/* 公开主页 hero 头像保持 legacy 圆角方形:显式声明圆角,避免被 public 捆绑里
|
||||
同特异性(0,1,1)的全局头像原语 img.avatar 抢先。 */
|
||||
border-radius: var(--radius);
|
||||
border: 4px solid var(--surface-1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
|
||||
+14
-4
@@ -38,6 +38,14 @@
|
||||
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) {
|
||||
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||
return {
|
||||
@@ -64,16 +72,18 @@
|
||||
var walletNumber = Number(wallet);
|
||||
var walletText = Number.isFinite(walletNumber) ? walletNumber.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(wallet);
|
||||
document.querySelectorAll('[data-aksrt-wallet-value]').forEach(function (element) {
|
||||
element.textContent = walletText;
|
||||
element.title = walletText;
|
||||
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(wallet) : walletText;
|
||||
element.textContent = text;
|
||||
element.title = text;
|
||||
});
|
||||
}
|
||||
if (points !== undefined && points !== null && points !== '') {
|
||||
var pointsNumber = Number(points);
|
||||
var pointsText = Number.isFinite(pointsNumber) ? pointsNumber.toLocaleString() : String(points);
|
||||
document.querySelectorAll('[data-aksrt-points-value]').forEach(function (element) {
|
||||
element.textContent = pointsText;
|
||||
element.title = pointsText;
|
||||
var text = element.dataset.aksrtCountFormat === 'cut-count' ? formatCutCount(points) : pointsText;
|
||||
element.textContent = text;
|
||||
element.title = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@
|
||||
this.bindFeatured();
|
||||
this.bindPay();
|
||||
this.bindLeaveWarning();
|
||||
this.bindAi();
|
||||
this.bindTagActive();
|
||||
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(); });
|
||||
|
||||
@@ -91,11 +91,6 @@
|
||||
return '¥' + raw;
|
||||
}
|
||||
|
||||
function aftersaleAmountText(value, order) {
|
||||
var amount = String(value == null ? '' : value);
|
||||
return order && order.is_points ? amount + ' ' + String((order && order.currency_symbol) || '积分') : '¥' + amount;
|
||||
}
|
||||
|
||||
function orderSummaryHtml(order) {
|
||||
if (!order) return '';
|
||||
return '<div class="muted-box padding-10 mb20 em09 aksrt-after-sale-summary">' +
|
||||
@@ -169,6 +164,46 @@
|
||||
'<span class="aksrt-after-sale-detail__value">' + escapeHtml(value) + '</span></div>';
|
||||
}
|
||||
|
||||
// 处理记录节点标题:用户侧提交/撤脱离不开工单状态,单独措辞;
|
||||
// 后台提交直接用目标状态,与后台列表口径一致。
|
||||
function handlingEntryTitle(entry) {
|
||||
var status = String((entry && entry.status) || '');
|
||||
if (String((entry && entry.source) || '') === 'user') {
|
||||
return status === 'cancelled' ? '用户撤销售后' : '用户提交申请';
|
||||
}
|
||||
return STATUS_LABELS[status] || status;
|
||||
}
|
||||
|
||||
// 处理记录时间线:左列绳结状态条(节点 + 状态 + 时间),右列处理备注。
|
||||
// 数据源为 latest.handling_log(工单行内 JSON,按时间正序)。
|
||||
function handlingLogHtml(log) {
|
||||
var entries = Array.isArray(log) ? log : [];
|
||||
if (!entries.length) {
|
||||
return '<div class="aksrt-after-sale-log"><div class="aksrt-after-sale-log__empty">暂无处理记录</div></div>';
|
||||
}
|
||||
var html = '<div class="aksrt-after-sale-log"><div class="muted-2-color em09 mb6">处理记录</div>';
|
||||
html += '<ol class="aksrt-after-sale-timeline">';
|
||||
entries.forEach(function (entry) {
|
||||
var status = String((entry && entry.status) || '');
|
||||
var note = String((entry && entry.note) || '');
|
||||
html += '<li class="aksrt-after-sale-timeline__item is-' + escapeHtml(status) + '">';
|
||||
html += '<span class="aksrt-after-sale-timeline__knot"></span>';
|
||||
html += '<div class="aksrt-after-sale-timeline__body">';
|
||||
// [节点][处理状态][时间] 同一行,处理备注另起一行
|
||||
html += '<div class="aksrt-after-sale-timeline__head">' +
|
||||
'<span class="aksrt-after-sale-timeline__status">' + escapeHtml(handlingEntryTitle(entry)) + '</span>' +
|
||||
'<time class="aksrt-after-sale-timeline__time">' + escapeHtml(String((entry && entry.created_at) || '')) + '</time>' +
|
||||
'</div>';
|
||||
if (note !== '') {
|
||||
html += '<p class="aksrt-after-sale-timeline__note">' + escapeHtml(note).replace(/\r\n|\r|\n/g, '<br>') + '</p>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</li>';
|
||||
});
|
||||
html += '</ol></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildDetailHtml(order) {
|
||||
var afterSale = (order && order.after_sale) || {};
|
||||
var latest = afterSale.latest || {};
|
||||
@@ -191,10 +226,9 @@
|
||||
|
||||
html += '<div class="aksrt-after-sale-detail__rows">';
|
||||
html += detailRow('申请时间', String(latest.created_at || ''));
|
||||
html += detailRow('退款金额', aftersaleAmountText(latest.refund_amount, order));
|
||||
html += detailRow('处理备注', String(latest.admin_note || ''));
|
||||
html += detailRow('完成时间', String(latest.completed_at || ''));
|
||||
html += '</div>';
|
||||
// 处理记录:每次后台提交一条,替代原先单行的「处理备注 / 完成时间」。
|
||||
html += handlingLogHtml(latest.handling_log);
|
||||
|
||||
// 申请表单分行展示:预置原因 / 申请资料 / 申请说明各自独立成行,
|
||||
// 与上方「申请时间」等明细行同格式;老数据无 apply_form 时兜底展示融合串。
|
||||
|
||||
@@ -106,26 +106,6 @@
|
||||
return "<div><dt>" + escapeHtml(label) + "</dt><dd>" + cell + "</dd></div>";
|
||||
}
|
||||
|
||||
function gatewayResultText(value) {
|
||||
if (!value) return "";
|
||||
var result = value;
|
||||
if (typeof value === "string") {
|
||||
try { result = JSON.parse(value); } catch (_) { return String(value); }
|
||||
}
|
||||
if (!result || typeof result !== "object") return String(result);
|
||||
var labels = { succeeded: "退款成功", failed: "渠道明确拒绝", pending: "渠道处理中", unknown: "结果待确认" };
|
||||
var parts = [];
|
||||
if (result.status) parts.push(labels[String(result.status)] || String(result.status));
|
||||
if (result.message) parts.push(String(result.message));
|
||||
if (result.refund_id) parts.push("退款单号 " + String(result.refund_id));
|
||||
return parts.join(" · ") || "渠道暂未提供可读结果";
|
||||
}
|
||||
|
||||
function afterSaleAmount(value, order) {
|
||||
var amount = String(value == null ? "" : value);
|
||||
return order && order.is_points ? amount + " " + String(order.currency_symbol || "积分") : "¥" + amount;
|
||||
}
|
||||
|
||||
function colorfulHeader(title, icon) {
|
||||
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||||
'<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>' +
|
||||
@@ -205,11 +185,6 @@
|
||||
} else if (item.reason) {
|
||||
html += '<p class="aksrt-order-detail__after-sale-reason">' + escapeHtml(item.reason).replace(/\r\n|\r|\n/g, "<br>") + "</p>";
|
||||
}
|
||||
html += '<dl class="aksrt-order-detail__after-sale-meta">';
|
||||
html += "<div><dt>退款金额</dt><dd>" + escapeHtml(afterSaleAmount(item.refund_amount, order)) + "</dd></div>";
|
||||
if (item.admin_note) html += "<div><dt>处理备注</dt><dd>" + escapeHtml(item.admin_note) + "</dd></div>";
|
||||
if (item.gateway_result) html += "<div><dt>渠道结果</dt><dd>" + escapeHtml(gatewayResultText(item.gateway_result)) + "</dd></div>";
|
||||
html += "</dl>";
|
||||
// 查看详细:逐记录构造售后详细弹窗载荷(after_sale.latest 指向本条记录);
|
||||
// 再次申请仅对最新一条且订单级可再申请时开放(after_sales 按 id 倒序,首条即最新)。
|
||||
var recordPayload = escapeHtml(JSON.stringify({
|
||||
@@ -230,8 +205,7 @@
|
||||
images: Array.isArray(item.images) ? item.images : [],
|
||||
refund_amount: String(item.refund_amount || "0"),
|
||||
created_at: String(item.created_at || ""),
|
||||
completed_at: String(item.completed_at || ""),
|
||||
admin_note: String(item.admin_note || ""),
|
||||
handling_log: Array.isArray(item.handling_log) ? item.handling_log : [],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
+110
-23
@@ -17,6 +17,14 @@
|
||||
return el;
|
||||
}
|
||||
|
||||
// Server-side schema sanitization limits these configured rich-text fields to post-safe HTML.
|
||||
function htmlNode(tag, className, value) {
|
||||
var el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (value !== undefined && value !== null) el.innerHTML = text(value);
|
||||
return el;
|
||||
}
|
||||
|
||||
function imageNode(src, className) {
|
||||
if (!src) return null;
|
||||
var image = document.createElement("img");
|
||||
@@ -61,7 +69,7 @@
|
||||
if (levelInfo && levelInfo.img_icon) {
|
||||
var img = node("img", "img-icon");
|
||||
img.src = text(levelInfo.img_icon);
|
||||
img.alt = text(levelInfo.name || "");
|
||||
img.alt = text(levelInfo.name_text || levelInfo.name || "");
|
||||
img.decoding = "async";
|
||||
return img;
|
||||
}
|
||||
@@ -115,7 +123,8 @@
|
||||
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
||||
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
|
||||
// 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。
|
||||
if (item.discount_tag) append(product, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
|
||||
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
||||
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||
append(label, product);
|
||||
@@ -179,7 +188,7 @@
|
||||
box.setAttribute("data-condition", "==");
|
||||
box.setAttribute("data-value", "card_pass");
|
||||
if (defaultMethod !== "card_pass") box.style.display = "none";
|
||||
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||
if (cardPass.desc) append(box, htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
||||
if (!cardPass.only_password) {
|
||||
var cardInput = node("input", "form-control mb6");
|
||||
@@ -384,12 +393,7 @@
|
||||
var avatarItem = node("li");
|
||||
var avatarWrap = node("div", "avatar-img");
|
||||
if (member.avatar_url) {
|
||||
var avatar = node("img");
|
||||
avatar.src = text(member.avatar_url);
|
||||
avatar.alt = "";
|
||||
avatar.width = 42;
|
||||
avatar.height = 42;
|
||||
append(avatarWrap, avatar);
|
||||
append(avatarWrap, imageNode(text(member.avatar_url), "avatar"));
|
||||
}
|
||||
append(avatarItem, avatarWrap);
|
||||
append(userBox, avatarItem);
|
||||
@@ -403,7 +407,7 @@
|
||||
} else if (member.state === "expired") {
|
||||
append(userInfo, node("div", "muted-2-color em09", "已过期"));
|
||||
} else if (catalog.desc) {
|
||||
append(userInfo, node("div", "muted-2-color em09", catalog.desc));
|
||||
append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc));
|
||||
}
|
||||
append(userBox, userInfo);
|
||||
append(modal, userBox);
|
||||
@@ -439,8 +443,8 @@
|
||||
append(miniIcon, vipIcon(levelInfo));
|
||||
append(miniIconbox, miniIcon);
|
||||
var miniInfo = node("div", "v-info");
|
||||
append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||
append(miniInfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level)));
|
||||
append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(miniIconbox, miniInfo);
|
||||
append(miniCard, miniIconbox);
|
||||
append(tab, miniCard);
|
||||
@@ -466,15 +470,12 @@
|
||||
append(vicon, vipIcon(levelInfo));
|
||||
append(iconbox, vicon);
|
||||
var vinfo = node("div", "v-info");
|
||||
append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||
append(vinfo, node("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level));
|
||||
append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || ""));
|
||||
append(iconbox, vinfo);
|
||||
append(info, iconbox);
|
||||
if (levelInfo.equity) {
|
||||
var equity = document.createElement("ul");
|
||||
equity.className = "v-equity";
|
||||
equity.innerHTML = text(levelInfo.equity);
|
||||
append(info, equity);
|
||||
append(info, htmlNode("div", "", levelInfo.equity));
|
||||
}
|
||||
var infoBa = node("div", "abs-center vip-baicon");
|
||||
append(infoBa, vipIcon(levelInfo));
|
||||
@@ -497,7 +498,7 @@
|
||||
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
||||
append(products, pointsLink);
|
||||
}
|
||||
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||
if (catalog.more) append(products, htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||
renderMethods(products, methods, cardPass, wallet);
|
||||
var actionName = actionKey(action);
|
||||
var actionAllowed = allowed[actionName] === true;
|
||||
@@ -524,8 +525,6 @@
|
||||
append(modal, tabs);
|
||||
append(modal, content);
|
||||
|
||||
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
|
||||
|
||||
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
|
||||
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
|
||||
applyVipMethodVisibility(pane);
|
||||
@@ -703,6 +702,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
|
||||
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
|
||||
function vipSkeleton() {
|
||||
var skeleton = node("div", "aksrt-vip-modal-skeleton");
|
||||
skeleton.setAttribute("aria-live", "polite");
|
||||
skeleton.setAttribute("aria-busy", "true");
|
||||
|
||||
// 用户框:头像圆 + 两行文字。
|
||||
var user = node("div", "aksrt-vip-modal-skeleton__user");
|
||||
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
|
||||
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
|
||||
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(user, userLines);
|
||||
append(skeleton, user);
|
||||
|
||||
// 等级卡行(vip-cardminis):两张 tab 占位。
|
||||
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
|
||||
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
|
||||
append(skeleton, tabs);
|
||||
|
||||
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
|
||||
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
|
||||
var info = node("div", "aksrt-vip-modal-skeleton__info");
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
|
||||
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(pane, info);
|
||||
var products = node("div", "aksrt-vip-modal-skeleton__products");
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
|
||||
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
|
||||
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
|
||||
append(pane, products);
|
||||
append(skeleton, pane);
|
||||
|
||||
return skeleton.outerHTML;
|
||||
}
|
||||
|
||||
function vipPay(trigger) {
|
||||
var level = trigger
|
||||
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
|
||||
@@ -711,11 +750,11 @@
|
||||
url: "/pay/vip/modal",
|
||||
query: { vip_level: level },
|
||||
className: "aksrt-vip-modal-shell",
|
||||
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>',
|
||||
skeleton: vipSkeleton(),
|
||||
telemetry: "vip",
|
||||
kind: "vip",
|
||||
responseType: "json",
|
||||
cacheTtl: 15000,
|
||||
cacheTtl: 60000,
|
||||
cacheTags: ["vip"],
|
||||
renderData: function (data, session) {
|
||||
renderModalView(session.content, data);
|
||||
@@ -733,6 +772,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
var prefetchedLevels = Object.create(null);
|
||||
|
||||
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
|
||||
function vipModalCacheKey(level) {
|
||||
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
|
||||
}
|
||||
|
||||
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
|
||||
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
|
||||
function prefetchVipModal(trigger) {
|
||||
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
|
||||
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
|
||||
if (level < 1 || level > 2) level = 1;
|
||||
if (prefetchedLevels[level]) return;
|
||||
prefetchedLevels[level] = true;
|
||||
var cache = window.AksrtModalCache;
|
||||
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
|
||||
try {
|
||||
cache.acquire(vipModalCacheKey(level), function () {
|
||||
return window.AksrtRequest.request("/pay/vip/modal", {
|
||||
method: "GET",
|
||||
query: { vip_level: level },
|
||||
});
|
||||
}, {
|
||||
ttl: 60000,
|
||||
tags: ["vip"],
|
||||
});
|
||||
} catch (error) {
|
||||
// 预热失败不影响点击后的正常打开路径。
|
||||
}
|
||||
}
|
||||
|
||||
function vipTriggerFrom(event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) return null;
|
||||
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
}
|
||||
|
||||
function bindPrefetch() {
|
||||
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
|
||||
["pointerenter", "focusin"].forEach(function (type) {
|
||||
document.addEventListener(type, function (event) {
|
||||
prefetchVipModal(vipTriggerFrom(event));
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
function onDocClick(event) {
|
||||
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
|
||||
if (!trigger) return;
|
||||
@@ -773,6 +859,7 @@
|
||||
if (window.__aksrtPayVipBound) return;
|
||||
window.__aksrtPayVipBound = true;
|
||||
document.addEventListener("click", onDocClick, true);
|
||||
bindPrefetch();
|
||||
bindPointsModal();
|
||||
bindPointsEntry();
|
||||
}
|
||||
|
||||
@@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||||
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||||
\AKSRT\Core\SEOHead::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
||||
}
|
||||
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')) :
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
if (!$actions) {
|
||||
// Last resort: keep a write-post entry so the top-nav chip is never blank.
|
||||
$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',
|
||||
]];
|
||||
return '';
|
||||
}
|
||||
|
||||
$a = $actions[0];
|
||||
if ($label === '') {
|
||||
$label = __('发布', 'aksrt');
|
||||
}
|
||||
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
|
||||
|
||||
// Split paint classes vs wrapper tokens.
|
||||
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$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;
|
||||
return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
|
||||
. $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
*/
|
||||
function aksrt_render_header_menu_buttons(): string {
|
||||
$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']),
|
||||
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))),
|
||||
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']),
|
||||
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
|
||||
];
|
||||
$enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
|
||||
$text = trim((string) ($settings['nav_newposts_text'] ?? ''));
|
||||
$pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
|
||||
$skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
|
||||
|
||||
$html = '';
|
||||
if (!empty($cfg['enabled'])) {
|
||||
if ($enabled) {
|
||||
// legacy default: but nowave jb-blue (+ radius).
|
||||
$class = 'but nowave navbar-newposts';
|
||||
$skin = trim((string) ($cfg['class'] ?? ''));
|
||||
if ($skin === '') {
|
||||
$skin = 'jb-blue';
|
||||
}
|
||||
$class .= ' ' . $skin;
|
||||
$pill = !empty($cfg['radius_pill']);
|
||||
$class = 'but nowave ' . $skin;
|
||||
if ($pill && !str_contains($class, 'radius')) {
|
||||
$class .= ' radius';
|
||||
}
|
||||
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
|
||||
$label = $text !== '' ? $text : __('发布', 'aksrt');
|
||||
$btn = aksrt_render_new_add_btns(['post'], $class, $label);
|
||||
if ($btn !== '') {
|
||||
$html .= $btn;
|
||||
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
$show_vip = false;
|
||||
if (class_exists('\AKSRT\Settings\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) {
|
||||
$is_vip = false;
|
||||
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
|
||||
if (method_exists($svc, 'render_vip_card')) {
|
||||
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
|
||||
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
|
||||
$html .= '<span class="hover-show inline-block navbar-vip-hover">';
|
||||
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"'
|
||||
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
|
||||
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
|
||||
$html .= '</a>' . $hover . '</span>';
|
||||
// legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
|
||||
$html .= '<span class="hover-show inline-block">';
|
||||
$html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
|
||||
. '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. esc_html__('开通会员', 'aksrt') . '</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.
|
||||
*
|
||||
* @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
|
||||
* <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();
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\Credits\Signin')) {
|
||||
return '';
|
||||
@@ -56,38 +55,39 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
|
||||
|
||||
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
|
||||
if ($signed) {
|
||||
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
|
||||
if ($class !== null) {
|
||||
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
|
||||
// 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
|
||||
return aksrt_get_user_checkin_details_link(
|
||||
$class . ' is-done',
|
||||
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
|
||||
$class,
|
||||
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
|
||||
. esc_html__('已签到', 'aksrt')
|
||||
);
|
||||
}
|
||||
return aksrt_get_user_checkin_details_link(
|
||||
'but block c-yellow aksrt-header-checkin is-done',
|
||||
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
|
||||
'but block c-yellow is-done',
|
||||
'<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):
|
||||
// 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).
|
||||
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-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>'
|
||||
. '</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') . '">'
|
||||
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
|
||||
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
|
||||
. '</button>';
|
||||
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
|
||||
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
|
||||
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
|
||||
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
|
||||
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
|
||||
. '</a>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -118,41 +118,65 @@ add_action('wp_footer', 'aksrt_render_signin_captcha');
|
||||
|
||||
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 {
|
||||
$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 '';
|
||||
}
|
||||
$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) . '">';
|
||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-database" aria-hidden="true"></i> '
|
||||
. esc_html($currency) . ' <b data-aksrt-points-value data-credits-value>' . esc_html(number_format_i18n($credits)) . '</b></span>';
|
||||
$html .= '<span class="aksrt-header-wallet__item"><i class="fa fas fa-money-bill" aria-hidden="true"></i> '
|
||||
. esc_html__('余额', 'aksrt') . ' <b data-aksrt-wallet-value>' . esc_html(number_format_i18n($wallet, 2)) . '</b></span>';
|
||||
$html .= '</a>';
|
||||
return $html;
|
||||
$balance_on = !empty(\AKSRT\Settings\Settings::get_payment_recharge_settings()['enabled']);
|
||||
$points_on = \AKSRT\Settings\Settings::credits_enabled();
|
||||
if (!$balance_on && !$points_on) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : '#';
|
||||
$box = '';
|
||||
$balance_icon = aksrt_icon_markup('zibsvg-money-color-2', 'em14');
|
||||
$points_icon = aksrt_icon_markup('zibsvg-points-color', 'em14');
|
||||
|
||||
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>' . $balance_icon . '</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>' . $points_icon . '</div></a>';
|
||||
}
|
||||
|
||||
return '<div class="flex ab jsb col-ml6">' . $box . '</div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
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 {
|
||||
$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 '';
|
||||
}
|
||||
// Certification is displayed beside the name and level icon in the profile row.
|
||||
return '';
|
||||
$items = aksrt_get_user_badges($user_id);
|
||||
if ($items === '') {
|
||||
return '';
|
||||
}
|
||||
return '<div class="em09 author-tag mb10 mt6 flex jc">' . $items . '</div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -173,30 +197,33 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
||||
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;
|
||||
if ($level > 0) {
|
||||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||||
$name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||
$name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt')));
|
||||
// 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到
|
||||
// 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久
|
||||
// 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。
|
||||
$exp_label = '';
|
||||
if (function_exists('aksrt_get_vip_info')) {
|
||||
$info = aksrt_get_vip_info($user_id);
|
||||
$expires = max(0, (int) ($info['expires'] ?? 0));
|
||||
$exp_label = $expires > 0
|
||||
? sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires))
|
||||
: (string) ($info['expires_label'] ?? '');
|
||||
if (function_exists('aksrt_get_user_vip_state')) {
|
||||
$state = aksrt_get_user_vip_state($user_id);
|
||||
$expires = max(0, (int) ($state['expires'] ?? 0));
|
||||
if ($expires > 0) {
|
||||
$exp_label = $expires <= time()
|
||||
? __('会员已过期', '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 .= '<span class="aksrt-header-payvip__level">';
|
||||
$html .= '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||||
$html .= '<span>' . esc_html($name) . '</span>';
|
||||
$html .= '</span>';
|
||||
$html = '<div class="mt10 em09" style="padding:2px;">';
|
||||
$html .= '<span class="radius4 payvip-icon btn-block text-center">';
|
||||
$html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : '';
|
||||
$html .= '<span>' . wp_kses_post($name) . '</span>';
|
||||
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>';
|
||||
return $html;
|
||||
@@ -205,11 +232,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
|
||||
return '';
|
||||
}
|
||||
|
||||
// Match check-in CTA geometry (full width, 6px radius) — not top-nav navbar-cta chips.
|
||||
return '<div class="mt10 em09 aksrt-header-payvip">'
|
||||
. '<a class="pay-vip but block payvip-icon aksrt-header-payvip__cta" href="javascript:;" vip-level="1">'
|
||||
. '<i class="fa fas fa-gem" aria-hidden="true"></i>'
|
||||
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>'
|
||||
// 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
|
||||
return '<div class="mt10 em09" style="padding:2px;">'
|
||||
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
|
||||
. '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
|
||||
. esc_html__('开通会员 尊享会员权益', 'aksrt')
|
||||
. '</a></div>';
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -42,7 +42,7 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon_map')) :
|
||||
/**
|
||||
* Short key → FA5 class string (allowlisted).
|
||||
* Short key → FA5 class string or built-in SVG sprite key (allowlisted).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
|
||||
return $map;
|
||||
}
|
||||
|
||||
// FA5 Free has no bilibili/gitee brands — use solid stand-ins.
|
||||
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
|
||||
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
|
||||
// a solid stand-in; every other key stays on FA5.
|
||||
$map = [
|
||||
'github' => 'fa fab fa-github',
|
||||
'google' => 'fa fab fa-google',
|
||||
'google' => 'zibsvg-google-color',
|
||||
'x' => 'fa fab fa-twitter',
|
||||
'twitter' => 'fa fab fa-twitter',
|
||||
'email' => 'fa fas fa-envelope',
|
||||
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
|
||||
'instagram' => 'fa fab fa-instagram',
|
||||
'web' => 'fa fas fa-globe',
|
||||
'globe' => 'fa fas fa-globe',
|
||||
'gitee' => 'fa fas fa-code-branch',
|
||||
'gitee' => 'zibsvg-gitee',
|
||||
'baidu' => 'fa fas fa-paw',
|
||||
'alipay' => 'fa fab fa-alipay',
|
||||
];
|
||||
@@ -127,10 +129,10 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon_class')) :
|
||||
/**
|
||||
* Resolve a short key or FA class string to an allowlisted FA5 class.
|
||||
* Resolve a short key, FA class string or sprite key to its icon value.
|
||||
*
|
||||
* @param string $name Short key or full FA class string.
|
||||
* @return string e.g. 'fa fab fa-github', or '' if unknown.
|
||||
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
|
||||
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
|
||||
*/
|
||||
function aksrt_brand_icon_class(string $name): string {
|
||||
$raw = trim($name);
|
||||
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Built-in SVG sprite and inline SVG values pass straight through; they are
|
||||
// not FA class strings and must survive to the markup renderer untouched.
|
||||
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
// Pass through already-valid full FA class strings (footer contact_more).
|
||||
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
|
||||
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
||||
@@ -164,14 +172,21 @@ endif;
|
||||
|
||||
if (!function_exists('aksrt_brand_icon')) :
|
||||
/**
|
||||
* Escaped FA <i> markup for a brand/contact icon.
|
||||
* Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
|
||||
*
|
||||
* @param string $name Short key or FA class string.
|
||||
* @param string $name Short key, FA class string or zibsvg-/customsvg- value.
|
||||
* @param array|string $extra_classes Optional extra classes.
|
||||
* @return string Safe HTML or empty string when unknown.
|
||||
*/
|
||||
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
||||
$class = aksrt_brand_icon_class($name);
|
||||
|
||||
// Sprite and inline-SVG values carry their own element, so the shared icon
|
||||
// renderer owns the markup (it emits <svg class="icon"><use …>).
|
||||
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
|
||||
return aksrt_icon_markup($class, $extra_classes);
|
||||
}
|
||||
|
||||
if ($class === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
$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-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-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) . '"><i class="fa fas fa-comment-dots" aria-hidden="true"></i>' . $com_n . '</a>';
|
||||
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>';
|
||||
} 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.
|
||||
|
||||
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
|
||||
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
|
||||
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
|
||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||
$social = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||
if ($social !== '') {
|
||||
$body .= '<div class="social_loginbar">' . $social . '</div>';
|
||||
}
|
||||
$body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
|
||||
}
|
||||
$body .= '</div>';
|
||||
} else {
|
||||
|
||||
@@ -1309,6 +1309,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
||||
}
|
||||
|
||||
$level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level));
|
||||
$level_name_html = (string) ($level_settings['name_html'] ?? $level_name);
|
||||
$price = $vip_minor === 0
|
||||
? esc_html__('免费', 'aksrt')
|
||||
: $mark . esc_html(aksrt_format_price($value));
|
||||
@@ -1319,7 +1320,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) {
|
||||
? ' data-toggle="tooltip" title="' . esc_attr($title) . '"'
|
||||
: '';
|
||||
|
||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . esc_html($level_name) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||
$items[] = '<span class="but vip-price' . $action_class . '" vip-level="' . esc_attr((string) $level) . '" data-vip-level="' . esc_attr((string) $level) . '"' . $title_attr . '>' . $icon . wp_kses_post($level_name_html) . '<span class="em12 ml3 vip-price-text">' . $price . '</span></span>';
|
||||
}
|
||||
|
||||
$html = implode('', $items);
|
||||
@@ -1384,10 +1385,13 @@ if (!function_exists('aksrt_get_paywall_html')) {
|
||||
$vip_action = $vip_level > 0
|
||||
? sprintf(__('升级%s', 'aksrt'), $level_name)
|
||||
: __('开通会员', 'aksrt');
|
||||
$vip_action_html = $vip_level > 0
|
||||
? __('升级', 'aksrt') . wp_kses_post((string) ($level_settings['name_html'] ?? $level_name))
|
||||
: esc_html__('开通会员', 'aksrt');
|
||||
$action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader';
|
||||
$icon = aksrt_get_pay_vip_icon_html($level_settings);
|
||||
$button = '<div class="badg c-yellow em09 mb6"><i class="fa fas fa-fw fa-info-circle mr6" aria-hidden="true"></i>' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '</div>';
|
||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . esc_html($vip_action) . '</a>';
|
||||
$button .= '<a href="javascript:;" vip-level="' . esc_attr((string) $pay_limit) . '" data-vip-level="' . esc_attr((string) $pay_limit) . '" class="but btn-block jb-vip' . esc_attr((string) $pay_limit) . ' ' . esc_attr($action_class) . ' padding-lg">' . $icon . $vip_action_html . '</a>';
|
||||
} elseif (!is_user_logged_in()) {
|
||||
if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) {
|
||||
$button = '<button type="button" class="but jb-red padding-lg cashier-link' . $block_class . '" data-aksrt-pay-content="' . esc_attr((string) $post->ID) . '"><i class="fa fas fa-shopping-cart" aria-hidden="true"></i> ' . esc_html__('立即购买', 'aksrt') . '</button>';
|
||||
|
||||
@@ -285,11 +285,12 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
}
|
||||
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
||||
|
||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg');
|
||||
// 会员/禁封徽章不再挂头像角标,改由名字行的会员位出图(aksrt_get_user_name)。
|
||||
$avatar_box = aksrt_get_avatar_box($author_id, 'avatar-img avatar-lg', true, false);
|
||||
$name_html = aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'flex1 flex ac',
|
||||
'vip' => false,
|
||||
'vip' => true,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
|
||||
@@ -481,7 +481,7 @@ endif;
|
||||
* or Chinese keyword heuristics — that mangles intentional HTML.
|
||||
*
|
||||
* Rules:
|
||||
* - Sanitize with a small allowlist.
|
||||
* - Sanitize with WordPress post-safe HTML.
|
||||
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
|
||||
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
|
||||
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
|
||||
@@ -497,21 +497,7 @@ if (!function_exists('aksrt_format_vip_equity_html')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'li' => ['class' => true],
|
||||
'ul' => ['class' => true],
|
||||
'ol' => ['class' => true],
|
||||
'strong' => [],
|
||||
'b' => [],
|
||||
'em' => [],
|
||||
'i' => ['class' => true],
|
||||
'span' => ['class' => true],
|
||||
'br' => [],
|
||||
'p' => ['class' => true],
|
||||
'div' => ['class' => true],
|
||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
||||
];
|
||||
$clean = trim(wp_kses($raw, $allowed));
|
||||
$clean = trim(wp_kses_post($raw));
|
||||
if ($clean === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -138,10 +138,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$con = '';
|
||||
|
||||
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;
|
||||
$avatar = aksrt_get_avatar_html($user_id, 44, ['alt' => $display_name]);
|
||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img', true, false);
|
||||
$desc = (string) ($ctx['user_desc'] ?? '');
|
||||
$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_badge = (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html'))
|
||||
@@ -174,13 +174,13 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
|
||||
$msg_icon = '';
|
||||
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 .= '<i class="fa fas fa-bell" aria-hidden="true"></i>';
|
||||
if ($unread > 0) {
|
||||
$msg_icon .= '<span class="aksrt-notification-badge">' . esc_html((string) min(99, $unread)) . '</span>';
|
||||
}
|
||||
$msg_icon .= '</a>';
|
||||
$msg_icon = '<a class="msg-news-icon abs-right" href="' . esc_url($msg_url) . '" title="' . esc_attr__('消息中心', 'aksrt') . '">';
|
||||
$msg_icon .= '<span class="toggle-radius msg-icon"><i class="fa fas fa-bell" aria-hidden="true"></i></span>';
|
||||
if ($unread > 0) {
|
||||
$msg_icon .= '<badge class="top">' . esc_html((string) min(99, $unread)) . '</badge>';
|
||||
}
|
||||
$msg_icon .= '</a>';
|
||||
}
|
||||
|
||||
$href = '<div class="flex jsa header-user-href">';
|
||||
$href .= '<a rel="nofollow" href="' . esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')) . '">';
|
||||
@@ -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 .= '<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>';
|
||||
// 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 .= '<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>';
|
||||
@@ -234,7 +238,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$con .= $avatar;
|
||||
$con .= '<div class="user-right flex flex1 ac jsb ml10">';
|
||||
$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>';
|
||||
$con .= '</div>';
|
||||
$con .= $msg_icon;
|
||||
@@ -264,17 +271,22 @@ 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">'
|
||||
. 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>';
|
||||
$href .= '</div>';
|
||||
|
||||
$con = '<div class="text-center">' . $href . '</div>';
|
||||
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
|
||||
$oauth_html = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
||||
if ($oauth_html !== '') {
|
||||
$con .= '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html__('快速登录', 'aksrt') . '</p>';
|
||||
$con .= '<div class="social_loginbar">' . $oauth_html . '</div>';
|
||||
}
|
||||
$con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function aksrt_menu_button(array $ctx): void {
|
||||
@@ -355,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
|
||||
$account = '<ul class="list-inline splitters relative aksrt-navbar-account'
|
||||
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
|
||||
. $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;">'
|
||||
. '<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>';
|
||||
|
||||
$html = $fold . $button;
|
||||
$html = $fold . $button . $right_but;
|
||||
|
||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ require __DIR__ . '/sidebar-vip.php';
|
||||
require __DIR__ . '/sidebar-commerce.php';
|
||||
require __DIR__ . '/sidebar-creator.php';
|
||||
require __DIR__ . '/sidebar-nav.php';
|
||||
$drawer_trigger_icon = aksrt_icon_markup('zibsvg-menu');
|
||||
?>
|
||||
<button type="button" class="uc-sidebar__drawer-trigger" id="uc-drawer-trigger" aria-label="<?php esc_attr_e('打开导航', 'aksrt'); ?>">
|
||||
<i class="fa fas fa-th-large" aria-hidden="true"></i>
|
||||
<?php echo $drawer_trigger_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</button>
|
||||
@@ -26,10 +26,14 @@ foreach ((array) ($oauth_data['bindings'] ?? []) as $binding) {
|
||||
if (!$key) {
|
||||
continue;
|
||||
}
|
||||
// Reference parity: the provider type (clogin_ prefix stripped) drives the
|
||||
// .social-login-item brand colour, matching zibll's binding list.
|
||||
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||
$binding_rows[$key] = [
|
||||
'key' => $key,
|
||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||
'icon' => $binding['icon'] ?? '',
|
||||
'icon_type' => $icon_type,
|
||||
'is_bound' => true,
|
||||
'url' => '',
|
||||
];
|
||||
@@ -39,10 +43,12 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
if (!$key) {
|
||||
continue;
|
||||
}
|
||||
$icon_type = str_starts_with($key, 'clogin_') ? substr($key, 7) : $key;
|
||||
$binding_rows[$key] = [
|
||||
'key' => $key,
|
||||
'name' => (string) ($binding['label'] ?? $binding_name_map[$key] ?? $binding['name'] ?? ucfirst($key)),
|
||||
'icon' => $binding['icon'] ?? '',
|
||||
'icon_type' => $icon_type,
|
||||
'is_bound' => false,
|
||||
'url' => $binding['start_url'] ?? $binding['url'] ?? '',
|
||||
];
|
||||
@@ -64,7 +70,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
<?php foreach ($binding_rows as $binding) : ?>
|
||||
<?php if (!empty($binding['is_bound'])) : ?>
|
||||
<div class="aksrt-account-row binding-item">
|
||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||
<span class="binding-status">
|
||||
<i class="fa fas fa-check-circle badge__icon" aria-hidden="true"></i>
|
||||
@@ -76,7 +82,7 @@ foreach ((array) ($oauth_data['available'] ?? []) as $binding) {
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<a href="<?php echo esc_url($binding['url']); ?>" class="aksrt-account-row binding-item binding-item--linkable" target="_self" data-provider="<?php echo esc_attr($binding['key']); ?>" data-bind-start-url="<?php echo esc_url($binding['url']); ?>">
|
||||
<span class="binding-item__icon"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="social-login-item circular <?php echo esc_attr($binding['icon_type']); ?>"><?php echo $binding['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="aksrt-account-row__social-name"><?php echo esc_html($binding['name']); ?></span>
|
||||
<span class="binding-status binding-status--idle"><?php esc_html_e('未绑定', 'aksrt'); ?></span>
|
||||
<span class="but hollow c-blue aksrt-account-row__action"><?php esc_html_e('绑定', 'aksrt'); ?></span>
|
||||
|
||||
@@ -20,23 +20,26 @@ try {
|
||||
$wallet = '0.00';
|
||||
}
|
||||
$points = max(0, (int) ($credit_balance ?? 0));
|
||||
$wallet_icon = aksrt_icon_markup('zibsvg-money-color-2');
|
||||
$points_icon = aksrt_icon_markup('zibsvg-points-color');
|
||||
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||
?>
|
||||
<div class="uc-sidebar__pair uc-sidebar__pair--assets">
|
||||
<?php if ($show_wallet) : ?>
|
||||
<a class="uc-summary-card uc-summary-card--wallet" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
||||
<span class="uc-summary-card__head"><?php esc_html_e('账户余额', 'aksrt'); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-summary-card__body">
|
||||
<strong data-aksrt-wallet-value><?php echo esc_html($wallet); ?></strong>
|
||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
||||
<?php echo $wallet_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($show_credits) : ?>
|
||||
<a class="uc-summary-card uc-summary-card--points" href="<?php echo esc_url($assets_url); ?>" data-tab="credits">
|
||||
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><i class="fa fas fa-angle-right" aria-hidden="true"></i></span>
|
||||
<span class="uc-summary-card__head"><?php echo esc_html((string) ($currency_name ?? __('积分', 'aksrt'))); ?><?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-summary-card__body">
|
||||
<strong data-aksrt-points-value data-credits-value><?php echo esc_html(aksrt_format_points($points)); ?></strong>
|
||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
||||
<?php echo $points_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -13,20 +13,23 @@ $revenue_url = isset($dashboard_tabs['revenue']) ? aksrt_get_user_center_tab_url
|
||||
if ($creation_url === '' && $revenue_url === '') {
|
||||
return;
|
||||
}
|
||||
$creation_icon = aksrt_icon_markup('zibsvg-merchant-color');
|
||||
$revenue_icon = aksrt_icon_markup('zibsvg-money-color');
|
||||
$arrow_icon = aksrt_icon_markup('zibsvg-right');
|
||||
?>
|
||||
<div class="uc-sidebar__pair uc-sidebar__pair--centers">
|
||||
<?php if ($creation_url !== '') : ?>
|
||||
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>" data-tab="creator">
|
||||
<span class="uc-center-card__icon"><i class="fa fas fa-pen" aria-hidden="true"></i></span>
|
||||
<span class="uc-center-card__icon"><?php echo $creation_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span><strong><?php esc_html_e('创作收益', 'aksrt'); ?></strong><small><?php esc_html_e('作者收益明细', 'aksrt'); ?></small></span>
|
||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
||||
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($revenue_url !== '') : ?>
|
||||
<a class="uc-center-card uc-center-card--revenue" href="<?php echo esc_url($revenue_url); ?>" data-tab="revenue">
|
||||
<span class="uc-center-card__icon"><i class="fa fas fa-chart-line" aria-hidden="true"></i></span>
|
||||
<span class="uc-center-card__icon"><?php echo $revenue_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span><strong><?php esc_html_e('推广收益', 'aksrt'); ?></strong><small><?php esc_html_e('推广佣金明细', 'aksrt'); ?></small></span>
|
||||
<i class="fa fas fa-angle-right" aria-hidden="true"></i>
|
||||
<?php echo $arrow_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -2,6 +2,9 @@
|
||||
/**
|
||||
* User center sidebar: services and account settings.
|
||||
*
|
||||
* Icon set mirrors the reference theme user-center sidebar: every entry renders
|
||||
* an inline SVG from the theme sprite through aksrt_icon_markup().
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -17,22 +20,20 @@ $orders_url = $orders_available ? (string) ($orders_url ?? '') : '';
|
||||
$services = array_values(array_filter([
|
||||
[
|
||||
'label' => __('我的等级', 'aksrt'),
|
||||
'icon' => 'chart-line',
|
||||
'tone' => 'level',
|
||||
'icon' => 'trend-color',
|
||||
'url' => $level_url,
|
||||
'tab' => 'level',
|
||||
],
|
||||
|
||||
[
|
||||
'label' => __('官方认证', 'aksrt'),
|
||||
'icon' => 'id-badge',
|
||||
'tone' => 'auth',
|
||||
'icon' => 'user-auth',
|
||||
'url' => $auth_url,
|
||||
'tab' => 'auth',
|
||||
],
|
||||
[
|
||||
'label' => __('我的订单', 'aksrt'),
|
||||
'icon' => 'receipt',
|
||||
'tone' => 'orders',
|
||||
'icon' => 'order-color',
|
||||
'url' => $orders_url,
|
||||
'tab' => 'orders',
|
||||
],
|
||||
@@ -41,35 +42,38 @@ $services = array_values(array_filter([
|
||||
$settings = array_values(array_filter([
|
||||
[
|
||||
'label' => __('消息通知', 'aksrt'),
|
||||
'icon' => 'bell',
|
||||
'tone' => 'notice',
|
||||
'icon' => 'msg-color',
|
||||
'url' => isset($dashboard_tabs['msg']) ? aksrt_get_user_center_tab_url('msg') : '',
|
||||
'tab' => 'msg',
|
||||
],
|
||||
[
|
||||
'label' => __('个人资料', 'aksrt'),
|
||||
'icon' => 'user-edit',
|
||||
'tone' => 'profile',
|
||||
'icon' => 'user-color',
|
||||
'url' => isset($dashboard_tabs['profile']) ? aksrt_get_user_center_tab_url('profile') : '',
|
||||
'tab' => 'profile',
|
||||
],
|
||||
[
|
||||
'label' => __('账户安全', 'aksrt'),
|
||||
'icon' => 'shield-alt',
|
||||
'tone' => 'security',
|
||||
'icon' => 'security-color',
|
||||
'url' => isset($dashboard_tabs['settings']) ? aksrt_get_user_center_tab_url('settings') : '',
|
||||
'tab' => 'settings',
|
||||
],
|
||||
], static fn(array $item): bool => (string) $item['url'] !== ''));
|
||||
|
||||
$render_grid = static function (array $items): void {
|
||||
?>
|
||||
<div class="uc-service-grid">
|
||||
<?php foreach ($items as $item) : ?>
|
||||
<a class="uc-service-grid__item uc-service-grid__item--<?php echo esc_attr((string) $item['tone']); ?><?php echo !empty($item['tab']) && ($active_tab ?? '') === $item['tab'] ? ' is-active' : ''; ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if (!empty($item['tab'])) : ?> data-tab="<?php echo esc_attr((string) $item['tab']); ?>"<?php endif; ?>>
|
||||
<span class="uc-service-grid__icon">
|
||||
<i class="fa fas fa-<?php echo esc_attr((string) $item['icon']); ?>" aria-hidden="true"></i>
|
||||
<?php if (!empty($item['count'])) : ?><small><?php echo esc_html((string) $item['count']); ?></small><?php endif; ?>
|
||||
</span>
|
||||
<span><?php echo esc_html((string) $item['label']); ?></span>
|
||||
<div class="flex ac hh text-center icon-but-box user-icon-but-box">
|
||||
<?php foreach ($items as $item) :
|
||||
$tab = (string) ($item['tab'] ?? '');
|
||||
$item_class = 'icon-but-' . ($tab !== '' ? $tab : 'item');
|
||||
if (($active_tab ?? '') === $tab) {
|
||||
$item_class .= ' is-active';
|
||||
}
|
||||
$icon_html = aksrt_icon_markup('zibsvg-' . (string) $item['icon']);
|
||||
?>
|
||||
<a class="<?php echo esc_attr($item_class); ?>" href="<?php echo esc_url((string) $item['url']); ?>"<?php if ($tab !== '') : ?> data-tab="<?php echo esc_attr($tab); ?>"<?php endif; ?>>
|
||||
<div class="em16"><?php echo $icon_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
<div class="px12 muted-color mt3"><?php echo esc_html((string) $item['label']); ?></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ if ($status === '') {
|
||||
}
|
||||
|
||||
if ($vip_level > 0) {
|
||||
$title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||
$title = (string) ($vip_levels[$vip_level]['name_html'] ?? $vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||
if ($expires <= 0 && ($vip_state['source'] ?? 'none') !== 'order') {
|
||||
$status = __('永久会员', 'aksrt');
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ if ($vip_level > 0) {
|
||||
);
|
||||
}
|
||||
} elseif ($expired_level > 0) {
|
||||
$title = (string) ($vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$title = (string) ($vip_levels[$expired_level]['name_html'] ?? $vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$status = __('已过期', 'aksrt');
|
||||
}
|
||||
|
||||
@@ -49,13 +49,14 @@ $sidebar_level = $vip_level > 0 ? $vip_level : $expired_level;
|
||||
$sidebar_icon_url = esc_url((string) ($vip_levels[$sidebar_level]['img_icon'] ?? ''));
|
||||
$sidebar_icon = $sidebar_icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $sidebar_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
: '<i class="fa fas fa-certificate" aria-hidden="true"></i>';
|
||||
: aksrt_icon_markup('zibsvg-vip_1');
|
||||
$sidebar_arrow = aksrt_icon_markup('zibsvg-right', 'uc-sidebar-vip__arrow');
|
||||
?>
|
||||
<a class="uc-sidebar-vip" href="<?php echo esc_url($vip_url); ?>" data-tab="vip">
|
||||
<span class="uc-sidebar-vip__icon"><?php echo $sidebar_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
||||
<span class="uc-sidebar-vip__content">
|
||||
<strong><?php echo esc_html($title); ?></strong>
|
||||
<small><?php echo esc_html($status); ?></small>
|
||||
<strong><?php echo wp_kses_post($title); ?></strong>
|
||||
<small><?php echo wp_kses_post($status); ?></small>
|
||||
</span>
|
||||
<i class="fa fas fa-angle-right uc-sidebar-vip__arrow" aria-hidden="true"></i>
|
||||
<?php echo $sidebar_arrow; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
@@ -29,6 +29,8 @@ $vip_state_name = (string) ($vip_state['state'] ?? 'unopened');
|
||||
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
||||
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
||||
$renew_desc = (string) ($vip_settings['renew']['desc'] ?? '');
|
||||
$upgrade_desc = (string) ($vip_settings['upgrade']['desc'] ?? '');
|
||||
|
||||
// Membership summary: title → level → intro → expiry.
|
||||
$vip_state = 'unopened';
|
||||
@@ -36,13 +38,15 @@ $vip_state_label = __('未开通', 'aksrt');
|
||||
$vip_level_number = 0;
|
||||
$vip_level_name = __('未开通会员', 'aksrt');
|
||||
$vip_description = $desc_top !== '' ? $desc_top : __('开通会员,尊享专属特权', 'aksrt');
|
||||
$vip_expiry_text = __('到期时间:未开通', 'aksrt');
|
||||
// 未开通时不留到期文案:状态标签已显示「未开通」,参考主题对从未开通
|
||||
// 用户同样不输出任何到期信息(仅生效中/永久/已过期才有)。
|
||||
$vip_expiry_text = '';
|
||||
$vip_is_permanent = false;
|
||||
$btn_type = 'pay';
|
||||
|
||||
if ($level > 0) {
|
||||
$vip_level_number = $level;
|
||||
$vip_level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$vip_level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$level_desc = (string) ($levels[$level]['desc'] ?? '');
|
||||
$vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt');
|
||||
|
||||
@@ -59,7 +63,7 @@ if ($level > 0) {
|
||||
$vip_state_label = __('生效中', 'aksrt');
|
||||
$vip_expiry_text = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y年m月d日', $expires));
|
||||
if ($base_level > 0 && $base_level < $level) {
|
||||
$base_name = (string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level));
|
||||
$base_name = wp_strip_all_tags((string) ($levels[$base_level]['name'] ?? ('VIP' . $base_level)));
|
||||
$base_expiry_text = $base_expires > 0
|
||||
? wp_date('Y年m月d日', $base_expires)
|
||||
: __('永久', 'aksrt');
|
||||
@@ -71,7 +75,7 @@ if ($level > 0) {
|
||||
$vip_state = 'expired';
|
||||
$vip_state_label = __('已过期', 'aksrt');
|
||||
$vip_level_number = $expired_level;
|
||||
$vip_level_name = (string) ($levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$vip_level_name = (string) ($levels[$expired_level]['name_html'] ?? $levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$expired_desc = (string) ($levels[$expired_level]['desc'] ?? '');
|
||||
$vip_description = $expired_desc !== ''
|
||||
? $expired_desc
|
||||
@@ -91,11 +95,12 @@ $status_html = '<div class="uc-vip-status" role="status">'
|
||||
. '<span class="uc-vip-status__state">' . esc_html($vip_state_label) . '</span>'
|
||||
. '</div>'
|
||||
. '<div class="uc-vip-status__main uc-vip-status__main--level-' . (int) $vip_level_number . '">'
|
||||
. '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>'
|
||||
. '<span class="uc-vip-status__level">' . esc_html($vip_level_name) . '</span>'
|
||||
// 未开通(level 0)不输出图标位:名称前不放通用证书图标。
|
||||
. ($vip_level_number > 0 ? '<span class="uc-vip-status__icon" aria-hidden="true">' . $status_icon . '</span>' : '')
|
||||
. '<span class="uc-vip-status__level">' . wp_kses_post($vip_level_name) . '</span>'
|
||||
. '</div>'
|
||||
. '<div class="uc-vip-status__desc">' . esc_html($vip_description) . '</div>'
|
||||
. '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>'
|
||||
. '<div class="uc-vip-status__desc">' . wp_kses_post($vip_description) . '</div>'
|
||||
. ($vip_expiry_text !== '' ? '<div class="uc-vip-status__expiry"><i class="fa fas fa-clock" aria-hidden="true"></i><span>' . esc_html($vip_expiry_text) . '</span></div>' : '')
|
||||
. '</div>';
|
||||
|
||||
// Level cards.
|
||||
@@ -104,15 +109,18 @@ foreach ([1, 2] as $lv) {
|
||||
if (empty($levels[$lv]['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$lv_name = (string) ($levels[$lv]['name_html'] ?? $levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$lv_desc = (string) ($levels[$lv]['desc'] ?? '');
|
||||
// Preserve admin equity HTML as-is (sanitize/wrap only; never re-split into items).
|
||||
$equity_raw = (string) ($levels[$lv]['equity'] ?? '');
|
||||
$equity = function_exists('aksrt_format_vip_equity_html')
|
||||
? aksrt_format_vip_equity_html($equity_raw)
|
||||
: '';
|
||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
||||
$cta = __('开通', 'aksrt') . wp_kses_post($lv_name);
|
||||
$vip_attr = $lv;
|
||||
// 参考主题 zibpay_get_vip_card:续费/升级档位卡展示各自的一句话简介。
|
||||
$card_action = 'pay';
|
||||
$card_desc = $lv_desc;
|
||||
$lv_icon_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
||||
$lv_icon = $lv_icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
@@ -120,9 +128,11 @@ foreach ([1, 2] as $lv) {
|
||||
if (str_starts_with($btn_type, 'renew_')) {
|
||||
$renew_lv = (int) substr($btn_type, 6);
|
||||
if ($lv === $renew_lv) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'renew';
|
||||
} elseif ($lv > $renew_lv) {
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'upgrade';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -130,25 +140,32 @@ foreach ([1, 2] as $lv) {
|
||||
if ($lv <= $level) {
|
||||
continue;
|
||||
}
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$cta = __('升级', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'upgrade';
|
||||
$vip_attr = 2;
|
||||
} elseif ($level > 0 && $lv < $level) {
|
||||
continue;
|
||||
} elseif ($level > 0 && $lv === $level) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$cta = __('续费', 'aksrt') . wp_kses_post($lv_name);
|
||||
$card_action = 'renew';
|
||||
}
|
||||
if ($card_action === 'renew' && $renew_desc !== '') {
|
||||
$card_desc = $renew_desc;
|
||||
} elseif ($card_action === 'upgrade' && $upgrade_desc !== '') {
|
||||
$card_desc = $upgrade_desc;
|
||||
}
|
||||
|
||||
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
||||
$cards .= '<div class="relative uc-vip-level-card__inner">';
|
||||
$cards .= '<div class="v-iconbox mb10">';
|
||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . esc_html($lv_name) . '</div>';
|
||||
if ($lv_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($lv_desc) . '</div>';
|
||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . wp_kses_post($lv_name) . '</div>';
|
||||
if ($card_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . wp_kses_post($card_desc) . '</div>';
|
||||
}
|
||||
$cards .= '</div>';
|
||||
$cards .= $equity;
|
||||
$cards .= '<a class="padding-lg but jb-vip' . $lv . ' radius pay-vip uc-vip-cta" href="javascript:;" vip-level="' . (int) $vip_attr . '">'
|
||||
. '<span>' . esc_html($cta) . '</span>'
|
||||
. '<span>' . $cta . '</span>'
|
||||
. ' <i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></a>';
|
||||
$cards .= '</div></div>';
|
||||
}
|
||||
@@ -169,10 +186,4 @@ foreach ([1, 2] as $lv) {
|
||||
// 下方的全宽横条;仅非会员 + 积分兑换开启 + 有可兑换套餐时输出)。
|
||||
echo aksrt_get_vip_points_exchange_entry_html($user_id, $level); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
|
||||
?>
|
||||
|
||||
<?php if (!empty($vip_settings['more'])) : ?>
|
||||
<div class="muted-2-color em09 muted-box padding-10 mini-radius mt15">
|
||||
<?php echo $vip_settings['more']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
+1
-105
@@ -19,30 +19,12 @@ class AIController extends ControllerBase {
|
||||
}
|
||||
|
||||
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', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_generate'],
|
||||
'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', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'summary_suggest'],
|
||||
@@ -76,66 +58,6 @@ class AIController extends ControllerBase {
|
||||
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 三字段。
|
||||
*/
|
||||
@@ -170,32 +92,6 @@ class AIController extends ControllerBase {
|
||||
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 {
|
||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
@@ -224,7 +120,7 @@ class AIController extends ControllerBase {
|
||||
$ai = \AKSRT\Ai\AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return $this->error(
|
||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
||||
__('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||
503
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
/**
|
||||
* @package AKSRT
|
||||
*
|
||||
* 后台列表页分页条(WP 原生 paginate_links)。
|
||||
* 后台列表页分页条 —— 1:1 复刻「卡密管理」页的 zibpay_admin_pagenavi 结构。
|
||||
*
|
||||
* 支付中心各页面(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 / 提现审核 / 卡密管理)
|
||||
* 原先各写一份几乎相同的 paginate_links 调用,且基准 URL 都写成
|
||||
* add_query_arg('paged', '%#%') —— 只有两个参数时 WordPress 会退到
|
||||
* $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,列条件也容易丢。
|
||||
* 这里统一到一处:显式以「page 参数 + 当前筛选参数」为基准拼装,页码先夹到有效范围。
|
||||
* 原先各写一份几乎相同的翻页代码。这里统一到一处,标记沿用卡密管理那一套:
|
||||
* `« ‹ 第 X 页,共 Y 页 › »` 四枚按钮 + 「每页 X 共Y」计数,没有数字页码链接。
|
||||
*
|
||||
* 与参考实现的两处差异,均为缺陷修复:
|
||||
* 1) 基准 URL 显式以「page 参数 + 当前筛选参数」拼装。历史写法里 add_query_arg('paged', N)
|
||||
* 只有两个参数时会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态
|
||||
* 漂移,列条件也容易丢。
|
||||
* 2) 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导。历史写法在此处会留下
|
||||
* 「第 2 页 / 共 1 页」这类越界状态,四个分支一个都不命中,上一页/下一页同时停在
|
||||
* '#':按钮既不禁用也点不动。
|
||||
*
|
||||
* 单页也渲染分页条(只要总数 > 0):与参考实现一致,运营能直接看到「每页 20 共 5」
|
||||
* 这样的口径,四枚按钮全 disabled 也明确了「没有更多页」。
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
@@ -20,12 +29,14 @@ class AdminListPagination {
|
||||
/** 每页条数默认值。所有列表页一致,且不提供「每页显示」控件。 */
|
||||
public const DEFAULT_PER_PAGE = 20;
|
||||
|
||||
/** @var bool 内联样式只输出一次 */
|
||||
private static $assets_printed = false;
|
||||
|
||||
/**
|
||||
* 渲染分页条。
|
||||
*
|
||||
* 页码夹取到 [1, total_pages] 再决定渲染与否:历史写法在此处会留下「第 2 页 / 共 1 页」
|
||||
* 这类越界状态。真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,
|
||||
* 这里只负责把显示状态收敛到自洽。
|
||||
* 真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,这里只把页码
|
||||
* 夹到有效范围,保证显示状态自洽。
|
||||
*
|
||||
* @param string $page_slug 当前 admin.php 的 page 参数。
|
||||
* @param int $total_count 当前筛选条件下的记录总数。
|
||||
@@ -44,31 +55,57 @@ class AdminListPagination {
|
||||
return;
|
||||
}
|
||||
|
||||
self::print_assets();
|
||||
|
||||
$total_pages = max(1, (int) ceil($total_count / $per_page));
|
||||
$current_page = min(max(1, $current_page), $total_pages);
|
||||
|
||||
if ($total_pages <= 1) {
|
||||
// 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。
|
||||
$base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php'));
|
||||
$page_url = static function (int $page) use ($base): string {
|
||||
return add_query_arg('paged', $page, $base);
|
||||
};
|
||||
|
||||
$on_first = $current_page <= 1;
|
||||
$on_last = $current_page >= $total_pages;
|
||||
|
||||
$first_page_url = $on_first ? '#' : $page_url(1);
|
||||
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
||||
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
||||
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
||||
?>
|
||||
<div class="tablenav bottom">
|
||||
<div class="tablenav-pages">
|
||||
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
|
||||
<span class="pagination-links">
|
||||
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
||||
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
||||
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
||||
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
||||
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* 行内样式。
|
||||
*
|
||||
* 这些原生后台页面不加载主题 CSS(admin-main.css 只在 edit.php 文章列表入队),
|
||||
* 所以分页条依赖的规则随标记一起内联输出。
|
||||
*/
|
||||
private static function print_assets(): void {
|
||||
if (self::$assets_printed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。
|
||||
$base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php'));
|
||||
$base = add_query_arg('paged', '%#%', $base);
|
||||
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo '<span class="displaying-num">' . esc_html(sprintf(
|
||||
/* translators: %s: 记录总数 */
|
||||
__('共 %s 项', 'aksrt'),
|
||||
number_format_i18n($total_count)
|
||||
)) . '</span>';
|
||||
echo paginate_links([
|
||||
'base' => $base,
|
||||
'format' => '',
|
||||
'current' => $current_page,
|
||||
'total' => $total_pages,
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
]); // phpcs:ignore WordPress.Security.EscapeOutput -- paginate_links 自行转义。
|
||||
echo '</div></div>';
|
||||
self::$assets_printed = true;
|
||||
?>
|
||||
<style>
|
||||
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
||||
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
||||
.tablenav-pages a.disabled { pointer-events: none; }
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -351,6 +351,13 @@ class PayOrders {
|
||||
|
||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
|
||||
// 售后记录区块:工单卡片 + 处理记录列表(与售后面板共用 .aksrt-as-log 样式)。
|
||||
echo '<style>'
|
||||
. '.aksrt-as-ticket{padding:10px 0;border-bottom:1px solid #f0f0f1;}'
|
||||
. '.aksrt-as-ticket:last-child{padding-bottom:0;border-bottom:0;}'
|
||||
. '.aksrt-as-ticket__head{margin:0 0 4px;}'
|
||||
. $this->after_sale_log_css()
|
||||
. '</style>';
|
||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||
$rows = [
|
||||
__('订单号', 'aksrt') => $order_number,
|
||||
@@ -371,12 +378,17 @@ class PayOrders {
|
||||
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
||||
if ($after_sales) {
|
||||
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
||||
$status_labels = ['pending' => '待审核', 'processing' => '处理中', 'completed' => '已完成', 'rejected' => '已拒绝', 'cancelled' => '已取消'];
|
||||
$status_labels = $this->after_sale_status_labels();
|
||||
foreach ($after_sales as $application) {
|
||||
echo '<p><strong>' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . '</strong> · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '<br>' . esc_html((string) ($application['reason'] ?? ''));
|
||||
if (!empty($application['admin_note'])) echo '<br>' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']);
|
||||
if (!empty($application['gateway_result'])) echo '<br><code>' . esc_html((string) $application['gateway_result']) . '</code>';
|
||||
echo '</p>';
|
||||
$as_status = (string) ($application['status'] ?? '');
|
||||
echo '<div class="aksrt-as-ticket is-' . esc_attr($as_status) . '">';
|
||||
echo '<p class="aksrt-as-ticket__head"><strong>' . esc_html($status_labels[$as_status] ?? $as_status) . '</strong>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['refund_amount'] ?? '')) . '</span>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['created_at'] ?? '')) . '</span></p>';
|
||||
echo '<p>' . nl2br(esc_html((string) ($application['reason'] ?? ''))) . '</p>';
|
||||
// 处理记录:后台每一次提交都留一条(状态 + 备注 + 时间),与售后面板同款标记。
|
||||
echo $this->render_after_sale_log((array) ($application['handling_log'] ?? []), $status_labels); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
@@ -429,7 +441,7 @@ class PayOrders {
|
||||
|
||||
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_AFTER_SALES_SLUG) . '" />';
|
||||
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
||||
foreach ([\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消'] as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||
foreach ($this->after_sale_status_labels() as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
@@ -452,12 +464,13 @@ class PayOrders {
|
||||
. '.aksrt-as-review textarea{flex:1 1 240px;min-width:0;box-sizing:border-box;margin:0;}'
|
||||
. '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}'
|
||||
. '.aksrt-as-field--wide{grid-column:1/-1;}'
|
||||
. $this->after_sale_log_css()
|
||||
. '@media (min-width:1500px){.aksrt-as-detail-grid{grid-template-columns:repeat(5,minmax(0,1fr));}}'
|
||||
. '@media (max-width:600px){.aksrt-as-detail-grid{grid-template-columns:minmax(0,1fr);}}'
|
||||
. '</style>';
|
||||
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>申请资料</th><th>申请说明</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
||||
if (!$items) echo '<tr><td colspan="9">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
||||
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消'];
|
||||
$status_labels = $this->after_sale_status_labels();
|
||||
foreach ($items as $item) {
|
||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
@@ -487,13 +500,15 @@ class PayOrders {
|
||||
$image_urls = array_values(array_filter((array) ($item['images'] ?? []), 'is_string'));
|
||||
$thumbs = [];
|
||||
foreach ($image_urls as $url) {
|
||||
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" class="aksrt-as-thumb" loading="lazy" /></a>';
|
||||
// 不写原生 loading 懒加载属性:lazyload 契约测试全仓库禁 loading=lazy/eager,
|
||||
// 且后台不加载 lazySizes 运行时,写了也不会被主题那套接管。
|
||||
// 与 UsersMenusVerify 的后台缩略图保持一致(光 <img src>)。
|
||||
$thumbs[] = '<a href="' . esc_url($url) . '" target="_blank" rel="noopener"><img src="' . esc_url($url) . '" alt="" class="aksrt-as-thumb" /></a>';
|
||||
}
|
||||
// 举证图片并入申请说明:缩略图展示在描述文字下方,不再单独成列。
|
||||
$desc_cell = ($desc_text !== '' ? nl2br(esc_html($desc_text)) : '') . ($thumbs ? '<div class="aksrt-as-desc-thumbs" style="margin-top:6px;">' . implode('', $thumbs) . '</div>' : '');
|
||||
if ($desc_cell === '') $desc_cell = '—';
|
||||
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
|
||||
$admin_note = (string) ($item['admin_note'] ?? '');
|
||||
$order_refunded = is_array($order) && (string) ($order['status'] ?? '') === \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED;
|
||||
// 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与状态面板
|
||||
echo '<tr class="aksrt-as-row">';
|
||||
@@ -517,13 +532,15 @@ class PayOrders {
|
||||
__('当前状态', 'aksrt') => esc_html($status_label),
|
||||
__('售后原因', 'aksrt') => $reason_cell,
|
||||
__('申请资料', 'aksrt') => $materials_cell,
|
||||
__('处理备注', 'aksrt') => $admin_note !== '' ? esc_html($admin_note) : '—',
|
||||
__('审核时间', 'aksrt') => !empty($item['reviewed_at']) ? esc_html((string) $item['reviewed_at']) : '—',
|
||||
];
|
||||
echo '<div class="aksrt-as-detail"><div class="aksrt-as-detail-grid">';
|
||||
foreach ($detail_fields as $field_label => $field_value) {
|
||||
echo '<span class="aksrt-as-field"><span class="aksrt-as-label">' . esc_html($field_label) . '</span><span class="aksrt-as-value">' . $field_value . '</span></span>';
|
||||
}
|
||||
// 处理备注:单独占整行,放在申请说明上方;每次提交一条,含状态与时间。
|
||||
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('处理备注', 'aksrt') . '</span><span class="aksrt-as-value">'
|
||||
. $this->render_after_sale_log((array) ($item['handling_log'] ?? []), $status_labels) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
. '</span></span>';
|
||||
// 举证图片并入申请说明,单独占整行展示。
|
||||
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('申请说明', 'aksrt') . '</span><span class="aksrt-as-value">' . $desc_cell . '</span></span>';
|
||||
echo '</div>';
|
||||
@@ -616,6 +633,58 @@ class PayOrders {
|
||||
];
|
||||
}
|
||||
|
||||
/** 售后状态中文标签:订单明细页与售后面板(含筛选下拉)共用,取代两处内联数组。 */
|
||||
/** 处理记录列表样式:两个后台页面各自输出同一套规则,不依赖对方加载。 */
|
||||
private function after_sale_log_css(): string {
|
||||
return '.aksrt-as-log{display:grid;gap:5px;margin:6px 0 0;padding:0;list-style:none;}'
|
||||
. '.aksrt-as-log__item{display:flex;flex-wrap:wrap;gap:3px 10px;align-items:baseline;font-size:12px;line-height:1.7;}'
|
||||
. '.aksrt-as-log__time{color:#646970;font-variant-numeric:tabular-nums;}'
|
||||
. '.aksrt-as-log__status{font-weight:600;}'
|
||||
. '.aksrt-as-log__item.is-pending .aksrt-as-log__status{color:#b26200;}'
|
||||
. '.aksrt-as-log__item.is-processing .aksrt-as-log__status{color:#2271b1;}'
|
||||
. '.aksrt-as-log__item.is-completed .aksrt-as-log__status{color:#007017;}'
|
||||
. '.aksrt-as-log__item.is-rejected .aksrt-as-log__status,.aksrt-as-log__item.is-cancelled .aksrt-as-log__status{color:#b32d2e;}'
|
||||
. '.aksrt-as-log__note{flex:1 1 100%;color:#3c434a;}';
|
||||
}
|
||||
|
||||
private function after_sale_status_labels(): array {
|
||||
return [
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理记录列表:每一次状态提交一条「时间 + 状态 + 备注」,备注为空也留痕。
|
||||
* 后台订单明细页与售后面板共用同一套标记与样式。
|
||||
*
|
||||
* @param list<array<string,mixed>> $log
|
||||
* @param array<string,string> $status_labels
|
||||
*/
|
||||
private function render_after_sale_log(array $log, array $status_labels): string {
|
||||
if (!$log) {
|
||||
return '<p class="description">' . esc_html__('暂无处理记录', 'aksrt') . '</p>';
|
||||
}
|
||||
$service = \AKSRT\Payment\AfterSaleService::class;
|
||||
$html = '<ul class="aksrt-as-log">';
|
||||
foreach ($log as $entry) {
|
||||
$status = (string) ($entry['status'] ?? '');
|
||||
$label = (string) ($entry['source'] ?? '') === $service::SOURCE_USER
|
||||
? ($status === $service::STATUS_CANCELLED ? esc_html__('用户撤销售后', 'aksrt') : esc_html__('用户提交申请', 'aksrt'))
|
||||
: ($status_labels[$status] ?? $status);
|
||||
$note = (string) ($entry['note'] ?? '');
|
||||
$html .= '<li class="aksrt-as-log__item is-' . esc_attr($status) . '">'
|
||||
. '<span class="aksrt-as-log__time">' . esc_html((string) ($entry['created_at'] ?? '')) . '</span>'
|
||||
. '<span class="aksrt-as-log__status">' . esc_html($label) . '</span>'
|
||||
. '<span class="aksrt-as-log__note">' . ($note !== '' ? nl2br(esc_html($note)) : '—') . '</span>'
|
||||
. '</li>';
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
public function handle_after_sale_status(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
||||
|
||||
@@ -92,7 +92,9 @@ class PostListEnhance {
|
||||
|
||||
$order = $this->current_order();
|
||||
$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 = [];
|
||||
foreach ($columns as $key => $label) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class GlobalSchema {
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||
['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_title', 'label' => 'SEO标题(title)', '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_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' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||
['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_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
||||
['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' => 'channel', 'title' => '收录通道', 'fields' => [
|
||||
['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['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],
|
||||
]],
|
||||
],
|
||||
|
||||
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
|
||||
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
|
||||
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_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]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class SchemaPayment {
|
||||
['key' => 'points_pass_exchange_desc', 'label' => '卡密兑换积分说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '用户使用卡密兑换积分时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'points_pass_exchange_only_password', 'label' => '积分卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true], 'help' => '积分卡密仅需输入密码,不需要卡号'],
|
||||
['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可在会员购买/续费弹窗选择「卡密」渠道,使用会员卡密直接兑换会员'],
|
||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换会员时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
['key' => 'pay_vip_pass_charge_only_password', 'label' => '会员卡密单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '会员卡密仅需输入密码,不需要卡号;默认关闭,会员卡需输入卡号和密码(与其他卡密兑换一致)'],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||
@@ -178,7 +178,7 @@ class SchemaPayment {
|
||||
'vip' => [
|
||||
'key' => 'vip',
|
||||
'title' => 'VIP会员',
|
||||
'version' => '1.1.0',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||
@@ -188,17 +188,8 @@ class SchemaPayment {
|
||||
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
||||
['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'],
|
||||
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '一级会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享一级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '留空使用内置 vip-1.svg 图标'],
|
||||
['key' => 'vip_level_order_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'danger', 'dependsOn' => ['all' => [['key' => 'pay_user_vip_2_s', 'value' => true], ['key' => 'pay_user_vip_1_s', 'operator' => 'falsy']]], 'content' => '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>'],
|
||||
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '二级会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享二级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '留空使用内置 vip-2.svg 图标'],
|
||||
['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
||||
@@ -206,11 +197,11 @@ class SchemaPayment {
|
||||
['key' => 'pay_download_limit', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '普通用户每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_1', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '一级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_1', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_2', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '二级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_2', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
@@ -218,36 +209,110 @@ class SchemaPayment {
|
||||
]],
|
||||
['key' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
||||
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => self::vip_level_tab_fields(1)],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => self::vip_level_tab_fields(2)],
|
||||
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
||||
['key' => 'vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_renew', 'value' => true]],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['key' => 'vip_renew_desc', 'label' => '续费介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介,支持HTML'],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'select', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['label' => '相同价格', 'value' => 'same'],
|
||||
['label' => '折扣价格', 'value' => 'discount'],
|
||||
['label' => '优惠立减', 'value' => 'reduce'],
|
||||
]],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
['label' => '自定义商品', 'value' => 'customize'],
|
||||
], 'help' => '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价'],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣', 'type' => 'slider', 'sanitize_type' => 'float', 'default' => 8, 'min' => 0.1, 'max' => 9.9, 'step' => 0.1, 'unit' => '折', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount'], 'help' => '在会员商品基础上打几折,如 8 = 八折'],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'spinner', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 500, 'step' => 5, 'unit' => '元', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce'], 'help' => '在会员商品基础上优惠的金额,优惠后总金额不能为0元'],
|
||||
[
|
||||
'key' => 'vip_1_renew_product', 'label' => '一级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '一级会员续费的商品选项',
|
||||
...self::vip_product_group_options(),
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
[
|
||||
'key' => 'vip_2_renew_product', 'label' => '二级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '二级会员续费的商品选项',
|
||||
...self::vip_product_group_options(),
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]],
|
||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介,支持HTML'],
|
||||
[
|
||||
'key' => 'vip_upgrade_product', 'label' => '升级价格', 'type' => 'accordion', 'default' => [
|
||||
'month_to_month_s' => true, 'unit_price' => '10.00', 'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'unit_tag_class' => 'jb-yellow',
|
||||
'jump_s' => false, 'jump_price' => '199.00', 'jump_show_price' => '299.00', 'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'jump_tag_class' => 'jb-red',
|
||||
'permanent_price' => '199.00', 'permanent_show_price' => '299.00', 'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'permanent_tag_class' => 'jb-red',
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_upgrade', 'value' => true],
|
||||
'help' => '会员升级的商品选项',
|
||||
'sections' => [
|
||||
['title' => '月费会员升级月费会员', 'keys' => ['_upgrade_unit_notice', 'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class']],
|
||||
['title' => '月费会员升级永久会员', 'keys' => ['_upgrade_jump_notice', 'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class']],
|
||||
['title' => '永久会员升级永久会员', 'keys' => ['permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class']],
|
||||
],
|
||||
'fields' => [
|
||||
['key' => '_upgrade_unit_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价'],
|
||||
['key' => 'month_to_month_s', 'label' => '月费会员升级月费会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'unit_price', 'label' => '天单价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true]],
|
||||
['key' => 'unit_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'rows' => 1, 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'unit_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['all' => [['key' => 'month_to_month_s', 'value' => true], ['key' => 'unit_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => '_upgrade_jump_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项'],
|
||||
['key' => 'jump_s', 'label' => '月费会员升级永久会员', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'jump_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '月费会员升级永久会员的价格'],
|
||||
['key' => 'jump_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'jump_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'jump_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['all' => [['key' => 'jump_s', 'value' => true], ['key' => 'jump_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'permanent_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'help' => '永久会员升级永久会员的价格'],
|
||||
['key' => 'permanent_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'permanent_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'permanent_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['key' => 'permanent_tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
],
|
||||
],
|
||||
]],
|
||||
]],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;普通用户可开通,已有会员可续费同等级或升级更高等级)'],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费'],
|
||||
[
|
||||
'key' => 'pay_vip_points_exchange_product', 'label' => '积分兑换明细', 'type' => 'group', 'max' => 16,
|
||||
'button_title' => '添加积分兑换商品', 'default' => [
|
||||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true],
|
||||
'help' => '每一行为一个可兑换的会员套餐',
|
||||
'collapsible' => true,
|
||||
'defaultCollapsed' => true,
|
||||
'listPreview' => ['prefix' => '积分:', 'titleKey' => 'points', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||
'fields' => [
|
||||
['key' => 'points', 'label' => '使用积分', 'type' => 'number', 'default' => 100, 'min' => 0, 'unit' => '积分'],
|
||||
['key' => 'level', 'label' => '兑换会员', 'type' => 'radio', 'default' => 1, 'inline' => true, 'options' => [
|
||||
['label' => '一级会员', 'value' => 1],
|
||||
['label' => '二级会员', 'value' => 2],
|
||||
]],
|
||||
['key' => 'time', 'label' => '兑换时长', 'type' => 'input', 'default' => '', 'help' => '填 Permanent 为永久'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'day', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'neq', 'value' => 'Permanent'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '用户使用积分兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
]],
|
||||
['key' => 'modal-copy', 'title' => '界面文案', 'fields' => [
|
||||
['key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]], 'help' => '显示在开通界面顶部的一句话简介,支持HTML'],
|
||||
['key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>', 'rows' => 4, 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
@@ -536,6 +601,19 @@ class SchemaPayment {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品/续费商品的手风琴配置:默认收起,标题行显示价格与时长摘要。
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function vip_product_group_options(): array {
|
||||
return [
|
||||
'collapsible' => true,
|
||||
'defaultCollapsed' => true,
|
||||
'listPreview' => ['prefix' => '价格:', 'titleKey' => 'price', 'descKey' => 'time', 'descUnitKey' => 'unit'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal / Stripe 结算货币选项(参考主题同款 26 种)。
|
||||
*
|
||||
@@ -576,4 +654,92 @@ class SchemaPayment {
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品/续费商品的统一字段集(参考主题 CFS_Module::vip_product 1:1)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_fields(): array {
|
||||
return [
|
||||
['key' => 'price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '699.00', 'min' => 0, 'suffix' => '元', 'help' => '会员的实际购买价格'],
|
||||
['key' => 'show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '999.00', 'min' => 0, 'suffix' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['key' => 'tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'time', 'label' => '会员有效时间', 'type' => 'spinner', 'default' => 3, 'min' => 0, 'max' => 3600, 'step' => 1, 'help' => '开通会员的时长。填0则为永久会员'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'month', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'truthy'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
['key' => '_time_permanent_hint', 'label' => '', 'type' => 'alert', 'alertType' => 'success', 'ui_only' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'eq', 'value' => 0], 'content' => '会员有效时间已设置为:永久会员'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 的字段集(参考主题 CFS_Module::vip_tab 1:1,显示名称并入 Tab)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_level_tab_fields(int $level): array {
|
||||
$dep = ['key' => 'pay_user_vip_' . $level . '_s', 'value' => true];
|
||||
$equity = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
return [
|
||||
['key' => 'pay_user_vip_' . $level . '_name', 'label' => '会员显示名称', 'type' => 'input', 'sanitize_type' => 'html', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称,支持HTML'],
|
||||
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '尊享专属特权', 'dependsOn' => $dep, 'help' => '支持HTML'],
|
||||
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '支持安全HTML,建议使用 <li> 列表项'],
|
||||
['key' => 'vip_' . $level . '_product_s', 'label' => '购买会员', 'type' => 'switch', 'default' => true, 'dependsOn' => $dep, 'help' => '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能'],
|
||||
[
|
||||
'key' => 'vip_' . $level . '_product', 'label' => '会员商品', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加会员商品', 'default' => self::vip_product_defaults(),
|
||||
...self::vip_product_group_options(),
|
||||
'dependsOn' => ['all' => [$dep, ['key' => 'vip_' . $level . '_product_s', 'value' => true]]],
|
||||
'help' => '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
['key' => 'vip_' . $level . '_img_icon', 'label' => '会员图标', 'type' => 'upload', 'default' => '', 'dependsOn' => $dep, 'help' => '留空使用内置 vip-' . $level . '.svg 图标;建议尺寸300x300'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_defaults(): array {
|
||||
return [
|
||||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签颜色调色板(参考主题 zib_palette(array(), array('b','jb')))。
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
private static function vip_tag_palette(): array {
|
||||
$colors = [
|
||||
'b-red' => '#f74b3d',
|
||||
'b-yellow' => '#f3920a',
|
||||
'b-cyan' => '#08c4c1',
|
||||
'b-blue' => '#0a8cf3',
|
||||
'b-green' => '#1fd05a',
|
||||
'b-purple' => '#c133f5',
|
||||
'b-black' => '#121517',
|
||||
'jb-red' => 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)',
|
||||
'jb-pink' => 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)',
|
||||
'jb-yellow' => 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)',
|
||||
'jb-cyan' => 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)',
|
||||
'jb-blue' => 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)',
|
||||
'jb-green' => 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)',
|
||||
'jb-purple' => 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)',
|
||||
'jb-vip1' => 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)',
|
||||
'jb-vip2' => 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)',
|
||||
];
|
||||
$options = [];
|
||||
foreach ($colors as $value => $css) {
|
||||
$options[] = ['label' => $css, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -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]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -866,6 +866,47 @@ class SchemaProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a submitted enum value to the value declared by its option.
|
||||
*
|
||||
* Payloads reach the sanitizer as strings (or numbers), so a strict match
|
||||
* against the schema would reject a valid choice whenever the option value
|
||||
* is not itself a string -- e.g. 积分兑换明细.level declares 1 / 2 while the
|
||||
* SPA submits them as numbers and sanitize_text_field() then turns them into
|
||||
* "1" / "2". Returning the declared value keeps the stored type aligned with
|
||||
* the schema, which the radio/select controls compare with === when they
|
||||
* re-render a saved setting.
|
||||
*
|
||||
* Returns null when nothing matches, so validation still reports the
|
||||
* invalid value instead of silently rewriting the operator's input.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array<int,mixed> $options
|
||||
* @return mixed
|
||||
*/
|
||||
private function match_option_value($value, array $options) {
|
||||
foreach ($options as $opt) {
|
||||
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
|
||||
if ($opt_value === $value) {
|
||||
return $opt_value;
|
||||
}
|
||||
}
|
||||
if (is_bool($value) || !is_scalar($value)) {
|
||||
return null;
|
||||
}
|
||||
$needle = (string) $value;
|
||||
foreach ($options as $opt) {
|
||||
$opt_value = is_array($opt) ? ($opt['value'] ?? null) : $opt;
|
||||
if (is_bool($opt_value) || !is_scalar($opt_value)) {
|
||||
continue;
|
||||
}
|
||||
if ((string) $opt_value === $needle) {
|
||||
return $opt_value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function sanitize_field_value($value, array $field) {
|
||||
if (!empty($field['captcha_provider'])) {
|
||||
return function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
@@ -898,6 +939,15 @@ class SchemaProvider {
|
||||
return $normalized !== null ? $normalized : (string) ($field['default'] ?? '');
|
||||
}
|
||||
|
||||
// Enum fields keep the option's declared value so the stored type always
|
||||
// matches what validate() compares against with in_array(..., true).
|
||||
if (in_array($type, ['select', 'radio', 'palette', 'image_select'], true) && empty($field['multiple']) && !empty($field['options'])) {
|
||||
$matched = $this->match_option_value($value, (array) $field['options']);
|
||||
if ($matched !== null) {
|
||||
return $matched;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($type, ['fieldset', 'accordion'], true)) {
|
||||
return $this->sanitize_object_fields($value, (array) ($field['fields'] ?? []));
|
||||
}
|
||||
@@ -994,6 +1044,12 @@ class SchemaProvider {
|
||||
if (!is_array($field) || empty($field['key'])) {
|
||||
continue;
|
||||
}
|
||||
// UI-only chrome (notice / alert rows) is presentation only and must
|
||||
// never become a persisted settings key, matching the top-level
|
||||
// behaviour of collect_persisted_fields().
|
||||
if (!empty($field['ui_only']) || ($field['type'] ?? '') === 'notice') {
|
||||
continue;
|
||||
}
|
||||
$key = (string) $field['key'];
|
||||
if (array_key_exists($key, $value)) {
|
||||
$clean[$key] = $this->sanitize_field_value($value[$key], $field);
|
||||
|
||||
@@ -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 {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
||||
|
||||
+9
-314
@@ -11,9 +11,6 @@ class AI {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const PROVIDER_NONE = 'none';
|
||||
const PROVIDER_OPENAI = 'openai';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
@@ -21,95 +18,10 @@ class AI {
|
||||
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 是否真正可用。
|
||||
*
|
||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应抢占主题 provider。
|
||||
* 仅有函数存根不够:没有 Connectors / wp_supports_ai=false 时不应声称可用。
|
||||
*/
|
||||
public function is_wp_ai_available(): bool {
|
||||
if (!function_exists('wp_ai_client_prompt')) {
|
||||
@@ -124,89 +36,24 @@ class AI {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题 OpenAI 兼容 provider 是否可用。
|
||||
*/
|
||||
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 !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 任一后端可用即可生成。
|
||||
* AI 生成是否可用(连接由 WordPress AI Connectors 提供)。
|
||||
*/
|
||||
public function is_available(): bool {
|
||||
return $this->is_theme_provider_available() || $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(),
|
||||
];
|
||||
return $this->is_wp_ai_available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一文本生成出口。
|
||||
*
|
||||
* @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}
|
||||
*/
|
||||
public function generate_text(string $prompt, array $options = []): array {
|
||||
if (!$this->is_available()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->check_rate_limit()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('请求过于频繁,请稍后再试', 'aksrt'),
|
||||
'message' => __('AI 服务不可用,请配置 WordPress AI Connectors。', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -214,42 +61,12 @@ class AI {
|
||||
? (string) $options['system_instruction']
|
||||
: __('你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。', 'aksrt');
|
||||
|
||||
$last_error = [
|
||||
'success' => false,
|
||||
'message' => __('AI 服务未启用或未配置。请配置 WordPress AI Connectors,或在「扩展 → AI 辅助」填写 API Key。', 'aksrt'),
|
||||
];
|
||||
|
||||
// 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.
|
||||
$result = $this->generate_via_wp_ai($prompt, $system, $options);
|
||||
if (!empty($result['success'])) {
|
||||
$result['backend'] = 'wp_ai';
|
||||
}
|
||||
|
||||
// 2) WordPress AI Client (Connectors).
|
||||
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);
|
||||
return $result;
|
||||
}
|
||||
|
||||
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. 直接输出描述文本本身,不要任何前缀、引号或解释。';
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧单 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 {
|
||||
$content = mb_substr($content, 0, 3000);
|
||||
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
|
||||
*/
|
||||
|
||||
+41
-19
@@ -219,35 +219,57 @@ final class OAuth {
|
||||
'name' => $provider,
|
||||
'label' => OAuthProviderCatalog::label($provider),
|
||||
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
|
||||
'url' => esc_url($url),
|
||||
// Raw, matching get_frontend_bindings_data(): every surface escapes
|
||||
// once on output. esc_url() is idempotent on its own output, so
|
||||
// escaping here too was redundant rather than broken.
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference-parity social login block (legacy zib_social_login()).
|
||||
*
|
||||
* Emits the muted separator plus a .social_loginbar of .social-login-item
|
||||
* buttons, one per enabled provider. Returns '' when nothing is enabled.
|
||||
*
|
||||
* @param string $title Separator label; an empty title drops the separator.
|
||||
*/
|
||||
public function render_social_buttons(string $title = ''): string {
|
||||
$items = $this->get_auth_providers_data();
|
||||
if ($items === []) {
|
||||
return '';
|
||||
}
|
||||
ob_start();
|
||||
?>
|
||||
<div class="social-login-divider">
|
||||
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<div class="oauth-row">
|
||||
<?php foreach ($items as $item) : ?>
|
||||
<a href="<?php echo esc_url((string) $item['url']); ?>"
|
||||
class="oauth-btn oauth-btn--<?php echo esc_attr((string) $item['name']); ?>"
|
||||
data-provider="<?php echo esc_attr((string) $item['name']); ?>"
|
||||
aria-label="<?php echo esc_attr((string) $item['label']); ?>"
|
||||
title="<?php echo esc_attr((string) $item['label']); ?>">
|
||||
<?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
return (string) ob_get_clean();
|
||||
|
||||
$large = (bool) \AKSRT\Settings\Settings::get_value('oauth_button_lg', false);
|
||||
$html = $title !== ''
|
||||
? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
|
||||
: '';
|
||||
$html .= '<div class="social_loginbar">';
|
||||
foreach ($items as $item) {
|
||||
$name = sanitize_html_class((string) ($item['name'] ?? ''));
|
||||
$type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
|
||||
$label = trim((string) ($item['label'] ?? ''));
|
||||
$url = (string) ($item['url'] ?? '');
|
||||
if ($name === '' || $type === '' || $label === '' || $url === '') {
|
||||
continue;
|
||||
}
|
||||
$class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
|
||||
if ($name === 'weixingzh') {
|
||||
// Reference parity: the WeChat entry opens the QR panel instead of
|
||||
// navigating, so auth.js routes it to the modal's oauth tab.
|
||||
$class .= ' qrcode-signin';
|
||||
}
|
||||
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="' . esc_attr($class) . '"'
|
||||
. ' data-provider="' . esc_attr($name) . '"'
|
||||
. ' aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '"'
|
||||
. ' title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">'
|
||||
. $item['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
. ($large ? '<span>' . esc_html(sprintf(__('%s登录', 'aksrt'), $label)) . '</span>' : '')
|
||||
. '</a>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
public function get_frontend_bindings_data(int $user_id): array {
|
||||
|
||||
@@ -38,8 +38,9 @@ trait VipServiceModal {
|
||||
: wp_kses_post($equity);
|
||||
$level_view[] = [
|
||||
'level' => $level,
|
||||
'name' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||||
'desc' => (string) ($levels[$level]['desc'] ?? ''),
|
||||
'name' => wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level))),
|
||||
'name_text' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||||
'desc' => wp_kses_post((string) ($levels[$level]['desc'] ?? '')),
|
||||
'equity' => $equity,
|
||||
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
|
||||
];
|
||||
@@ -115,7 +116,6 @@ trait VipServiceModal {
|
||||
'prefer_level' => $prefer_level,
|
||||
'catalog' => [
|
||||
'desc' => (string) ($catalog['desc'] ?? ''),
|
||||
'help_text' => (string) ($catalog['help_text'] ?? ''),
|
||||
'more' => (string) ($vip['more'] ?? ''),
|
||||
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
||||
'level_packages' => $groups,
|
||||
@@ -215,15 +215,17 @@ trait VipServiceModal {
|
||||
}
|
||||
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
||||
$points = max(0, (int) ($package['points'] ?? 0));
|
||||
$days = max(0, (int) ($package['days'] ?? 0));
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久。
|
||||
$duration = $days > 0
|
||||
? (string) $days . '<span class="px12">' . esc_html__('天', 'aksrt') . '</span>'
|
||||
: esc_html__('永久', 'aksrt');
|
||||
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
|
||||
$is_permanent = !empty($package['is_permanent']);
|
||||
$pkg_time = max(0, (int) ($package['time'] ?? 0));
|
||||
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$duration = ($is_permanent || $pkg_time <= 0)
|
||||
? esc_html__('永久', 'aksrt')
|
||||
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')) . '</span>';
|
||||
$level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||||
|
||||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . esc_html($level_name) . '</span>'
|
||||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . wp_kses_post($level_name) . '</span>'
|
||||
. '<div class="flex jsb ac hh em16 mt10">'
|
||||
. '<span class="shrink0 mr10">' . $duration . '</span>'
|
||||
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
|
||||
@@ -237,16 +239,18 @@ trait VipServiceModal {
|
||||
return '';
|
||||
}
|
||||
|
||||
$desc_text = trim((string) ($points_ex['desc'] ?? ''));
|
||||
$desc_text = trim((string) ($vip['points_exchange']['desc'] ?? ''));
|
||||
$desc = $desc_text !== ''
|
||||
? '<div class="muted-box muted-2-color padding-10 em09 mt20">' . esc_html($desc_text) . '</div>'
|
||||
// 管理员编辑的富文本说明:保存端已 wp_kses_post,这里同级别输出
|
||||
// (参考主题 zibpay_get_vip_points_exchange_modal 原样拼接)。
|
||||
? '<div class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
|
||||
: '';
|
||||
|
||||
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
||||
// (参考主题源码写作 `!$min_val > $user_points`,因运算符优先级恒假,
|
||||
// 此处按其原意取真实比较,保证提示分支可达。)
|
||||
if ($min_points > $user_points) {
|
||||
$form = '<div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div>';
|
||||
$form = '<div class="mt20"><div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div></div>';
|
||||
} else {
|
||||
$form = '<div class="mt20">'
|
||||
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
|
||||
@@ -289,14 +293,14 @@ trait VipServiceModal {
|
||||
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
|
||||
*/
|
||||
private function points_exchange_card_icon(array $level_settings): string {
|
||||
$name = (string) ($level_settings['name'] ?? '');
|
||||
$name_text = (string) ($level_settings['name'] ?? '');
|
||||
$icon_url = (string) ($level_settings['img_icon'] ?? '');
|
||||
if ($icon_url === '') {
|
||||
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
|
||||
}
|
||||
|
||||
return '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
|
||||
. ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '">';
|
||||
. ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,26 +331,27 @@ trait VipServiceModal {
|
||||
if (empty($levels[$level]['enabled'])) {
|
||||
return '';
|
||||
}
|
||||
$name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$name = wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level)));
|
||||
$name_text = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
$equity = (string) ($levels[$level]['equity'] ?? '');
|
||||
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
|
||||
|
||||
$equity_html = $equity !== ''
|
||||
? '<ul class="mb10 relative">' . wp_kses_post($equity) . '</ul>'
|
||||
: '';
|
||||
$equity_html = $equity !== '' && function_exists('aksrt_format_vip_equity_html')
|
||||
? aksrt_format_vip_equity_html($equity, 'mb10 relative')
|
||||
: ($equity !== '' ? '<div class="v-equity mb10 relative">' . wp_kses_post($equity) . '</div>' : '');
|
||||
|
||||
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
|
||||
$icon = $icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '" decoding="async">'
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '" decoding="async">'
|
||||
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||||
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
|
||||
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
|
||||
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
|
||||
$html .= '<div class="relative">';
|
||||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . esc_html($name) . '</div>';
|
||||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . wp_kses_post($name) . '</div>';
|
||||
$html .= $equity_html;
|
||||
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
||||
. esc_html(sprintf(__('开通%s', 'aksrt'), $name))
|
||||
. wp_kses_post(sprintf(__('开通%s', 'aksrt'), $name))
|
||||
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
|
||||
$html .= '</div></div>';
|
||||
return $html;
|
||||
|
||||
+120
-314
@@ -11,82 +11,6 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parse per-level product lines (legacy vip_product).
|
||||
*
|
||||
* Preferred: price|show_price|time|unit|tag|title
|
||||
* Legacy: price|days|title
|
||||
*
|
||||
* @return array<int, array{price:string,show_price:string,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
||||
*/
|
||||
public function parse_level_products(string $raw): array {
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$out = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$price_minor = $this->parse_vip_money_minor($parts[0] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$n = count($parts);
|
||||
// New format: price|show_price|time|unit|tag|title (6+)
|
||||
// Or: price|show_price|time|unit|title (5)
|
||||
if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true)
|
||||
|| (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) {
|
||||
$show_minor = $this->parse_vip_money_minor($parts[1] ?? '0');
|
||||
$time = isset($parts[2]) ? (int) $parts[2] : 0;
|
||||
$unit_raw = strtolower((string) ($parts[3] ?? 'month'));
|
||||
$unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month';
|
||||
$tag = '';
|
||||
$title = '';
|
||||
if ($n >= 6) {
|
||||
$tag = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
$title = sanitize_text_field((string) ($parts[5] ?? ''));
|
||||
} elseif ($n === 5) {
|
||||
// price|show|time|unit|title
|
||||
$title = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
}
|
||||
} else {
|
||||
// Legacy: price|days|title
|
||||
$show_minor = 0;
|
||||
$days_legacy = isset($parts[1]) ? (int) $parts[1] : 0;
|
||||
$time = $days_legacy;
|
||||
$unit = 'day';
|
||||
$tag = '';
|
||||
$title = isset($parts[2]) ? sanitize_text_field($parts[2]) : '';
|
||||
}
|
||||
|
||||
$time = max(0, $time);
|
||||
$days = $this->duration_to_days($time, $unit);
|
||||
if ($title === '') {
|
||||
if ($time <= 0) {
|
||||
$title = __('永久会员', 'aksrt');
|
||||
} elseif ($unit === 'day') {
|
||||
$title = sprintf(__('%d天会员', 'aksrt'), $time);
|
||||
} else {
|
||||
$title = sprintf(__('%d个月会员', 'aksrt'), $time);
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor),
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'tag' => $tag,
|
||||
'tag_class' => '',
|
||||
'title' => $title,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate days for catalog labels / order meta; activate prefers time+unit calendar math.
|
||||
*/
|
||||
@@ -100,118 +24,59 @@ trait VipServicePackages {
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines.
|
||||
*
|
||||
* @return array<int, array{price:string,days:int,title:string}>
|
||||
*/
|
||||
public function parse_packages(string $raw): array {
|
||||
$parsed = $this->parse_level_products($raw);
|
||||
$out = [];
|
||||
foreach ($parsed as $p) {
|
||||
$out[] = [
|
||||
'price' => (string) $p['price'],
|
||||
'days' => (int) $p['days'],
|
||||
'title' => (string) $p['title'],
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Build upgrade package items (legacy vip_upgrade_product parity).
|
||||
*
|
||||
* upgrade.product multi-line formats:
|
||||
* - permanent VIP1: permanent|price|title[|show_price]
|
||||
* - fixed (alias): price|days|title
|
||||
* - timed unit: unit|daily_price|title
|
||||
* - timed jump permanent: jump|price|title[|show_price]
|
||||
*
|
||||
* Rules:
|
||||
* - permanent user: only permanent/fixed (days=0 preferred)
|
||||
* - timed user: unit (keep expiry) + optional jump (to permanent)
|
||||
*
|
||||
* package index: 901 permanent/fixed, 902 unit, 903 jump
|
||||
*
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
* Three blocks (reference theme vip_upgrade_product accordion):
|
||||
* - month_to_month_s: timed VIP1 → VIP2, priced by remaining days, expiry unchanged
|
||||
* - jump_s: timed VIP1 → permanent VIP2 at a fixed price
|
||||
* - permanent_*: permanent VIP1 → permanent VIP2 at a fixed price
|
||||
*
|
||||
* package index: 901 permanent, 902 unit, 903 jump
|
||||
*
|
||||
* @param array<string,mixed> $config
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, array $config): array {
|
||||
$items = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$has_permanent = false;
|
||||
$has_unit = false;
|
||||
$has_jump = false;
|
||||
$now = time();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
// 月费会员升级月费会员:按剩余天数计价,会员有效期不变(仅提升等级)。
|
||||
if (!$is_permanent && $expires > $now && !empty($config['month_to_month_s'])) {
|
||||
$daily_minor = $this->parse_vip_money_minor((string) ($config['unit_price'] ?? ''));
|
||||
if ($daily_minor > 0) {
|
||||
$remaining = (int) ceil(max(1, ($expires - $now) / DAY_IN_SECONDS));
|
||||
if ($daily_minor <= intdiv(PHP_INT_MAX, $remaining)) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($remaining * $daily_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => (string) ($config['unit_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['unit_tag_class'] ?? ''),
|
||||
'days' => $remaining,
|
||||
'title' => sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
}
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$mode = strtolower((string) ($parts[0] ?? ''));
|
||||
}
|
||||
|
||||
// unit|daily|title — timed VIP1 only.
|
||||
if ($mode === 'unit') {
|
||||
if ($is_permanent || $expires <= time() || $has_unit) {
|
||||
continue;
|
||||
}
|
||||
$daily_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($daily_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
|
||||
if ($daily_minor > intdiv(PHP_INT_MAX, $remaining)) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $remaining * $daily_minor;
|
||||
// 月费会员升级永久会员:固定价格直接转为永久二级会员。
|
||||
if (!$is_permanent && !empty($config['jump_s'])) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['jump_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
|
||||
}
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('剩余天数', 'aksrt'),
|
||||
'days' => $remaining,
|
||||
'title' => $title,
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
$has_unit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// jump|price|title[|show_price] — timed VIP1 only → permanent VIP2.
|
||||
if ($mode === 'jump') {
|
||||
if ($is_permanent || $has_jump) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('升级为永久会员', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['jump_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 903),
|
||||
@@ -221,9 +86,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('永久升级', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['jump_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['jump_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('升级为永久会员', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -231,25 +97,15 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'jump',
|
||||
];
|
||||
$has_jump = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// permanent|price|title[|show_price]
|
||||
if ($mode === 'permanent') {
|
||||
if ($has_permanent) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
// 永久会员升级永久会员。
|
||||
if ($is_permanent) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['permanent_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('永久会员升级', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['permanent_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
@@ -259,9 +115,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['permanent_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['permanent_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('永久会员升级', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -269,49 +126,7 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'permanent',
|
||||
];
|
||||
$has_permanent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// fixed: price|days|title
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (empty($parsed[0]) || $has_permanent) {
|
||||
continue;
|
||||
}
|
||||
// For permanent VIP1, only accept days=0 fixed lines as permanent upgrade.
|
||||
if ($is_permanent && (int) $parsed[0]['days'] > 0) {
|
||||
continue;
|
||||
}
|
||||
$pkg = $parsed[0];
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
'level' => 2,
|
||||
'index' => 901,
|
||||
'action' => 'upgrade',
|
||||
'price' => (string) $pkg['price'],
|
||||
'base_price' => (string) $pkg['price'],
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'days' => (int) $pkg['days'],
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $pkg['price']),
|
||||
'show_price_label' => '',
|
||||
'days_label' => ((int) $pkg['days'] <= 0)
|
||||
? __('永久', 'aksrt')
|
||||
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => ((int) $pkg['days'] <= 0) ? 'permanent' : 'fixed',
|
||||
];
|
||||
$has_permanent = true;
|
||||
}
|
||||
|
||||
// Permanent VIP1 should not see unit/jump.
|
||||
if ($is_permanent) {
|
||||
$items = array_values(array_filter($items, static function ($it) {
|
||||
$m = (string) ($it['upgrade_mode'] ?? '');
|
||||
return in_array($m, ['permanent', 'fixed'], true);
|
||||
}));
|
||||
}
|
||||
|
||||
return $items;
|
||||
@@ -320,7 +135,10 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
/**
|
||||
* Points exchange packages: level|points|days|title
|
||||
* Points exchange packages from the structured 积分兑换明细 group.
|
||||
*
|
||||
* Each row is points / level / time / unit, where time is the literal
|
||||
* string "Permanent" for a permanent grant (reference theme contract).
|
||||
*
|
||||
* @param array<string,mixed> $vip
|
||||
* @param array<int,mixed> $levels
|
||||
@@ -328,47 +146,49 @@ trait VipServicePackages {
|
||||
*/
|
||||
public function get_points_exchange_packages(array $vip, array $levels): array {
|
||||
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
|
||||
$raw = (string) ($points_ex['packages_raw'] ?? '');
|
||||
$rows = isset($points_ex['packages']) && is_array($points_ex['packages']) ? $points_ex['packages'] : [];
|
||||
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$out = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$i = 0;
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
if (count($parts) < 3) {
|
||||
continue;
|
||||
}
|
||||
$lv = (int) ($parts[0] ?? 0);
|
||||
$pts = (int) ($parts[1] ?? 0);
|
||||
$days = (int) ($parts[2] ?? 0);
|
||||
$title = sanitize_text_field((string) ($parts[3] ?? ''));
|
||||
$lv = (int) ($row['level'] ?? 0);
|
||||
$pts = max(0, (int) ($row['points'] ?? 0));
|
||||
if ($lv < 1 || $lv > 2 || $pts <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (empty($levels[$lv]['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
if ($title === '') {
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$title = $days > 0
|
||||
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
|
||||
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
|
||||
$is_permanent = !empty($row['is_permanent']);
|
||||
$time = max(0, (int) ($row['time'] ?? 0));
|
||||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$days = $is_permanent ? 0 : max(0, (int) ($row['days'] ?? 0));
|
||||
if (!$is_permanent && $time <= 0) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$out[] = [
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => max(0, $days),
|
||||
'title' => $title,
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => $days,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'is_permanent' => $is_permanent,
|
||||
'title' => $is_permanent
|
||||
? sprintf(__('%s · 永久', 'aksrt'), $lv_name)
|
||||
: sprintf(__('%1$s · %2$d%3$s', 'aksrt'), $lv_name, $time, $unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')),
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $is_permanent
|
||||
? __('永久', 'aksrt')
|
||||
: ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'duration_label' => $is_permanent ? __('永久', 'aksrt') : ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
@@ -376,41 +196,18 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Single upgrade product line: price|days|title
|
||||
*
|
||||
* @return array{price:float,days:int,title:string}|null
|
||||
*/
|
||||
public function parse_upgrade_product_line(string $raw): ?array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '' || str_starts_with(strtolower($line), 'unit|')) {
|
||||
continue;
|
||||
}
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (!empty($parsed[0])) {
|
||||
return $parsed[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Apply renew discount/reduce to same-level packages.
|
||||
*
|
||||
* `customize` reuses the dedicated renew product's own price/show_price/tag,
|
||||
* so the caller passes that row's whole item in as $pkg.
|
||||
*
|
||||
* @param array<string,mixed> $renew
|
||||
* @param array<string,mixed> $upgrade
|
||||
* @return array{price:string,show_price:string,tag:string}|null
|
||||
* @return array{price:string,show_price:string,tag:string,tag_class:string}|null
|
||||
*/
|
||||
private function apply_action_price(
|
||||
string $base_price,
|
||||
array $pkg,
|
||||
string $action,
|
||||
array $renew,
|
||||
array $upgrade,
|
||||
@@ -420,6 +217,7 @@ trait VipServicePackages {
|
||||
bool $is_permanent,
|
||||
int $days
|
||||
): ?array {
|
||||
$base_price = (string) ($pkg['price'] ?? '');
|
||||
$base_minor = $this->parse_vip_money_minor($base_price);
|
||||
if ($base_minor <= 0) {
|
||||
return null;
|
||||
@@ -434,32 +232,31 @@ trait VipServicePackages {
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
||||
'show_price' => '0.00',
|
||||
'tag' => '',
|
||||
'tag_class' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
||||
// discount/reduce 的标签由优惠幅度计算得出,若实际没有优惠则属于虚假折扣。
|
||||
$computed_tag = in_array($type, ['discount', 'reduce'], true);
|
||||
$price_minor = $base_minor;
|
||||
$show_minor = $base_minor;
|
||||
$tag = '';
|
||||
$tag_class = '';
|
||||
|
||||
if ($type === 'discount') {
|
||||
$discount = (int) ($renew['discount'] ?? 8);
|
||||
$discount = (float) ($renew['discount'] ?? 8);
|
||||
if ($discount <= 0) {
|
||||
$discount = 8;
|
||||
}
|
||||
if ($discount <= 10) {
|
||||
$percent = $discount * 10;
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折续费', 'aksrt'),
|
||||
(string) $discount
|
||||
);
|
||||
} else {
|
||||
$percent = min(100, $discount);
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
|
||||
}
|
||||
// 折值:8 = 八折(参考主题 slider 0.1–9.9 语义)。
|
||||
$discount = max(0.1, min(9.9, $discount));
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $discount * 10);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折优惠', 'aksrt'),
|
||||
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') ?: '0'
|
||||
);
|
||||
} elseif ($type === 'reduce') {
|
||||
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
||||
$price_minor = max(1, $base_minor - $reduce_minor);
|
||||
@@ -468,6 +265,11 @@ trait VipServicePackages {
|
||||
__('续费立减%s', 'aksrt'),
|
||||
$this->format_cash_label(\AKSRT\Ledger\AssetAmount::decimal_from_minor($reduce_minor))
|
||||
);
|
||||
} elseif ($type === 'customize') {
|
||||
// 自定义商品:沿用商品自身的原价、促销标签与标签颜色。
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($pkg['show_price'] ?? ''));
|
||||
$tag = (string) ($pkg['tag'] ?? '');
|
||||
$tag_class = (string) ($pkg['tag_class'] ?? '');
|
||||
} else {
|
||||
$show_minor = 0;
|
||||
$tag = __('续费同价', 'aksrt');
|
||||
@@ -477,14 +279,18 @@ trait VipServicePackages {
|
||||
$price_minor = 1;
|
||||
}
|
||||
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
||||
// 参考主题契约:原价仅在低于执行价时展示。
|
||||
$show_minor = 0;
|
||||
$tag = __('续费', 'aksrt');
|
||||
if ($computed_tag) {
|
||||
$tag = __('续费', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
||||
'tag' => $tag,
|
||||
'tag_class' => $tag_class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -137,22 +137,32 @@ trait VipServicePurchase {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$name = trim(wp_strip_all_tags((string) ($levels[$lv]['name'] ?? ('VIP' . $lv))));
|
||||
$items = [];
|
||||
$packages = $product_s
|
||||
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
|
||||
: [];
|
||||
// 自定义续费商品时按续费商品组计价,否则按会员商品组计价。
|
||||
$level_action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$level_action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$level_action = 'upgrade';
|
||||
}
|
||||
$renew_custom = $level_action === 'renew'
|
||||
&& sanitize_key((string) ($renew['price_type'] ?? 'discount')) === 'customize';
|
||||
$packages = [];
|
||||
if ($product_s) {
|
||||
$packages = $renew_custom
|
||||
? (array) ($levels[$lv]['renew_products'] ?? [])
|
||||
: (array) ($levels[$lv]['products'] ?? []);
|
||||
}
|
||||
foreach ($packages as $idx => $pkg) {
|
||||
if (!is_array($pkg)) {
|
||||
continue;
|
||||
}
|
||||
$base_price = (string) $pkg['price'];
|
||||
$days = (int) $pkg['days'];
|
||||
$action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$action = 'upgrade';
|
||||
}
|
||||
$action = $level_action;
|
||||
|
||||
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
$priced = $this->apply_action_price($pkg, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
if ($priced === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -167,6 +177,10 @@ trait VipServicePurchase {
|
||||
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
||||
$discount_tag = (string) $pkg['tag'];
|
||||
}
|
||||
$tag_class = (string) ($priced['tag_class'] ?? '');
|
||||
if ($tag_class === '' && !empty($pkg['tag_class'])) {
|
||||
$tag_class = (string) $pkg['tag_class'];
|
||||
}
|
||||
|
||||
$days_label = __('永久', 'aksrt');
|
||||
if ((int) $pkg['time'] > 0) {
|
||||
@@ -184,10 +198,10 @@ trait VipServicePurchase {
|
||||
'base_price' => $base_price,
|
||||
'show_price' => $show_price,
|
||||
'discount_tag' => $discount_tag,
|
||||
'tag_class' => $tag_class,
|
||||
'days' => $days,
|
||||
'time' => (int) ($pkg['time'] ?? 0),
|
||||
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $priced['price']),
|
||||
'show_price_label' => $show_minor > 0
|
||||
? $this->format_cash_label($show_price)
|
||||
@@ -201,7 +215,7 @@ trait VipServicePurchase {
|
||||
|
||||
// Dedicated upgrade product: fixed or remaining-days unit price.
|
||||
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, $upgrade);
|
||||
foreach ($upgrade_items as $ui) {
|
||||
$items[] = $ui;
|
||||
}
|
||||
@@ -232,21 +246,19 @@ trait VipServicePurchase {
|
||||
}
|
||||
}
|
||||
|
||||
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
|
||||
$balances = \AKSRT\Ledger\LedgerService::get_instance()->points_and_balance($user_id);
|
||||
$methods = $this->available_payment_methods();
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
|
||||
$catalog = [
|
||||
'enabled' => !empty($vip['enabled']),
|
||||
'desc' => (string) ($vip['desc'] ?? ''),
|
||||
'help_text' => (string) ($vip['help_text'] ?? ''),
|
||||
'user_level' => $level,
|
||||
'user_expires' => $expires,
|
||||
'user_is_permanent' => $is_permanent,
|
||||
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
|
||||
'user_points' => $points_balance,
|
||||
'wallet_balance' => $wallet_balance,
|
||||
'user_points' => $balances['points'],
|
||||
'wallet_balance' => $balances['balance'],
|
||||
'currency_name' => $currency_name,
|
||||
'renew_enabled' => $renew_enabled,
|
||||
'upgrade_enabled' => $upgrade_enabled,
|
||||
@@ -443,11 +455,12 @@ trait VipServicePurchase {
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => $expires_at,
|
||||
'subject' => sprintf(
|
||||
/* translators: 1: action 2: vip name 3: package title */
|
||||
__('%1$s%2$s · %3$s', 'aksrt'),
|
||||
/* translators: 1: action 2: vip name 3: price label 4: duration label */
|
||||
__('%1$s%2$s · %3$s / %4$s', 'aksrt'),
|
||||
$action_label,
|
||||
$level_name,
|
||||
(string) ($pkg['title'] ?? '')
|
||||
(string) ($pkg['price_label'] ?? ''),
|
||||
(string) ($pkg['days_label'] ?? '')
|
||||
),
|
||||
];
|
||||
|
||||
@@ -759,6 +772,11 @@ trait VipServicePurchase {
|
||||
$cost = (int) ($match['points'] ?? 0);
|
||||
$level = (int) ($match['level'] ?? 1);
|
||||
$days = (int) ($match['days'] ?? 0);
|
||||
$pkg_time = max(0, (int) ($match['time'] ?? 0));
|
||||
$pkg_unit = sanitize_key((string) ($match['unit'] ?? 'day'));
|
||||
if (!in_array($pkg_unit, ['day', 'month'], true)) {
|
||||
$pkg_unit = 'day';
|
||||
}
|
||||
if ($cost <= 0) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
@@ -781,6 +799,8 @@ trait VipServicePurchase {
|
||||
'vip_pay_type' => 'points_exchange',
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'time' => $pkg_time,
|
||||
'unit' => $pkg_unit,
|
||||
'points' => $cost,
|
||||
'package_id' => $package_id,
|
||||
'package_title' => (string) ($match['title'] ?? ''),
|
||||
|
||||
@@ -17,7 +17,10 @@ class DBMigrator {
|
||||
// 2.5.1: order_aftersale 新增 images 列(售后举证图片)。dbDelta 仅在版本变化时
|
||||
// 重跑,新增/调整列必须同步升版本,否则老安装不会执行 ALTER TABLE。
|
||||
// 2.5.2: order_aftersale 新增 apply_form 列(结构化申请表单:预置原因 / 申请资料 / 申请说明)。
|
||||
const SCHEMA_VERSION = '2.5.2';
|
||||
// 2.6.0: order_aftersale 新增 handling_log 列(处理记录:每次状态提交一条
|
||||
// 「状态 + 备注 + 来源 + 时间」),取代单一的 admin_note / reviewed_at / completed_at,
|
||||
// 并移除从未被写入的 gateway_request_id / gateway_refund_id / gateway_result。
|
||||
const SCHEMA_VERSION = '2.6.0';
|
||||
|
||||
const OPT_KEY = 'aksrt_db_schema_version';
|
||||
|
||||
|
||||
@@ -213,15 +213,10 @@ trait DBMigratorPart2 {
|
||||
reason TEXT NOT NULL,
|
||||
apply_form LONGTEXT DEFAULT NULL,
|
||||
images LONGTEXT DEFAULT NULL,
|
||||
handling_log LONGTEXT DEFAULT NULL,
|
||||
refund_amount VARCHAR(32) NOT NULL DEFAULT '0',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT DEFAULT NULL,
|
||||
gateway_request_id VARCHAR(128) DEFAULT NULL,
|
||||
gateway_refund_id VARCHAR(128) DEFAULT NULL,
|
||||
gateway_result LONGTEXT DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
reviewed_at DATETIME DEFAULT NULL,
|
||||
completed_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order_status (order_number, status),
|
||||
KEY idx_user_created (user_id, created_at),
|
||||
|
||||
@@ -210,16 +210,16 @@ class SEOMetaBox {
|
||||
true
|
||||
);
|
||||
|
||||
$status = class_exists('\AKSRT\Ai\AI') ? \AKSRT\Ai\AI::get_instance()->get_status() : [];
|
||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||
$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() : [];
|
||||
|
||||
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'ai' => [
|
||||
'available' => !empty($status['available']),
|
||||
'available' => $ai_available,
|
||||
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
||||
'write_assist' => !empty($status['available']),
|
||||
'write_assist' => $ai_available,
|
||||
],
|
||||
'i18n' => [
|
||||
'generating' => __('生成中…', 'aksrt'),
|
||||
@@ -262,7 +262,7 @@ class SEOMetaBox {
|
||||
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
||||
$title = !$available
|
||||
? esc_attr__('请先配置 AI 连接(扩展 → AI 连接配置 或 WP Connectors)', 'aksrt')
|
||||
? esc_attr__('请先配置 WordPress AI Connectors', 'aksrt')
|
||||
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
||||
|
||||
return sprintf(
|
||||
|
||||
@@ -63,7 +63,10 @@ trait ScriptsPart1b {
|
||||
};
|
||||
|
||||
// 顶部导航用户卡片(legacy header-userbox 模块样式)。
|
||||
$enqueue_module('header-userbox');
|
||||
// 仅登录用户渲染用户卡(aksrt-header-userbox.php 的渲染条件),访客页面不加载。
|
||||
if (is_user_logged_in()) {
|
||||
$enqueue_module('header-userbox');
|
||||
}
|
||||
|
||||
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
|
||||
$auth_css = get_template_directory() . '/assets/css/routes/sign.css';
|
||||
|
||||
@@ -30,6 +30,9 @@ class StatsTracker {
|
||||
|
||||
/**
|
||||
* 追踪访问
|
||||
*
|
||||
* 访客判定与 setcookie 必须留在 wp_head(响应头尚未发送);
|
||||
* 计数读写改挂 shutdown,避免每次前台请求都在渲染关键路径上写库。
|
||||
*/
|
||||
public function track_visit(): void {
|
||||
// 排除管理员、爬虫、AJAX请求
|
||||
@@ -41,6 +44,12 @@ class StatsTracker {
|
||||
return;
|
||||
}
|
||||
|
||||
static $scheduled = false;
|
||||
if ($scheduled) {
|
||||
return;
|
||||
}
|
||||
$scheduled = true;
|
||||
|
||||
$today = current_time('Y-m-d');
|
||||
$option_key = 'aksrt_daily_stats_' . $today;
|
||||
|
||||
@@ -48,6 +57,19 @@ class StatsTracker {
|
||||
$visitor_cookie = 'aksrt_visitor_' . $today;
|
||||
$is_unique = empty($_COOKIE[$visitor_cookie]);
|
||||
|
||||
if ($is_unique) {
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
add_action('shutdown', static function () use ($option_key, $is_unique): void {
|
||||
self::get_instance()->record_stats($option_key, $is_unique);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数落库(shutdown 阶段执行;请求中断时本次统计丢弃)。
|
||||
*/
|
||||
public function record_stats(string $option_key, bool $is_unique): void {
|
||||
// 获取或初始化今日统计
|
||||
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
|
||||
|
||||
@@ -59,7 +81,6 @@ class StatsTracker {
|
||||
$stats['pageviews']++;
|
||||
if ($is_unique) {
|
||||
$stats['visitors']++;
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
update_option($option_key, $stats, false);
|
||||
|
||||
@@ -314,8 +314,13 @@ class Signin {
|
||||
}
|
||||
|
||||
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
|
||||
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, true);
|
||||
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
||||
// 会员图标不再挂头像角标,改到昵称行最前;禁封徽章依旧占据会员图标位。
|
||||
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, false);
|
||||
$name_badge = function_exists('aksrt_get_user_ban_name_badge') ? aksrt_get_user_ban_name_badge($user_id) : '';
|
||||
if ($name_badge === '' && function_exists('aksrt_get_vip_icon')) {
|
||||
$name_badge = aksrt_get_vip_icon($user_id);
|
||||
}
|
||||
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . $name_badge . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
|
||||
|
||||
// 每日签到奖励卡片(参考主题:积分徽章 + 经验值徽章,经验值随等级系统展示)
|
||||
$reward_badge = $base ? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>' : '';
|
||||
|
||||
+279
-63
@@ -9,6 +9,14 @@ defined('ABSPATH') || exit;
|
||||
|
||||
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;
|
||||
|
||||
public static function get_instance(): self {
|
||||
@@ -17,16 +25,20 @@ class SearchPush {
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。
|
||||
* 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。
|
||||
*/
|
||||
public function init(): void {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章发布/更新时主动推送链接到百度。
|
||||
*
|
||||
* 与参考主题一致:推送结果写入 meta,成功后不再重复提交;
|
||||
* 编辑页勾选「重新提交」可清空记录并强制再次推送。
|
||||
*
|
||||
* @param string $new_status 新状态。
|
||||
* @param string $old_status 旧状态。
|
||||
* @param \WP_Post $post 文章对象。
|
||||
@@ -42,27 +54,230 @@ class SearchPush {
|
||||
return;
|
||||
}
|
||||
// 仅推送公开可索引的内容类型。
|
||||
$allowed = function_exists('aksrt_searchable_post_types')
|
||||
? aksrt_searchable_post_types()
|
||||
: ['post'];
|
||||
if (!in_array($post->post_type, $allowed, true)) {
|
||||
if (!in_array($post->post_type, self::pushable_post_types(), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
if (!$this->should_push($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resubmit_requested()) {
|
||||
self::clear_push_record($post->ID);
|
||||
} else {
|
||||
$is_new = $old_status !== 'publish';
|
||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
||||
return;
|
||||
}
|
||||
if (!$is_new && empty($config['baidu_push_on_update'])) {
|
||||
return;
|
||||
}
|
||||
// 已成功提交过的链接不再重复推送。
|
||||
if ($this->record_is_complete(self::get_push_record($post->ID), $config)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$is_new = $old_status !== 'publish';
|
||||
if ($is_new && empty($config['baidu_push_on_publish'])) {
|
||||
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 (!$is_new && empty($config['baidu_push_on_update'])) {
|
||||
return;
|
||||
if (!empty($config['baidu_push_fast'])) {
|
||||
$channel = $result['daily'] ?? null;
|
||||
$record['daily_push'] = is_array($channel) && !empty($channel['success']);
|
||||
$record['daily_result'] = $channel;
|
||||
}
|
||||
|
||||
$this->push_url((string) get_permalink($post));
|
||||
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' => []];
|
||||
}
|
||||
|
||||
$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, [
|
||||
'timeout' => 10,
|
||||
@@ -105,7 +363,7 @@ class SearchPush {
|
||||
]);
|
||||
|
||||
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' => []];
|
||||
}
|
||||
|
||||
@@ -113,59 +371,15 @@ class SearchPush {
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
$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 ?? []];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送 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 代码
|
||||
*/
|
||||
@@ -175,7 +389,6 @@ class SearchPush {
|
||||
return '';
|
||||
}
|
||||
|
||||
$site_url = home_url('/');
|
||||
return <<<SCRIPT
|
||||
<script>
|
||||
(function(){
|
||||
@@ -211,13 +424,15 @@ SCRIPT;
|
||||
'provider' => 'baidu',
|
||||
'configured' => $enabled && $token !== '',
|
||||
'available' => $enabled && $token !== '',
|
||||
'verified' => !empty($config['baidu_push_verified']),
|
||||
// 百度链接提交接口无单独校验握手,Token 配置完成即可推送。
|
||||
'verified' => false,
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => $missing,
|
||||
'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_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];
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,46 @@ final class LedgerService {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read points and cash balance together (VIP modal catalog).
|
||||
*
|
||||
* The meta adapter serves both assets from user meta; the asset-account
|
||||
* fallback answers both rows with a single query.
|
||||
*
|
||||
* @return array{points:int,balance:string}
|
||||
*/
|
||||
public function points_and_balance(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['points' => 0, 'balance' => '0.00'];
|
||||
}
|
||||
if (class_exists('\AKSRT\Credits\MetaAssetService')) {
|
||||
$meta = \AKSRT\Credits\MetaAssetService::get_instance();
|
||||
return [
|
||||
'points' => $meta->balance_points($user_id),
|
||||
'balance' => $meta->balance_decimal($user_id),
|
||||
];
|
||||
}
|
||||
global $wpdb;
|
||||
$table = $this->table('asset_account');
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT asset, balance_minor FROM {$table} WHERE user_id = %d AND asset IN (%s, %s)",
|
||||
$user_id,
|
||||
AssetType::POINTS->value,
|
||||
AssetType::BALANCE->value
|
||||
), ARRAY_A);
|
||||
$minor = ['points' => 0, 'balance' => 0];
|
||||
foreach ((array) $rows as $row) {
|
||||
$asset = (string) ($row['asset'] ?? '');
|
||||
if (isset($minor[$asset])) {
|
||||
$minor[$asset] = max(0, (int) ($row['balance_minor'] ?? 0));
|
||||
}
|
||||
}
|
||||
return [
|
||||
'points' => $minor['points'],
|
||||
'balance' => AssetAmount::from_minor(AssetType::BALANCE, $minor['balance'])->decimal(),
|
||||
];
|
||||
}
|
||||
|
||||
public function format_balance(int $minor): string {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ final class AfterSaleService {
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
/** 处理记录来源:后台提交 / 用户提交(申请与撤销)。 */
|
||||
public const SOURCE_ADMIN = 'admin';
|
||||
public const SOURCE_USER = 'user';
|
||||
/** 处理记录上限:与 MetaAssetService::MAX_RECORDS 同口径,超出后只保留最近的一段。 */
|
||||
private const MAX_LOG_ENTRIES = 50;
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function get_instance(): self { return self::$instance ??= new self(); }
|
||||
@@ -144,6 +149,8 @@ final class AfterSaleService {
|
||||
try {
|
||||
$order = OrderRepository::get_instance()->get_user_order($user_id, $order_number);
|
||||
if (!$order || (string) ($order['status'] ?? '') !== Order::STATUS_PAID) return new \WP_Error('aksrt_after_sale_order', __('只有已支付订单可以申请售后', 'aksrt'), ['status' => 409]);
|
||||
// 只挡「仍在流转」的工单。已完结(completed)的工单不挡——能否再售后由
|
||||
// 「订单是否已退款」决定,而订单已退款时上面的 STATUS_PAID 校验已先挡掉。
|
||||
$active = $this->find_active($order_number);
|
||||
if ($active) {
|
||||
if ((int) ($active['user_id'] ?? 0) === $user_id && hash_equals((string) ($active['reason'] ?? ''), $reason)) {
|
||||
@@ -153,13 +160,15 @@ final class AfterSaleService {
|
||||
}
|
||||
$amount = $this->refund_amount($order);
|
||||
if ($amount === null) return new \WP_Error('aksrt_after_sale_amount', __('订单退款金额无效', 'aksrt'), ['status' => 400]);
|
||||
$now = current_time('mysql');
|
||||
$inserted = $wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number, 'user_id' => $user_id, 'aftersale_type' => 'refund',
|
||||
'reason' => $reason,
|
||||
'apply_form' => $apply_form === [] ? null : wp_json_encode($apply_form, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'images' => wp_json_encode($images), 'refund_amount' => $amount,
|
||||
'status' => self::STATUS_PENDING, 'created_at' => current_time('mysql'),
|
||||
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
'status' => self::STATUS_PENDING, 'created_at' => $now,
|
||||
'handling_log' => $this->encode_log([$this->log_entry(self::STATUS_PENDING, '', self::SOURCE_USER, $now)]),
|
||||
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
return $inserted === 1 ? $this->get((int) $wpdb->insert_id) : new \WP_Error('aksrt_after_sale_db', __('售后申请保存失败', 'aksrt'), ['status' => 500]);
|
||||
} finally {
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||
@@ -212,6 +221,15 @@ final class AfterSaleService {
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款金额:积分单取整,现金单经 AssetAmount 归一为两位小数;
|
||||
* 金额无法解析时返回 null,由 create() 拒掉这次申请。
|
||||
*/
|
||||
private function refund_amount(array $order): ?string {
|
||||
if ((string) ($order['currency'] ?? '') === 'points') return (string) max(0, (int) ($order['amount'] ?? 0));
|
||||
try { return \AKSRT\Ledger\AssetAmount::balance((string) ($order['amount'] ?? '0'))->decimal(); } catch (\InvalidArgumentException $e) { return null; }
|
||||
}
|
||||
|
||||
/** 最新一条售后工单(列表视图只取最新态,避免整表拉取)。 */
|
||||
public function latest_for_order(string $order_number): ?array {
|
||||
global $wpdb;
|
||||
@@ -229,6 +247,7 @@ final class AfterSaleService {
|
||||
if (!$row || (int) ($row['user_id'] ?? 0) !== $user_id) return new \WP_Error('aksrt_after_sale_missing', __('没有可取消的售后申请', 'aksrt'), ['status' => 404]);
|
||||
if ((string) ($row['status'] ?? '') !== self::STATUS_PENDING) return new \WP_Error('aksrt_after_sale_state', __('售后申请已进入处理阶段,无法取消', 'aksrt'), ['status' => 409]);
|
||||
if (!$this->update((int) $row['id'], ['status' => self::STATUS_CANCELLED])) return new \WP_Error('aksrt_after_sale_db', __('取消售后申请失败', 'aksrt'), ['status' => 500]);
|
||||
$this->append_log((int) $row['id'], $row, self::STATUS_CANCELLED, '', self::SOURCE_USER);
|
||||
return $this->get((int) $row['id']);
|
||||
} finally {
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||
@@ -298,6 +317,10 @@ final class AfterSaleService {
|
||||
|
||||
/**
|
||||
* 管理员直接设置售后工单状态:纯状态更新,不触发订单、资产与支付渠道联动。
|
||||
*
|
||||
* 每次提交都往 handling_log 落一条「状态 + 备注 + 来源 + 时间」,备注为空也留痕,
|
||||
* 后台与前台才能回放整条处理链路。读取-追加-写回需要与 create()/cancel_for_user()
|
||||
* 同一把订单锁,否则两条并发提交会互相覆盖日志。
|
||||
*/
|
||||
public function set_status(int $id, string $status, string $note = '') {
|
||||
if (!current_user_can('manage_options')) return new \WP_Error('aksrt_after_sale_forbidden', __('权限不足', 'aksrt'), ['status' => 403]);
|
||||
@@ -305,12 +328,48 @@ final class AfterSaleService {
|
||||
if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED], true)) return new \WP_Error('aksrt_after_sale_status', __('售后状态无效', 'aksrt'), ['status' => 400]);
|
||||
$row = $this->get($id);
|
||||
if (!$row) return new \WP_Error('aksrt_after_sale_state', __('售后申请不存在', 'aksrt'), ['status' => 409]);
|
||||
$note = sanitize_textarea_field($note);
|
||||
$data = ['status' => $status];
|
||||
if ($note !== '') $data['admin_note'] = $note;
|
||||
if ($status !== self::STATUS_PENDING) $data['reviewed_at'] = current_time('mysql');
|
||||
if (!$this->update($id, $data)) return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败', 'aksrt'), ['status' => 500]);
|
||||
return $this->get($id);
|
||||
global $wpdb;
|
||||
$lock = 'aksrt_after_sale_' . md5((string) ($row['order_number'] ?? ''));
|
||||
if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock)) !== 1) return new \WP_Error('aksrt_after_sale_busy', __('订单正在处理中,请稍后重试', 'aksrt'), ['status' => 409]);
|
||||
try {
|
||||
// 锁内重读:追加必须基于最新快照,否则两条并发提交仍会互相覆盖日志。
|
||||
$row = $this->get($id);
|
||||
if (!$row) return new \WP_Error('aksrt_after_sale_state', __('售后申请不存在', 'aksrt'), ['status' => 409]);
|
||||
$note = sanitize_textarea_field($note);
|
||||
if (!$this->update($id, ['status' => $status])) return new \WP_Error('aksrt_after_sale_db', __('售后状态保存失败', 'aksrt'), ['status' => 500]);
|
||||
$this->append_log($id, $row, $status, $note, self::SOURCE_ADMIN);
|
||||
return $this->get($id);
|
||||
} finally {
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加一条处理记录。日志按时间正序存放,超出 MAX_LOG_ENTRIES 条时只保留最近的一段。
|
||||
*
|
||||
* @param array<string,mixed> $current 追加前的工单行(含已规范化的 handling_log)。
|
||||
*/
|
||||
private function append_log(int $id, array $current, string $status, string $note, string $source): void {
|
||||
$log = is_array($current['handling_log'] ?? null) ? $current['handling_log'] : [];
|
||||
$log[] = $this->log_entry($status, $note, $source, current_time('mysql'));
|
||||
if (count($log) > self::MAX_LOG_ENTRIES) {
|
||||
$log = array_slice($log, -self::MAX_LOG_ENTRIES);
|
||||
}
|
||||
$this->update($id, ['handling_log' => $this->encode_log($log)]);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function log_entry(string $status, string $note, string $source, string $created_at): array {
|
||||
return [
|
||||
'status' => $status,
|
||||
'note' => $note,
|
||||
'source' => $source,
|
||||
'created_at' => $created_at,
|
||||
];
|
||||
}
|
||||
|
||||
private function encode_log(array $log): string {
|
||||
return wp_json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/** 该订单当前仍在处理中的售后工单(待审核/处理中),退款闸门只认它。 */
|
||||
@@ -345,6 +404,30 @@ final class AfterSaleService {
|
||||
: [];
|
||||
$apply_form = json_decode((string) ($row['apply_form'] ?? ''), true);
|
||||
$row['apply_form'] = is_array($apply_form) ? $apply_form : null;
|
||||
$row['handling_log'] = $this->normalize_log(json_decode((string) ($row['handling_log'] ?? ''), true));
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理记录规范化:只保留 status 非空的条目,source 只认 user,其余归 admin;
|
||||
* 非数组(含损坏 JSON)整体视为空,界面对老工单按「暂无处理记录」处理。
|
||||
*
|
||||
* @return list<array<string,string>>
|
||||
*/
|
||||
private function normalize_log($log): array {
|
||||
if (!is_array($log)) return [];
|
||||
$clean = [];
|
||||
foreach ($log as $entry) {
|
||||
if (!is_array($entry)) continue;
|
||||
$status = sanitize_key((string) ($entry['status'] ?? ''));
|
||||
if ($status === '') continue;
|
||||
$clean[] = [
|
||||
'status' => $status,
|
||||
'note' => (string) ($entry['note'] ?? ''),
|
||||
'source' => (string) ($entry['source'] ?? '') === self::SOURCE_USER ? self::SOURCE_USER : self::SOURCE_ADMIN,
|
||||
'created_at' => (string) ($entry['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
}
|
||||
@@ -290,9 +290,15 @@ class UserOrders {
|
||||
// - pending / processing:工单仍在流转(待审核 / 处理中),不允许重复申请;
|
||||
// - rejected:审核未通过,允许补充资料后再次申请;
|
||||
// - cancelled:用户自行取消,允许重新发起;
|
||||
// - completed:退款已完成,工单终结,不允许再次申请。
|
||||
$reapplyable_statuses = [AfterSaleService::STATUS_REJECTED, AfterSaleService::STATUS_CANCELLED];
|
||||
// - completed:工单已完结,但只要订单没被另标「已退款」(enabled 仍为 true),
|
||||
// 依旧允许再次申请。能否再售后由「订单是否已退款」决定,不由工单状态决定:
|
||||
// 订单已退款时 $group 变成 refunded、enabled 直接为 false,上面那条自然挡死。
|
||||
$latest_status = (string) ($latest['status'] ?? '');
|
||||
$reapplyable_statuses = [
|
||||
AfterSaleService::STATUS_REJECTED,
|
||||
AfterSaleService::STATUS_CANCELLED,
|
||||
AfterSaleService::STATUS_COMPLETED,
|
||||
];
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
@@ -308,9 +314,8 @@ class UserOrders {
|
||||
'images' => array_values((array) ($latest['images'] ?? [])),
|
||||
'refund_amount' => (string) ($latest['refund_amount'] ?? '0'),
|
||||
'created_at' => (string) ($latest['created_at'] ?? ''),
|
||||
'reviewed_at' => (string) ($latest['reviewed_at'] ?? ''),
|
||||
'completed_at' => (string) ($latest['completed_at'] ?? ''),
|
||||
'admin_note' => (string) ($latest['admin_note'] ?? ''),
|
||||
// 处理记录:每次状态提交一条(状态 + 备注 + 来源 + 时间),前台时间线的唯一数据源。
|
||||
'handling_log' => array_values((array) ($latest['handling_log'] ?? [])),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -24,10 +24,6 @@ trait DisplayPageGroups {
|
||||
'ai_seo_title_prompt' => trim((string) ($s['ai_seo_title_prompt'] ?? '')),
|
||||
'ai_seo_keywords_prompt' => trim((string) ($s['ai_seo_keywords_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_default' => esc_url_raw((string) ($s['seo_image_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 {
|
||||
$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 [
|
||||
'baidu_push_enabled' => $enabled,
|
||||
'baidu_push_token' => $token,
|
||||
'baidu_push_enabled' => !empty($settings['baidu_push_enabled']),
|
||||
'baidu_push_token' => (string) ($settings['baidu_push_token'] ?? ''),
|
||||
'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_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_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']),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -210,42 +210,6 @@ class HomeCms {
|
||||
return $settings['home_exclude_cats'];
|
||||
}
|
||||
|
||||
public static function get_paid_content_settings(): array {
|
||||
return PaymentRuntime::get_paid_content_settings();
|
||||
}
|
||||
|
||||
public static function get_content_coupon_settings(): array {
|
||||
return PaymentRuntime::get_content_coupon_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_recharge_settings(): array {
|
||||
return PaymentRuntime::get_payment_recharge_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_vip_settings(): array {
|
||||
return PaymentRuntime::get_payment_vip_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_revenue_settings(): array {
|
||||
return PaymentRuntime::get_payment_revenue_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_affiliate_settings(): array {
|
||||
return PaymentRuntime::get_payment_affiliate_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_gateway_settings(): array {
|
||||
return PaymentRuntime::get_payment_gateway_settings();
|
||||
}
|
||||
|
||||
public static function get_order_settings(): array {
|
||||
return PaymentRuntime::get_order_settings();
|
||||
}
|
||||
|
||||
public static function get_withdrawal_settings(): array {
|
||||
return PaymentRuntime::get_withdrawal_settings();
|
||||
}
|
||||
|
||||
private static function normalize_sort_options(array $raw): array {
|
||||
$allowed = self::homepage_allowed_sorts();
|
||||
$result = [];
|
||||
|
||||
+214
-51
@@ -121,7 +121,9 @@ class PaymentRuntime {
|
||||
],
|
||||
'vip' => [
|
||||
'enabled' => !empty($settings['pay_vip_pass_charge_s']),
|
||||
'desc' => sanitize_textarea_field((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
|
||||
// 保存端已按 post 级 HTML 放行(schema sanitize_type html),
|
||||
// 输出端不能再被 textarea 净化剥掉标签。
|
||||
'desc' => wp_kses_post((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
|
||||
// 默认非单密码(卡号+密码),对齐参考主题 pay_vip_pass_charge_only_password 默认 false。
|
||||
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
||||
],
|
||||
@@ -168,48 +170,28 @@ class PaymentRuntime {
|
||||
$vip2_enabled = !empty($settings['pay_user_vip_2_s']);
|
||||
$enabled = $vip1_enabled || $vip2_enabled;
|
||||
|
||||
$name1 = trim((string) ($settings['pay_user_vip_1_name'] ?? ''));
|
||||
$name2 = trim((string) ($settings['pay_user_vip_2_name'] ?? ''));
|
||||
$name1_html = trim(wp_kses_post((string) ($settings['pay_user_vip_1_name'] ?? '')));
|
||||
$name2_html = trim(wp_kses_post((string) ($settings['pay_user_vip_2_name'] ?? '')));
|
||||
$name1 = sanitize_text_field(wp_strip_all_tags($name1_html));
|
||||
$name2 = sanitize_text_field(wp_strip_all_tags($name2_html));
|
||||
if ($name1 === '') {
|
||||
$name1 = __('一级会员', 'aksrt');
|
||||
$name1 = __('黄金会员', 'aksrt');
|
||||
$name1_html = $name1;
|
||||
}
|
||||
if ($name2 === '') {
|
||||
$name2 = __('二级会员', 'aksrt');
|
||||
$name2 = __('钻石会员', 'aksrt');
|
||||
$name2_html = $name2;
|
||||
}
|
||||
|
||||
$badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'aksrt'));
|
||||
|
||||
$renew_price_type = sanitize_key((string) ($settings['vip_renew_price_type'] ?? 'discount'));
|
||||
if (!in_array($renew_price_type, ['same', 'discount', 'reduce'], true)) {
|
||||
if (!in_array($renew_price_type, ['same', 'discount', 'reduce', 'customize'], true)) {
|
||||
$renew_price_type = 'discount';
|
||||
}
|
||||
|
||||
$default_l1 = "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡";
|
||||
$default_l2 = "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡";
|
||||
$products_1 = sanitize_textarea_field((string) ($settings['payment_vip_1_products'] ?? $default_l1));
|
||||
$products_2 = sanitize_textarea_field((string) ($settings['payment_vip_2_products'] ?? $default_l2));
|
||||
// Restore newlines lost only if empty after sanitize — sanitize_textarea_field keeps \n.
|
||||
if (trim($products_1) === '') {
|
||||
$products_1 = $default_l1;
|
||||
}
|
||||
if (trim($products_2) === '') {
|
||||
$products_2 = $default_l2;
|
||||
}
|
||||
|
||||
$desc = sanitize_textarea_field((string) ($settings['pay_user_vip_desc'] ?? ''));
|
||||
$more = (string) ($settings['pay_user_vip_more'] ?? '');
|
||||
$allowed_equity = [
|
||||
'li' => [],
|
||||
'ul' => [],
|
||||
'ol' => [],
|
||||
'strong' => [],
|
||||
'b' => [],
|
||||
'em' => [],
|
||||
'i' => ['class' => true],
|
||||
'span' => ['class' => true],
|
||||
'br' => [],
|
||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
||||
];
|
||||
$desc = wp_kses_post((string) ($settings['pay_user_vip_desc'] ?? __('开通VIP会员,享受会员专属折扣以及多项特权', 'aksrt')));
|
||||
$more = (string) ($settings['pay_user_vip_more'] ?? '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>');
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
@@ -218,27 +200,32 @@ class PaymentRuntime {
|
||||
'name' => $vip1_enabled ? $name1 : $badge,
|
||||
'badge_text' => $badge,
|
||||
'desc' => $desc,
|
||||
'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]),
|
||||
'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'),
|
||||
// 管理员编辑的富文本说明:与保存端同为 post 级 HTML(wp_kses_post),
|
||||
// 不再用权益列表的窄白名单二次剥标签(h/img/hr/table 等)。
|
||||
'more' => wp_kses_post($more),
|
||||
'levels' => [
|
||||
1 => [
|
||||
'enabled' => $vip1_enabled,
|
||||
'name' => $name1,
|
||||
'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享一级会员特权', 'aksrt'))),
|
||||
'equity' => wp_kses((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>'), $allowed_equity),
|
||||
'name_html' => $name1_html,
|
||||
'desc' => wp_kses_post((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'aksrt'))),
|
||||
'equity' => wp_kses_post((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>')),
|
||||
'img_icon' => self::get_vip_icon_url(1, (string) ($settings['vip_1_img_icon'] ?? '')),
|
||||
'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']),
|
||||
'products_raw' => $products_1,
|
||||
'products' => self::normalize_vip_product_items($settings['vip_1_product'] ?? null, self::vip_product_defaults()),
|
||||
'renew_products' => self::normalize_vip_product_items($settings['vip_1_renew_product'] ?? null, self::vip_renew_product_defaults(1)),
|
||||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))),
|
||||
],
|
||||
2 => [
|
||||
'enabled' => $vip2_enabled,
|
||||
'name' => $name2,
|
||||
'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享二级会员特权', 'aksrt'))),
|
||||
'equity' => wp_kses((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>'), $allowed_equity),
|
||||
'name_html' => $name2_html,
|
||||
'desc' => wp_kses_post((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'aksrt'))),
|
||||
'equity' => wp_kses_post((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>')),
|
||||
'img_icon' => self::get_vip_icon_url(2, (string) ($settings['vip_2_img_icon'] ?? '')),
|
||||
'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']),
|
||||
'products_raw' => $products_2,
|
||||
'products' => self::normalize_vip_product_items($settings['vip_2_product'] ?? null, self::vip_product_defaults()),
|
||||
'renew_products' => self::normalize_vip_product_items($settings['vip_2_renew_product'] ?? null, self::vip_renew_product_defaults(2)),
|
||||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))),
|
||||
],
|
||||
],
|
||||
@@ -247,21 +234,16 @@ class PaymentRuntime {
|
||||
],
|
||||
'renew' => [
|
||||
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
|
||||
'desc' => sanitize_textarea_field((string) ($settings['vip_renew_desc'] ?? '')),
|
||||
'desc' => wp_kses_post((string) ($settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益')),
|
||||
'price_type' => $renew_price_type,
|
||||
'discount' => max(1, min(100, (int) ($settings['vip_renew_discount'] ?? 8))),
|
||||
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 999900),
|
||||
],
|
||||
'upgrade' => [
|
||||
'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']),
|
||||
'desc' => sanitize_textarea_field((string) ($settings['vip_upgrade_desc'] ?? '')),
|
||||
// Multi-line upgrade product rules — must not collapse newlines.
|
||||
'product' => sanitize_textarea_field((string) ($settings['vip_upgrade_product'] ?? '')),
|
||||
'discount' => max(0.1, min(9.9, (float) ($settings['vip_renew_discount'] ?? 8))),
|
||||
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 50000),
|
||||
],
|
||||
'upgrade' => self::normalize_vip_upgrade_settings($settings),
|
||||
'points_exchange' => [
|
||||
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
|
||||
'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
||||
'packages_raw' => sanitize_textarea_field((string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久")),
|
||||
'desc' => wp_kses_post((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
||||
'packages' => self::normalize_vip_points_packages($settings['pay_vip_points_exchange_product'] ?? null),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -728,4 +710,185 @@ class PaymentRuntime {
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate days for catalog labels / order meta; activation prefers
|
||||
* calendar time+unit math.
|
||||
*/
|
||||
private static function duration_to_days(int $time, string $unit = 'day'): int {
|
||||
if ($time <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return $unit === 'month' ? $time * 30 : $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_defaults(): array {
|
||||
return [
|
||||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_renew_product_defaults(int $level): array {
|
||||
if ($level === 2) {
|
||||
return [
|
||||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
return [
|
||||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签颜色可选值(参考主题 zib_palette(array(), array('b','jb')))。
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private static function vip_tag_palette_keys(): array {
|
||||
return ['b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', 'b-black', 'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple', 'jb-vip1', 'jb-vip2'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a structured 会员商品 group into purchasable rows.
|
||||
*
|
||||
* @param mixed $raw Stored group value.
|
||||
* @param array<int,array<string,mixed>> $fallback Default rows when unset.
|
||||
* @return array<int,array{price:string,show_price:string,tag:string,tag_class:string,time:int,unit:string,days:int}>
|
||||
*/
|
||||
private static function normalize_vip_product_items($raw, array $fallback): array {
|
||||
$rows = is_array($raw) ? array_values($raw) : [];
|
||||
if ($rows === []) {
|
||||
$rows = $fallback;
|
||||
}
|
||||
$palette = self::vip_tag_palette_keys();
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$price = self::money_decimal($row['price'] ?? '', '0.00');
|
||||
if (bccomp($price, '0.00', 2) <= 0) {
|
||||
continue;
|
||||
}
|
||||
$time = max(0, (int) ($row['time'] ?? 0));
|
||||
$unit = ((string) ($row['unit'] ?? 'month')) === 'day' ? 'day' : 'month';
|
||||
$tag_class = (string) ($row['tag_class'] ?? '');
|
||||
if (!in_array($tag_class, $palette, true)) {
|
||||
$tag_class = '';
|
||||
}
|
||||
$out[] = [
|
||||
'price' => $price,
|
||||
'show_price' => self::money_decimal($row['show_price'] ?? '', '0.00'),
|
||||
'tag' => wp_kses_post((string) ($row['tag'] ?? '')),
|
||||
'tag_class' => $tag_class,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'days' => self::duration_to_days($time, $unit),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the structured 升级价格 accordion.
|
||||
*
|
||||
* @param array<string,mixed> $settings
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalize_vip_upgrade_settings(array $settings): array {
|
||||
$raw = isset($settings['vip_upgrade_product']) && is_array($settings['vip_upgrade_product'])
|
||||
? $settings['vip_upgrade_product']
|
||||
: [];
|
||||
$palette = self::vip_tag_palette_keys();
|
||||
$money = static function ($value, string $default): string {
|
||||
return self::money_decimal(is_string($value) ? $value : (is_numeric($value) ? (string) $value : ''), $default);
|
||||
};
|
||||
$tag = static function ($value, string $default): string {
|
||||
$text = trim((string) $value);
|
||||
return $text !== '' ? wp_kses_post($text) : $default;
|
||||
};
|
||||
$tag_class = static function ($value) use ($palette): string {
|
||||
$key = (string) $value;
|
||||
return in_array($key, $palette, true) ? $key : '';
|
||||
};
|
||||
$switch = static function ($value, bool $default): bool {
|
||||
return isset($value) ? !empty($value) : $default;
|
||||
};
|
||||
|
||||
return [
|
||||
'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']),
|
||||
'desc' => wp_kses_post((string) ($settings['vip_upgrade_desc'] ?? '升级VIP会员,享更多会员权益')),
|
||||
'month_to_month_s' => $switch($raw['month_to_month_s'] ?? null, true),
|
||||
'unit_price' => $money($raw['unit_price'] ?? null, '10.00'),
|
||||
'unit_tag' => $tag($raw['unit_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐'),
|
||||
'unit_tag_class' => $tag_class($raw['unit_tag_class'] ?? 'jb-yellow'),
|
||||
'jump_s' => $switch($raw['jump_s'] ?? null, false),
|
||||
'jump_price' => $money($raw['jump_price'] ?? null, '199.00'),
|
||||
'jump_show_price' => $money($raw['jump_show_price'] ?? null, '299.00'),
|
||||
'jump_tag' => $tag($raw['jump_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||||
'jump_tag_class' => $tag_class($raw['jump_tag_class'] ?? 'jb-red'),
|
||||
'permanent_price' => $money($raw['permanent_price'] ?? null, '199.00'),
|
||||
'permanent_show_price' => $money($raw['permanent_show_price'] ?? null, '299.00'),
|
||||
'permanent_tag' => $tag($raw['permanent_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||||
'permanent_tag_class' => $tag_class($raw['permanent_tag_class'] ?? 'jb-red'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分兑换明细默认行(对齐旧 textarea 的开箱配置)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_points_package_defaults(): array {
|
||||
return [
|
||||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the structured 积分兑换明细 group. `time` is the literal string
|
||||
* "Permanent" for a permanent grant, otherwise a day/month count.
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @return array<int,array{points:int,level:int,time:int,unit:string,is_permanent:bool,days:int}>
|
||||
*/
|
||||
private static function normalize_vip_points_packages($raw): array {
|
||||
$rows = is_array($raw) ? array_values($raw) : self::vip_points_package_defaults();
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$points = max(0, (int) ($row['points'] ?? 0));
|
||||
$level = (int) ($row['level'] ?? 1);
|
||||
$time_raw = trim((string) ($row['time'] ?? ''));
|
||||
if ($points <= 0 || $level < 1 || $level > 2 || $time_raw === '') {
|
||||
continue;
|
||||
}
|
||||
$is_permanent = strcasecmp($time_raw, 'Permanent') === 0;
|
||||
$time = $is_permanent ? 0 : max(0, (int) $time_raw);
|
||||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$out[] = [
|
||||
'points' => $points,
|
||||
'level' => $level,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'is_permanent' => $is_permanent,
|
||||
'days' => $is_permanent ? 0 : self::duration_to_days($time, $unit),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ final class UserRenderer {
|
||||
if ($name === '' || $style_name === '' || $label === '' || $url === '') {
|
||||
continue;
|
||||
}
|
||||
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . '" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr($label) . '">' . (string) ($item['icon'] ?? '') . '</a>';
|
||||
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . ' toggle-radius" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">' . (string) ($item['icon'] ?? '') . '</a>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
@@ -162,11 +162,12 @@ final class UserRenderer {
|
||||
'post_style' => 'card',
|
||||
]);
|
||||
|
||||
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img avatar-lg');
|
||||
// 会员/禁封徽章不再挂头像角标,改由名字行的会员位出图(aksrt_get_user_name)。
|
||||
$avatar = aksrt_get_avatar_box($user_id, 'avatar-img avatar-lg', true, false);
|
||||
$name = aksrt_get_user_name([
|
||||
'id' => $user_id,
|
||||
'class' => 'flex1 flex ac',
|
||||
'vip' => false,
|
||||
'vip' => true,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
@@ -192,7 +193,7 @@ final class UserRenderer {
|
||||
]);
|
||||
}
|
||||
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']) {
|
||||
|
||||
@@ -106,11 +106,6 @@ if (file_exists($js)) {
|
||||
],
|
||||
|
||||
'imageUploadMultiple' => $image_upload_multiple,
|
||||
'ai' => [
|
||||
'available' => !empty($ctx['ai_available']),
|
||||
'summary' => !empty($ctx['ai_available']),
|
||||
'tags' => !empty($ctx['ai_available']),
|
||||
],
|
||||
'editor' => [
|
||||
'canUploadImage' => !empty($ctx['can_upload_img']),
|
||||
'canUploadVideo' => !empty($ctx['can_upload_video']),
|
||||
@@ -142,10 +137,6 @@ if (file_exists($js)) {
|
||||
'submitted' => __('投稿已提交', 'aksrt'),
|
||||
'error' => __('操作失败,请重试', 'aksrt'),
|
||||
'confirmDelete' => __('确定要删除这个草稿吗?', 'aksrt'),
|
||||
'aiGenerating' => __('生成中…', 'aksrt'),
|
||||
'aiFailed' => __('AI 生成失败', 'aksrt'),
|
||||
'needContent' => __('请先填写正文内容', 'aksrt'),
|
||||
'aiFilled' => __('已填入,可继续编辑', 'aksrt'),
|
||||
'deleting' => __('正在删除草稿…', 'aksrt'),
|
||||
'deleted' => __('草稿已删除', 'aksrt'),
|
||||
'uploading' => __('正在上传…', 'aksrt'),
|
||||
|
||||
@@ -147,6 +147,5 @@ $oauth_bind_pass = $oauth_intent === 'bind'
|
||||
<?php foreach ($oauth_providers as $provider) : ?><?php if (($provider['name'] ?? '') === 'weixingzh') continue; ?><?php $provider_label = (string) ($provider['label'] ?? $provider['name'] ?? ''); ?><a class="social-login-item <?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" href="<?php echo esc_url((string) ($provider['url'] ?? '#')); ?>" data-provider="<?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" aria-label="<?php echo esc_attr(sprintf(__('%s登录', 'aksrt'), $provider_label)); ?>" title="<?php echo esc_attr($provider_label); ?>"><?php echo $provider['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php if ($oauth_large) : ?><span><?php printf(esc_html__('%s登录', 'aksrt'), esc_html($provider_label)); ?></span><?php endif; ?></a><?php endforeach; ?>
|
||||
<?php if ($wechat_qr) : ?><button type="button" class="social-login-item weixingzh" data-auth-tab="oauth" aria-label="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>" title="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>"><i class="fa fab fa-weixin" aria-hidden="true"></i><?php if ($oauth_large) : ?><span><?php esc_html_e('微信登录', 'aksrt'); ?></span><?php endif; ?></button><?php endif; ?>
|
||||
</div></div>
|
||||
<?php if (!empty($agreement['enabled'])) : ?><div class="sign-agreement sign-agreement-implicit"><?php esc_html_e('使用社交账号登录即表示', 'aksrt'); ?><?php echo $agreement['label_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -177,8 +177,6 @@ if ($is_guest) {
|
||||
$submit_label = __('保存', 'aksrt');
|
||||
}
|
||||
|
||||
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
||||
|
||||
return [
|
||||
'front' => $front,
|
||||
'user_id' => $user_id,
|
||||
@@ -207,7 +205,6 @@ return [
|
||||
'vip' => $vip,
|
||||
'points_enabled' => $points_enabled,
|
||||
'submit_label' => $submit_label,
|
||||
'ai_available' => $ai_available,
|
||||
'can_pay' => $cap('new_post_pay'),
|
||||
'can_pay_download' => $cap('new_post_pay_download'),
|
||||
'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/);
|
||||
});
|
||||
@@ -13,8 +13,9 @@
|
||||
* 3) ice_perpage 只在服务端被读取、默认每页条数偏大。条数小于默认值时永远只有一页,
|
||||
* 四个按钮恒为 disabled —— 看上去就是「分页是坏的」。
|
||||
*
|
||||
* 现状:渲染已迁移到共享助手 AdminListPagination(WP 原生 paginate_links),
|
||||
* 基准 URL 与页码夹取都在那里做。本文件只固化卡密管理仍特有的部分:
|
||||
* 现状:渲染已迁移到共享助手 AdminListPagination,四按钮标记由助手统一输出
|
||||
* (与这里原来那套逐行一致),基准 URL 与页码夹取也都在那里做。
|
||||
* 本文件只固化卡密管理仍特有的部分:
|
||||
* - ice_perpage 仍作为 URL 参数被尊重,且工具栏不新增「每页显示」控件;
|
||||
* - 越界页码回退到最后一页并重查,不留空态死胡同;
|
||||
* - 筛选参数白名单化拼装,排序与每页条数随链接带走。
|
||||
@@ -55,7 +56,7 @@ test('渲染已迁移到共享助手 AdminListPagination', () => {
|
||||
assert.doesNotMatch(page, /self::DEFAULT_PER_PAGE/);
|
||||
assert.match(page, /AdminListPagination::DEFAULT_PER_PAGE/);
|
||||
|
||||
// 只服务自定义禁用按钮的 CSS 规则一并删除。
|
||||
// 只服务自定义禁用按钮的 CSS 规则已随标记一起搬进助手。
|
||||
assert.doesNotMatch(page, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/);
|
||||
});
|
||||
|
||||
|
||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user